diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bad9d92d5..ca68ccb07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,7 +146,7 @@ jobs: - run: mise run //bindings/python:test linux-x64-egl env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build linux-x64-egl + - run: mise run //bindings/dart:test linux-x64-egl env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -202,9 +202,6 @@ jobs: - run: mise run //bindings/rust:test:download env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:test:download - env: - MISE_TASK_SKIP: "//:build" - run: mise run //bindings/zig:test linux-x64-vulkan env: MISE_TASK_SKIP: "//:build" @@ -238,7 +235,7 @@ jobs: - run: mise run //bindings/python:test linux-x64-vulkan env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build linux-x64-vulkan + - run: mise run //bindings/dart:test linux-x64-vulkan env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -328,7 +325,7 @@ jobs: - run: mise run //bindings/python:test linux-arm64-egl env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build linux-arm64-egl + - run: mise run //bindings/dart:test linux-arm64-egl env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -414,7 +411,7 @@ jobs: - run: mise run //bindings/python:test linux-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build linux-arm64-vulkan + - run: mise run //bindings/dart:test linux-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -575,7 +572,7 @@ jobs: - run: mise run //bindings/python:test macos-arm64-metal env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build macos-arm64-metal + - run: mise run //bindings/dart:test macos-arm64-metal env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -657,7 +654,7 @@ jobs: - run: mise run //bindings/python:test macos-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build macos-arm64-vulkan + - run: mise run //bindings/dart:test macos-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -736,7 +733,7 @@ jobs: - run: mise run //bindings/python:test macos-arm64-egl env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build macos-arm64-egl + - run: mise run //bindings/dart:test macos-arm64-egl env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -1369,7 +1366,7 @@ jobs: - run: mise run //bindings/python:test windows-x64-wgl env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build windows-x64-wgl + - run: mise run //bindings/dart:test windows-x64-wgl env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -1445,7 +1442,7 @@ jobs: - run: mise run //bindings/python:test windows-x64-vulkan env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build windows-x64-vulkan + - run: mise run //bindings/dart:test windows-x64-vulkan env: MISE_TASK_SKIP: "//:build" - name: Save Zig packages @@ -1511,7 +1508,7 @@ jobs: - run: mise run //bindings/python:test windows-arm64-wgl env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build windows-arm64-wgl + - run: mise run //bindings/dart:test windows-arm64-wgl env: MISE_TASK_SKIP: "//:build" - name: Upload native artifact @@ -1568,7 +1565,7 @@ jobs: - run: mise run //bindings/python:test windows-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - - run: mise run //bindings/dart:build windows-arm64-vulkan + - run: mise run //bindings/dart:test windows-arm64-vulkan env: MISE_TASK_SKIP: "//:build" - name: Upload native artifact diff --git a/.gitignore b/.gitignore index 7c92dbdee..fa943860d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ bindings/openharmony/library/BuildProfile.ets .kotlin .devcontainer/mise-oci/ .devcontainer/mise-oci.tar +does-not-exist.db diff --git a/.mise/bin/sync-submodules b/.mise/bin/sync-submodules index a1882a358..0da18c479 100755 --- a/.mise/bin/sync-submodules +++ b/.mise/bin/sync-submodules @@ -56,7 +56,6 @@ mlt_vendor_paths=( # pointing at maplibre/maplibre-native. See patches/maplibre-native/README.md. mln_patches=( "$repo_root/patches/maplibre-native/0002-windows-local-file-urls.patch" - "$repo_root/patches/maplibre-native/0003-run-loop-process-gate.patch" ) # A patch counts as applied when it reverses cleanly, which is also what makes diff --git a/CMakePresets.json b/CMakePresets.json index 859ccf7ef..2e919cf15 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -542,6 +542,11 @@ "configurePreset": "tvos-simulator-arm64-metal", "inherits": "_test-base" }, + { + "name": "ohos-x64-egl", + "configurePreset": "ohos-x64-egl", + "inherits": "_test-base" + }, { "name": "windows-x64-wgl", "configurePreset": "windows-x64-wgl", diff --git a/Cargo.lock b/Cargo.lock index 6ea77f038..be3e2c52b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1824,6 +1824,7 @@ dependencies = [ "glow", "glutin", "glutin-winit", + "libc", "maplibre-native-ffi", "objc2 0.5.2", "objc2-app-kit 0.2.2", diff --git a/Package.swift b/Package.swift index a28119682..1f2d4df25 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,6 @@ let testSourceFiles = [ "MaplibreNativeFFITests/SupportHelperTests.swift", "MaplibreNativeFFITests/SyntheticHandles.swift", "MaplibreNativeFFITests/ValueTests.swift", - "MaplibreNativeFFITests/WakeSourceTests.swift", ] let products: [Product] = [ diff --git a/bindings/dart/README.md b/bindings/dart/README.md index 01de8c62d..604f393c0 100644 --- a/bindings/dart/README.md +++ b/bindings/dart/README.md @@ -2,8 +2,8 @@ `maplibre_native_ffi` is the low-level Dart binding for the public MapLibre Native C API. The package exposes explicit native handle lifetimes, copied value -types, runtime event batches, resource callbacks, offline operations, and the -render backend descriptors used by host integrations. +types, owned runtime event batches, ordinary futures, resource callbacks, and +the render backend descriptors used by host integrations. ## Build and test @@ -30,6 +30,12 @@ native preset, and verifies that Flutter packages its code asset. Device and simulator iOS use separate presets because their dynamic libraries target different Apple SDKs. +Use Metal or Vulkan for Dart on macOS. The macOS EGL target is temporarily +excluded because the prebuilt ANGLE libraries do not have enough Mach-O header +padding for Dart native assets to update their install names. The target can be +enabled after the ANGLE libraries are relinked with +`-headerpad_max_install_names` and the repository uses the new artifact. + The native library reaches Dart as a code asset that `hook/build.dart` declares, which is how the generated `@Native` declarations resolve it. Build hooks run in a semi-hermetic environment that strips arbitrary environment variables, so the @@ -69,40 +75,27 @@ copy of the same library. ## Ownership and execution -Owned handles have an idempotent `close()` or `discard()` operation. Close child -maps, render sessions, frames, snapshots, request handles, and offline +Runtime and map handles have an idempotent `close()`. Runtime close remains +asynchronous in Dart so callback roots stay alive through native teardown. Close +child maps, render sessions, frames, snapshots, request handles, and offline operations before their parent runtime. Scoped backend values remain valid only until their frame or owner is closed. -Runtime and map work is synchronous and owner-thread-affine. Keep a handle and -all calls that use it on the isolate that created it. Run queued callbacks with -`RuntimeHandle.pump()`, then take the events it produced with -`RuntimeHandle.drainEvents()`. Narrow what a map or a runtime queues with -`setEventMask`. - -A render session is the exception: it belongs to the isolate that attached it, -which need not be the map's. A `MapHandle` cannot cross isolates, so -`MapHandle.attachRef()` produces a `MapAttachRef` that can. It carries the -native address and attaches; every other map call stays on the map's isolate. - -## Known draft deviation: do not await in an isolate that holds a handle - -The C API keys owner-thread checks on the OS thread. This binding keys them on -`Isolate.current.hashCode`, and the two are not equivalent: the Dart VM moves an -isolate between OS threads, and it does so when an isolate resumes from awaited -I/O. The isolate hash does not change, so the binding's own check still passes -while the native check starts failing. - -Until that is addressed, do not `await` I/O on an isolate that holds a runtime, -map, projection, or render session. Create the handles, use them, and close them -without yielding to I/O in between. Dart offers no equivalent of Go's -`runtime.LockOSThread()`, so the binding cannot pin the isolate on your behalf. - -Exceeding this produces `wrongThread` from every call on the handle, including -`close()`. Because close fails too, the native runtime is never destroyed and -`mln_runtime_destroy` refuses for the rest of the process. - -Tracked in [#412](https://github.com/maplibre/maplibre-native-ffi/issues/412). +Projection handles are created asynchronously and are synchronous after that: +every projection call, `close()` included, runs on the calling isolate's thread, +may be made from any isolate, and never observes map changes made after creation +and remains usable after its source map and runtime close. + +Create runtimes and maps with `await`. Runtime and map commands copy their input +and return `Future` values. Snapshot methods synchronously copy immutable state. +Ordered queries and lifecycle operations also return `Future` values. Direct +wake callbacks report queued events without participating in future completion. +Read queued events with `RuntimeHandle.drainEvents()`. Narrow what a map or a +runtime queues with `setEventMask`. + +Runtime, map, camera, and projection calls remain valid when Dart resumes an +isolate on another native thread after `await`. Attach a render session directly +from its map on the isolate that will own the graphics session. Resource-request completion is one-shot. Calling `complete()` or `close()` releases the provider reference even when completion reports a native error. diff --git a/bindings/dart/lib/maplibre_native_ffi.dart b/bindings/dart/lib/maplibre_native_ffi.dart index 811fb9fe9..feb1e3a99 100644 --- a/bindings/dart/lib/maplibre_native_ffi.dart +++ b/bindings/dart/lib/maplibre_native_ffi.dart @@ -19,6 +19,6 @@ export 'src/runtime/runtime.dart' CustomMvtVectorCallbackLifecycleProbe, customMvtVectorCallbackProbeForTesting, decodeRuntimeEventBatchForTesting, - mapAttachRefIdForTesting, + mapHandleIdForTesting, runtimeHandleIdForTesting; export 'src/style/style.dart'; diff --git a/bindings/dart/lib/src/camera/camera.dart b/bindings/dart/lib/src/camera/camera.dart index 6bdf3fb7b..39b2da45d 100644 --- a/bindings/dart/lib/src/camera/camera.dart +++ b/bindings/dart/lib/src/camera/camera.dart @@ -6,6 +6,23 @@ import '../geo/geo.dart'; export '../geo/geo.dart' show EdgeInsets, LatLng, LatLngBounds, Quaternion, ScreenPoint, Vec3; +/// Native execution mode for one atomic camera update. +enum CameraUpdateMode { + /// Applies the camera immediately. + jump(0), + + /// Animates with easing. + ease(1), + + /// Animates along a flight path. + fly(2); + + const CameraUpdateMode(this.rawValue); + + /// Native camera update mode value. + final int rawValue; +} + /// Cubic easing curve for animated camera transitions. final class UnitBezier { /// Creates a cubic unit bezier. @@ -148,6 +165,49 @@ final class AnimationOptions { Object.hash(durationMs, velocity, minZoom, easing, transitionId); } +/// Relative camera operation kind. +enum CameraDeltaKind { + move(0), + scale(1), + bearing(2), + pitch(3); + + const CameraDeltaKind(this.rawValue); + + /// Native camera delta kind value. + final int rawValue; +} + +/// One relative camera operation. +final class CameraDelta { + /// Creates a relative camera operation. + const CameraDelta({ + this.kind = CameraDeltaKind.move, + this.offset = const ScreenPoint(0, 0), + this.amount = 0, + this.anchor, + this.animation = const AnimationOptions(), + }); + + final CameraDeltaKind kind; + final ScreenPoint offset; + final double amount; + final ScreenPoint? anchor; + final AnimationOptions animation; + + @override + bool operator ==(Object other) => + other is CameraDelta && + other.kind == kind && + other.offset == offset && + other.amount == amount && + other.anchor == anchor && + other.animation == animation; + + @override + int get hashCode => Object.hash(kind, offset, amount, anchor, animation); +} + /// Optional fitting controls for camera-for-viewport queries. final class CameraFitOptions { /// Creates camera fit options. diff --git a/bindings/dart/lib/src/error/maplibre_exception.dart b/bindings/dart/lib/src/error/maplibre_exception.dart index 380b3e709..7d5aa5fac 100644 --- a/bindings/dart/lib/src/error/maplibre_exception.dart +++ b/bindings/dart/lib/src/error/maplibre_exception.dart @@ -20,6 +20,21 @@ final class MaplibreStatus { /// A native MapLibre error or C++ exception was converted to status. static const nativeError = MaplibreStatus._('nativeError', -5); + /// The operation completed after cancellation was requested. + static const cancelled = MaplibreStatus._('cancelled', -6); + + /// The call raced a driver call in flight; retry once it returns. + static const busy = MaplibreStatus._('busy', -7); + + /// The render target was lost and the session cannot continue with it. + static const targetLost = MaplibreStatus._('targetLost', -8); + + /// No result was ready yet; poll or wait for the wake and retry. + static const notReady = MaplibreStatus._('notReady', -9); + + /// No object has the requested ID. + static const notFound = MaplibreStatus._('notFound', -10); + /// An unknown status value returned by a newer or incompatible native build. static MaplibreStatus unknown(int nativeStatusCode) => MaplibreStatus._('unknown', nativeStatusCode); @@ -39,6 +54,11 @@ final class MaplibreStatus { -3 => wrongThread, -4 => unsupported, -5 => nativeError, + -6 => cancelled, + -7 => busy, + -8 => targetLost, + -9 => notReady, + -10 => notFound, _ => unknown(nativeStatusCode), }; @@ -79,6 +99,7 @@ abstract final class MaplibreException implements Exception { -3 => WrongThreadException(nativeStatusCode, diagnostic), -4 => UnsupportedFeatureException(nativeStatusCode, diagnostic), -5 => NativeErrorException(nativeStatusCode, diagnostic), + -10 => NotFoundException(nativeStatusCode, diagnostic), _ => UnknownMaplibreException(status, nativeStatusCode, diagnostic), }; } @@ -149,6 +170,13 @@ final class NativeErrorException extends MaplibreException { : super(MaplibreStatus.nativeError, nativeStatusCode, diagnostic); } +/// Native not-found failure: no object has the requested ID. +final class NotFoundException extends MaplibreException { + /// Creates a not-found exception. + const NotFoundException(int? nativeStatusCode, String diagnostic) + : super(MaplibreStatus.notFound, nativeStatusCode, diagnostic); +} + /// Loaded native library uses a different C ABI contract version. final class AbiVersionMismatchException extends MaplibreException { /// Creates an ABI-version mismatch exception. diff --git a/bindings/dart/lib/src/internal/c/maplibre_native_c.g.dart b/bindings/dart/lib/src/internal/c/maplibre_native_c.g.dart index 661faa3d5..04590b18f 100644 --- a/bindings/dart/lib/src/internal/c/maplibre_native_c.g.dart +++ b/bindings/dart/lib/src/internal/c/maplibre_native_c.g.dart @@ -15,6 +15,99 @@ library; import 'dart:ffi' as ffi; +@ffi.Native< + ffi.Int32 Function( + mln_acquired_frame, + ffi.Pointer, + ) +>() +external int mln_acquired_frame_get_metal_texture( + int frame, + ffi.Pointer out_frame, +); + +@ffi.Native< + ffi.Int32 Function( + mln_acquired_frame, + ffi.Pointer, + ) +>() +external int mln_acquired_frame_get_opengl_texture( + int frame, + ffi.Pointer out_frame, +); + +@ffi.Native)>() +external int mln_acquired_frame_get_producer_sync( + int frame, + ffi.Pointer out_sync, +); + +@ffi.Native< + ffi.Int32 Function(mln_acquired_frame, ffi.Pointer) +>() +external int mln_acquired_frame_get_result( + int frame, + ffi.Pointer out_result, +); + +@ffi.Native< + ffi.Int32 Function( + mln_acquired_frame, + ffi.Pointer, + ) +>() +external int mln_acquired_frame_get_vulkan_texture( + int frame, + ffi.Pointer out_frame, +); + +@ffi.Native< + ffi.Int32 Function( + mln_acquired_frame, + ffi.Pointer, + ) +>() +external int mln_acquired_frame_get_webgpu_texture( + int frame, + ffi.Pointer out_frame, +); + +@ffi.Native< + ffi.Int32 Function(ffi.Pointer, ffi.Pointer) +>() +external int mln_acquired_frame_release( + ffi.Pointer frame, + ffi.Pointer consumer_completion, +); + +@ffi.Native< + ffi.Int32 Function( + ffi.Uint32, + ffi.Size, + mln_adapter_completion_listener, + ffi.Pointer, + ffi.Pointer, + ) +>() +external int mln_adapter_completion_create( + int copy_kind, + int element_size, + mln_adapter_completion_listener listener, + ffi.Pointer user_data, + ffi.Pointer out_completion, +); + +@ffi.Native)>() +external void mln_adapter_completion_record_destroy( + ffi.Pointer record, +); + +@ffi.Native)>() +external void mln_adapter_completion_reject( + ffi.Pointer completion, +); + @ffi.Native< ffi.Void Function( mln_custom_geometry_source_tile_callback, @@ -93,6 +186,28 @@ external int mln_adapter_log_callback( ffi.Pointer message, ); +@ffi.Native< + ffi.Int32 Function( + mln_adapter_log_queue, + ffi.Pointer>, + ) +>() +external int mln_adapter_log_queue_acquire( + int queue, + ffi.Pointer> out_record, +); + +@ffi.Native() +external void mln_adapter_log_queue_close(int queue); + +@ffi.Native< + ffi.Int32 Function(ffi.Pointer, ffi.Pointer) +>() +external int mln_adapter_log_queue_create( + ffi.Pointer wake, + ffi.Pointer out_queue, +); + @ffi.Native)>() external void mln_adapter_log_record_destroy(ffi.Pointer record); @@ -114,13 +229,6 @@ external int mln_adapter_queued_resource_provider_callback( int handle, ); -@ffi.Native< - ffi.Void Function(ffi.Pointer) ->() -external void mln_adapter_queued_resource_provider_retire( - ffi.Pointer provider, -); - @ffi.Native)>() external void mln_adapter_resource_provider_request_destroy( ffi.Pointer request, @@ -139,6 +247,31 @@ external int mln_adapter_resource_provider_rules_callback( int handle, ); +@ffi.Native< + ffi.Int32 Function( + mln_adapter_resource_request_queue, + ffi.Pointer>, + ) +>() +external int mln_adapter_resource_request_queue_acquire( + int queue, + ffi.Pointer> out_request, +); + +@ffi.Native() +external void mln_adapter_resource_request_queue_close(int queue); + +@ffi.Native< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ) +>() +external int mln_adapter_resource_request_queue_create( + ffi.Pointer wake, + ffi.Pointer out_queue, +); + @ffi.Native< ffi.Int32 Function( ffi.Pointer, @@ -182,12 +315,18 @@ external int mln_buffer_get(int buffer, ffi.Pointer out_view); @ffi.Native() external int mln_c_version(); +@ffi.Native() +external mln_camera_delta mln_camera_delta_default(); + @ffi.Native() external mln_camera_fit_options mln_camera_fit_options_default(); @ffi.Native() external mln_camera_options mln_camera_options_default(); +@ffi.Native() +external mln_camera_update mln_camera_update_default(); + @ffi.Native() external mln_custom_geometry_source_options mln_custom_geometry_source_options_default(); @@ -196,6 +335,20 @@ mln_custom_geometry_source_options_default(); external mln_custom_mvt_vector_source_options mln_custom_mvt_vector_source_options_default(); +@ffi.Native< + ffi.Int32 Function(mln_event_batch, ffi.Pointer) +>() +external int mln_event_batch_get( + int batch, + ffi.Pointer out_view, +); + +@ffi.Native() +external void mln_event_batch_release(int batch); + +@ffi.Native() +external mln_frame_demand mln_frame_demand_default(); + @ffi.Native() external mln_free_camera_options mln_free_camera_options_default(); @@ -218,6 +371,9 @@ external void mln_geojson_source_data_destroy(int data); @ffi.Native() external mln_geojson_source_options mln_geojson_source_options_default(); +@ffi.Native() +external mln_gpu_sync mln_gpu_sync_default(); + @ffi.Native< ffi.Int32 Function( ffi.Pointer, @@ -249,20 +405,34 @@ external int mln_log_clear_callback(); @ffi.Native() external int mln_log_set_async_severity_mask(int mask); -@ffi.Native)>() +@ffi.Native< + ffi.Int32 Function( + mln_log_callback, + ffi.Pointer, + mln_log_callback_release, + ) +>() external int mln_log_set_callback( mln_log_callback callback, ffi.Pointer user_data, + mln_log_callback_release release_user_data, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_buffer_view, mln_buffer_view) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) >() external int mln_map_add_color_relief_layer( int map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, + ffi.Pointer completion, ); @ffi.Native< @@ -270,12 +440,14 @@ external int mln_map_add_color_relief_layer( mln_map, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_custom_geometry_source( int map, mln_buffer_view source_id, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -283,21 +455,29 @@ external int mln_map_add_custom_geometry_source( mln_map, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_custom_mvt_vector_source( int map, mln_buffer_view source_id, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_geojson_source_data) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_geojson_source_data, + ffi.Pointer, + ) >() external int mln_map_add_geojson_source_data( int map, mln_buffer_view source_id, int data, + ffi.Pointer completion, ); @ffi.Native< @@ -306,6 +486,7 @@ external int mln_map_add_geojson_source_data( mln_buffer_view, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_geojson_source_url( @@ -313,16 +494,24 @@ external int mln_map_add_geojson_source_url( mln_buffer_view source_id, mln_buffer_view url, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_buffer_view, mln_buffer_view) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) >() external int mln_map_add_hillshade_layer( int map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, + ffi.Pointer completion, ); @ffi.Native< @@ -332,6 +521,7 @@ external int mln_map_add_hillshade_layer( ffi.Pointer, ffi.Size, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_image_source_image( @@ -340,6 +530,7 @@ external int mln_map_add_image_source_image( ffi.Pointer coordinates, int coordinate_count, ffi.Pointer image, + ffi.Pointer completion, ); @ffi.Native< @@ -349,6 +540,7 @@ external int mln_map_add_image_source_image( ffi.Pointer, ffi.Size, mln_buffer_view, + ffi.Pointer, ) >() external int mln_map_add_image_source_url( @@ -357,13 +549,22 @@ external int mln_map_add_image_source_url( ffi.Pointer coordinates, int coordinate_count, mln_buffer_view url, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_add_location_indicator_layer( int map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + ffi.Pointer completion, ); @ffi.Native< @@ -373,6 +574,7 @@ external int mln_map_add_location_indicator_layer( ffi.Pointer, ffi.Size, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_raster_dem_source_tiles( @@ -381,6 +583,7 @@ external int mln_map_add_raster_dem_source_tiles( ffi.Pointer tiles, int tile_count, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -389,6 +592,7 @@ external int mln_map_add_raster_dem_source_tiles( mln_buffer_view, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_raster_dem_source_url( @@ -396,6 +600,7 @@ external int mln_map_add_raster_dem_source_url( mln_buffer_view source_id, mln_buffer_view url, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -405,6 +610,7 @@ external int mln_map_add_raster_dem_source_url( ffi.Pointer, ffi.Size, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_raster_source_tiles( @@ -413,6 +619,7 @@ external int mln_map_add_raster_source_tiles( ffi.Pointer tiles, int tile_count, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -421,6 +628,7 @@ external int mln_map_add_raster_source_tiles( mln_buffer_view, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_raster_source_url( @@ -428,20 +636,37 @@ external int mln_map_add_raster_source_url( mln_buffer_view source_id, mln_buffer_view url, ffi.Pointer options, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_add_style_layer_json( int map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_add_style_source_json( int map, mln_buffer_view source_id, mln_buffer_view source_json, + ffi.Pointer completion, ); @ffi.Native< @@ -451,6 +676,7 @@ external int mln_map_add_style_source_json( ffi.Pointer, ffi.Size, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_vector_source_tiles( @@ -459,6 +685,7 @@ external int mln_map_add_vector_source_tiles( ffi.Pointer tiles, int tile_count, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -467,6 +694,7 @@ external int mln_map_add_vector_source_tiles( mln_buffer_view, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_add_vector_source_url( @@ -474,6 +702,20 @@ external int mln_map_add_vector_source_url( mln_buffer_view source_id, mln_buffer_view url, ffi.Pointer options, + ffi.Pointer completion, +); + +@ffi.Native< + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) +>() +external int mln_map_apply_camera_delta( + int map, + ffi.Pointer delta, + ffi.Pointer completion, ); @ffi.Native< @@ -481,14 +723,14 @@ external int mln_map_add_vector_source_url( mln_map, mln_buffer_view, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_camera_for_geometry( int map, mln_buffer_view geometry, ffi.Pointer fit_options, - ffi.Pointer out_camera, + ffi.Pointer completion, ); @ffi.Native< @@ -496,14 +738,14 @@ external int mln_map_camera_for_geometry( mln_map, mln_lat_lng_bounds, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_camera_for_lat_lng_bounds( int map, mln_lat_lng_bounds bounds, ffi.Pointer fit_options, - ffi.Pointer out_camera, + ffi.Pointer completion, ); @ffi.Native< @@ -512,7 +754,7 @@ external int mln_map_camera_for_lat_lng_bounds( ffi.Pointer, ffi.Size, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_camera_for_lat_lngs( @@ -520,531 +762,285 @@ external int mln_map_camera_for_lat_lngs( ffi.Pointer coordinates, int coordinate_count, ffi.Pointer fit_options, - ffi.Pointer out_camera, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_cancel_transitions(int map); +@ffi.Native)>() +external int mln_map_camera_query( + int map, + ffi.Pointer completion, +); @ffi.Native< ffi.Int32 Function( mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, + ffi.Pointer, + ffi.Pointer, ) >() -external int mln_map_copy_layer_source_id( +external int mln_map_camera_snapshot_get( int map, - mln_buffer_view layer_id, - ffi.Pointer out_source_id, - int source_id_capacity, - ffi.Pointer out_source_id_size, + ffi.Pointer out_camera, + ffi.Pointer out_generation, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_copy_layer_source_layer( +external int mln_map_copy_layer_source_id( int map, mln_buffer_view layer_id, - ffi.Pointer out_source_layer, - int source_layer_capacity, - ffi.Pointer out_source_layer_size, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_copy_loaded_style_json( +external int mln_map_copy_layer_source_layer( int map, - ffi.Pointer out_json, - int json_capacity, - ffi.Pointer out_json_size, + mln_buffer_view layer_id, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_copy_style_image_premultiplied_rgba8( int map, mln_buffer_view image_id, - ffi.Pointer out_pixels, - int pixel_capacity, - ffi.Pointer out_byte_length, - ffi.Pointer out_found, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_copy_style_image_stretches( int map, mln_buffer_view image_id, - ffi.Pointer out_stretch_x, - int stretch_x_capacity, - ffi.Pointer out_stretch_x_count, - ffi.Pointer out_stretch_y, - int stretch_y_capacity, - ffi.Pointer out_stretch_y_count, - ffi.Pointer out_found, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_copy_style_source_attribution( int map, mln_buffer_view source_id, - ffi.Pointer out_attribution, - int attribution_capacity, - ffi.Pointer out_attribution_size, - ffi.Pointer out_found, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_copy_style_source_url( int map, mln_buffer_view source_id, - ffi.Pointer out_url, - int url_capacity, - ffi.Pointer out_url_size, - ffi.Pointer out_found, -); - -@ffi.Native< - ffi.Int32 Function( - mln_map, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ) ->() -external int mln_map_copy_style_url( - int map, - ffi.Pointer out_url, - int url_capacity, - ffi.Pointer out_url_size, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_create( int runtime, ffi.Pointer options, - ffi.Pointer out_map, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_destroy(int map); - -@ffi.Native() -external int mln_map_dump_debug_logs(int map); - -@ffi.Native< - ffi.Int32 Function( - mln_map, - ffi.Pointer, - ffi.Pointer, - ) ->() -external int mln_map_ease_to( +@ffi.Native)>() +external int mln_map_dump_debug_logs( int map, - ffi.Pointer camera, - ffi.Pointer animation, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, - ffi.Pointer, - ffi.Pointer, + ffi.Pointer, + ffi.Pointer, ) >() -external int mln_map_fly_to( - int map, - ffi.Pointer camera, - ffi.Pointer animation, -); - -@ffi.Native)>() -external int mln_map_get_bounds( - int map, - ffi.Pointer out_options, -); - -@ffi.Native)>() -external int mln_map_get_camera( - int map, - ffi.Pointer out_camera, -); - -@ffi.Native)>() -external int mln_map_get_debug_options( +external int mln_map_get_feature_state( int map, - ffi.Pointer out_options, + ffi.Pointer selector, + ffi.Pointer completion, ); -@ffi.Native)>() -external int mln_map_get_event_mask(int map, ffi.Pointer out_mask); - @ffi.Native< - ffi.Int32 Function( - mln_map, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_feature_state( +external int mln_map_get_image_source_coordinates( int map, - ffi.Pointer selector, - ffi.Pointer out_state, + mln_buffer_view source_id, + ffi.Pointer completion, ); -@ffi.Native)>() -external int mln_map_get_free_camera_options( +@ffi.Native< + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) +>() +external int mln_map_get_layer_filter( int map, - ffi.Pointer out_options, + mln_buffer_view layer_id, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, mln_buffer_view, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Pointer, + mln_buffer_view, + ffi.Pointer, ) >() -external int mln_map_get_image_source_coordinates( +external int mln_map_get_layer_property( int map, - mln_buffer_view source_id, - ffi.Pointer out_coordinates, - int coordinate_capacity, - ffi.Pointer out_coordinate_count, - ffi.Pointer out_found, + mln_buffer_view layer_id, + mln_buffer_view property_name, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_layer_filter( +external int mln_map_get_style_image_info( int map, - mln_buffer_view layer_id, - ffi.Pointer out_filter, + mln_buffer_view image_id, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_layer_max_zoom( +external int mln_map_get_style_layer_info( int map, mln_buffer_view layer_id, - ffi.Pointer out_max_zoom, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_layer_min_zoom( +external int mln_map_get_style_layer_json( int map, mln_buffer_view layer_id, - ffi.Pointer out_min_zoom, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - mln_buffer_view, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_layer_property( +external int mln_map_get_style_light_property( int map, - mln_buffer_view layer_id, mln_buffer_view property_name, - ffi.Pointer out_value, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) ->() -external int mln_map_get_layer_visibility( - int map, - mln_buffer_view layer_id, - ffi.Pointer out_visibility, -); - -@ffi.Native)>() -external int mln_map_get_projection_mode( - int map, - ffi.Pointer out_mode, -); - -@ffi.Native)>() -external int mln_map_get_rendering_stats_view_enabled( - int map, - ffi.Pointer out_enabled, -); - -@ffi.Native< - ffi.Int32 Function( - mln_map, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ) ->() -external int mln_map_get_size( - int map, - ffi.Pointer out_width, - ffi.Pointer out_height, - ffi.Pointer out_scale_factor, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_style_image_info( +external int mln_map_get_style_source_info( int map, - mln_buffer_view image_id, - ffi.Pointer out_info, - ffi.Pointer out_found, + mln_buffer_view source_id, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Pointer, - ) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() -external int mln_map_get_style_layer_json( - int map, - mln_buffer_view layer_id, - ffi.Pointer out_layer, - ffi.Pointer out_found, -); - -@ffi.Native< - ffi.Int32 Function( - mln_map, - mln_buffer_view, - ffi.Pointer, - ffi.Pointer, - ) ->() -external int mln_map_get_style_layer_type( +external int mln_map_get_style_source_tile_urls( int map, - mln_buffer_view layer_id, - ffi.Pointer out_layer_type, - ffi.Pointer out_found, + mln_buffer_view source_id, + ffi.Pointer completion, ); -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) ->() -external int mln_map_get_style_light_property( +@ffi.Native)>() +external int mln_map_get_style_transition_options( int map, - mln_buffer_view property_name, - ffi.Pointer out_value, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, mln_buffer_view, - ffi.Pointer, - ffi.Pointer, + mln_lat_lng_bounds, + ffi.Pointer, ) >() -external int mln_map_get_style_source_info( +external int mln_map_invalidate_custom_geometry_source_region( int map, mln_buffer_view source_id, - ffi.Pointer out_info, - ffi.Pointer out_found, + mln_lat_lng_bounds bounds, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, mln_buffer_view, - ffi.Pointer, - ffi.Pointer, + mln_canonical_tile_id, + ffi.Pointer, ) >() -external int mln_map_get_style_source_tile_urls( +external int mln_map_invalidate_custom_geometry_source_tile( int map, mln_buffer_view source_id, - ffi.Pointer out_tile_urls, - ffi.Pointer out_found, + mln_canonical_tile_id tile_id, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, mln_buffer_view, - ffi.Pointer, - ffi.Pointer, + mln_canonical_tile_id, + ffi.Pointer, ) >() -external int mln_map_get_style_source_type( - int map, - mln_buffer_view source_id, - ffi.Pointer out_source_type, - ffi.Pointer out_found, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, ffi.Pointer) ->() -external int mln_map_get_style_transition_options( - int map, - ffi.Pointer out_options, -); - -@ffi.Native)>() -external int mln_map_get_tile_options( - int map, - ffi.Pointer out_options, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, ffi.Pointer) ->() -external int mln_map_get_viewport_options( - int map, - ffi.Pointer out_options, -); - -@ffi.Native() -external int mln_map_invalidate_custom_geometry_source_region( - int map, - mln_buffer_view source_id, - mln_lat_lng_bounds bounds, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_canonical_tile_id) ->() -external int mln_map_invalidate_custom_geometry_source_tile( - int map, - mln_buffer_view source_id, - mln_canonical_tile_id tile_id, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_canonical_tile_id) ->() external int mln_map_invalidate_custom_mvt_vector_source_tile( int map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + ffi.Pointer completion, ); -@ffi.Native)>() -external int mln_map_is_fully_loaded(int map, ffi.Pointer out_loaded); - -@ffi.Native)>() -external int mln_map_is_gesture_in_progress( - int map, - ffi.Pointer out_in_progress, -); - -@ffi.Native)>() -external int mln_map_jump_to(int map, ffi.Pointer camera); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_lat_lng_bounds_for_camera( int map, ffi.Pointer camera, - ffi.Pointer out_bounds, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_lat_lng_bounds_for_camera_unwrapped( int map, ffi.Pointer camera, - ffi.Pointer out_bounds, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_screen_point, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_screen_point, ffi.Pointer) >() external int mln_map_lat_lng_for_pixel( int map, mln_screen_point point, - ffi.Pointer out_coordinate, + ffi.Pointer completion, ); @ffi.Native< @@ -1052,75 +1048,59 @@ external int mln_map_lat_lng_for_pixel( mln_map, ffi.Pointer, ffi.Size, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_lat_lngs_for_pixels( int map, ffi.Pointer points, int point_count, - ffi.Pointer out_coordinates, + ffi.Pointer completion, ); -@ffi.Native)>() +@ffi.Native)>() external int mln_map_list_style_layer_ids( int map, - ffi.Pointer out_layer_ids, + ffi.Pointer completion, ); -@ffi.Native)>() +@ffi.Native)>() external int mln_map_list_style_source_ids( int map, - ffi.Pointer out_source_ids, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_move_by(int map, double delta_x, double delta_y); +@ffi.Native)>() +external int mln_map_loaded_style_json( + int map, + ffi.Pointer completion, +); @ffi.Native< ffi.Int32 Function( mln_map, - ffi.Double, - ffi.Double, - ffi.Pointer, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, ) >() -external int mln_map_move_by_animated( - int map, - double delta_x, - double delta_y, - ffi.Pointer animation, -); - -@ffi.Native() external int mln_map_move_style_layer( int map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + ffi.Pointer completion, ); @ffi.Native() external mln_map_options mln_map_options_default(); -@ffi.Native() -external int mln_map_pitch_by(int map, double pitch); - @ffi.Native< - ffi.Int32 Function(mln_map, ffi.Double, ffi.Pointer) ->() -external int mln_map_pitch_by_animated( - int map, - double pitch, - ffi.Pointer animation, -); - -@ffi.Native< - ffi.Int32 Function(mln_map, mln_lat_lng, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_lat_lng, ffi.Pointer) >() external int mln_map_pixel_for_lat_lng( int map, mln_lat_lng coordinate, - ffi.Pointer out_point, + ffi.Pointer completion, ); @ffi.Native< @@ -1128,25 +1108,25 @@ external int mln_map_pixel_for_lat_lng( mln_map, ffi.Pointer, ffi.Size, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_pixels_for_lat_lngs( int map, ffi.Pointer coordinates, int coordinate_count, - ffi.Pointer out_points, + ffi.Pointer completion, ); -@ffi.Native)>() +@ffi.Native() +external int mln_map_projection_close(int projection); + +@ffi.Native)>() external int mln_map_projection_create( int map, - ffi.Pointer out_projection, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_projection_destroy(int projection); - @ffi.Native< ffi.Int32 Function(mln_map_projection, ffi.Pointer) >() @@ -1213,97 +1193,81 @@ external int mln_map_projection_set_visible_geometry( mln_edge_insets padding, ); +@ffi.Native)>() +external int mln_map_release(int map, ffi.Pointer completion); + @ffi.Native< - ffi.Int32 Function(mln_map, ffi.Pointer) + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_map_remove_feature_state( int map, ffi.Pointer selector, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_remove_style_image( int map, mln_buffer_view image_id, - ffi.Pointer out_removed, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_remove_style_layer( int map, mln_buffer_view layer_id, - ffi.Pointer out_removed, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) >() external int mln_map_remove_style_source( int map, mln_buffer_view source_id, - ffi.Pointer out_removed, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_request_repaint(int map); - -@ffi.Native() -external int mln_map_request_still_image(int map); - -@ffi.Native() -external int mln_map_rotate_by( +@ffi.Native)>() +external int mln_map_request_repaint( int map, - mln_screen_point first, - mln_screen_point second, + ffi.Pointer completion, ); -@ffi.Native< - ffi.Int32 Function( - mln_map, - mln_screen_point, - mln_screen_point, - ffi.Pointer, - ) ->() -external int mln_map_rotate_by_animated( +@ffi.Native)>() +external int mln_map_request_still_image( int map, - mln_screen_point first, - mln_screen_point second, - ffi.Pointer animation, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, ffi.Double, ffi.Pointer) + ffi.Int32 Function(mln_map, mln_logical_extent, ffi.Pointer) >() -external int mln_map_scale_by( +external int mln_map_resize( int map, - double scale, - ffi.Pointer anchor, + mln_logical_extent extent, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, - ffi.Double, - ffi.Pointer, - ffi.Pointer, + ffi.Pointer, + ffi.Pointer, ) >() -external int mln_map_scale_by_animated( - int map, - double scale, - ffi.Pointer anchor, - ffi.Pointer animation, -); - -@ffi.Native)>() external int mln_map_set_bounds( int map, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< @@ -1312,6 +1276,7 @@ external int mln_map_set_bounds( mln_buffer_view, mln_canonical_tile_id, mln_buffer_view, + ffi.Pointer, ) >() external int mln_map_set_custom_geometry_source_tile_data( @@ -1319,6 +1284,7 @@ external int mln_map_set_custom_geometry_source_tile_data( mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, + ffi.Pointer completion, ); @ffi.Native< @@ -1327,6 +1293,7 @@ external int mln_map_set_custom_geometry_source_tile_data( mln_buffer_view, mln_canonical_tile_id, mln_buffer_view, + ffi.Pointer, ) >() external int mln_map_set_custom_mvt_vector_source_tile_data( @@ -1334,6 +1301,7 @@ external int mln_map_set_custom_mvt_vector_source_tile_data( mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, + ffi.Pointer completion, ); @ffi.Native< @@ -1342,6 +1310,7 @@ external int mln_map_set_custom_mvt_vector_source_tile_data( mln_buffer_view, mln_canonical_tile_id, mln_buffer_view, + ffi.Pointer, ) >() external int mln_map_set_custom_mvt_vector_source_tile_error( @@ -1349,65 +1318,107 @@ external int mln_map_set_custom_mvt_vector_source_tile_error( mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_set_debug_options(int map, int options); +@ffi.Native< + ffi.Int32 Function(mln_map, ffi.Uint32, ffi.Pointer) +>() +external int mln_map_set_debug_options( + int map, + int options, + ffi.Pointer completion, +); -@ffi.Native() -external int mln_map_set_event_mask(int map, int mask); +@ffi.Native< + ffi.Int32 Function(mln_map, ffi.Uint64, ffi.Pointer) +>() +external int mln_map_set_event_mask( + int map, + int mask, + ffi.Pointer completion, +); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, mln_buffer_view, + ffi.Pointer, ) >() external int mln_map_set_feature_state( int map, ffi.Pointer selector, mln_buffer_view state, + ffi.Pointer completion, ); -@ffi.Native)>() +@ffi.Native< + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) +>() external int mln_map_set_free_camera_options( int map, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_geojson_source_data) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_geojson_source_data, + ffi.Pointer, + ) >() external int mln_map_set_geojson_source_data( int map, mln_buffer_view source_id, int data, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Bool, + ffi.Pointer, + ) +>() external int mln_map_set_geojson_source_synchronous_tiling( int map, mln_buffer_view source_id, bool enabled, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_set_geojson_source_url( int map, mln_buffer_view source_id, mln_buffer_view url, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_set_gesture_in_progress(int map, bool in_progress); - @ffi.Native< ffi.Int32 Function( mln_map, mln_buffer_view, ffi.Pointer, ffi.Size, + ffi.Pointer, ) >() external int mln_map_set_image_source_coordinates( @@ -1415,6 +1426,7 @@ external int mln_map_set_image_source_coordinates( mln_buffer_view source_id, ffi.Pointer coordinates, int coordinate_count, + ffi.Pointer completion, ); @ffi.Native< @@ -1422,117 +1434,223 @@ external int mln_map_set_image_source_coordinates( mln_map, mln_buffer_view, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_set_image_source_image( int map, mln_buffer_view source_id, ffi.Pointer image, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_set_image_source_url( int map, mln_buffer_view source_id, mln_buffer_view url, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_map_set_layer_filter( int map, mln_buffer_view layer_id, ffi.Pointer filter, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Double, + ffi.Pointer, + ) +>() external int mln_map_set_layer_max_zoom( int map, mln_buffer_view layer_id, double max_zoom, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Double, + ffi.Pointer, + ) +>() external int mln_map_set_layer_min_zoom( int map, mln_buffer_view layer_id, double min_zoom, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_buffer_view, mln_buffer_view) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) >() external int mln_map_set_layer_property( int map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_set_layer_source_id( int map, mln_buffer_view layer_id, mln_buffer_view source_id, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_set_layer_source_layer( int map, mln_buffer_view layer_id, mln_buffer_view source_layer, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Uint32, + ffi.Pointer, + ) +>() external int mln_map_set_layer_visibility( int map, mln_buffer_view layer_id, int visibility, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Double, + ffi.Pointer, + ) +>() external int mln_map_set_location_indicator_accuracy_radius( int map, mln_buffer_view layer_id, double radius, + ffi.Pointer completion, ); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Double, + ffi.Pointer, + ) +>() external int mln_map_set_location_indicator_bearing( int map, mln_buffer_view layer_id, double bearing, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Uint32, mln_buffer_view) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + ffi.Uint32, + mln_buffer_view, + ffi.Pointer, + ) >() external int mln_map_set_location_indicator_image_name( int map, mln_buffer_view layer_id, int image_kind, mln_buffer_view image_id, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, mln_lat_lng, ffi.Double) + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_lat_lng, + ffi.Double, + ffi.Pointer, + ) >() external int mln_map_set_location_indicator_location( int map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, + ffi.Pointer completion, ); -@ffi.Native)>() +@ffi.Native< + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) +>() external int mln_map_set_projection_mode( int map, ffi.Pointer mode, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_set_rendering_stats_view_enabled(int map, bool enabled); +@ffi.Native< + ffi.Int32 Function(mln_map, ffi.Bool, ffi.Pointer) +>() +external int mln_map_set_rendering_stats_view_enabled( + int map, + bool enabled, + ffi.Pointer completion, +); @ffi.Native< ffi.Int32 Function( @@ -1540,6 +1658,7 @@ external int mln_map_set_rendering_stats_view_enabled(int map, bool enabled); mln_buffer_view, ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_map_set_style_image( @@ -1547,76 +1666,119 @@ external int mln_map_set_style_image( mln_buffer_view image_id, ffi.Pointer image, ffi.Pointer options, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_map_set_style_json(int map, mln_buffer_view json); +@ffi.Native< + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) +>() +external int mln_map_set_style_json( + int map, + mln_buffer_view json, + ffi.Pointer completion, +); -@ffi.Native() -external int mln_map_set_style_light_json(int map, mln_buffer_view light_json); +@ffi.Native< + ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) +>() +external int mln_map_set_style_light_json( + int map, + mln_buffer_view light_json, + ffi.Pointer completion, +); -@ffi.Native() +@ffi.Native< + ffi.Int32 Function( + mln_map, + mln_buffer_view, + mln_buffer_view, + ffi.Pointer, + ) +>() external int mln_map_set_style_light_property( int map, mln_buffer_view property_name, mln_buffer_view value, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, ffi.Pointer) + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_map_set_style_transition_options( int map, ffi.Pointer options, + ffi.Pointer completion, ); -@ffi.Native)>() -external int mln_map_set_style_url(int map, ffi.Pointer url); +@ffi.Native< + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) +>() +external int mln_map_set_style_url( + int map, + ffi.Pointer url, + ffi.Pointer completion, +); -@ffi.Native)>() +@ffi.Native< + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) +>() external int mln_map_set_tile_options( int map, ffi.Pointer options, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_map, ffi.Pointer) + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_map_set_viewport_options( int map, ffi.Pointer options, + ffi.Pointer completion, ); -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) ->() -external int mln_map_style_image_exists( +@ffi.Native)>() +external int mln_map_snapshot_get( int map, - mln_buffer_view image_id, - ffi.Pointer out_exists, + ffi.Pointer out_snapshot, ); -@ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) ->() -external int mln_map_style_layer_exists( - int map, - mln_buffer_view layer_id, - ffi.Pointer out_exists, -); +@ffi.Native)>() +external int mln_map_style_url(int map, ffi.Pointer completion); + +@ffi.Native() +external mln_map_tile_options mln_map_tile_options_default(); @ffi.Native< - ffi.Int32 Function(mln_map, mln_buffer_view, ffi.Pointer) + ffi.Int32 Function( + mln_map, + ffi.Pointer, + ffi.Pointer, + ) >() -external int mln_map_style_source_exists( +external int mln_map_update_camera( int map, - mln_buffer_view source_id, - ffi.Pointer out_exists, + ffi.Pointer update, + ffi.Pointer completion, ); -@ffi.Native() -external mln_map_tile_options mln_map_tile_options_default(); - @ffi.Native() external mln_map_viewport_options mln_map_viewport_options_default(); @@ -1624,13 +1786,17 @@ external mln_map_viewport_options mln_map_viewport_options_default(); ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_metal_borrowed_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -1641,63 +1807,51 @@ mln_metal_borrowed_texture_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_metal_borrowed_texture_set_target( int session, ffi.Pointer descriptor, -); - -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_metal_owned_texture_acquire_frame( - int session, - ffi.Pointer out_frame, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_metal_owned_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() external mln_metal_owned_texture_descriptor mln_metal_owned_texture_descriptor_default(); -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_metal_owned_texture_release_frame( - int session, - ffi.Pointer frame, -); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_metal_surface_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -1707,11 +1861,13 @@ external mln_metal_surface_descriptor mln_metal_surface_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_metal_surface_set_target( int session, ffi.Pointer descriptor, + ffi.Pointer completion, ); @ffi.Native)>() @@ -1762,13 +1918,17 @@ external int mln_offline_region_snapshot_get( ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_opengl_borrowed_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -1779,52 +1939,36 @@ mln_opengl_borrowed_texture_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_opengl_borrowed_texture_set_target( int session, ffi.Pointer descriptor, -); - -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_opengl_owned_texture_acquire_frame( - int session, - ffi.Pointer out_frame, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_opengl_owned_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() external mln_opengl_owned_texture_descriptor mln_opengl_owned_texture_descriptor_default(); -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_opengl_owned_texture_release_frame( - int session, - ffi.Pointer frame, -); - @ffi.Native() external int mln_opengl_supported_context_provider_mask(); @@ -1832,13 +1976,17 @@ external int mln_opengl_supported_context_provider_mask(); ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_opengl_surface_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -1848,11 +1996,13 @@ external mln_opengl_surface_descriptor mln_opengl_surface_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_opengl_surface_set_target( int session, ffi.Pointer descriptor, + ffi.Pointer completion, ); @ffi.Native() @@ -1880,34 +2030,129 @@ external int mln_queried_feature_list_count( ffi.Pointer out_count, ); -@ffi.Native() -external void mln_queried_feature_list_destroy(int list); - +@ffi.Native() +external void mln_queried_feature_list_destroy(int list); + +@ffi.Native< + ffi.Int32 Function( + mln_queried_feature_list, + ffi.Size, + ffi.Pointer, + ) +>() +external int mln_queried_feature_list_get( + int list, + int index, + ffi.Pointer out_feature, +); + +@ffi.Native)>() +external int mln_render_frame_batch_count( + int batch, + ffi.Pointer out_count, +); + +@ffi.Native< + ffi.Int32 Function( + mln_render_frame_batch, + ffi.Size, + ffi.Pointer, + ) +>() +external int mln_render_frame_batch_get( + int batch, + int index, + ffi.Pointer out_result, +); + +@ffi.Native() +external void mln_render_frame_batch_release(int batch); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_abandon( + int session, + ffi.Pointer out_result, +); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_acquire_frame( + int session, + ffi.Pointer out_frame, +); + +@ffi.Native() +external mln_render_session_attach_options +mln_render_session_attach_options_default(); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_barrier( + int session, + ffi.Pointer completion, +); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_clear_data( + int session, + ffi.Pointer completion, +); + +@ffi.Native() +external int mln_render_session_destroy(int session); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_detach( + int session, + ffi.Pointer completion, +); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_drain_frame_results( + int session, + ffi.Pointer out_batch, +); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_dump_debug_logs( + int session, + ffi.Pointer completion, +); + +@ffi.Native< + ffi.Int32 Function( + mln_render_session, + ffi.Pointer, + ) +>() +external int mln_render_session_get_capabilities( + int session, + ffi.Pointer out_capabilities, +); + @ffi.Native< ffi.Int32 Function( - mln_queried_feature_list, - ffi.Size, - ffi.Pointer, + mln_render_session, + ffi.Pointer, ) >() -external int mln_queried_feature_list_get( - int list, - int index, - ffi.Pointer out_feature, +external int mln_render_session_get_snapshot( + int session, + ffi.Pointer out_snapshot, ); -@ffi.Native() -external int mln_render_session_clear_data(int session); - -@ffi.Native() -external int mln_render_session_destroy(int session); - -@ffi.Native() -external int mln_render_session_detach(int session); - -@ffi.Native() -external int mln_render_session_dump_debug_logs(int session); - @ffi.Native< ffi.Int32 Function( mln_render_session, @@ -1916,7 +2161,7 @@ external int mln_render_session_dump_debug_logs(int session); mln_buffer_view, mln_buffer_view, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_render_session_query_feature_extensions( @@ -1926,7 +2171,7 @@ external int mln_render_session_query_feature_extensions( mln_buffer_view extension, mln_buffer_view extension_field, ffi.Pointer arguments, - ffi.Pointer out_result, + ffi.Pointer completion, ); @ffi.Native< @@ -1934,14 +2179,14 @@ external int mln_render_session_query_feature_extensions( mln_render_session, ffi.Pointer, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_render_session_query_rendered_features( int session, ffi.Pointer geometry, ffi.Pointer options, - ffi.Pointer out_result, + ffi.Pointer completion, ); @ffi.Native< @@ -1949,40 +2194,52 @@ external int mln_render_session_query_rendered_features( mln_render_session, mln_buffer_view, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() external int mln_render_session_query_source_features( int session, mln_buffer_view source_id, ffi.Pointer options, - ffi.Pointer out_result, + ffi.Pointer completion, ); -@ffi.Native() -external int mln_render_session_reduce_memory_use(int session); +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_reduce_memory_use( + int session, + ffi.Pointer completion, +); + +@ffi.Native< + ffi.Int32 Function(mln_render_session, ffi.Pointer) +>() +external int mln_render_session_request_frame( + int session, + ffi.Pointer demand, +); @ffi.Native< ffi.Int32 Function( mln_render_session, - ffi.Pointer, - ffi.Pointer, + ffi.Pointer, + ffi.Pointer, ) >() -external int mln_render_session_render_update( +external int mln_render_session_resize( int session, - ffi.Pointer out_result, - ffi.Pointer out_needs_repaint, + ffi.Pointer extent, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_render_session, ffi.Uint32, ffi.Uint32, ffi.Double) + ffi.Int32 Function(mln_render_session, ffi.Size, ffi.Pointer) >() -external int mln_render_session_resize( +external int mln_render_session_service_driver_work( int session, - int width, - int height, - double scale_factor, + int max_work, + ffi.Pointer out_serviced, ); @ffi.Native< @@ -2058,14 +2315,29 @@ external int mln_resource_transform_response_set_url( int url_size, ); -@ffi.Native() -external int mln_runtime_clear_http_header_transform(int runtime); +@ffi.Native)>() +external int mln_runtime_barrier( + int runtime, + ffi.Pointer completion, +); + +@ffi.Native)>() +external int mln_runtime_clear_http_header_transform( + int runtime, + ffi.Pointer completion, +); -@ffi.Native() -external int mln_runtime_clear_resource_provider(int runtime); +@ffi.Native)>() +external int mln_runtime_clear_resource_provider( + int runtime, + ffi.Pointer completion, +); -@ffi.Native() -external int mln_runtime_clear_resource_transform(int runtime); +@ffi.Native)>() +external int mln_runtime_clear_resource_transform( + int runtime, + ffi.Pointer completion, +); @ffi.Native< ffi.Int32 Function(ffi.Pointer, ffi.Pointer) @@ -2075,145 +2347,85 @@ external int mln_runtime_create( ffi.Pointer out_runtime, ); -@ffi.Native() -external int mln_runtime_destroy(int runtime); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - ffi.Size, - ffi.Pointer, - ) ->() +@ffi.Native)>() external int mln_runtime_drain_events( int runtime, - int max_events, - ffi.Pointer out_batch, + ffi.Pointer out_batch, ); -@ffi.Native() -external mln_runtime_event_batch mln_runtime_event_batch_default(); - @ffi.Native)>() external int mln_runtime_get_event_mask( int runtime, ffi.Pointer out_mask, ); -@ffi.Native() -external int mln_runtime_offline_operation_discard( - int runtime, - int operation_id, -); - @ffi.Native< ffi.Int32 Function( mln_runtime, ffi.Pointer, ffi.Pointer, ffi.Size, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_create_start( +external int mln_runtime_offline_region_create( int runtime, ffi.Pointer definition, ffi.Pointer metadata, int metadata_size, - ffi.Pointer out_operation_id, -); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_region_create_take_result( - int runtime, - int operation_id, - ffi.Pointer out_region, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, mln_offline_region_id, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_delete_start( +external int mln_runtime_offline_region_delete( int runtime, int region_id, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, mln_offline_region_id, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_get_start( +external int mln_runtime_offline_region_get( int runtime, int region_id, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, mln_offline_region_id, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_get_status_start( +external int mln_runtime_offline_region_get_status( int runtime, int region_id, - ffi.Pointer out_operation_id, -); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_region_get_status_take_result( - int runtime, - int operation_id, - ffi.Pointer out_status, -); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_region_get_take_result( - int runtime, - int operation_id, - ffi.Pointer out_region, - ffi.Pointer out_found, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, mln_offline_region_id, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_invalidate_start( +external int mln_runtime_offline_region_invalidate( int runtime, int region_id, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< @@ -2221,14 +2433,14 @@ external int mln_runtime_offline_region_invalidate_start( mln_runtime, mln_offline_region_id, ffi.Uint32, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_set_download_state_start( +external int mln_runtime_offline_region_set_download_state( int runtime, int region_id, int state, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< @@ -2236,14 +2448,14 @@ external int mln_runtime_offline_region_set_download_state_start( mln_runtime, mln_offline_region_id, ffi.Bool, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_set_observed_start( +external int mln_runtime_offline_region_set_observed( int runtime, int region_id, bool observed, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< @@ -2252,140 +2464,103 @@ external int mln_runtime_offline_region_set_observed_start( mln_offline_region_id, ffi.Pointer, ffi.Size, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_region_update_metadata_start( +external int mln_runtime_offline_region_update_metadata( int runtime, int region_id, ffi.Pointer metadata, int metadata_size, - ffi.Pointer out_operation_id, -); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_region_update_metadata_take_result( - int runtime, - int operation_id, - ffi.Pointer out_region, -); - -@ffi.Native< - ffi.Int32 Function(mln_runtime, ffi.Pointer) ->() -external int mln_runtime_offline_regions_list_start( - int runtime, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_regions_list_take_result( +@ffi.Native)>() +external int mln_runtime_offline_regions_list( int runtime, - int operation_id, - ffi.Pointer out_regions, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_runtime, ffi.Pointer, - ffi.Pointer, + ffi.Pointer, ) >() -external int mln_runtime_offline_regions_merge_database_start( +external int mln_runtime_offline_regions_merge_database( int runtime, ffi.Pointer side_database_path, - ffi.Pointer out_operation_id, -); - -@ffi.Native< - ffi.Int32 Function( - mln_runtime, - mln_offline_operation_id, - ffi.Pointer, - ) ->() -external int mln_runtime_offline_regions_merge_database_take_result( - int runtime, - int operation_id, - ffi.Pointer out_regions, + ffi.Pointer completion, ); @ffi.Native() external mln_runtime_options mln_runtime_options_default(); -@ffi.Native() -external int mln_runtime_pump(int runtime, int timeout_ms, int budget_ms); +@ffi.Native)>() +external int mln_runtime_release( + int runtime, + ffi.Pointer completion, +); @ffi.Native< - ffi.Int32 Function( - mln_runtime, - ffi.Uint32, - ffi.Pointer, - ) + ffi.Int32 Function(mln_runtime, ffi.Uint32, ffi.Pointer) >() -external int mln_runtime_run_ambient_cache_operation_start( +external int mln_runtime_run_ambient_cache_operation( int runtime, int operation, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native() external int mln_runtime_set_event_mask(int runtime, int mask); @ffi.Native< - ffi.Int32 Function(mln_runtime, ffi.Pointer) + ffi.Int32 Function( + mln_runtime, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_runtime_set_http_header_transform( int runtime, ffi.Pointer transform, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function( - mln_runtime, - ffi.Uint64, - ffi.Pointer, - ) + ffi.Int32 Function(mln_runtime, ffi.Uint64, ffi.Pointer) >() -external int mln_runtime_set_maximum_ambient_cache_size_start( +external int mln_runtime_set_maximum_ambient_cache_size( int runtime, int size, - ffi.Pointer out_operation_id, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_runtime, ffi.Pointer) + ffi.Int32 Function( + mln_runtime, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_runtime_set_resource_provider( int runtime, ffi.Pointer provider, + ffi.Pointer completion, ); @ffi.Native< - ffi.Int32 Function(mln_runtime, ffi.Pointer) + ffi.Int32 Function( + mln_runtime, + ffi.Pointer, + ffi.Pointer, + ) >() external int mln_runtime_set_resource_transform( int runtime, ffi.Pointer transform, -); - -@ffi.Native)>() -external int mln_runtime_wake_source_acquire( - int runtime, - ffi.Pointer out_source, + ffi.Pointer completion, ); @ffi.Native() @@ -2448,37 +2623,31 @@ external int mln_supported_render_backend_mask(); external mln_texture_image_info mln_texture_image_info_default(); @ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ) + ffi.Int32 Function(mln_render_session, ffi.Pointer) >() external int mln_texture_read_premultiplied_rgba8( int session, - ffi.Pointer out_data, - int out_data_capacity, - ffi.Pointer out_info, + ffi.Pointer completion, ); @ffi.Native Function()>() external ffi.Pointer mln_thread_last_error_message(); -@ffi.Native() -external int mln_thread_token(); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_vulkan_borrowed_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -2489,63 +2658,51 @@ mln_vulkan_borrowed_texture_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_vulkan_borrowed_texture_set_target( int session, ffi.Pointer descriptor, -); - -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_vulkan_owned_texture_acquire_frame( - int session, - ffi.Pointer out_frame, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_vulkan_owned_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() external mln_vulkan_owned_texture_descriptor mln_vulkan_owned_texture_descriptor_default(); -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_vulkan_owned_texture_release_frame( - int session, - ffi.Pointer frame, -); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_vulkan_surface_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -2555,30 +2712,30 @@ external mln_vulkan_surface_descriptor mln_vulkan_surface_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_vulkan_surface_set_target( int session, ffi.Pointer descriptor, + ffi.Pointer completion, ); -@ffi.Native() -external void mln_wake_source_destroy(int source); - -@ffi.Native() -external int mln_wake_source_signal(int source); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_webgpu_borrowed_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -2589,63 +2746,51 @@ mln_webgpu_borrowed_texture_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_webgpu_borrowed_texture_set_target( int session, ffi.Pointer descriptor, -); - -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_webgpu_owned_texture_acquire_frame( - int session, - ffi.Pointer out_frame, + ffi.Pointer completion, ); @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_webgpu_owned_texture_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() external mln_webgpu_owned_texture_descriptor mln_webgpu_owned_texture_descriptor_default(); -@ffi.Native< - ffi.Int32 Function( - mln_render_session, - ffi.Pointer, - ) ->() -external int mln_webgpu_owned_texture_release_frame( - int session, - ffi.Pointer frame, -); - @ffi.Native< ffi.Int32 Function( mln_map, ffi.Pointer, + ffi.Pointer, ffi.Pointer, + ffi.Pointer, ) >() external int mln_webgpu_surface_attach( int map, ffi.Pointer descriptor, + ffi.Pointer options, ffi.Pointer out_session, + ffi.Pointer completion, ); @ffi.Native() @@ -2655,11 +2800,13 @@ external mln_webgpu_surface_descriptor mln_webgpu_surface_descriptor_default(); ffi.Int32 Function( mln_render_session, ffi.Pointer, + ffi.Pointer, ) >() external int mln_webgpu_surface_set_target( int session, ffi.Pointer descriptor, + ffi.Pointer completion, ); const int MLN_ADAPTER_RESOURCE_KIND_ANY = 4294967295; @@ -2688,6 +2835,59 @@ final class UnnamedUnion$2 extends ffi.Union { external mln_screen_line_string line_string; } +typedef mln_acquired_frame = ffi.Uint64; +typedef Dartmln_acquired_frame = int; + +enum mln_adapter_completion_copy_kind { + MLN_ADAPTER_COMPLETION_COPY_FLAT(0), + MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS(1), + MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS(2), + MLN_ADAPTER_COMPLETION_COPY_STYLE_SOURCE(3), + MLN_ADAPTER_COMPLETION_COPY_STYLE_LAYER(4), + MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE(5), + MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE_STRETCHES(6), + MLN_ADAPTER_COMPLETION_COPY_QUERIED_FEATURES(7), + MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK(8); + + final int value; + const mln_adapter_completion_copy_kind(this.value); + + static mln_adapter_completion_copy_kind fromValue(int value) => + switch (value) { + 0 => MLN_ADAPTER_COMPLETION_COPY_FLAT, + 1 => MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + 2 => MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS, + 3 => MLN_ADAPTER_COMPLETION_COPY_STYLE_SOURCE, + 4 => MLN_ADAPTER_COMPLETION_COPY_STYLE_LAYER, + 5 => MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE, + 6 => MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE_STRETCHES, + 7 => MLN_ADAPTER_COMPLETION_COPY_QUERIED_FEATURES, + 8 => MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK, + _ => throw ArgumentError( + 'Unknown value for mln_adapter_completion_copy_kind: $value', + ), + }; +} + +typedef mln_adapter_completion_listener = + ffi.Pointer>; +typedef mln_adapter_completion_listenerFunction = + ffi.Void Function( + ffi.Pointer user_data, + ffi.Pointer record, + ); +typedef Dartmln_adapter_completion_listenerFunction = + void Function( + ffi.Pointer user_data, + ffi.Pointer record, + ); + +final class mln_adapter_completion_record extends ffi.Struct { + external ffi.Pointer owner; + + external mln_completion_result result; +} + final class mln_adapter_http_header extends ffi.Struct { external ffi.Pointer name; @@ -2747,26 +2947,35 @@ final class mln_adapter_http_header_transform_rules extends ffi.Struct { } final class mln_adapter_log_callback_state extends ffi.Struct { - external mln_adapter_log_record_listener listener; + @mln_adapter_log_queue() + external int queue; @ffi.Uint32() external int consume; + external mln_log_callback_release release_user_data; + + external ffi.Pointer release_context; + static ffi.Pointer $allocate( ffi.Allocator $allocator, { - required mln_adapter_log_record_listener listener, + required int queue, required int consume, + required mln_log_callback_release release_user_data, + required ffi.Pointer release_context, }) => $allocator() - ..ref.listener = listener - ..ref.consume = consume; + ..ref.queue = queue + ..ref.consume = consume + ..ref.release_user_data = release_user_data + ..ref.release_context = release_context; } +typedef mln_adapter_log_queue = ffi.Uint64; +typedef Dartmln_adapter_log_queue = int; + final class mln_adapter_log_record extends ffi.Struct { external ffi.Pointer owner; - @ffi.Bool() - external bool retire_callback; - @ffi.Uint32() external int severity; @@ -2781,44 +2990,36 @@ final class mln_adapter_log_record extends ffi.Struct { static ffi.Pointer $allocate( ffi.Allocator $allocator, { required ffi.Pointer owner, - required bool retire_callback, required int severity, required int event, required int code, required ffi.Pointer message, }) => $allocator() ..ref.owner = owner - ..ref.retire_callback = retire_callback ..ref.severity = severity ..ref.event = event ..ref.code = code ..ref.message = message; } -typedef mln_adapter_log_record_listener = - ffi.Pointer>; -typedef mln_adapter_log_record_listenerFunction = - ffi.Void Function(ffi.Pointer record); -typedef Dartmln_adapter_log_record_listenerFunction = - void Function(ffi.Pointer record); - final class mln_adapter_queued_resource_provider extends ffi.Struct { external ffi.Pointer routes; @ffi.Size() external int route_count; - external mln_adapter_queued_resource_request_listener listener; + @mln_adapter_resource_request_queue() + external int queue; static ffi.Pointer $allocate( ffi.Allocator $allocator, { required ffi.Pointer routes, required int route_count, - required mln_adapter_queued_resource_request_listener listener, + required int queue, }) => $allocator() ..ref.routes = routes ..ref.route_count = route_count - ..ref.listener = listener; + ..ref.queue = queue; } final class mln_adapter_queued_resource_provider_route extends ffi.Struct { @@ -2937,15 +3138,6 @@ final class mln_adapter_queued_resource_request extends ffi.Struct { ..ref.prior_data_size = prior_data_size; } -typedef mln_adapter_queued_resource_request_listener = - ffi.Pointer< - ffi.NativeFunction - >; -typedef mln_adapter_queued_resource_request_listenerFunction = - ffi.Void Function(ffi.Pointer request); -typedef Dartmln_adapter_queued_resource_request_listenerFunction = - void Function(ffi.Pointer request); - final class mln_adapter_resource_provider_rule extends ffi.Struct { @ffi.Uint32() external int kind; @@ -2973,6 +3165,9 @@ final class mln_adapter_resource_provider_rules extends ffi.Struct { ..ref.count = count; } +typedef mln_adapter_resource_request_queue = ffi.Uint64; +typedef Dartmln_adapter_resource_request_queue = int; + final class mln_adapter_resource_rewrite_rule extends ffi.Struct { @ffi.Uint32() external int kind; @@ -3191,6 +3386,44 @@ enum mln_camera_change_mode { }; } +final class mln_camera_delta extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int kind; + + external mln_screen_point offset; + + @ffi.Double() + external double amount; + + @ffi.Bool() + external bool has_anchor; + + external mln_screen_point anchor; + + external mln_animation_options animation; +} + +enum mln_camera_delta_kind { + MLN_CAMERA_DELTA_MOVE(0), + MLN_CAMERA_DELTA_SCALE(1), + MLN_CAMERA_DELTA_BEARING(2), + MLN_CAMERA_DELTA_PITCH(3); + + final int value; + const mln_camera_delta_kind(this.value); + + static mln_camera_delta_kind fromValue(int value) => switch (value) { + 0 => MLN_CAMERA_DELTA_MOVE, + 1 => MLN_CAMERA_DELTA_SCALE, + 2 => MLN_CAMERA_DELTA_BEARING, + 3 => MLN_CAMERA_DELTA_PITCH, + _ => throw ArgumentError('Unknown value for mln_camera_delta_kind: $value'), + }; +} + enum mln_camera_fit_option_field { MLN_CAMERA_FIT_OPTION_PADDING(1), MLN_CAMERA_FIT_OPTION_BEARING(2), @@ -3284,11 +3517,60 @@ final class mln_camera_options extends ffi.Struct { @ffi.Double() external double pitch; - @ffi.Double() - external double roll; + @ffi.Double() + external double roll; + + @ffi.Double() + external double field_of_view; +} + +final class mln_camera_query_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + @ffi.Uint64() + external int generation; + + external mln_camera_options camera; +} + +final class mln_camera_update extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int mode; + + external mln_camera_options camera; + + external mln_animation_options animation; + + @ffi.Uint32() + external int gesture_phase; + + @ffi.Uint32() + external int reserved; +} + +enum mln_camera_update_mode { + MLN_CAMERA_UPDATE_MODE_JUMP(0), + MLN_CAMERA_UPDATE_MODE_EASE(1), + MLN_CAMERA_UPDATE_MODE_FLY(2); - @ffi.Double() - external double field_of_view; + final int value; + const mln_camera_update_mode(this.value); + + static mln_camera_update_mode fromValue(int value) => switch (value) { + 0 => MLN_CAMERA_UPDATE_MODE_JUMP, + 1 => MLN_CAMERA_UPDATE_MODE_EASE, + 2 => MLN_CAMERA_UPDATE_MODE_FLY, + _ => throw ArgumentError( + 'Unknown value for mln_camera_update_mode: $value', + ), + }; } final class mln_canonical_tile_id extends ffi.Struct { @@ -3312,6 +3594,92 @@ final class mln_canonical_tile_id extends ffi.Struct { ..ref.y = y; } +enum mln_command_disposition { + MLN_COMMAND_DISPOSITION_COMMITTED(0), + MLN_COMMAND_DISPOSITION_SUPERSEDED(1), + MLN_COMMAND_DISPOSITION_FAILED(2), + MLN_COMMAND_DISPOSITION_CANCELLED(3); + + final int value; + const mln_command_disposition(this.value); + + static mln_command_disposition fromValue(int value) => switch (value) { + 0 => MLN_COMMAND_DISPOSITION_COMMITTED, + 1 => MLN_COMMAND_DISPOSITION_SUPERSEDED, + 2 => MLN_COMMAND_DISPOSITION_FAILED, + 3 => MLN_COMMAND_DISPOSITION_CANCELLED, + _ => throw ArgumentError( + 'Unknown value for mln_command_disposition: $value', + ), + }; +} + +final class mln_completion extends ffi.Struct { + @ffi.Uint32() + external int size; + + external mln_completion_callback callback; + + external ffi.Pointer user_data; + + external mln_completion_release release_user_data; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required mln_completion_callback callback, + required ffi.Pointer user_data, + required mln_completion_release release_user_data, + }) => $allocator() + ..ref.size = size + ..ref.callback = callback + ..ref.user_data = user_data + ..ref.release_user_data = release_user_data; +} + +typedef mln_completion_callback = + ffi.Pointer>; +typedef mln_completion_callbackFunction = + ffi.Void Function( + ffi.Pointer user_data, + ffi.Pointer result, + ); +typedef Dartmln_completion_callbackFunction = + void Function( + ffi.Pointer user_data, + ffi.Pointer result, + ); +typedef mln_completion_release = + ffi.Pointer>; +typedef mln_completion_releaseFunction = + ffi.Void Function(ffi.Pointer user_data); +typedef Dartmln_completion_releaseFunction = + void Function(ffi.Pointer user_data); + +final class mln_completion_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Int32() + external int status; + + @ffi.Uint32() + external int disposition; + + @ffi.Uint32() + external int reserved; + + @ffi.Uint64() + external int generation; + + external mln_buffer_view diagnostic; + + external ffi.Pointer value; + + @ffi.Size() + external int value_count; +} + enum mln_constrain_mode { MLN_CONSTRAIN_MODE_NONE(0), MLN_CONSTRAIN_MODE_HEIGHT_ONLY(1), @@ -3591,6 +3959,9 @@ final class mln_egl_context_descriptor extends ffi.Struct { ..ref.get_proc_address = get_proc_address; } +typedef mln_event_batch = ffi.Uint64; +typedef Dartmln_event_batch = int; + final class mln_feature_state_selector extends ffi.Struct { @ffi.Uint32() external int size; @@ -3626,6 +3997,51 @@ enum mln_feature_state_selector_field { }; } +final class mln_frame_demand extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int flags; + + @ffi.Uint64() + external int token; + + @ffi.Uint64() + external int coalescing_boundary; + + @ffi.Uint64() + external int timeout_ns; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int flags, + required int token, + required int coalescing_boundary, + required int timeout_ns, + }) => $allocator() + ..ref.size = size + ..ref.flags = flags + ..ref.token = token + ..ref.coalescing_boundary = coalescing_boundary + ..ref.timeout_ns = timeout_ns; +} + +enum mln_frame_demand_flag { + MLN_FRAME_DEMAND_IF_NEEDED(1), + MLN_FRAME_DEMAND_PRESENT(2); + + final int value; + const mln_frame_demand_flag(this.value); + + static mln_frame_demand_flag fromValue(int value) => switch (value) { + 1 => MLN_FRAME_DEMAND_IF_NEEDED, + 2 => MLN_FRAME_DEMAND_PRESENT, + _ => throw ArgumentError('Unknown value for mln_frame_demand_flag: $value'), + }; +} + enum mln_free_camera_option_field { MLN_FREE_CAMERA_OPTION_POSITION(1), MLN_FREE_CAMERA_OPTION_ORIENTATION(2); @@ -3737,6 +4153,71 @@ final class mln_geojson_source_options extends ffi.Struct { external bool synchronous_tiling; } +enum mln_gesture_phase { + MLN_GESTURE_PHASE_NONE(0), + MLN_GESTURE_PHASE_BEGIN(1), + MLN_GESTURE_PHASE_UPDATE(2), + MLN_GESTURE_PHASE_END(3), + MLN_GESTURE_PHASE_CANCEL(4); + + final int value; + const mln_gesture_phase(this.value); + + static mln_gesture_phase fromValue(int value) => switch (value) { + 0 => MLN_GESTURE_PHASE_NONE, + 1 => MLN_GESTURE_PHASE_BEGIN, + 2 => MLN_GESTURE_PHASE_UPDATE, + 3 => MLN_GESTURE_PHASE_END, + 4 => MLN_GESTURE_PHASE_CANCEL, + _ => throw ArgumentError('Unknown value for mln_gesture_phase: $value'), + }; +} + +final class mln_gpu_sync extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int kind; + + external ffi.Pointer object; + + @ffi.Uint64() + external int value; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int kind, + required ffi.Pointer object, + required int value, + }) => $allocator() + ..ref.size = size + ..ref.kind = kind + ..ref.object = object + ..ref.value = value; +} + +enum mln_gpu_sync_kind { + MLN_GPU_SYNC_CPU_COMPLETE(0), + MLN_GPU_SYNC_METAL_SHARED_EVENT(1), + MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE(2), + MLN_GPU_SYNC_OPENGL_FENCE(3), + MLN_GPU_SYNC_WEBGPU_TOKEN(4); + + final int value; + const mln_gpu_sync_kind(this.value); + + static mln_gpu_sync_kind fromValue(int value) => switch (value) { + 0 => MLN_GPU_SYNC_CPU_COMPLETE, + 1 => MLN_GPU_SYNC_METAL_SHARED_EVENT, + 2 => MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE, + 3 => MLN_GPU_SYNC_OPENGL_FENCE, + 4 => MLN_GPU_SYNC_WEBGPU_TOKEN, + _ => throw ArgumentError('Unknown value for mln_gpu_sync_kind: $value'), + }; +} + final class mln_http_header_transform extends ffi.Struct { @ffi.Uint32() external int size; @@ -3745,15 +4226,19 @@ final class mln_http_header_transform extends ffi.Struct { external ffi.Pointer user_data; + external mln_runtime_callback_release release_user_data; + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required int size, required mln_http_header_transform_callback callback, required ffi.Pointer user_data, + required mln_runtime_callback_release release_user_data, }) => $allocator() ..ref.size = size ..ref.callback = callback - ..ref.user_data = user_data; + ..ref.user_data = user_data + ..ref.release_user_data = release_user_data; } typedef mln_http_header_transform_callback = @@ -3889,6 +4374,12 @@ typedef Dartmln_log_callbackFunction = int code, ffi.Pointer message, ); +typedef mln_log_callback_release = + ffi.Pointer>; +typedef mln_log_callback_releaseFunction = + ffi.Void Function(ffi.Pointer user_data); +typedef Dartmln_log_callback_releaseFunction = + void Function(ffi.Pointer user_data); enum mln_log_event { MLN_LOG_EVENT_GENERAL(0), @@ -3970,6 +4461,27 @@ enum mln_log_severity_mask { }; } +final class mln_logical_extent extends ffi.Struct { + @ffi.Uint32() + external int width; + + @ffi.Uint32() + external int height; + + @ffi.Double() + external double scale_factor; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int width, + required int height, + required double scale_factor, + }) => $allocator() + ..ref.width = width + ..ref.height = height + ..ref.scale_factor = scale_factor; +} + typedef mln_map = ffi.Uint64; typedef Dartmln_map = int; @@ -4017,14 +4529,7 @@ final class mln_map_options extends ffi.Struct { @ffi.Uint32() external int size; - @ffi.Uint32() - external int width; - - @ffi.Uint32() - external int height; - - @ffi.Double() - external double scale_factor; + external mln_logical_extent initial_extent; @ffi.Uint32() external int map_mode; @@ -4034,29 +4539,54 @@ final class mln_map_options extends ffi.Struct { @ffi.Uint64() external int event_mask; - - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required int size, - required int width, - required int height, - required double scale_factor, - required int map_mode, - required bool fast_pfor_enabled, - required int event_mask, - }) => $allocator() - ..ref.size = size - ..ref.width = width - ..ref.height = height - ..ref.scale_factor = scale_factor - ..ref.map_mode = map_mode - ..ref.fast_pfor_enabled = fast_pfor_enabled - ..ref.event_mask = event_mask; } typedef mln_map_projection = ffi.Uint64; typedef Dartmln_map_projection = int; +final class mln_map_snapshot extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int debug_options; + + @ffi.Uint64() + external int generation; + + external mln_camera_options camera; + + external mln_logical_extent logical_extent; + + external mln_projection_mode projection_mode; + + external mln_map_viewport_options viewport; + + @ffi.Bool() + external bool fully_loaded; + + @ffi.Bool() + external bool rendering_stats_view_enabled; + + @ffi.Bool() + external bool repaint_demand; + + @ffi.Uint8() + external int reserved_flags; + + @ffi.Uint64() + external int event_mask; + + @ffi.Uint64() + external int latest_render_update_generation; + + external mln_map_tile_options tile; + + external mln_bound_options bounds; + + external mln_free_camera_options free_camera; +} + enum mln_map_tile_option_field { MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA(1), MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS(2), @@ -4318,68 +4848,6 @@ final class mln_offline_geometry_region_definition extends ffi.Struct { external bool include_ideographs; } -typedef mln_offline_operation_id = ffi.Uint64; -typedef Dartmln_offline_operation_id = int; - -enum mln_offline_operation_kind { - MLN_OFFLINE_OPERATION_AMBIENT_CACHE(1), - MLN_OFFLINE_OPERATION_REGION_CREATE(2), - MLN_OFFLINE_OPERATION_REGION_GET(3), - MLN_OFFLINE_OPERATION_REGIONS_LIST(4), - MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE(5), - MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA(6), - MLN_OFFLINE_OPERATION_REGION_GET_STATUS(7), - MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED(8), - MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE(9), - MLN_OFFLINE_OPERATION_REGION_INVALIDATE(10), - MLN_OFFLINE_OPERATION_REGION_DELETE(11), - MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE(12); - - final int value; - const mln_offline_operation_kind(this.value); - - static mln_offline_operation_kind fromValue(int value) => switch (value) { - 1 => MLN_OFFLINE_OPERATION_AMBIENT_CACHE, - 2 => MLN_OFFLINE_OPERATION_REGION_CREATE, - 3 => MLN_OFFLINE_OPERATION_REGION_GET, - 4 => MLN_OFFLINE_OPERATION_REGIONS_LIST, - 5 => MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE, - 6 => MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA, - 7 => MLN_OFFLINE_OPERATION_REGION_GET_STATUS, - 8 => MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED, - 9 => MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE, - 10 => MLN_OFFLINE_OPERATION_REGION_INVALIDATE, - 11 => MLN_OFFLINE_OPERATION_REGION_DELETE, - 12 => MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE, - _ => throw ArgumentError( - 'Unknown value for mln_offline_operation_kind: $value', - ), - }; -} - -enum mln_offline_operation_result_kind { - MLN_OFFLINE_OPERATION_RESULT_NONE(0), - MLN_OFFLINE_OPERATION_RESULT_REGION(1), - MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION(2), - MLN_OFFLINE_OPERATION_RESULT_REGION_LIST(3), - MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS(4); - - final int value; - const mln_offline_operation_result_kind(this.value); - - static mln_offline_operation_result_kind fromValue(int value) => - switch (value) { - 0 => MLN_OFFLINE_OPERATION_RESULT_NONE, - 1 => MLN_OFFLINE_OPERATION_RESULT_REGION, - 2 => MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION, - 3 => MLN_OFFLINE_OPERATION_RESULT_REGION_LIST, - 4 => MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS, - _ => throw ArgumentError( - 'Unknown value for mln_offline_operation_result_kind: $value', - ), - }; -} - final class mln_offline_region_definition extends ffi.Struct { @ffi.Uint32() external int size; @@ -4884,24 +5352,126 @@ enum mln_queried_feature_field { typedef mln_queried_feature_list = ffi.Uint64; typedef Dartmln_queried_feature_list = int; +enum mln_render_abandon_disposition { + MLN_RENDER_ABANDON_DISPOSITION_CLEAN(0), + MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED(1); + + final int value; + const mln_render_abandon_disposition(this.value); + + static mln_render_abandon_disposition fromValue(int value) => switch (value) { + 0 => MLN_RENDER_ABANDON_DISPOSITION_CLEAN, + 1 => MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED, + _ => throw ArgumentError( + 'Unknown value for mln_render_abandon_disposition: $value', + ), + }; +} + +final class mln_render_abandon_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int disposition; + + @ffi.Uint32() + external int quarantined_resource_count; + + @ffi.Uint32() + external int reserved; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int disposition, + required int quarantined_resource_count, + required int reserved, + }) => $allocator() + ..ref.size = size + ..ref.disposition = disposition + ..ref.quarantined_resource_count = quarantined_resource_count + ..ref.reserved = reserved; +} + enum mln_render_backend_flag { MLN_RENDER_BACKEND_FLAG_METAL(1), MLN_RENDER_BACKEND_FLAG_VULKAN(2), MLN_RENDER_BACKEND_FLAG_OPENGL(4), MLN_RENDER_BACKEND_FLAG_WEBGPU(8); - final int value; - const mln_render_backend_flag(this.value); + final int value; + const mln_render_backend_flag(this.value); + + static mln_render_backend_flag fromValue(int value) => switch (value) { + 1 => MLN_RENDER_BACKEND_FLAG_METAL, + 2 => MLN_RENDER_BACKEND_FLAG_VULKAN, + 4 => MLN_RENDER_BACKEND_FLAG_OPENGL, + 8 => MLN_RENDER_BACKEND_FLAG_WEBGPU, + _ => throw ArgumentError( + 'Unknown value for mln_render_backend_flag: $value', + ), + }; +} + +enum mln_render_driver_kind { + MLN_RENDER_DRIVER_CORE_WORKER(1), + MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD(2); + + final int value; + const mln_render_driver_kind(this.value); + + static mln_render_driver_kind fromValue(int value) => switch (value) { + 1 => MLN_RENDER_DRIVER_CORE_WORKER, + 2 => MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + _ => throw ArgumentError( + 'Unknown value for mln_render_driver_kind: $value', + ), + }; +} + +typedef mln_render_frame_batch = ffi.Uint64; +typedef Dartmln_render_frame_batch = int; + +final class mln_render_frame_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int disposition; + + @ffi.Uint64() + external int token; + + @ffi.Uint64() + external int map_update_generation; + + @ffi.Uint64() + external int extent_generation; + + @ffi.Uint64() + external int frame_generation; + + @ffi.Bool() + external bool needs_repaint; - static mln_render_backend_flag fromValue(int value) => switch (value) { - 1 => MLN_RENDER_BACKEND_FLAG_METAL, - 2 => MLN_RENDER_BACKEND_FLAG_VULKAN, - 4 => MLN_RENDER_BACKEND_FLAG_OPENGL, - 8 => MLN_RENDER_BACKEND_FLAG_WEBGPU, - _ => throw ArgumentError( - 'Unknown value for mln_render_backend_flag: $value', - ), - }; + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int disposition, + required int token, + required int map_update_generation, + required int extent_generation, + required int frame_generation, + required bool needs_repaint, + }) => $allocator() + ..ref.size = size + ..ref.disposition = disposition + ..ref.token = token + ..ref.map_update_generation = map_update_generation + ..ref.extent_generation = extent_generation + ..ref.frame_generation = frame_generation + ..ref.needs_repaint = needs_repaint; } enum mln_render_mode { @@ -4922,7 +5492,9 @@ enum mln_render_result { MLN_RENDER_RESULT_RENDERED(0), MLN_RENDER_RESULT_NO_UPDATE(1), MLN_RENDER_RESULT_SIZE_PENDING(2), - MLN_RENDER_RESULT_TARGET_NOT_READY(3); + MLN_RENDER_RESULT_TARGET_NOT_READY(3), + MLN_RENDER_RESULT_SUPERSEDED(4), + MLN_RENDER_RESULT_DEADLINE_MISSED(5); final int value; const mln_render_result(this.value); @@ -4932,6 +5504,8 @@ enum mln_render_result { 1 => MLN_RENDER_RESULT_NO_UPDATE, 2 => MLN_RENDER_RESULT_SIZE_PENDING, 3 => MLN_RENDER_RESULT_TARGET_NOT_READY, + 4 => MLN_RENDER_RESULT_SUPERSEDED, + 5 => MLN_RENDER_RESULT_DEADLINE_MISSED, _ => throw ArgumentError('Unknown value for mln_render_result: $value'), }; } @@ -4939,6 +5513,141 @@ enum mln_render_result { typedef mln_render_session = ffi.Uint64; typedef Dartmln_render_session = int; +final class mln_render_session_attach_options extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int driver; + + @ffi.Uint32() + external int requested_texture_ring_depth; + + @ffi.Uint32() + external int reserved; + + external mln_wake frame_wake; + + external mln_wake driver_work_wake; +} + +final class mln_render_session_capabilities extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int driver; + + @ffi.Uint32() + external int texture_ring_depth; + + @ffi.Uint32() + external int flags; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int driver, + required int texture_ring_depth, + required int flags, + }) => $allocator() + ..ref.size = size + ..ref.driver = driver + ..ref.texture_ring_depth = texture_ring_depth + ..ref.flags = flags; +} + +enum mln_render_session_capability_flag { + MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION(1), + MLN_RENDER_SESSION_CAPABILITY_READBACK(2), + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC(4), + MLN_RENDER_SESSION_CAPABILITY_PRESENTATION(8); + + final int value; + const mln_render_session_capability_flag(this.value); + + static mln_render_session_capability_flag fromValue(int value) => + switch (value) { + 1 => MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION, + 2 => MLN_RENDER_SESSION_CAPABILITY_READBACK, + 4 => MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC, + 8 => MLN_RENDER_SESSION_CAPABILITY_PRESENTATION, + _ => throw ArgumentError( + 'Unknown value for mln_render_session_capability_flag: $value', + ), + }; +} + +final class mln_render_session_snapshot extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int state; + + @ffi.Uint32() + external int driver; + + @ffi.Uint32() + external int latest_result; + + external mln_render_target_extent extent; + + @ffi.Uint64() + external int generation; + + @ffi.Uint64() + external int map_update_generation; + + @ffi.Uint64() + external int rendered_update_generation; + + @ffi.Uint64() + external int extent_generation; + + @ffi.Uint64() + external int frame_generation; + + @ffi.Uint64() + external int latest_demand_token; + + @ffi.Uint32() + external int pending_demand_count; + + @ffi.Uint32() + external int acquired_frame_count; + + @ffi.Bool() + external bool target_ready; + + @ffi.Bool() + external bool pending_changes; +} + +enum mln_render_session_state { + MLN_RENDER_SESSION_STATE_ATTACHING(1), + MLN_RENDER_SESSION_STATE_ATTACHED(2), + MLN_RENDER_SESSION_STATE_DETACHING(3), + MLN_RENDER_SESSION_STATE_DETACHED(4), + MLN_RENDER_SESSION_STATE_TARGET_LOST(5), + MLN_RENDER_SESSION_STATE_ABANDONED(6); + + final int value; + const mln_render_session_state(this.value); + + static mln_render_session_state fromValue(int value) => switch (value) { + 1 => MLN_RENDER_SESSION_STATE_ATTACHING, + 2 => MLN_RENDER_SESSION_STATE_ATTACHED, + 3 => MLN_RENDER_SESSION_STATE_DETACHING, + 4 => MLN_RENDER_SESSION_STATE_DETACHED, + 5 => MLN_RENDER_SESSION_STATE_TARGET_LOST, + 6 => MLN_RENDER_SESSION_STATE_ABANDONED, + _ => throw ArgumentError( + 'Unknown value for mln_render_session_state: $value', + ), + }; +} + final class mln_render_target_extent extends ffi.Struct { @ffi.Uint32() external int size; @@ -5159,15 +5868,19 @@ final class mln_resource_provider extends ffi.Struct { external ffi.Pointer user_data; + external mln_runtime_callback_release release_user_data; + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required int size, required mln_resource_provider_callback callback, required ffi.Pointer user_data, + required mln_runtime_callback_release release_user_data, }) => $allocator() ..ref.size = size ..ref.callback = callback - ..ref.user_data = user_data; + ..ref.user_data = user_data + ..ref.release_user_data = release_user_data; } typedef mln_resource_provider_callback = @@ -5413,15 +6126,19 @@ final class mln_resource_transform extends ffi.Struct { external ffi.Pointer user_data; + external mln_runtime_callback_release release_user_data; + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required int size, required mln_resource_transform_callback callback, required ffi.Pointer user_data, + required mln_runtime_callback_release release_user_data, }) => $allocator() ..ref.size = size ..ref.callback = callback - ..ref.user_data = user_data; + ..ref.user_data = user_data + ..ref.release_user_data = release_user_data; } typedef mln_resource_transform_callback = @@ -5476,6 +6193,12 @@ enum mln_resource_usage { typedef mln_runtime = ffi.Uint64; typedef Dartmln_runtime = int; +typedef mln_runtime_callback_release = + ffi.Pointer>; +typedef mln_runtime_callback_releaseFunction = + ffi.Void Function(ffi.Pointer user_data); +typedef Dartmln_runtime_callback_releaseFunction = + void Function(ffi.Pointer user_data); final class mln_runtime_event extends ffi.Struct { @ffi.Uint32() @@ -5493,7 +6216,7 @@ final class mln_runtime_event extends ffi.Struct { @ffi.Uint32() external int payload_type; - @ffi.Uint32() + @ffi.Uint64() external int message_offset; @ffi.Uint32() @@ -5502,7 +6225,7 @@ final class mln_runtime_event extends ffi.Struct { external mln_runtime_event_payload payload; } -final class mln_runtime_event_batch extends ffi.Struct { +final class mln_runtime_event_batch_view extends ffi.Struct { @ffi.Uint32() external int size; @@ -5519,10 +6242,7 @@ final class mln_runtime_event_batch extends ffi.Struct { @ffi.Size() external int messages_size; - @ffi.Size() - external int remaining_count; - - static ffi.Pointer $allocate( + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required int size, required int event_size, @@ -5530,15 +6250,13 @@ final class mln_runtime_event_batch extends ffi.Struct { required int event_count, required ffi.Pointer messages, required int messages_size, - required int remaining_count, - }) => $allocator() + }) => $allocator() ..ref.size = size ..ref.event_size = event_size ..ref.events = events ..ref.event_count = event_count ..ref.messages = messages - ..ref.messages_size = messages_size - ..ref.remaining_count = remaining_count; + ..ref.messages_size = messages_size; } final class mln_runtime_event_camera_transition_finished extends ffi.Struct { @@ -5573,14 +6291,13 @@ enum mln_runtime_event_mask { MLN_RUNTIME_EVENT_MASK_MAP_RENDER_MAP_FINISHED(65536), MLN_RUNTIME_EVENT_MASK_MAP_STYLE_IMAGE_MISSING(131072), MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION(262144), - MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED(8388608), + MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED(4194304), MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED(524288), MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR(1048576), MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED(2097152), - MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED(4194304), - MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS(8912894), - MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS(7864320), - MLN_RUNTIME_EVENT_MASK_ALL(16777214); + MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS(4718590), + MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS(3670016), + MLN_RUNTIME_EVENT_MASK_ALL(8388606); final int value; const mln_runtime_event_mask(this.value); @@ -5605,51 +6322,19 @@ enum mln_runtime_event_mask { 65536 => MLN_RUNTIME_EVENT_MASK_MAP_RENDER_MAP_FINISHED, 131072 => MLN_RUNTIME_EVENT_MASK_MAP_STYLE_IMAGE_MISSING, 262144 => MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION, - 8388608 => MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED, + 4194304 => MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED, 524288 => MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED, 1048576 => MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR, 2097152 => MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED, - 4194304 => MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED, - 8912894 => MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS, - 7864320 => MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS, - 16777214 => MLN_RUNTIME_EVENT_MASK_ALL, + 4718590 => MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS, + 3670016 => MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS, + 8388606 => MLN_RUNTIME_EVENT_MASK_ALL, _ => throw ArgumentError( 'Unknown value for mln_runtime_event_mask: $value', ), }; } -final class mln_runtime_event_offline_operation_completed extends ffi.Struct { - @mln_offline_operation_id() - external int operation_id; - - @ffi.Uint32() - external int operation_kind; - - @ffi.Uint32() - external int result_kind; - - @ffi.Int32() - external int result_status; - - @ffi.Bool() - external bool found; - - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required int operation_id, - required int operation_kind, - required int result_kind, - required int result_status, - required bool found, - }) => $allocator() - ..ref.operation_id = operation_id - ..ref.operation_kind = operation_kind - ..ref.result_kind = result_kind - ..ref.result_status = result_status - ..ref.found = found; -} - final class mln_runtime_event_offline_region_response_error extends ffi.Struct { @mln_offline_region_id() external int region_id; @@ -5706,9 +6391,6 @@ final class mln_runtime_event_payload extends ffi.Union { external mln_runtime_event_offline_region_tile_count_limit offline_region_tile_count_limit; - external mln_runtime_event_offline_operation_completed - offline_operation_completed; - external mln_runtime_event_camera_transition_finished camera_transition_finished; } @@ -5721,8 +6403,7 @@ enum mln_runtime_event_payload_type { MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS(5), MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR(6), MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT(7), - MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED(8), - MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED(9); + MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED(8); final int value; const mln_runtime_event_payload_type(this.value); @@ -5735,8 +6416,7 @@ enum mln_runtime_event_payload_type { 5 => MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS, 6 => MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR, 7 => MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT, - 8 => MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED, - 9 => MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED, + 8 => MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED, _ => throw ArgumentError( 'Unknown value for mln_runtime_event_payload_type: $value', ), @@ -5811,8 +6491,7 @@ enum mln_runtime_event_type { MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED(19), MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR(20), MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED(21), - MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED(22), - MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED(23); + MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED(22); final int value; const mln_runtime_event_type(this.value); @@ -5839,8 +6518,7 @@ enum mln_runtime_event_type { 19 => MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED, 20 => MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR, 21 => MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED, - 22 => MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED, - 23 => MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, + 22 => MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, _ => throw ArgumentError( 'Unknown value for mln_runtime_event_type: $value', ), @@ -5861,19 +6539,7 @@ final class mln_runtime_options extends ffi.Struct { @ffi.Uint64() external int event_mask; - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required int size, - required int flags, - required ffi.Pointer asset_path, - required ffi.Pointer cache_path, - required int event_mask, - }) => $allocator() - ..ref.size = size - ..ref.flags = flags - ..ref.asset_path = asset_path - ..ref.cache_path = cache_path - ..ref.event_mask = event_mask; + external mln_wake event_wake; } final class mln_screen_box extends ffi.Struct { @@ -5964,6 +6630,11 @@ sealed class mln_status { static const MLN_STATUS_WRONG_THREAD = -3; static const MLN_STATUS_UNSUPPORTED = -4; static const MLN_STATUS_NATIVE_ERROR = -5; + static const MLN_STATUS_CANCELLED = -6; + static const MLN_STATUS_BUSY = -7; + static const MLN_STATUS_TARGET_LOST = -8; + static const MLN_STATUS_NOT_READY = -9; + static const MLN_STATUS_NOT_FOUND = -10; } typedef mln_style_id_list = ffi.Uint64; @@ -6073,6 +6744,62 @@ final class mln_style_image_options extends ffi.Struct { external bool sdf; } +final class mln_style_image_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + external mln_style_image_info info; + + external mln_buffer_view pixels; + + external ffi.Pointer stretch_x; + + @ffi.Size() + external int stretch_x_count; + + external ffi.Pointer stretch_y; + + @ffi.Size() + external int stretch_y_count; +} + +final class mln_style_image_stretches_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + external ffi.Pointer stretch_x; + + @ffi.Size() + external int stretch_x_count; + + external ffi.Pointer stretch_y; + + @ffi.Size() + external int stretch_y_count; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required int reserved, + required ffi.Pointer stretch_x, + required int stretch_x_count, + required ffi.Pointer stretch_y, + required int stretch_y_count, + }) => $allocator() + ..ref.size = size + ..ref.reserved = reserved + ..ref.stretch_x = stretch_x + ..ref.stretch_x_count = stretch_x_count + ..ref.stretch_y = stretch_y + ..ref.stretch_y_count = stretch_y_count; +} + enum mln_style_image_text_fit { MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK(0), MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY(1), @@ -6091,6 +6818,61 @@ enum mln_style_image_text_fit { }; } +final class mln_style_layer_info extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int fields; + + external mln_buffer_view type; + + @ffi.Double() + external double min_zoom; + + @ffi.Double() + external double max_zoom; + + @ffi.Uint32() + external int visibility; + + @ffi.Size() + external int source_id_size; + + @ffi.Size() + external int source_layer_size; +} + +enum mln_style_layer_info_field { + MLN_STYLE_LAYER_INFO_SOURCE_ID(1), + MLN_STYLE_LAYER_INFO_SOURCE_LAYER(2); + + final int value; + const mln_style_layer_info_field(this.value); + + static mln_style_layer_info_field fromValue(int value) => switch (value) { + 1 => MLN_STYLE_LAYER_INFO_SOURCE_ID, + 2 => MLN_STYLE_LAYER_INFO_SOURCE_LAYER, + _ => throw ArgumentError( + 'Unknown value for mln_style_layer_info_field: $value', + ), + }; +} + +final class mln_style_layer_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + external mln_style_layer_info info; + + external mln_buffer_view source_id; + + external mln_buffer_view source_layer; +} + enum mln_style_layer_visibility { MLN_STYLE_LAYER_VISIBILITY_VISIBLE(0), MLN_STYLE_LAYER_VISIBILITY_NONE(1); @@ -6196,6 +6978,25 @@ enum mln_style_source_info_field { }; } +final class mln_style_source_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + external mln_style_source_info info; + + external mln_buffer_view attribution; + + external mln_buffer_view url; + + external ffi.Pointer tile_urls; + + @ffi.Size() + external int tile_url_count; +} + enum mln_style_source_type { MLN_STYLE_SOURCE_TYPE_UNKNOWN(0), MLN_STYLE_SOURCE_TYPE_VECTOR(1), @@ -6399,6 +7200,18 @@ final class mln_texture_image_info extends ffi.Struct { ..ref.byte_length = byte_length; } +final class mln_texture_readback_result extends ffi.Struct { + @ffi.Uint32() + external int size; + + @ffi.Uint32() + external int reserved; + + external mln_buffer_view data; + + external mln_texture_image_info info; +} + final class mln_tile_id extends ffi.Struct { @ffi.Uint32() external int overscaled_z; @@ -6679,23 +7492,69 @@ final class mln_vulkan_surface_descriptor extends ffi.Struct { external ffi.Pointer surface; } -typedef mln_wake_source = ffi.Uint64; -typedef Dartmln_wake_source = int; +final class mln_wake extends ffi.Struct { + @ffi.Uint32() + external int size; + + external mln_wake_callback callback; + + external ffi.Pointer user_data; + + external mln_wake_release release_user_data; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int size, + required mln_wake_callback callback, + required ffi.Pointer user_data, + required mln_wake_release release_user_data, + }) => $allocator() + ..ref.size = size + ..ref.callback = callback + ..ref.user_data = user_data + ..ref.release_user_data = release_user_data; +} + +typedef mln_wake_callback = + ffi.Pointer>; +typedef mln_wake_callbackFunction = + ffi.Void Function(ffi.Pointer user_data); +typedef Dartmln_wake_callbackFunction = + void Function(ffi.Pointer user_data); +typedef mln_wake_release = + ffi.Pointer>; +typedef mln_wake_releaseFunction = + ffi.Void Function(ffi.Pointer user_data); +typedef Dartmln_wake_releaseFunction = + void Function(ffi.Pointer user_data); final class mln_webgl_context_descriptor extends ffi.Struct { @ffi.Uint32() external int size; + @ffi.Uint32() + external int kind; + @ffi.Int32() external int context; - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required int size, - required int context, - }) => $allocator() - ..ref.size = size - ..ref.context = context; + external mln_buffer_view canvas_selector; +} + +enum mln_webgl_context_kind { + MLN_WEBGL_CONTEXT_EXISTING(0), + MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS(1); + + final int value; + const mln_webgl_context_kind(this.value); + + static mln_webgl_context_kind fromValue(int value) => switch (value) { + 0 => MLN_WEBGL_CONTEXT_EXISTING, + 1 => MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS, + _ => throw ArgumentError( + 'Unknown value for mln_webgl_context_kind: $value', + ), + }; } final class mln_webgpu_borrowed_texture_descriptor extends ffi.Struct { diff --git a/bindings/dart/lib/src/internal/callback/callback_state.dart b/bindings/dart/lib/src/internal/callback/callback_state.dart index fc803370f..95c35b843 100644 --- a/bindings/dart/lib/src/internal/callback/callback_state.dart +++ b/bindings/dart/lib/src/internal/callback/callback_state.dart @@ -41,6 +41,23 @@ abstract base class RetainedCallbackState { _scheduleCloseIfReady(); } + /// Retires and releases callback resources before this call returns. + /// + /// The caller must first detach the native callback so that no new upcall can + /// start. An active upcall keeps the ordinary deferred close behavior. + void closeSynchronously() { + if (_closed) { + return; + } + _retired = true; + if (_activeUpcalls != 0) { + _scheduleCloseIfReady(); + return; + } + _closed = true; + closeResources(); + } + void _scheduleCloseIfReady() { if (!_retired || _closed || _closeScheduled || _activeUpcalls != 0) { return; diff --git a/bindings/dart/lib/src/internal/callback/completion.dart b/bindings/dart/lib/src/internal/callback/completion.dart new file mode 100644 index 000000000..f0875d517 --- /dev/null +++ b/bindings/dart/lib/src/internal/callback/completion.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; + +import '../c/maplibre_native_c.g.dart' as raw; +import '../memory/memory.dart'; +import '../status/status.dart'; + +typedef NativeCompletionStart = int Function(Pointer); +typedef NativeCompletionDecoder = T Function(raw.mln_completion_result); + +abstract interface class _PendingCompletionBase { + void finish(Pointer record); + void fail(Object error); +} + +final class _PendingCompletion implements _PendingCompletionBase { + _PendingCompletion( + this.completer, + this.decode, + this.submissionStack, + this.acceptErrorStatus, + ); + + final Completer completer; + final NativeCompletionDecoder decode; + final StackTrace submissionStack; + final bool acceptErrorStatus; + + @override + void finish(Pointer record) { + try { + final result = record.ref.result; + if (!acceptErrorStatus) { + checkNativeStatus( + result.status, + () => copyCompletionDiagnostic(result.diagnostic), + ); + } + completer.complete(decode(result)); + } catch (error) { + completer.completeError(error, submissionStack); + } finally { + raw.mln_adapter_completion_record_destroy(record); + } + } + + @override + void fail(Object error) => completer.completeError(error, submissionStack); +} + +final _pendingCompletions = {}; +var _nextCompletionToken = 1; + +final NativeCallable +_completionListener = + NativeCallable.listener(( + Pointer userData, + Pointer record, + ) { + final pending = _pendingCompletions.remove(userData.address); + if (pending == null) { + if (record != nullptr) { + raw.mln_adapter_completion_record_destroy(record); + } + return; + } + if (record == nullptr) { + pending.fail( + StateError('native completion adapter could not copy the result'), + ); + return; + } + pending.finish(record); + }); + +Future startNativeCompletion({ + required raw.mln_adapter_completion_copy_kind copyKind, + required int elementSize, + required NativeCompletionStart start, + required NativeCompletionDecoder decode, + bool acceptErrorStatus = false, + void Function()? onRejected, +}) { + final completer = Completer(); + final token = _nextCompletionToken++; + final userData = Pointer.fromAddress(token); + _pendingCompletions[token] = _PendingCompletion( + completer, + decode, + StackTrace.current, + acceptErrorStatus, + ); + + try { + withNativeArena((arena) { + final completion = arena(); + checkNativeStatus( + raw.mln_adapter_completion_create( + copyKind.value, + elementSize, + _completionListener.nativeFunction, + userData, + completion, + ), + () => 'failed to create native completion adapter', + ); + var rejected = false; + try { + final status = start(completion); + if (status != nativeStatusOk) { + raw.mln_adapter_completion_reject(completion); + rejected = true; + checkNativeStatus(status, () => 'native operation was rejected'); + } + } catch (_) { + if (!rejected) raw.mln_adapter_completion_reject(completion); + rethrow; + } + }); + } catch (_) { + _pendingCompletions.remove(token); + onRejected?.call(); + rethrow; + } + return completer.future; +} + +String copyCompletionDiagnostic(raw.mln_buffer_view diagnostic) { + if (diagnostic.size == 0 || diagnostic.data == nullptr) { + return 'native operation failed'; + } + return utf8.decode( + diagnostic.data.cast().asTypedList(diagnostic.size), + allowMalformed: true, + ); +} diff --git a/bindings/dart/lib/src/internal/callback/wake.dart b/bindings/dart/lib/src/internal/callback/wake.dart new file mode 100644 index 000000000..8dc07b09d --- /dev/null +++ b/bindings/dart/lib/src/internal/callback/wake.dart @@ -0,0 +1,66 @@ +import 'dart:async'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +import '../c/maplibre_native_c.g.dart' as raw; + +final _wakeStates = {}; + +final NativeCallable _wakeReleaseListener = + NativeCallable.listener(( + Pointer userData, + ) { + _wakeStates.remove(userData.address)?._releasedByNative(); + }); + +/// Owns one Dart wake callback until native code reports it quiescent. +final class NativeWakeState { + NativeWakeState(void Function() callback) { + _token = calloc(); + _listener = NativeCallable.listener(( + Pointer _, + ) { + callback(); + }); + _wakeStates[_token.address] = this; + } + + late final Pointer _token; + late final NativeCallable _listener; + final _released = Completer(); + var _retired = false; + + /// Completes after native code can no longer invoke the wake callback. + Future get released => _released.future; + + /// Writes this callback into an owning C wake descriptor. + void writeTo(raw.mln_wake wake) { + wake.size = sizeOf(); + wake.callback = _listener.nativeFunction; + wake.user_data = _token.cast(); + wake.release_user_data = _wakeReleaseListener.nativeFunction; + } + + /// Releases a descriptor that its owning C call rejected. + void reject() { + if (_retired) return; + _retired = true; + _wakeStates.remove(_token.address); + _listener.close(); + calloc.free(_token); + _released.complete(); + } + + void _releasedByNative() { + if (_retired) return; + _retired = true; + // A NativeCallable.listener returns to C after queueing the Dart upcall. + // Defer disposal by one event turn so any earlier queued wake can finish. + Timer.run(() { + _listener.close(); + calloc.free(_token); + _released.complete(); + }); + } +} diff --git a/bindings/dart/lib/src/internal/lifecycle/frame_construction.dart b/bindings/dart/lib/src/internal/lifecycle/frame_construction.dart index d87ee2376..d28711a43 100644 --- a/bindings/dart/lib/src/internal/lifecycle/frame_construction.dart +++ b/bindings/dart/lib/src/internal/lifecycle/frame_construction.dart @@ -3,8 +3,8 @@ import '../status/status.dart'; /// Cleans up a native frame when its Dart wrapper could not be constructed. /// /// A successful release transfers the descriptor to [releaseSucceeded]. A -/// failed release transfers it to [releaseFailed] so an owner-thread retry can -/// retain the still-live native frame. +/// failed release transfers it to [releaseFailed] so a retry can retain the +/// still-live native frame. void cleanupFailedFrameConstruction({ required int Function() release, required void Function() releaseSucceeded, diff --git a/bindings/dart/lib/src/internal/lifecycle/lifecycle.dart b/bindings/dart/lib/src/internal/lifecycle/lifecycle.dart index b9e2c6e9c..77e4376cc 100644 --- a/bindings/dart/lib/src/internal/lifecycle/lifecycle.dart +++ b/bindings/dart/lib/src/internal/lifecycle/lifecycle.dart @@ -1,5 +1,5 @@ +import 'dart:async'; import 'dart:ffi'; -import 'dart:isolate'; import 'package:ffi/ffi.dart'; @@ -13,9 +13,6 @@ final NativeFinalizer _leakReporter = NativeFinalizer( const _createLeakToken = raw.mln_adapter_handle_leak_token_create; -/// Opaque token for the calling native thread, stable for its life. -const _threadToken = raw.mln_thread_token; - const _destroyLeakToken = raw.mln_adapter_handle_leak_token_destroy; /// Attaches the binding's non-owning native-handle leak diagnostic to [owner]. @@ -53,15 +50,7 @@ final class NativeLeakReporter { /// Close-once state for an owned native handle. final class NativeHandleState implements Finalizable { /// Creates state for a live native handle. - NativeHandleState( - this._handle, - this.typeName, { - int? ownerIsolateHash, - int? ownerThreadToken, - bool leakReporting = true, - this.threadAffine = true, - }) : _ownerIsolateHash = ownerIsolateHash ?? Isolate.current.hashCode, - _ownerThreadToken = ownerThreadToken ?? _threadToken() { + NativeHandleState(this._handle, this.typeName, {bool leakReporting = true}) { if (_handle.isNull) { throwInvalidArgument('$typeName handle must not be the null handle'); } @@ -72,34 +61,21 @@ final class NativeHandleState implements Finalizable { final H _handle; bool _closed = false; - final int _ownerIsolateHash; - - /// The native thread this handle was created on. The C API keys its - /// owner-thread checks on that thread, not on the isolate, and the Dart VM - /// moves an isolate between threads when it resumes from awaited I/O. - final int _ownerThreadToken; + Future? _closeFuture; final Object _finalizerDetachToken = Object(); Pointer? _leakToken; /// Native handle type name used in diagnostics. final String typeName; - /// Whether the C API pins this handle kind to its creating native thread. - /// - /// A non-affine handle is callable from any native thread, so its owner - /// isolate keeps using it after the Dart VM moves the isolate to another - /// thread on resuming from awaited I/O. - final bool threadAffine; - /// Whether this binding object has released its native handle. bool get isClosed => _closed; - /// The issued handle id without owner-isolate validation. + /// The issued handle ID. int get handleId => _handle.raw; /// Returns the live handle, or throws when it is closed. H get handle { - _checkOwnerIsolate(); if (_closed) { throwInvalidArgument('$typeName is closed'); } @@ -108,7 +84,6 @@ final class NativeHandleState implements Finalizable { /// Releases the native handle with [destroy] exactly once after success. void close(int Function(H) destroy, String Function() diagnostic) { - _checkOwnerIsolate(); if (_closed) { return; } @@ -119,21 +94,36 @@ final class NativeHandleState implements Finalizable { _detachLeakReporter(); } - void _checkOwnerIsolate() { - if (Isolate.current.hashCode != _ownerIsolateHash) { - throwWrongThread('$typeName belongs to a different Dart isolate'); + /// Releases the native handle asynchronously exactly once after success. + /// + /// [close] starts before this returns, so a release that rejects on the + /// calling turn still leaves this state open for a later attempt. + Future closeAsync(Future Function(H) close) { + if (_closed) { + return Future.value(); } - if (threadAffine && _threadToken() != _ownerThreadToken) { - // Same isolate, different native thread: every native call on this - // handle would now fail, including close, which leaks it permanently. - throwWrongThread( - '$typeName is owned by a native thread its isolate has since left. ' - 'The Dart VM moves an isolate between native threads when it resumes ' - 'from awaited I/O, so do not await I/O on an isolate that holds a ' - 'runtime, map, or render session. See ' - 'https://github.com/maplibre/maplibre-native-ffi/issues/412', - ); + final pending = _closeFuture; + if (pending != null) { + return pending; } + // The completer holds the shared future before the release runs, because a + // rejected release reports its failure while `close` is still on the stack. + final completer = Completer(); + _closeFuture = completer.future; + _runClose(close).then( + (_) => completer.complete(), + onError: (Object error, StackTrace stack) { + _closeFuture = null; + completer.completeError(error, stack); + }, + ); + return completer.future; + } + + Future _runClose(Future Function(H) close) async { + await close(_handle); + _closed = true; + _detachLeakReporter(); } void _attachLeakReporter(NativeHandle handle) { diff --git a/bindings/dart/lib/src/internal/lifecycle/native_handles.dart b/bindings/dart/lib/src/internal/lifecycle/native_handles.dart index 2782bc4db..699160218 100644 --- a/bindings/dart/lib/src/internal/lifecycle/native_handles.dart +++ b/bindings/dart/lib/src/internal/lifecycle/native_handles.dart @@ -25,26 +25,6 @@ extension type const NativeRenderSession(int raw) implements NativeHandle {} /// Owned native buffer handle id. extension type const NativeOwnedBufferHandle(int raw) implements NativeHandle {} -/// Offline region snapshot handle id. -extension type const NativeOfflineRegionSnapshot(int raw) - implements NativeHandle {} - -/// Offline region list handle id. -extension type const NativeOfflineRegionList(int raw) implements NativeHandle {} - -/// Style id list handle id. -extension type const NativeStyleIdList(int raw) implements NativeHandle {} - -/// Style string list handle id. -extension type const NativeStyleStringList(int raw) implements NativeHandle {} - -/// Queried feature list handle id. -extension type const NativeQueriedFeatureList(int raw) - implements NativeHandle {} - -/// Wake source handle id. -extension type const NativeWakeSource(int raw) implements NativeHandle {} - /// Prepared GeoJSON source data handle id. extension type const NativeGeoJsonSourceData(int raw) implements NativeHandle {} diff --git a/bindings/dart/lib/src/internal/status/status.dart b/bindings/dart/lib/src/internal/status/status.dart index af342f4d9..e93d59601 100644 --- a/bindings/dart/lib/src/internal/status/status.dart +++ b/bindings/dart/lib/src/internal/status/status.dart @@ -9,9 +9,6 @@ const int nativeStatusInvalidArgument = -1; /// Native C status code for invalid state. const int nativeStatusInvalidState = -2; -/// Native C status code for wrong-thread access. -const int nativeStatusWrongThread = -3; - /// Native C status code for unsupported behavior. const int nativeStatusUnsupported = -4; @@ -35,11 +32,3 @@ Never throwInvalidArgument(String diagnostic) { Never throwInvalidState(String diagnostic) { throw MaplibreException.invalidState(diagnostic); } - -/// Reports Dart-side owner-isolate misuse before calling native code. -Never throwWrongThread(String diagnostic) { - throw MaplibreException.forNativeStatusCode( - nativeStatusWrongThread, - diagnostic, - ); -} diff --git a/bindings/dart/lib/src/log/log.dart b/bindings/dart/lib/src/log/log.dart index 23a79d4c5..edc2a1d27 100644 --- a/bindings/dart/lib/src/log/log.dart +++ b/bindings/dart/lib/src/log/log.dart @@ -136,7 +136,7 @@ final class LogRecord { final String message; } -/// Log callback run asynchronously on Dart's owner isolate. +/// Log callback run asynchronously on its receiver isolate. typedef LogCallback = void Function(LogRecord record); /// Log severity mask for asynchronous native log dispatch. diff --git a/bindings/dart/lib/src/map/map.dart b/bindings/dart/lib/src/map/map.dart index c636440ce..8c83d4455 100644 --- a/bindings/dart/lib/src/map/map.dart +++ b/bindings/dart/lib/src/map/map.dart @@ -1,8 +1,18 @@ export '../runtime/runtime.dart' - show MapDebugOptions, MapHandle, MapMode, MapOptions; + show + CameraSnapshot, + MapDebugOptions, + MapHandle, + MapMode, + MapOptions, + MapSize, + MapSnapshot; export '../camera/camera.dart' show BoundOptions, + CameraDelta, + CameraDeltaKind, + CameraUpdateMode, ConstrainMode, FreeCameraOptions, MapTileOptions, diff --git a/bindings/dart/lib/src/maplibre.dart b/bindings/dart/lib/src/maplibre.dart index a543c4ab3..05ce9db72 100644 --- a/bindings/dart/lib/src/maplibre.dart +++ b/bindings/dart/lib/src/maplibre.dart @@ -13,48 +13,83 @@ import 'log/log.dart'; import 'render/targets.dart'; final class _LogCallbackState extends RetainedCallbackState { - _LogCallbackState(LogCallback callback, {required bool consume}) { - listener = - NativeCallable.listener(( - Pointer record, - ) { - if (record == nullptr) { - close(); - return; - } - final ran = runUpcall(() { - try { - final copied = record.cast().ref; - if (copied.retire_callback) { - close(); - return; - } - try { - callback(_copyLogRecord(copied)); - } catch (_) { - // An exception must not escape into native callback delivery. - } - } finally { - Maplibre._c.adapterLogRecordDestroy(record); - } - }); - if (!ran) { - Maplibre._c.adapterLogRecordDestroy(record); - } - }); + _LogCallbackState(LogCallback callback, {required bool consume}) + : _callback = callback { + listener = NativeCallable.listener(( + Pointer _, + ) { + runUpcall(_drain); + }); + final outQueue = calloc(); + final wake = calloc(); + try { + wake.ref.size = sizeOf(); + wake.ref.callback = listener.nativeFunction; + wake.ref.user_data = nullptr; + wake.ref.release_user_data = nullptr; + Maplibre._checkStatus(raw.mln_adapter_log_queue_create(wake, outQueue)); + queue = outQueue.value; + } catch (_) { + if (queue != 0) { + raw.mln_adapter_log_queue_close(queue); + } + listener.close(); + rethrow; + } finally { + calloc.free(wake); + calloc.free(outQueue); + } pointer = calloc(); - pointer.ref.listener = listener.nativeFunction; + pointer.ref.queue = queue; pointer.ref.consume = consume ? 1 : 0; + releaseListener = + NativeCallable.listener(( + Pointer context, + ) { + Maplibre._releaseLogCallbackState(context); + }); + pointer.ref.release_user_data = releaseListener.nativeFunction; + pointer.ref.release_context = pointer.cast(); } + final LogCallback _callback; late final Pointer pointer; - late final NativeCallable - listener; + late final NativeCallable listener; + late final NativeCallable + releaseListener; + var queue = 0; + + void _drain() { + withNativeArena((arena) { + final outRecord = arena>(); + while (true) { + outRecord.value = nullptr; + Maplibre._checkStatus( + raw.mln_adapter_log_queue_acquire(queue, outRecord), + ); + final record = outRecord.value; + if (record == nullptr) { + return; + } + try { + try { + _callback(_copyLogRecord(record.ref)); + } catch (_) { + // An exception must not escape into notification delivery. + } + } finally { + raw.mln_adapter_log_record_destroy(record.cast()); + } + } + }); + } @override void closeResources() { + raw.mln_adapter_log_queue_close(queue); calloc.free(pointer); listener.close(); + releaseListener.close(); } } @@ -80,9 +115,20 @@ final class Maplibre { Maplibre._(); static final MaplibreNativeCApi _c = MaplibreNativeCApi.open(); - // Retains the Dart listener while native code owns its callback pointer. + static final _logCallbackRoots = {}; static _LogCallbackState? _logCallbackState; + static void _releaseLogCallbackState(Pointer context) { + final state = _logCallbackRoots.remove(context.address); + if (state == null) { + return; + } + if (identical(_logCallbackState, state)) { + _logCallbackState = null; + } + state.close(); + } + /// Returns the native C ABI contract version. /// /// The one entry point that does not gate on [ensureAbiVersion], because it @@ -125,10 +171,12 @@ final class Maplibre { // native callback would still point at. ensureAbiVersion(); final state = _LogCallbackState(callback, consume: consume); + _logCallbackRoots[state.pointer.address] = state; try { _checkStatus(raw.mln_adapter_log_set_callback(state.pointer)); _logCallbackState = state; } catch (_) { + _logCallbackRoots.remove(state.pointer.address); state.close(); rethrow; } diff --git a/bindings/dart/lib/src/offline/offline.dart b/bindings/dart/lib/src/offline/offline.dart index a4971fb82..d8f75c540 100644 --- a/bindings/dart/lib/src/offline/offline.dart +++ b/bindings/dart/lib/src/offline/offline.dart @@ -1,4 +1,4 @@ -/// Offline region definitions, status values, metadata, and operation handles. +/// Offline region definitions, status values, metadata, and futures. library; import 'dart:typed_data'; diff --git a/bindings/dart/lib/src/render/render.dart b/bindings/dart/lib/src/render/render.dart index 64030bbe3..8f3f2f620 100644 --- a/bindings/dart/lib/src/render/render.dart +++ b/bindings/dart/lib/src/render/render.dart @@ -1,7 +1,6 @@ /// Render session, render target, texture frame, and readback APIs. library; -export '../runtime/runtime.dart' - show RenderResult, RenderSessionHandle, RenderUpdate; +export '../runtime/runtime.dart' show RenderResult, RenderSessionHandle; export 'native_pointer.dart'; export 'targets.dart'; diff --git a/bindings/dart/lib/src/render/targets.dart b/bindings/dart/lib/src/render/targets.dart index 33f4bafb5..1780fc14e 100644 --- a/bindings/dart/lib/src/render/targets.dart +++ b/bindings/dart/lib/src/render/targets.dart @@ -232,7 +232,8 @@ final class OpenGLContextProviderMask { 'OpenGLContextProviderMask[bits=0x${bits.toRadixString(16)}]'; } -/// How a session's OpenGL context relates to the thread that attached it. +/// How a session's OpenGL context relates to its driver thread and host +/// graphics state. /// /// Known values use the named constants. Values added by a newer compatible /// native library retain their raw integer through [fromRawValue]. @@ -321,8 +322,8 @@ sealed class OpenGLContextDescriptor { /// Whether the session shares its thread with host graphics work. /// - /// WGL and EGL surface sessions support both. A texture session hands its - /// texture to the host, so it is shared only. + /// A private EGL owned texture target uses dedicated ownership and grants + /// readback without frame acquisition. final OpenGLContextOwnership ownership; } @@ -387,6 +388,23 @@ final class EglContextDescriptor extends OpenGLContextDescriptor { final NativePointer getProcAddress; } +/// Existing or transferred browser WebGL context. +final class WebGLContextDescriptor extends OpenGLContextDescriptor { + /// Uses a host-created WebGL context on the caller graphics thread. + const WebGLContextDescriptor.existing(this.context) + : canvasSelector = null, + super(ownership: OpenGLContextOwnership.shared); + + /// Creates WebGL 2 on a core worker from a transferred canvas. + const WebGLContextDescriptor.transferredCanvas(this.canvasSelector) + : context = 0, + super(ownership: OpenGLContextOwnership.dedicated); + + final int context; + final String? canvasSelector; + bool get isTransferredCanvas => canvasSelector != null; +} + /// Metal native surface session attachment options. final class MetalSurfaceDescriptor { /// Creates a Metal surface descriptor. @@ -585,3 +603,63 @@ final class OpenGLBorrowedTextureDescriptor { /// Backend-native OpenGL texture target, such as `GL_TEXTURE_2D`. final int target; } + +/// WebGPU device, instance, and queue used by a render target. +final class WebGPUContextDescriptor { + const WebGPUContextDescriptor({ + this.instance = NativePointer.nullPointer, + required this.device, + this.queue = NativePointer.nullPointer, + }); + + final NativePointer instance; + final NativePointer device; + final NativePointer queue; +} + +/// WebGPU native-surface attachment options. +final class WebGPUSurfaceDescriptor { + const WebGPUSurfaceDescriptor({ + required this.extent, + required this.context, + required this.surface, + required this.format, + }); + + final RenderTargetExtent extent; + final WebGPUContextDescriptor context; + final NativePointer surface; + final int format; +} + +/// WebGPU session-owned texture-ring attachment options. +final class WebGPUOwnedTextureDescriptor { + const WebGPUOwnedTextureDescriptor({ + required this.extent, + required this.context, + }); + + final RenderTargetExtent extent; + final WebGPUContextDescriptor context; +} + +/// WebGPU caller-owned texture attachment options. +final class WebGPUBorrowedTextureDescriptor { + const WebGPUBorrowedTextureDescriptor({ + required this.extent, + required this.physicalWidth, + required this.physicalHeight, + required this.context, + required this.texture, + required this.textureView, + required this.format, + }); + + final RenderTargetExtent extent; + final int physicalWidth; + final int physicalHeight; + final WebGPUContextDescriptor context; + final NativePointer texture; + final NativePointer textureView; + final int format; +} diff --git a/bindings/dart/lib/src/runtime/runtime.dart b/bindings/dart/lib/src/runtime/runtime.dart index e35ef938c..77f339005 100644 --- a/bindings/dart/lib/src/runtime/runtime.dart +++ b/bindings/dart/lib/src/runtime/runtime.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -7,11 +9,12 @@ import '../camera/camera.dart'; import '../error/maplibre_exception.dart'; import '../geo/geo.dart'; import '../internal/callback/callback_state.dart'; +import '../internal/callback/completion.dart'; +import '../internal/callback/wake.dart'; import '../internal/c/maplibre_native_c.dart'; import '../internal/c/maplibre_native_c.g.dart' as raw; import '../internal/lifecycle/lifecycle.dart'; import '../internal/lifecycle/native_handles.dart'; -import '../internal/lifecycle/frame_construction.dart'; import '../internal/memory/memory.dart'; import '../internal/status/status.dart'; import '../internal/struct/struct.dart' as native_struct; @@ -70,11 +73,11 @@ CustomMvtVectorCallbackLifecycleProbe? customMvtVectorCallbackProbeForTesting( return state == null ? null : CustomMvtVectorCallbackLifecycleProbe._(state); } -/// Dart resource provider callback run on the owner isolate. +/// Dart resource provider callback run asynchronously on its receiver isolate. typedef ResourceProviderCallback = void Function(ResourceRequest request, ResourceRequestHandle handle); -/// Owner-isolate resource provider definition. +/// Receiver-isolate resource provider definition. final class ResourceProvider { /// Creates a resource provider with native-owned routing rules. ResourceProvider({ @@ -85,7 +88,7 @@ final class ResourceProvider { /// Exact routes handled by this provider. final List routes; - /// Callback invoked on the owner isolate for matching requests. + /// Callback invoked on the receiver isolate for matching requests. final ResourceProviderCallback callback; } @@ -117,114 +120,153 @@ final class RuntimeOptions { RuntimeEventMask(raw.mln_runtime_options_default().event_mask); } -/// Owner-thread runtime handle for MapLibre Native work and event draining. +/// Runtime handle with autonomous native execution. final class RuntimeHandle { - RuntimeHandle._(NativeRuntime handle) - : _state = NativeHandleState(handle, 'RuntimeHandle'); - + RuntimeHandle._(NativeRuntime handle, this._eventWake) + : _state = NativeHandleState(handle, 'RuntimeHandle') { + _callbackReleaseListener = + NativeCallable.listener( + _releaseNativeCallback, + ); + } final NativeHandleState _state; - - /// Batch struct reused by every drain, so a pump loop allocates nothing. - Pointer _eventBatch = - calloc(); + final NativeWakeState _eventWake; + late final NativeCallable + _callbackReleaseListener; final _maps = >{}; - final _offlineOperations = >{}; - _ResourceTransformState? _resourceTransformState; - _HttpHeaderTransformState? _httpHeaderTransformState; - _ResourceProviderRulesState? _resourceProviderRulesState; - _ResourceProviderCallbackState? _resourceProviderCallbackState; + final _queuedRuntimeEvents = []; + final _nativeCallbackReleases = {}; + final _resourceProviderQueues = {}; - /// Creates a runtime on the current native thread. - /// - /// Do not `await` I/O on this isolate while the runtime or any handle under it - /// is live. Owner-thread checks key on the OS thread, and the Dart VM may - /// resume an isolate on a different one, after which every call on the handle - /// fails with `wrongThread` — including [close]. - factory RuntimeHandle.create({ + void _releaseNativeCallback(Pointer userData) { + _nativeCallbackReleases.remove(userData.address)?.call(); + } + + void _registerNativeCallback( + Pointer userData, + void Function() release, + ) { + _nativeCallbackReleases[userData.address] = release; + } + + void _cancelNativeCallback(Pointer userData) { + _nativeCallbackReleases.remove(userData.address)?.call(); + } + + Future _startCommand( + NativeCompletionStart start, { + void Function()? onRejected, + }) => startNativeCompletion( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: 0, + start: start, + onRejected: onRejected, + acceptErrorStatus: true, + decode: (result) => CommandCompletion( + disposition: CommandDisposition.fromRawValue(result.disposition), + generation: uint64FromNative(result.generation), + status: MaplibreStatus.fromNativeStatusCode(result.status), + diagnostic: copyCompletionDiagnostic(result.diagnostic), + ), + ); + + Future _startValue({ + required raw.mln_adapter_completion_copy_kind copyKind, + required int elementSize, + required NativeCompletionStart start, + required NativeCompletionDecoder decode, + void Function()? onRejected, + }) => startNativeCompletion( + copyKind: copyKind, + elementSize: elementSize, + start: start, + decode: decode, + onRejected: onRejected, + ); + + /// Creates a runtime. + static RuntimeHandle create({ RuntimeOptions options = const RuntimeOptions(), }) { - // Check before the native call: an incompatible library would otherwise - // create a runtime that the failed check leaves no way to destroy. ensureAbiVersion(); - return withNativeArena((arena) { - final nativeOptions = arena(); - nativeOptions.ref = _runtimeOptionsToNative(options, arena); - final outRuntime = arena(); - outRuntime.value = 0; - _check(raw.mln_runtime_create(nativeOptions, outRuntime)); - return RuntimeHandle._(NativeRuntime(outRuntime.value)); + RuntimeHandle? runtime; + final eventWake = NativeWakeState(() { + scheduleMicrotask(() { + final target = runtime; + if (target != null && !target.isClosed) { + target._queuedRuntimeEvents.addAll( + target._drainNativeEvents().events, + ); + } + }); }); + var runtimeHandle = 0; + try { + runtimeHandle = withNativeArena((arena) { + final nativeOptions = arena(); + nativeOptions.ref = _runtimeOptionsToNative(options, arena); + eventWake.writeTo(nativeOptions.ref.event_wake); + final outRuntime = arena()..value = 0; + _check(raw.mln_runtime_create(nativeOptions, outRuntime)); + return outRuntime.value; + }); + final created = RuntimeHandle._(NativeRuntime(runtimeHandle), eventWake); + runtime = created; + runtimeHandle = 0; + return created; + } catch (_) { + if (runtimeHandle != 0) { + // No wrapper owns this handle, so nothing waits for its teardown. + _releaseRuntimeHandle(runtimeHandle).ignore(); + } + eventWake.reject(); + rethrow; + } } + /// Starts native teardown of an owned runtime handle. + static Future _releaseRuntimeHandle(int handle) => + startNativeCompletion( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: 0, + start: (completion) => raw.mln_runtime_release(handle, completion), + decode: (_) {}, + ); + NativeRuntime get _handle => _state.handle; /// Whether this runtime has been closed by the Dart binding. bool get isClosed => _state.isClosed; - /// Advances this runtime, parking the owner isolate until there is work. - /// - /// The default zero [timeout] drains without parking; a null timeout parks - /// until the wake flag is set; a negative timeout collapses to no wait. - /// Native work, a queued runtime event, and [WakeSource.signal] each set the - /// wake flag, which this call clears before returning. Drain events with - /// [drainEvents] after every return. - /// - /// An unread event returns this call without parking, whatever [timeout] asks - /// for, so a host that stopped draining before the queue emptied keeps making - /// progress. - /// - /// The default null [budget] drains without a bound. A non-null budget stops - /// the drain at the first task boundary after that long, measured from the - /// start of the drain; the first queued task always runs, so a bounded pump - /// always makes progress. Tasks left behind set the wake flag, so the next - /// pump returns without parking and continues them. The budget bounds the - /// task queues alone: expired timers and ready file descriptors are serviced - /// regardless, and a single task runs to completion once started. - /// - /// A non-null, non-zero [timeout] blocks the calling isolate's event loop for - /// the duration of the park. Acquire a [WakeSource] with [acquireWakeSource] - /// so another isolate can release this one. - void pump({Duration? timeout = Duration.zero, Duration? budget}) { - _check( - raw.mln_runtime_pump( - _handle.raw, - _pumpTimeoutMilliseconds(timeout), - _pumpBudgetMilliseconds(budget), - ), - ); - } - - /// Acquires a wake source that releases this runtime's parked owner isolate. - /// - /// Each call returns a distinct handle. A wake source holds its own reference - /// to the runtime's wake state, so the two close in either order. - WakeSource acquireWakeSource() { - return withNativeArena((arena) { - final outSource = arena(); - outSource.value = 0; - _check(raw.mln_runtime_wake_source_acquire(_handle.raw, outSource)); - return WakeSource._(NativeWakeSource(outSource.value)); - }); - } - /// Drains queued runtime events into one batch of Dart-owned copies. /// /// Events arrive in queue order. Every field, message, and payload is copied /// before this returns, so a batch stays readable for as long as the host /// keeps it. - /// - /// The default zero [maxEvents] drains every queued event. A positive value - /// drains at most that many and reports the rest in - /// [RuntimeEventBatch.remainingCount]. - RuntimeEventBatch drainEvents({int maxEvents = 0}) { - final handle = _handle; - if (maxEvents < 0) { - throwInvalidArgument('maxEvents must not be negative'); - } - final batch = _eventBatch; - batch.ref = raw.mln_runtime_event_batch_default(); - _check(raw.mln_runtime_drain_events(handle.raw, maxEvents, batch)); - return RuntimeEventBatch._fromNative(batch.ref, this); + RuntimeEventBatch drainEvents() { + final _ = _handle; + _queuedRuntimeEvents.addAll(_drainNativeEvents().events); + final events = List.of(_queuedRuntimeEvents); + _queuedRuntimeEvents.clear(); + return RuntimeEventBatch._(events: events); + } + + RuntimeEventBatch _drainNativeEvents() { + return withNativeArena((arena) { + final outBatch = arena()..value = 0; + _check(raw.mln_runtime_drain_events(_handle.raw, outBatch)); + final view = arena(); + view.ref.size = sizeOf(); + try { + _check(raw.mln_event_batch_get(outBatch.value, view)); + return RuntimeEventBatch._fromNative(view.ref, this); + } finally { + raw.mln_event_batch_release(outBatch.value); + } + }); } /// Selects which runtime-originated event types this runtime queues. @@ -249,368 +291,353 @@ final class RuntimeHandle { } /// Registers exact native-owned URL rewrite rules for network resources. - void setResourceUrlRewriteRules(List rules) { + /// + Future setResourceUrlRewriteRules( + List rules, + ) { final state = _ResourceTransformState(rules); - try { - withNativeArena((arena) { + final userData = state.pointer.cast(); + _registerNativeCallback(userData, state.close); + return _startCommand( + (completion) => withNativeArena((arena) { final transform = arena(); transform.ref.size = sizeOf(); transform.ref.callback = _c.adapterResourceTransformRewriteCallback(); - transform.ref.user_data = state.pointer.cast(); - _check(raw.mln_runtime_set_resource_transform(_handle.raw, transform)); - }); - _resourceTransformState?.close(); - _resourceTransformState = state; - } catch (_) { - state.close(); - rethrow; - } + transform.ref.user_data = userData; + transform.ref.release_user_data = + _callbackReleaseListener.nativeFunction; + return raw.mln_runtime_set_resource_transform( + _handle.raw, + transform, + completion, + ); + }), + onRejected: () => _cancelNativeCallback(userData), + ); } /// Clears runtime-scoped URL rewrite rules. - void clearResourceTransform() { - _check(raw.mln_runtime_clear_resource_transform(_handle.raw)); - _resourceTransformState?.close(); - _resourceTransformState = null; - } + /// + Future clearResourceTransform() => _startCommand( + (completion) => + raw.mln_runtime_clear_resource_transform(_handle.raw, completion), + ); /// Registers native-owned HTTP header routes evaluated on network threads. - void setHttpHeaderTransformRules(List rules) { + /// + Future setHttpHeaderTransformRules( + List rules, + ) { final state = _HttpHeaderTransformState(rules); - try { - withNativeArena((arena) { + final userData = state.pointer.cast(); + _registerNativeCallback(userData, state.close); + return _startCommand( + (completion) => withNativeArena((arena) { final transform = arena(); transform.ref.size = sizeOf(); transform.ref.callback = _c.adapterHttpHeaderTransformCallback(); - transform.ref.user_data = state.pointer.cast(); - _check( - raw.mln_runtime_set_http_header_transform(_handle.raw, transform), + transform.ref.user_data = userData; + transform.ref.release_user_data = + _callbackReleaseListener.nativeFunction; + return raw.mln_runtime_set_http_header_transform( + _handle.raw, + transform, + completion, ); - }); - _httpHeaderTransformState?.close(); - _httpHeaderTransformState = state; - } catch (_) { - state.close(); - rethrow; - } + }), + onRejected: () => _cancelNativeCallback(userData), + ); } /// Clears native-owned HTTP header transform routes. - void clearHttpHeaderTransform() { - _check(raw.mln_runtime_clear_http_header_transform(_handle.raw)); - _httpHeaderTransformState?.close(); - _httpHeaderTransformState = null; - } + /// + Future clearHttpHeaderTransform() => _startCommand( + (completion) => + raw.mln_runtime_clear_http_header_transform(_handle.raw, completion), + ); /// Registers or replaces exact native-owned response rules. - void setResourceProviderRules(List rules) { + /// + Future setResourceProviderRules( + List rules, + ) { final state = _ResourceProviderRulesState(rules); - try { - withNativeArena((arena) { + final userData = state.pointer.cast(); + _registerNativeCallback(userData, state.close); + return _startCommand( + (completion) => withNativeArena((arena) { final provider = arena(); provider.ref.size = sizeOf(); provider.ref.callback = _c.adapterResourceProviderRulesCallback(); - provider.ref.user_data = state.pointer.cast(); - _check(raw.mln_runtime_set_resource_provider(_handle.raw, provider)); - }); - _resourceProviderRulesState?.close(); - _resourceProviderRulesState = state; - _resourceProviderCallbackState?.retire(); - _resourceProviderCallbackState = null; - } catch (_) { - state.close(); - rethrow; - } + provider.ref.user_data = userData; + provider.ref.release_user_data = + _callbackReleaseListener.nativeFunction; + return raw.mln_runtime_set_resource_provider( + _handle.raw, + provider, + completion, + ); + }), + onRejected: () => _cancelNativeCallback(userData), + ); } /// Registers or replaces a queued Dart resource provider callback. - void setResourceProvider(ResourceProvider provider) { + /// + Future setResourceProvider(ResourceProvider provider) { final state = _ResourceProviderCallbackState(provider); - try { - withNativeArena((arena) { + _resourceProviderQueues[state.queue] = state; + final userData = state.pointer.cast(); + _registerNativeCallback( + userData, + () => _closeResourceProviderCallback(state), + ); + return _startCommand( + (completion) => withNativeArena((arena) { final nativeProvider = arena(); nativeProvider.ref.size = sizeOf(); nativeProvider.ref.callback = _c .adapterQueuedResourceProviderCallback(); - nativeProvider.ref.user_data = state.pointer.cast(); - _check( - raw.mln_runtime_set_resource_provider(_handle.raw, nativeProvider), + nativeProvider.ref.user_data = userData; + nativeProvider.ref.release_user_data = + _callbackReleaseListener.nativeFunction; + return raw.mln_runtime_set_resource_provider( + _handle.raw, + nativeProvider, + completion, ); - }); - _resourceProviderCallbackState?.retire(); - _resourceProviderCallbackState = state; - _resourceProviderRulesState?.close(); - _resourceProviderRulesState = null; - } catch (_) { - state.close(); - rethrow; - } + }), + onRejected: () => _cancelNativeCallback(userData), + ); } /// Clears the runtime-scoped network resource provider. - void clearResourceProvider() { - _check(raw.mln_runtime_clear_resource_provider(_handle.raw)); - _resourceProviderRulesState?.close(); - _resourceProviderRulesState = null; - _resourceProviderCallbackState?.retire(); - _resourceProviderCallbackState = null; + /// + Future clearResourceProvider() => _startCommand( + (completion) => + raw.mln_runtime_clear_resource_provider(_handle.raw, completion), + ); + + void _closeResourceProviderCallback(_ResourceProviderCallbackState state) { + _resourceProviderQueues.remove(state.queue); + state.retire(); } /// Starts an ambient cache maintenance operation. - OfflineOperationHandle runAmbientCacheOperation( - AmbientCacheOperation operation, - ) { - return withNativeArena((arena) { - final outOperationId = arena(); - _check( - raw.mln_runtime_run_ambient_cache_operation_start( + Future runAmbientCacheOperation(AmbientCacheOperation operation) => + _startUnit( + (completion) => raw.mln_runtime_run_ambient_cache_operation( _handle.raw, operation.rawValue, - outOperationId, + completion, ), ); - return OfflineOperationHandle._( - this, - outOperationId.value, - _OfflineOperationKind.ambientCache, - _OfflineOperationResultKind.none, - ); - }); - } /// Starts a change to this runtime's maximum ambient cache size. /// /// MapLibre evicts ambient resources to fit the new budget, so lowering it /// discards cached resources. Offline regions are unaffected. - OfflineOperationHandle setMaximumAmbientCacheSize(BigInt size) { - return withNativeArena((arena) { - final outOperationId = arena(); - _check( - raw.mln_runtime_set_maximum_ambient_cache_size_start( - _handle.raw, - uint64ToNative(size, 'maximum ambient cache size'), - outOperationId, - ), - ); - return OfflineOperationHandle._( - this, - outOperationId.value, - _OfflineOperationKind.setMaximumAmbientCacheSize, - _OfflineOperationResultKind.none, - ); - }); - } + Future setMaximumAmbientCacheSize(BigInt size) => _startUnit( + (completion) => raw.mln_runtime_set_maximum_ambient_cache_size( + _handle.raw, + uint64ToNative(size, 'maximum ambient cache size'), + completion, + ), + ); /// Starts creating an offline region. - OfflineOperationHandle createOfflineRegion( + Future createOfflineRegion( OfflineRegionDefinition definition, { Uint8List? metadata, }) { - return withNativeArena((arena) { - final nativeDefinition = arena(); - nativeDefinition.ref = _offlineRegionDefinitionToNative( - definition, - arena, - ); - final nativeMetadata = _nativeBytes(metadata, arena); - final outOperationId = arena(); - _check( - raw.mln_runtime_offline_region_create_start( + return _startOfflineValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeDefinition = arena(); + nativeDefinition.ref = _offlineRegionDefinitionToNative( + definition, + arena, + ); + final nativeMetadata = _nativeBytes(metadata, arena); + return raw.mln_runtime_offline_region_create( _handle.raw, nativeDefinition, nativeMetadata, metadata?.length ?? 0, - outOperationId, - ), - ); - return OfflineOperationHandle._( - this, - outOperationId.value, - _OfflineOperationKind.regionCreate, - _OfflineOperationResultKind.region, - ); - }); + completion, + ); + }), + decode: (result) => _offlineRegionInfoFromNative( + result.value.cast().ref, + ), + ); } /// Starts getting an offline region snapshot by ID. - OfflineOperationHandle getOfflineRegion(int regionId) => - _startOfflineOperation( - _OfflineOperationKind.regionGet, - _OfflineOperationResultKind.optionalRegion, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_get_start( - _handle.raw, - regionId, - outOperationId, - ), - ); - }, + Future getOfflineRegion(int regionId) => + _startOfflineValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS, + elementSize: sizeOf(), + start: (completion) => raw.mln_runtime_offline_region_get( + _handle.raw, + regionId, + completion, + ), + decode: (result) => result.value_count == 0 + ? null + : _offlineRegionInfoFromNative( + result.value.cast().ref, + ), ); /// Starts listing offline region snapshots. - OfflineOperationHandle listOfflineRegions() => _startOfflineOperation( - _OfflineOperationKind.regionsList, - _OfflineOperationResultKind.regionList, - (outOperationId) { - _check( - raw.mln_runtime_offline_regions_list_start(_handle.raw, outOperationId), - ); - }, + Future> listOfflineRegions() => _startOfflineRegions( + (completion) => + raw.mln_runtime_offline_regions_list(_handle.raw, completion), ); /// Starts merging offline regions from another database path. - OfflineOperationHandle mergeOfflineRegionDatabase(String sideDatabasePath) { - return withNativeArena((arena) { + Future> mergeOfflineRegionDatabase( + String sideDatabasePath, + ) => _startOfflineRegions( + (completion) => withNativeArena((arena) { final nativePath = nativeUtf8CString(sideDatabasePath, arena); - final outOperationId = arena(); - _check( - raw.mln_runtime_offline_regions_merge_database_start( - _handle.raw, - nativePath.pointer.cast(), - outOperationId, - ), - ); - return OfflineOperationHandle._( - this, - outOperationId.value, - _OfflineOperationKind.regionsMergeDatabase, - _OfflineOperationResultKind.regionList, + return raw.mln_runtime_offline_regions_merge_database( + _handle.raw, + nativePath.pointer.cast(), + completion, ); - }); - } + }), + ); /// Starts updating opaque offline region metadata. - OfflineOperationHandle updateOfflineRegionMetadata( + Future updateOfflineRegionMetadata( int regionId, Uint8List metadata, ) { - return withNativeArena((arena) { - final nativeMetadata = _nativeBytes(metadata, arena); - final outOperationId = arena(); - _check( - raw.mln_runtime_offline_region_update_metadata_start( + return _startOfflineValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeMetadata = _nativeBytes(metadata, arena); + return raw.mln_runtime_offline_region_update_metadata( _handle.raw, regionId, nativeMetadata, metadata.length, - outOperationId, - ), - ); - return OfflineOperationHandle._( - this, - outOperationId.value, - _OfflineOperationKind.regionUpdateMetadata, - _OfflineOperationResultKind.region, - ); - }); + completion, + ); + }), + decode: (result) => _offlineRegionInfoFromNative( + result.value.cast().ref, + ), + ); } /// Starts getting the current offline region status. - OfflineOperationHandle getOfflineRegionStatus(int regionId) => - _startOfflineOperation( - _OfflineOperationKind.regionGetStatus, - _OfflineOperationResultKind.regionStatus, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_get_status_start( - _handle.raw, - regionId, - outOperationId, - ), - ); - }, + Future getOfflineRegionStatus(int regionId) => + _startOfflineValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => raw.mln_runtime_offline_region_get_status( + _handle.raw, + regionId, + completion, + ), + decode: (result) => _offlineRegionStatusFromNative( + result.value.cast().ref, + ), ); /// Starts enabling or disabling offline region observation. - OfflineOperationHandle setOfflineRegionObserved( - int regionId, - bool observed, - ) => _startOfflineOperation( - _OfflineOperationKind.regionSetObserved, - _OfflineOperationResultKind.none, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_set_observed_start( + Future setOfflineRegionObserved(int regionId, bool observed) => + _startUnit( + (completion) => raw.mln_runtime_offline_region_set_observed( _handle.raw, regionId, observed, - outOperationId, + completion, ), ); - }, - ); /// Starts changing an offline region's download state. - OfflineOperationHandle setOfflineRegionDownloadState( + Future setOfflineRegionDownloadState( int regionId, OfflineRegionDownloadState state, - ) => _startOfflineOperation( - _OfflineOperationKind.regionSetDownloadState, - _OfflineOperationResultKind.none, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_set_download_state_start( - _handle.raw, - regionId, - state.rawValue, - outOperationId, - ), - ); - }, + ) => _startUnit( + (completion) => raw.mln_runtime_offline_region_set_download_state( + _handle.raw, + regionId, + state.rawValue, + completion, + ), ); /// Starts invalidating cached resources for an offline region. - OfflineOperationHandle invalidateOfflineRegion(int regionId) => - _startOfflineOperation( - _OfflineOperationKind.regionInvalidate, - _OfflineOperationResultKind.none, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_invalidate_start( - _handle.raw, - regionId, - outOperationId, - ), - ); - }, - ); + Future invalidateOfflineRegion(int regionId) => _startUnit( + (completion) => raw.mln_runtime_offline_region_invalidate( + _handle.raw, + regionId, + completion, + ), + ); /// Starts deleting an offline region. - OfflineOperationHandle deleteOfflineRegion(int regionId) => - _startOfflineOperation( - _OfflineOperationKind.regionDelete, - _OfflineOperationResultKind.none, - (outOperationId) { - _check( - raw.mln_runtime_offline_region_delete_start( - _handle.raw, - regionId, - outOperationId, - ), - ); - }, - ); + Future deleteOfflineRegion(int regionId) => _startUnit( + (completion) => raw.mln_runtime_offline_region_delete( + _handle.raw, + regionId, + completion, + ), + ); - OfflineOperationHandle _startOfflineOperation( - _OfflineOperationKind kind, - _OfflineOperationResultKind resultKind, - void Function(Pointer outOperationId) start, - ) { - return withNativeArena((arena) { - final outOperationId = arena(); - start(outOperationId); - return OfflineOperationHandle._( - this, - outOperationId.value, - kind, - resultKind, - ); - }); - } + Future _startUnit(NativeCompletionStart start) => _startValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: 0, + start: start, + decode: (_) {}, + ); + + Future _startOfflineValue({ + required raw.mln_adapter_completion_copy_kind copyKind, + required int elementSize, + required NativeCompletionStart start, + required NativeCompletionDecoder decode, + }) => _startValue( + copyKind: copyKind, + elementSize: elementSize, + start: start, + decode: decode, + ); + + Future> _startOfflineRegions( + NativeCompletionStart start, + ) => _startOfflineValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS, + elementSize: sizeOf(), + start: start, + decode: (result) => [ + for (var index = 0; index < result.value_count; index += 1) + _offlineRegionInfoFromNative( + result.value.cast()[index], + ), + ], + ); - /// Creates a map owned by this runtime. - MapHandle createMap({MapOptions options = const MapOptions()}) => + /// Creates a map without blocking the calling isolate. + Future createMap({MapOptions options = const MapOptions()}) => MapHandle.create(this, options: options); void _registerMap(MapHandle map) { @@ -621,122 +648,36 @@ final class RuntimeHandle { _maps.remove(id); } - void _registerOfflineOperation(OfflineOperationHandle operation) { - _offlineOperations[operation._id] = WeakReference(operation); - } - - void _unregisterOfflineOperationId(int id) { - _offlineOperations.remove(id); - } - - /// Explicitly destroys this runtime. - void close() { - final collectedOperationIds = _offlineOperations.entries - .where((entry) => entry.value.target == null) - .map((entry) => entry.key) - .toList(growable: false); - for (final operationId in collectedOperationIds) { - _check( - raw.mln_runtime_offline_operation_discard(_handle.raw, operationId), - ); - _offlineOperations.remove(operationId); - } - if (_offlineOperations.isNotEmpty) { - throwInvalidState( - 'RuntimeHandle has ${_offlineOperations.length} live offline ' - 'operation(s); take or discard every result before closing', - ); - } - _state.close( - (handle) => raw.mln_runtime_destroy(handle.raw), - _c.threadLastErrorMessage, - ); - if (_eventBatch != nullptr) { - calloc.free(_eventBatch); - _eventBatch = nullptr; - } - _resourceTransformState?.close(); - _resourceTransformState = null; - _httpHeaderTransformState?.close(); - _httpHeaderTransformState = null; - _resourceProviderRulesState?.close(); - _resourceProviderRulesState = null; - _resourceProviderCallbackState?.retire(); - _resourceProviderCallbackState = null; - } -} - -int _pumpTimeoutMilliseconds(Duration? timeout) { - if (timeout == null) { - return -1; - } - final milliseconds = timeout.inMilliseconds; - return milliseconds < 0 ? 0 : milliseconds; -} - -int _pumpBudgetMilliseconds(Duration? budget) { - if (budget == null) { - return -1; - } - final milliseconds = budget.inMilliseconds; - return milliseconds < 0 ? 0 : milliseconds; -} - -/// Releases a runtime owner isolate parked in [RuntimeHandle.pump]. -/// -/// [signal] runs from any isolate. Sending a wake source to another isolate -/// copies the wrapper around one shared native handle: every copy may signal, -/// and exactly one copy calls [close], after every signalling isolate has -/// finished. -/// -/// A wake source outlives its runtime; signalling after the runtime closes -/// does nothing. It carries no native finalizer, because a [Finalizable] class -/// cannot cross an isolate boundary, so an unclosed wake source leaks -/// silently. -final class WakeSource { - WakeSource._(this._handle); - - NativeWakeSource? _handle; - - /// Whether this wake source has released its native handle. - bool get isClosed => _handle == null; + /// Completes after every previously accepted runtime command. + Future barrier() => _startUnit( + (completion) => raw.mln_runtime_barrier(_handle.raw, completion), + ); - /// Sets the runtime's wake flag and releases the parked owner isolate. + /// Releases this runtime's public native handle and waits for native + /// teardown. /// - /// A signal raised while the owner isolate is running sets the flag, so the - /// next [RuntimeHandle.pump] returns without parking. - void signal() { - // Reachable from an isolate that has not yet run the ABI check. - ensureAbiVersion(); - final handle = _handle; - if (handle == null) { - throwInvalidArgument('WakeSource is closed'); - } - _check(raw.mln_wake_source_signal(handle.raw)); - } - - /// Releases the wake source. - void close() { - final handle = _handle; - if (handle == null) { - return; + /// The native handle is consumed before this returns to the event loop, so a + /// rejected release throws and leaves the runtime open. The returned future + /// completes after every earlier accepted submission, including released + /// maps' teardown, has finished and the runtime's threads and resources are + /// gone, so a host that awaits it may exit the process. + Future close() async { + await _state.closeAsync((handle) => _releaseRuntimeHandle(handle.raw)); + while (_nativeCallbackReleases.isNotEmpty) { + await Future.delayed(Duration.zero); } - _handle = null; - // Reachable from an isolate that has not yet run the ABI check. - ensureAbiVersion(); - raw.mln_wake_source_destroy(handle.raw); + await _eventWake.released; + _callbackReleaseListener.close(); } } /// One batch of runtime events copied out of the native event arena. final class RuntimeEventBatch { - RuntimeEventBatch._({ - required List events, - required this.remainingCount, - }) : events = List.unmodifiable(events); + RuntimeEventBatch._({required List events}) + : events = List.unmodifiable(events); factory RuntimeEventBatch._fromNative( - raw.mln_runtime_event_batch batch, + raw.mln_runtime_event_batch_view batch, RuntimeHandle runtime, ) { final eventSize = batch.event_size; @@ -746,25 +687,21 @@ final class RuntimeEventBatch { // Events are reached by the batch's own stride, because a later C API // version widens the record beyond the size this binding compiled // against. - final event = (first + index * eventSize).cast(); - events.add( - RuntimeEvent._fromNative(event, eventSize, batch.messages, runtime), + final nativeEvent = (first + index * eventSize) + .cast(); + final event = RuntimeEvent._fromNative( + nativeEvent, + eventSize, + batch.messages, + runtime, ); + events.add(event); } - return RuntimeEventBatch._( - events: events, - remainingCount: batch.remaining_count, - ); + return RuntimeEventBatch._(events: events); } /// Drained events in queue order. final List events; - - /// Events still queued after this batch. - /// - /// A nonzero count means another drain reports more events, so a host that - /// bounds a drain learns to come back. - final int remainingCount; } /// Event types a map or a runtime queues, as one bit per [RuntimeEventType]. @@ -799,8 +736,8 @@ final class RuntimeEventMask { static const offlineRegionStatusChanged = RuntimeEventMask(1 << 19); static const offlineRegionResponseError = RuntimeEventMask(1 << 20); static const offlineRegionTileCountLimitExceeded = RuntimeEventMask(1 << 21); - static const offlineOperationCompleted = RuntimeEventMask(1 << 22); - static const mapCameraTransitionFinished = RuntimeEventMask(1 << 23); + + static const mapCameraTransitionFinished = RuntimeEventMask(1 << 22); static const _mapEventBits = (1 << 1) | @@ -821,9 +758,8 @@ final class RuntimeEventMask { (1 << 16) | (1 << 17) | (1 << 18) | - (1 << 23); - static const _runtimeEventBits = - (1 << 19) | (1 << 20) | (1 << 21) | (1 << 22); + (1 << 22); + static const _runtimeEventBits = (1 << 19) | (1 << 20) | (1 << 21); /// Selects every map-originated event type this binding names. static const allMapEvents = RuntimeEventMask(_mapEventBits); @@ -884,7 +820,7 @@ final class RuntimeEvent { source: RuntimeEventSource._fromNative(value, runtime), code: value.code, payloadType: value.payload_type, - payload: RuntimeEventPayload._fromNative(event, eventSize, runtime), + payload: RuntimeEventPayload._fromNative(event, eventSize), message: _copyNativeString( (messages + value.message_offset).cast(), value.message_size, @@ -919,7 +855,7 @@ final class RuntimeEvent { /// Decodes a synthesized batch through the production decoder for tests. RuntimeEventBatch decodeRuntimeEventBatchForTesting( - raw.mln_runtime_event_batch batch, + raw.mln_runtime_event_batch_view batch, RuntimeHandle runtime, ) => RuntimeEventBatch._fromNative(batch, runtime); @@ -987,12 +923,9 @@ final class RuntimeEventType { 21, 'offlineRegionTileCountLimitExceeded', ); - static const offlineOperationCompleted = RuntimeEventType._( - 22, - 'offlineOperationCompleted', - ); + static const mapCameraTransitionFinished = RuntimeEventType._( - 23, + 22, 'mapCameraTransitionFinished', ); @@ -1018,8 +951,8 @@ final class RuntimeEventType { 19 => offlineRegionStatusChanged, 20 => offlineRegionResponseError, 21 => offlineRegionTileCountLimitExceeded, - 22 => offlineOperationCompleted, - 23 => mapCameraTransitionFinished, + + 22 => mapCameraTransitionFinished, _ => RuntimeEventType._(rawValue, 'unknown($rawValue)'), }; @@ -1225,86 +1158,47 @@ final class TileOperation { final String name; } -/// Offline operation kind reported by runtime events. -final class OfflineOperationKind { - const OfflineOperationKind._(this.rawValue, this.name); - - static const ambientCache = OfflineOperationKind._(1, 'ambientCache'); - static const regionCreate = OfflineOperationKind._(2, 'regionCreate'); - static const regionGet = OfflineOperationKind._(3, 'regionGet'); - static const regionsList = OfflineOperationKind._(4, 'regionsList'); - static const regionsMergeDatabase = OfflineOperationKind._( - 5, - 'regionsMergeDatabase', - ); - static const regionUpdateMetadata = OfflineOperationKind._( - 6, - 'regionUpdateMetadata', - ); - static const regionGetStatus = OfflineOperationKind._(7, 'regionGetStatus'); - static const regionSetObserved = OfflineOperationKind._( - 8, - 'regionSetObserved', - ); - static const regionSetDownloadState = OfflineOperationKind._( - 9, - 'regionSetDownloadState', - ); - static const regionInvalidate = OfflineOperationKind._( - 10, - 'regionInvalidate', - ); - static const regionDelete = OfflineOperationKind._(11, 'regionDelete'); - static const setMaximumAmbientCacheSize = OfflineOperationKind._( - 12, - 'setMaximumAmbientCacheSize', - ); - - factory OfflineOperationKind.fromRawValue(int rawValue) => switch (rawValue) { - 1 => ambientCache, - 2 => regionCreate, - 3 => regionGet, - 4 => regionsList, - 5 => regionsMergeDatabase, - 6 => regionUpdateMetadata, - 7 => regionGetStatus, - 8 => regionSetObserved, - 9 => regionSetDownloadState, - 10 => regionInvalidate, - 11 => regionDelete, - 12 => setMaximumAmbientCacheSize, - _ => OfflineOperationKind._(rawValue, 'unknown($rawValue)'), +/// Terminal disposition of an accepted runtime command. +final class CommandDisposition { + const CommandDisposition._(this.rawValue, this.name); + + static const committed = CommandDisposition._(0, 'committed'); + static const superseded = CommandDisposition._(1, 'superseded'); + static const failed = CommandDisposition._(2, 'failed'); + static const cancelled = CommandDisposition._(3, 'cancelled'); + + factory CommandDisposition.fromRawValue(int rawValue) => switch (rawValue) { + 0 => committed, + 1 => superseded, + 2 => failed, + 3 => cancelled, + _ => CommandDisposition._(rawValue, 'unknown($rawValue)'), }; final int rawValue; final String name; -} -/// Offline operation result kind reported by runtime events. -final class OfflineOperationResultKind { - const OfflineOperationResultKind._(this.rawValue, this.name); + @override + bool operator ==(Object other) => + other is CommandDisposition && other.rawValue == rawValue; - static const none = OfflineOperationResultKind._(0, 'none'); - static const region = OfflineOperationResultKind._(1, 'region'); - static const optionalRegion = OfflineOperationResultKind._( - 2, - 'optionalRegion', - ); - static const regionList = OfflineOperationResultKind._(3, 'regionList'); - static const regionStatus = OfflineOperationResultKind._(4, 'regionStatus'); + @override + int get hashCode => rawValue.hashCode; +} - factory OfflineOperationResultKind.fromRawValue(int rawValue) => - switch (rawValue) { - 0 => none, - 1 => region, - 2 => optionalRegion, - 3 => regionList, - 4 => regionStatus, - _ => OfflineOperationResultKind._(rawValue, 'unknown($rawValue)'), - }; +/// Terminal outcome of an ordered native command. +final class CommandCompletion { + const CommandCompletion({ + required this.disposition, + required this.generation, + required this.status, + required this.diagnostic, + }); - final int rawValue; - final String name; + final CommandDisposition disposition; + final BigInt generation; + final MaplibreStatus status; + final String diagnostic; } /// Typed runtime event payload copied into Dart-owned values. @@ -1314,7 +1208,6 @@ sealed class RuntimeEventPayload { factory RuntimeEventPayload._fromNative( Pointer event, int eventSize, - RuntimeHandle runtime, ) { final rawPayloadType = event.ref.payload_type; final payload = event.ref.payload; @@ -1330,11 +1223,7 @@ sealed class RuntimeEventPayload { 7 => _offlineRegionTileCountLimitPayload( payload.offline_region_tile_count_limit, ), - 8 => _offlineOperationCompletedPayload( - payload.offline_operation_completed, - runtime, - ), - 9 => _cameraTransitionFinishedPayload(payload.camera_transition_finished), + 8 => _cameraTransitionFinishedPayload(payload.camera_transition_finished), _ => RuntimeEventPayloadUnknown( rawPayloadType, _copyRuntimePayloadWindow(event, eventSize), @@ -1427,34 +1316,6 @@ final class RuntimeEventOfflineRegionTileCountLimit final BigInt limit; } -/// Offline operation completion event payload. -final class RuntimeEventOfflineOperationCompleted extends RuntimeEventPayload { - const RuntimeEventOfflineOperationCompleted({ - required this.operationId, - required this.operation, - required this.operationKind, - required this.rawOperationKind, - required this.resultKind, - required this.rawResultKind, - required this.resultStatus, - required this.rawResultStatus, - required this.found, - }) : super(8); - - /// Identity of the operation that completed. - final int operationId; - - /// Matching live operation, or null when it is no longer tracked. - final OfflineOperationHandle? operation; - final OfflineOperationKind operationKind; - final int rawOperationKind; - final OfflineOperationResultKind resultKind; - final int rawResultKind; - final MaplibreStatus resultStatus; - final int rawResultStatus; - final bool found; -} - /// Camera-transition-finished event payload. final class RuntimeEventCameraTransitionFinished extends RuntimeEventPayload { const RuntimeEventCameraTransitionFinished({required this.transitionId}) @@ -1525,21 +1386,6 @@ RuntimeEventPayload _offlineRegionTileCountLimitPayload( limit: uint64FromNative(value.limit), ); -RuntimeEventPayload _offlineOperationCompletedPayload( - raw.mln_runtime_event_offline_operation_completed value, - RuntimeHandle runtime, -) => RuntimeEventOfflineOperationCompleted( - operationId: value.operation_id, - operation: runtime._offlineOperations[value.operation_id]?.target, - operationKind: OfflineOperationKind.fromRawValue(value.operation_kind), - rawOperationKind: value.operation_kind, - resultKind: OfflineOperationResultKind.fromRawValue(value.result_kind), - rawResultKind: value.result_kind, - resultStatus: MaplibreStatus.fromNativeStatusCode(value.result_status), - rawResultStatus: value.result_status, - found: value.found, -); - RuntimeEventPayload _cameraTransitionFinishedPayload( raw.mln_runtime_event_camera_transition_finished value, ) => RuntimeEventCameraTransitionFinished( @@ -1710,7 +1556,7 @@ final class MapSize { /// Logical viewport height. final int height; - /// Map pixel ratio, fixed for the map lifetime. + /// Map pixel ratio. final double scaleFactor; @override @@ -1724,26 +1570,98 @@ final class MapSize { int get hashCode => Object.hash(width, height, scaleFactor); } +/// Immutable camera snapshot and its map generation. +final class CameraSnapshot { + /// Creates a copied camera snapshot. + const CameraSnapshot({required this.camera, required this.generation}); + + /// Camera options copied from native memory. + final CameraOptions camera; + + /// Generation of the complete map snapshot that supplied [camera]. + final BigInt generation; +} + +/// Immutable map state copied from the latest published generation. +/// +/// Every committed map command publishes a new map snapshot. A snapshot whose +/// [generation] is at or past a command completion's generation observes that +/// commit. +final class MapSnapshot { + /// Creates a copied map snapshot. + const MapSnapshot({ + required this.generation, + required this.debugOptions, + required this.camera, + required this.size, + required this.projectionMode, + required this.viewportOptions, + required this.fullyLoaded, + required this.renderingStatsViewEnabled, + required this.repaintDemand, + required this.eventMask, + required this.latestRenderUpdateGeneration, + required this.tileOptions, + required this.bounds, + required this.freeCameraOptions, + }); + + /// Generation of every field in this snapshot. + final BigInt generation; + + /// Debug overlay options copied from the snapshot. + final MapDebugOptions debugOptions; + + /// Camera copied from the snapshot. + final CameraOptions camera; + + /// Logical extent copied from the snapshot. + final MapSize size; + + /// Projection mode copied from the snapshot. + final ProjectionModeOptions projectionMode; + + /// Viewport options copied from the snapshot. + final MapViewportOptions viewportOptions; + + /// Whether every requested style and tile resource finished loading. + final bool fullyLoaded; + + /// Whether the rendering stats overlay is enabled. + final bool renderingStatsViewEnabled; + + /// Whether the map currently requests a repaint. + final bool repaintDemand; + + /// Map event types selected in this snapshot. + final RuntimeEventMask eventMask; + + /// Generation of the latest render update. + final BigInt latestRenderUpdateGeneration; + + /// Tile prefetch and LOD tuning controls copied from the snapshot. + final MapTileOptions tileOptions; + + /// Map camera constraints copied from the snapshot. + final BoundOptions bounds; + + /// Free camera position and orientation copied from the snapshot. + final FreeCameraOptions freeCameraOptions; +} + /// Owned prepared GeoJSON source data. /// /// [prepare] parses one complete UTF-8 GeoJSON document and tiles or clusters /// it into an immutable prepared index with its options baked in. Preparation -/// needs no runtime or map, and this handle is not pinned to a native thread, -/// so its isolate keeps using it after resuming from awaited I/O. Like every -/// owned handle, it belongs to the isolate that created it; its native id is -/// copiable identity only, so a raw id sent to another isolate cannot become -/// an operable handle there. +/// needs no runtime or map. Its native id is copiable identity only, so a raw +/// id sent to another isolate cannot become an operable handle there. /// /// Install calls borrow this handle, so one prepared value may be installed on /// any number of sources and closed at any time afterward; [close] never /// invalidates a source the data was installed on. final class GeoJsonSourceDataHandle { GeoJsonSourceDataHandle._(NativeGeoJsonSourceData handle) - : _state = NativeHandleState( - handle, - 'GeoJsonSourceDataHandle', - threadAffine: false, - ); + : _state = NativeHandleState(handle, 'GeoJsonSourceDataHandle'); /// Parses and tiles one complete UTF-8 GeoJSON document with [options]. /// @@ -1785,37 +1703,54 @@ final class GeoJsonSourceDataHandle { } } -/// Owner-thread map handle bound to a retained runtime. +/// Map handle bound to a retained runtime. final class MapHandle { - MapHandle._(this._runtime, NativeMap handle) + MapHandle._(this._runtime, NativeMap handle, this._acceptedEventMask) : _state = NativeHandleState(handle, 'MapHandle'); - /// Creates a map owned by [runtime]. - factory MapHandle.create( + /// Creates a map without blocking the calling isolate. + static Future create( RuntimeHandle runtime, { MapOptions options = const MapOptions(), - }) { - return withNativeArena((arena) { - final nativeOptions = arena(); - nativeOptions.ref = raw.mln_map_options_default(); - nativeOptions.ref.width = _positiveUint32(options.width, 'map width'); - nativeOptions.ref.height = _positiveUint32(options.height, 'map height'); - nativeOptions.ref.scale_factor = options.scaleFactor; - nativeOptions.ref.map_mode = options.mapMode.rawValue; - nativeOptions.ref.fast_pfor_enabled = options.fastPforEnabled; - nativeOptions.ref.event_mask = options.eventMask.value; - final outMap = arena(); - outMap.value = 0; - - _check(raw.mln_map_create(runtime._handle.raw, nativeOptions, outMap)); - final map = MapHandle._(runtime, NativeMap(outMap.value)); - runtime._registerMap(map); - return map; - }); + }) async { + final map = await runtime._startValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeOptions = arena(); + nativeOptions.ref = raw.mln_map_options_default(); + nativeOptions.ref.initial_extent.width = _positiveUint32( + options.width, + 'map width', + ); + nativeOptions.ref.initial_extent.height = _positiveUint32( + options.height, + 'map height', + ); + nativeOptions.ref.initial_extent.scale_factor = options.scaleFactor; + nativeOptions.ref.map_mode = options.mapMode.rawValue; + nativeOptions.ref.fast_pfor_enabled = options.fastPforEnabled; + nativeOptions.ref.event_mask = options.eventMask.value; + return raw.mln_map_create( + runtime._handle.raw, + nativeOptions, + completion, + ); + }), + decode: (result) => MapHandle._( + runtime, + NativeMap(result.value.cast().value), + options.eventMask, + ), + ); + runtime._registerMap(map); + return map; } final RuntimeHandle _runtime; final NativeHandleState _state; + RuntimeEventMask _acceptedEventMask; /// Callback roots of the custom-geometry sources this map still holds, each /// released by the C API's own release callback. @@ -1826,58 +1761,106 @@ final class MapHandle { bool get isClosed => _state.isClosed; NativeMap get _handle { - // Touching the runtime's handle keeps its owner-isolate check on this path. final _ = _runtime._handle; return _state.handle; } - /// Loads a style URL through MapLibre Native style APIs. - void setStyleUrl(String url) { - withNativeArena((arena) { + Future _startCommand(NativeCompletionStart start) => + _runtime._startCommand(start); + + Future _startCommandInArena( + void Function(Arena, Pointer) start, + ) => _startCommand( + (completion) => withNativeArena((arena) { + start(arena, completion); + return nativeStatusOk; + }), + ); + + Future _startMapValue({ + required raw.mln_adapter_completion_copy_kind copyKind, + required int elementSize, + required NativeCompletionStart start, + required NativeCompletionDecoder decode, + }) => _runtime._startValue( + copyKind: copyKind, + elementSize: elementSize, + start: start, + decode: decode, + ); + + /// Loads a style URL. + Future setStyleUrl(String url) => _startCommand( + (completion) => withNativeArena((arena) { final nativeUrl = nativeUtf8CString(url, arena); - _check( - raw.mln_map_set_style_url(_handle.raw, nativeUrl.pointer.cast()), + return raw.mln_map_set_style_url( + _handle.raw, + nativeUrl.pointer.cast(), + completion, ); - }); - } + }), + ); - /// Loads inline style JSON through MapLibre Native style APIs. - void setStyleJson(Uint8List json) { - withNativeArena((arena) { + /// Loads inline style JSON. + Future setStyleJson(Uint8List json) => _startCommand( + (completion) => withNativeArena((arena) { final nativeJson = nativeBufferView(json, arena); - _check(raw.mln_map_set_style_json(_handle.raw, nativeJson)); - }); - } + return raw.mln_map_set_style_json(_handle.raw, nativeJson, completion); + }), + ); /// Sets per-feature state on this map. - void setFeatureState(FeatureStateSelector selector, Uint8List state) { - withNativeArena((arena) { + Future setFeatureState( + FeatureStateSelector selector, + Uint8List state, + ) { + return _startCommandInArena((arena, completion) { final nativeSelector = _featureStateSelectorToNative(selector, arena); final nativeState = nativeBufferView(state, arena); _check( - raw.mln_map_set_feature_state(_handle.raw, nativeSelector, nativeState), + raw.mln_map_set_feature_state( + _handle.raw, + nativeSelector, + nativeState, + completion, + ), ); }); } - /// Copies per-feature state from this map. - Uint8List getFeatureState(FeatureStateSelector selector) { - return withNativeArena((arena) { - final nativeSelector = _featureStateSelectorToNative(selector, arena); - final outState = arena(); - outState.value = 0; - _check( - raw.mln_map_get_feature_state(_handle.raw, nativeSelector, outState), - ); - return copyOwnedBuffer(NativeOwnedBufferHandle(outState.value)); - }); + /// Copies per-feature state from this map after prior commands. + /// + /// Missing feature state is reported as an empty JSON object. + Future getFeatureState(FeatureStateSelector selector) { + return _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeSelector = _featureStateSelectorToNative(selector, arena); + return raw.mln_map_get_feature_state( + _handle.raw, + nativeSelector, + completion, + ); + }), + decode: (result) => + _copyBufferView(result.value.cast().ref), + ); } /// Removes per-feature state from this map. - void removeFeatureState(FeatureStateSelector selector) { - withNativeArena((arena) { + Future removeFeatureState(FeatureStateSelector selector) { + return _startCommandInArena((arena, completion) { final nativeSelector = _featureStateSelectorToNative(selector, arena); - _check(raw.mln_map_remove_feature_state(_handle.raw, nativeSelector)); + _check( + raw.mln_map_remove_feature_state( + _handle.raw, + nativeSelector, + completion, + ), + ); }); } @@ -1887,8 +1870,17 @@ final class MapHandle { /// runtime mutations do not change it, and a failed parse leaves the /// previously parsed document in place. The result is empty when no document /// has been parsed. - Uint8List getLoadedStyleJson() { - return _copyMapData(_handle, raw.mln_map_copy_loaded_style_json); + Future getLoadedStyleJson() { + return _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: (completion) => + raw.mln_map_loaded_style_json(_handle.raw, completion), + decode: (result) => + _copyBufferView(result.value.cast().ref), + ); } /// Returns the URL this map's style was last requested from. @@ -1897,436 +1889,337 @@ final class MapHandle { /// response arrives, and [setStyleJson] clears it, so this can disagree with /// [getLoadedStyleJson] while a load is in flight or after one fails. The /// result is empty when no URL bytes are available. - String getStyleUrl() { - return _copyMapText(_handle, raw.mln_map_copy_style_url); + Future getStyleUrl() async { + final bytes = await _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: (completion) => raw.mln_map_style_url(_handle.raw, completion), + decode: (result) => + _copyBufferView(result.value.cast().ref), + ); + return utf8.decode(bytes); } - /// Selects which map-originated event types this map queues. - /// - /// The map reads the bits in [RuntimeEventMask.allMapEvents] and ignores the - /// rest, so [RuntimeEventMask.all] selects every map-originated type. - /// Narrowing gates later events and keeps queued ones. A bit outside - /// [RuntimeEventMask.all] reports invalid argument. - /// - /// Select every event type this host reads. The mask applies during map - /// construction. - void setEventMask(RuntimeEventMask mask) { - _check(raw.mln_map_set_event_mask(_handle.raw, mask.value)); + /// Selects map-originated events. + Future setEventMask(RuntimeEventMask mask) { + final completion = _startCommand( + (completion) => + raw.mln_map_set_event_mask(_handle.raw, mask.value, completion), + ); + _acceptedEventMask = mask; + return completion; } - /// Reports which map-originated event types this map queues. - /// - /// This reports the mask the map was created with or last narrowed to. - RuntimeEventMask get eventMask { + /// Reports the most recently accepted map event mask. + RuntimeEventMask get eventMask => _acceptedEventMask; + + /// Copies the latest immutable map snapshot. + MapSnapshot snapshot() { return withNativeArena((arena) { - final outMask = arena(); - _check(raw.mln_map_get_event_mask(_handle.raw, outMask)); - return RuntimeEventMask(outMask.value); - }); - } + final outSnapshot = arena(); + outSnapshot.ref.size = sizeOf(); + _check(raw.mln_map_snapshot_get(_handle.raw, outSnapshot)); + final value = outSnapshot.ref; + return MapSnapshot( + generation: uint64FromNative(value.generation), + debugOptions: MapDebugOptions(value.debug_options), + camera: native_struct.cameraOptionsFromNative(value.camera), + size: MapSize( + width: value.logical_extent.width, + height: value.logical_extent.height, + scaleFactor: value.logical_extent.scale_factor, + ), + projectionMode: native_struct.projectionModeOptionsFromNative( + value.projection_mode, + ), + viewportOptions: native_struct.mapViewportOptionsFromNative( + value.viewport, + ), + fullyLoaded: value.fully_loaded, + renderingStatsViewEnabled: value.rendering_stats_view_enabled, + repaintDemand: value.repaint_demand, + eventMask: RuntimeEventMask(value.event_mask), + latestRenderUpdateGeneration: uint64FromNative( + value.latest_render_update_generation, + ), + tileOptions: native_struct.mapTileOptionsFromNative(value.tile), + bounds: native_struct.boundOptionsFromNative(value.bounds), + freeCameraOptions: native_struct.freeCameraOptionsFromNative( + value.free_camera, + ), + ); + }); + } + + /// Resizes the map. + Future resize(MapSize size) => _startCommand( + (completion) => withNativeArena((arena) { + final extent = arena(); + extent.ref.width = _positiveUint32(size.width, 'map width'); + extent.ref.height = _positiveUint32(size.height, 'map height'); + extent.ref.scale_factor = size.scaleFactor; + return raw.mln_map_resize(_handle.raw, extent.ref, completion); + }), + ); - /// Requests a repaint for a continuous map. - void requestRepaint() { - _check(raw.mln_map_request_repaint(_handle.raw)); - } + /// Requests a repaint. + Future requestRepaint() => _startCommand( + (completion) => raw.mln_map_request_repaint(_handle.raw, completion), + ); - /// Requests one still image for a static or tile map. - void requestStillImage() { - _check(raw.mln_map_request_still_image(_handle.raw)); - } + /// Requests one still image without blocking the calling isolate. + Future requestStillImage() => _runtime._startUnit( + (completion) => raw.mln_map_request_still_image(_handle.raw, completion), + ); /// Applies MapLibre debug overlay options. - void setDebugOptions(MapDebugOptions options) { - _check(raw.mln_map_set_debug_options(_handle.raw, options.bits)); - } - - /// Copies current MapLibre debug overlay options. - MapDebugOptions debugOptions() { - return withNativeArena((arena) { - final outOptions = arena(); - _check(raw.mln_map_get_debug_options(_handle.raw, outOptions)); - return MapDebugOptions(outOptions.value); - }); - } + Future setDebugOptions(MapDebugOptions options) => + _startCommand( + (completion) => raw.mln_map_set_debug_options( + _handle.raw, + options.bits, + completion, + ), + ); /// Dumps map debug logs through MapLibre Native logging. - void dumpDebugLogs() { - _check(raw.mln_map_dump_debug_logs(_handle.raw)); - } + Future dumpDebugLogs() => _startCommand( + (completion) => raw.mln_map_dump_debug_logs(_handle.raw, completion), + ); - /// A reference to this map for attaching a render session, safe to send to - /// another isolate. - /// - /// A render session belongs to the isolate that attached it, which need not - /// be the map's; every other map call stays on the map's own isolate. The - /// reference does not keep the map alive, and attaching after the map closes - /// reports invalid argument. - MapAttachRef attachRef() => MapAttachRef._(_handle.raw); - - /// Copies the current camera snapshot. - CameraOptions camera() { + /// Copies the latest camera snapshot. + CameraSnapshot cameraSnapshot() { return withNativeArena((arena) { final outCamera = arena(); outCamera.ref.size = sizeOf(); - _check(raw.mln_map_get_camera(_handle.raw, outCamera)); - return native_struct.cameraOptionsFromNative(outCamera.ref); - }); - } - - /// Applies a camera jump command. - void jumpTo(CameraOptions camera) { - withNativeArena((arena) { - final nativeCamera = _nativeCamera(camera, arena); - _check(raw.mln_map_jump_to(_handle.raw, nativeCamera)); - }); - } - - /// Applies a camera ease transition command. - void easeTo(CameraOptions camera, {AnimationOptions? animation}) { - withNativeArena((arena) { - final nativeCamera = _nativeCamera(camera, arena); - final nativeAnimation = _nativeAnimation(animation, arena); - _check(raw.mln_map_ease_to(_handle.raw, nativeCamera, nativeAnimation)); - }); - } - - /// Applies a camera fly transition command. - void flyTo(CameraOptions camera, {AnimationOptions? animation}) { - withNativeArena((arena) { - final nativeCamera = _nativeCamera(camera, arena); - final nativeAnimation = _nativeAnimation(animation, arena); - _check(raw.mln_map_fly_to(_handle.raw, nativeCamera, nativeAnimation)); - }); - } - - /// Applies a screen-space pan command. - void moveBy(double deltaX, double deltaY, {AnimationOptions? animation}) { - withNativeArena((arena) { - final nativeAnimation = _nativeAnimation(animation, arena); + final outGeneration = arena(); _check( - animation == null - ? raw.mln_map_move_by(_handle.raw, deltaX, deltaY) - : raw.mln_map_move_by_animated( - _handle.raw, - deltaX, - deltaY, - nativeAnimation, - ), + raw.mln_map_camera_snapshot_get(_handle.raw, outCamera, outGeneration), + ); + return CameraSnapshot( + camera: native_struct.cameraOptionsFromNative(outCamera.ref), + generation: uint64FromNative(outGeneration.value), ); }); } - /// Applies a screen-space zoom command. - void scaleBy( - double scale, { - ScreenPoint? anchor, - AnimationOptions? animation, - }) { - withNativeArena((arena) { - final nativeAnchor = _nativeScreenPoint(anchor, arena); - final nativeAnimation = _nativeAnimation(animation, arena); - _check( - animation == null - ? raw.mln_map_scale_by(_handle.raw, scale, nativeAnchor) - : raw.mln_map_scale_by_animated( - _handle.raw, - scale, - nativeAnchor, - nativeAnimation, - ), + /// Reads the camera after every previously accepted command. + Future queryCamera() => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => raw.mln_map_camera_query(_handle.raw, completion), + decode: (result) { + final value = result.value.cast().ref; + return CameraSnapshot( + camera: native_struct.cameraOptionsFromNative(value.camera), + generation: uint64FromNative(value.generation), ); - }); - } + }, + ); - /// Applies a screen-space rotate command. - void rotateBy( - ScreenPoint first, - ScreenPoint second, { + /// Submits an atomic camera update. + Future updateCamera( + CameraOptions camera, { + CameraUpdateMode mode = CameraUpdateMode.jump, AnimationOptions? animation, + int gesturePhase = 0, }) { - withNativeArena((arena) { - final nativeFirst = native_struct.screenPointToNative(first); - final nativeSecond = native_struct.screenPointToNative(second); - final nativeAnimation = _nativeAnimation(animation, arena); - _check( - animation == null - ? raw.mln_map_rotate_by(_handle.raw, nativeFirst, nativeSecond) - : raw.mln_map_rotate_by_animated( - _handle.raw, - nativeFirst, - nativeSecond, - nativeAnimation, - ), - ); - }); - } - - /// Applies a pitch delta command. - void pitchBy(double pitch, {AnimationOptions? animation}) { - withNativeArena((arena) { - final nativeAnimation = _nativeAnimation(animation, arena); - _check( - animation == null - ? raw.mln_map_pitch_by(_handle.raw, pitch) - : raw.mln_map_pitch_by_animated( - _handle.raw, - pitch, - nativeAnimation, - ), - ); - }); - } - - /// Cancels active camera transitions. - void cancelTransitions() { - _check(raw.mln_map_cancel_transitions(_handle.raw)); - } - - /// Marks whether a host-driven gesture is in progress. - /// - /// The flag stays set until the host clears it, so pair every `true` with a - /// `false`. - void setGestureInProgress(bool inProgress) { - _check(raw.mln_map_set_gesture_in_progress(_handle.raw, inProgress)); + return _startCommand( + (completion) => withNativeArena((arena) { + final update = arena(); + update.ref = raw.mln_camera_update_default(); + update.ref.mode = mode.rawValue; + update.ref.camera = _nativeCamera(camera, arena).ref; + if (animation != null) { + update.ref.animation = _nativeAnimation(animation, arena).ref; + } + update.ref.gesture_phase = gesturePhase; + return raw.mln_map_update_camera(_handle.raw, update, completion); + }), + ); } - /// Copies whether a host-driven gesture is currently in progress. - bool isGestureInProgress() { - return withNativeArena((arena) { - final outInProgress = arena(); - _check(raw.mln_map_is_gesture_in_progress(_handle.raw, outInProgress)); - return outInProgress.value; - }); + /// Submits one relative camera operation. + Future applyCameraDelta(CameraDelta delta) { + return _startCommand( + (completion) => withNativeArena((arena) { + final native = arena(); + native.ref = raw.mln_camera_delta_default(); + native.ref.kind = delta.kind.rawValue; + native.ref.offset = native_struct.screenPointToNative(delta.offset); + native.ref.amount = delta.amount; + if (delta.anchor case final anchor?) { + native.ref.has_anchor = true; + native.ref.anchor = native_struct.screenPointToNative(anchor); + } + native.ref.animation = _nativeAnimation(delta.animation, arena).ref; + return raw.mln_map_apply_camera_delta(_handle.raw, native, completion); + }), + ); } /// Enables or disables the rendering stats overlay. - void setRenderingStatsViewEnabled(bool enabled) { - _check(raw.mln_map_set_rendering_stats_view_enabled(_handle.raw, enabled)); - } - - /// Copies whether the rendering stats overlay is enabled. - bool renderingStatsViewEnabled() { - return withNativeArena((arena) { - final outEnabled = arena(); - _check( - raw.mln_map_get_rendering_stats_view_enabled(_handle.raw, outEnabled), - ); - return outEnabled.value; - }); - } - - /// Copies whether MapLibre currently considers the map fully loaded. - bool isFullyLoaded() { - return withNativeArena((arena) { - final outLoaded = arena(); - _check(raw.mln_map_is_fully_loaded(_handle.raw, outLoaded)); - return outLoaded.value; - }); - } - - /// Copies the current logical viewport size and map pixel ratio. - MapSize size() { - return withNativeArena((arena) { - final outWidth = arena(); - final outHeight = arena(); - final outScaleFactor = arena(); - _check( - raw.mln_map_get_size(_handle.raw, outWidth, outHeight, outScaleFactor), - ); - return MapSize( - width: outWidth.value, - height: outHeight.value, - scaleFactor: outScaleFactor.value, + Future setRenderingStatsViewEnabled(bool enabled) => + _startCommand( + (completion) => raw.mln_map_set_rendering_stats_view_enabled( + _handle.raw, + enabled, + completion, + ), ); - }); - } - - /// Copies live map viewport and render-transform controls. - MapViewportOptions viewportOptions() { - return withNativeArena((arena) { - final outOptions = arena(); - outOptions.ref.size = sizeOf(); - _check(raw.mln_map_get_viewport_options(_handle.raw, outOptions)); - return native_struct.mapViewportOptionsFromNative(outOptions.ref); - }); - } /// Applies selected live map viewport and render-transform controls. - void setViewportOptions(MapViewportOptions options) { - withNativeArena((arena) { - final nativeOptions = arena(); - nativeOptions.ref = native_struct.mapViewportOptionsToNative( - options, - raw.mln_map_viewport_options_default(), + Future setViewportOptions(MapViewportOptions options) => + _startCommand( + (completion) => withNativeArena((arena) { + final nativeOptions = arena(); + nativeOptions.ref = native_struct.mapViewportOptionsToNative( + options, + raw.mln_map_viewport_options_default(), + ); + return raw.mln_map_set_viewport_options( + _handle.raw, + nativeOptions, + completion, + ); + }), ); - _check(raw.mln_map_set_viewport_options(_handle.raw, nativeOptions)); - }); - } - - /// Copies tile prefetch and LOD tuning controls. - MapTileOptions tileOptions() { - return withNativeArena((arena) { - final outOptions = arena(); - outOptions.ref.size = sizeOf(); - _check(raw.mln_map_get_tile_options(_handle.raw, outOptions)); - return native_struct.mapTileOptionsFromNative(outOptions.ref); - }); - } /// Applies selected tile prefetch and LOD tuning controls. - void setTileOptions(MapTileOptions options) { - withNativeArena((arena) { - final nativeOptions = arena(); - nativeOptions.ref = native_struct.mapTileOptionsToNative( - options, - raw.mln_map_tile_options_default(), + Future setTileOptions(MapTileOptions options) => + _startCommand( + (completion) => withNativeArena((arena) { + final nativeOptions = arena(); + nativeOptions.ref = native_struct.mapTileOptionsToNative( + options, + raw.mln_map_tile_options_default(), + ); + return raw.mln_map_set_tile_options( + _handle.raw, + nativeOptions, + completion, + ); + }), ); - _check(raw.mln_map_set_tile_options(_handle.raw, nativeOptions)); - }); - } - - /// Copies map camera constraint options. - BoundOptions bounds() { - return withNativeArena((arena) { - final outOptions = arena(); - outOptions.ref.size = sizeOf(); - _check(raw.mln_map_get_bounds(_handle.raw, outOptions)); - return native_struct.boundOptionsFromNative(outOptions.ref); - }); - } /// Applies selected map camera constraint options. - void setBounds(BoundOptions options) { - withNativeArena((arena) { + Future setBounds(BoundOptions options) => _startCommand( + (completion) => withNativeArena((arena) { final nativeOptions = arena(); nativeOptions.ref = native_struct.boundOptionsToNative( options, raw.mln_bound_options_default(), ); - _check(raw.mln_map_set_bounds(_handle.raw, nativeOptions)); - }); - } - - /// Copies the current free camera position and orientation. - FreeCameraOptions freeCameraOptions() { - return withNativeArena((arena) { - final outOptions = arena(); - outOptions.ref.size = sizeOf(); - _check(raw.mln_map_get_free_camera_options(_handle.raw, outOptions)); - return native_struct.freeCameraOptionsFromNative(outOptions.ref); - }); - } + return raw.mln_map_set_bounds(_handle.raw, nativeOptions, completion); + }), + ); /// Applies selected free camera position and orientation fields. - void setFreeCameraOptions(FreeCameraOptions options) { - withNativeArena((arena) { - final nativeOptions = arena(); - nativeOptions.ref = native_struct.freeCameraOptionsToNative( - options, - raw.mln_free_camera_options_default(), + Future setFreeCameraOptions(FreeCameraOptions options) => + _startCommand( + (completion) => withNativeArena((arena) { + final nativeOptions = arena(); + nativeOptions.ref = native_struct.freeCameraOptionsToNative( + options, + raw.mln_free_camera_options_default(), + ); + return raw.mln_map_set_free_camera_options( + _handle.raw, + nativeOptions, + completion, + ); + }), ); - _check(raw.mln_map_set_free_camera_options(_handle.raw, nativeOptions)); - }); - } - /// Copies the current axonometric rendering options. - ProjectionModeOptions projectionMode() { - return withNativeArena((arena) { - final outMode = arena(); - outMode.ref.size = sizeOf(); - _check(raw.mln_map_get_projection_mode(_handle.raw, outMode)); - return native_struct.projectionModeOptionsFromNative(outMode.ref); - }); - } + /// Copies axonometric rendering options from the latest map snapshot. + ProjectionModeOptions projectionMode() => snapshot().projectionMode; /// Applies selected axonometric rendering option fields. - void setProjectionMode(ProjectionModeOptions mode) { - withNativeArena((arena) { - final nativeMode = arena(); - nativeMode.ref = native_struct.projectionModeOptionsToNative( - mode, - raw.mln_projection_mode_default(), - ); - _check(raw.mln_map_set_projection_mode(_handle.raw, nativeMode)); - }); - } + Future setProjectionMode(ProjectionModeOptions mode) => + _startCommand( + (completion) => withNativeArena((arena) { + final nativeMode = arena(); + nativeMode.ref = native_struct.projectionModeOptionsToNative( + mode, + raw.mln_projection_mode_default(), + ); + return raw.mln_map_set_projection_mode( + _handle.raw, + nativeMode, + completion, + ); + }), + ); - /// Computes a camera that fits geographic bounds in the current viewport. - CameraOptions cameraForLatLngBounds( + /// Computes a camera that fits geographic bounds after prior commands. + Future cameraForLatLngBounds( LatLngBounds bounds, { CameraFitOptions fitOptions = const CameraFitOptions(), - }) { - return withNativeArena((arena) { - final outCamera = arena(); - outCamera.ref.size = sizeOf(); - final nativeFitOptions = arena(); - nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( - fitOptions, - raw.mln_camera_fit_options_default(), - ); - _check( - raw.mln_map_camera_for_lat_lng_bounds( - _handle.raw, - native_struct.latLngBoundsToNative(bounds), - nativeFitOptions, - outCamera, - ), - ); - return native_struct.cameraOptionsFromNative(outCamera.ref); - }); - } + }) => _cameraFitQuery((arena, completion) { + final nativeFitOptions = arena(); + nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( + fitOptions, + raw.mln_camera_fit_options_default(), + ); + return raw.mln_map_camera_for_lat_lng_bounds( + _handle.raw, + native_struct.latLngBoundsToNative(bounds), + nativeFitOptions, + completion, + ); + }); - /// Computes a camera that fits geographic coordinates in the current viewport. - CameraOptions cameraForLatLngs( + /// Computes a camera that fits geographic coordinates after prior commands. + Future cameraForLatLngs( List coordinates, { CameraFitOptions fitOptions = const CameraFitOptions(), - }) { - return withNativeArena((arena) { - final outCamera = arena(); - outCamera.ref.size = sizeOf(); - final nativeFitOptions = arena(); - nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( - fitOptions, - raw.mln_camera_fit_options_default(), - ); - _check( - raw.mln_map_camera_for_lat_lngs( - _handle.raw, - _latLngArray(coordinates, arena), - coordinates.length, - nativeFitOptions, - outCamera, - ), - ); - return native_struct.cameraOptionsFromNative(outCamera.ref); - }); - } + }) => _cameraFitQuery((arena, completion) { + final nativeFitOptions = arena(); + nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( + fitOptions, + raw.mln_camera_fit_options_default(), + ); + return raw.mln_map_camera_for_lat_lngs( + _handle.raw, + _latLngArray(coordinates, arena), + coordinates.length, + nativeFitOptions, + completion, + ); + }); - /// Computes a camera that fits a geometry in the current viewport. - CameraOptions cameraForGeometry( + /// Computes a camera that fits geometry after prior commands. + Future cameraForGeometry( Uint8List geometry, { CameraFitOptions fitOptions = const CameraFitOptions(), - }) { - return withNativeArena((arena) { - final outCamera = arena(); - outCamera.ref.size = sizeOf(); - final nativeFitOptions = arena(); - nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( - fitOptions, - raw.mln_camera_fit_options_default(), - ); - final nativeGeometry = nativeBufferView(geometry, arena); - _check( - raw.mln_map_camera_for_geometry( - _handle.raw, - nativeGeometry, - nativeFitOptions, - outCamera, - ), - ); - return native_struct.cameraOptionsFromNative(outCamera.ref); - }); - } + }) => _cameraFitQuery((arena, completion) { + final nativeFitOptions = arena(); + nativeFitOptions.ref = native_struct.cameraFitOptionsToNative( + fitOptions, + raw.mln_camera_fit_options_default(), + ); + return raw.mln_map_camera_for_geometry( + _handle.raw, + nativeBufferView(geometry, arena), + nativeFitOptions, + completion, + ); + }); + + Future _cameraFitQuery( + int Function(Arena, Pointer) start, + ) => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) => start(arena, completion)), + decode: (result) => native_struct.cameraOptionsFromNative( + result.value.cast().ref, + ), + ); /// Computes geographic bounds for a camera from two viewport corners. /// @@ -2334,7 +2227,7 @@ final class MapHandle { /// that camera in the current viewport. When bearing and pitch are zero, the /// box equals the visible area. Those corners are the northwest and southeast /// of the viewport. Longitudes stay in -180 to 180. - LatLngBounds latLngBoundsForCamera(CameraOptions camera) => + Future latLngBoundsForCamera(CameraOptions camera) => _latLngBoundsForCamera(camera, unwrapped: false); /// Computes geographic bounds for a camera from the four viewport corners. @@ -2343,129 +2236,143 @@ final class MapHandle { /// the projected viewport. Longitudes unwrap onto the shortest path through /// the center. A viewport that crosses the antimeridian reports values outside /// -180 to 180. - LatLngBounds latLngBoundsForCameraUnwrapped(CameraOptions camera) => + Future latLngBoundsForCameraUnwrapped(CameraOptions camera) => _latLngBoundsForCamera(camera, unwrapped: true); - LatLngBounds _latLngBoundsForCamera( + Future _latLngBoundsForCamera( CameraOptions camera, { required bool unwrapped, - }) { - return withNativeArena((arena) { + }) => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { final nativeCamera = _nativeCamera(camera, arena); - final outBounds = arena(); - _check( - unwrapped - ? raw.mln_map_lat_lng_bounds_for_camera_unwrapped( - _handle.raw, - nativeCamera, - outBounds, - ) - : raw.mln_map_lat_lng_bounds_for_camera( - _handle.raw, - nativeCamera, - outBounds, - ), - ); - return native_struct.latLngBoundsFromNative(outBounds.ref); - }); - } + return unwrapped + ? raw.mln_map_lat_lng_bounds_for_camera_unwrapped( + _handle.raw, + nativeCamera, + completion, + ) + : raw.mln_map_lat_lng_bounds_for_camera( + _handle.raw, + nativeCamera, + completion, + ); + }), + decode: (result) => native_struct.latLngBoundsFromNative( + result.value.cast().ref, + ), + ); - /// Converts a geographic world coordinate to a screen point. - ScreenPoint pixelForLatLng(LatLng coordinate) { - return withNativeArena((arena) { - final outPoint = arena(); - _check( - raw.mln_map_pixel_for_lat_lng( - _handle.raw, - native_struct.latLngToNative(coordinate), - outPoint, - ), - ); - return native_struct.screenPointFromNative(outPoint.ref); - }); - } + /// Converts a geographic coordinate after prior commands. + Future pixelForLatLng(LatLng coordinate) => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => raw.mln_map_pixel_for_lat_lng( + _handle.raw, + native_struct.latLngToNative(coordinate), + completion, + ), + decode: (result) => native_struct.screenPointFromNative( + result.value.cast().ref, + ), + ); - /// Converts a screen point to a geographic world coordinate. - LatLng latLngForPixel(ScreenPoint point) { - return withNativeArena((arena) { - final outCoordinate = arena(); - _check( - raw.mln_map_lat_lng_for_pixel( - _handle.raw, - native_struct.screenPointToNative(point), - outCoordinate, - ), - ); - return native_struct.latLngFromNative(outCoordinate.ref); - }); - } + /// Converts a screen point after prior commands. + Future latLngForPixel(ScreenPoint point) => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => raw.mln_map_lat_lng_for_pixel( + _handle.raw, + native_struct.screenPointToNative(point), + completion, + ), + decode: (result) => native_struct.latLngFromNative( + result.value.cast().ref, + ), + ); - /// Converts geographic coordinates to screen points. - List pixelsForLatLngs(List coordinates) { - return withNativeArena((arena) { - final outPoints = coordinates.isEmpty - ? nullptr.cast() - : arena(coordinates.length); - _check( - raw.mln_map_pixels_for_lat_lngs( + /// Converts geographic coordinates after prior commands. + Future> pixelsForLatLngs(List coordinates) => + _coordinateListQuery( + sizeOf(), + (arena, completion) => raw.mln_map_pixels_for_lat_lngs( _handle.raw, _latLngArray(coordinates, arena), coordinates.length, - outPoints, + completion, + ), + (values, index) => native_struct.screenPointFromNative( + values.cast()[index], ), ); - return [ - for (var index = 0; index < coordinates.length; index += 1) - native_struct.screenPointFromNative(outPoints[index]), - ]; - }); - } - /// Converts screen points to geographic coordinates. - List latLngsForPixels(List points) { - return withNativeArena((arena) { + /// Converts screen points after prior commands. + Future> latLngsForPixels( + List points, + ) => _coordinateListQuery( + sizeOf(), + (arena, completion) { + // Keep argument storage alive until native copies it below. final nativePoints = points.isEmpty ? nullptr.cast() : arena(points.length); for (var index = 0; index < points.length; index += 1) { nativePoints[index] = native_struct.screenPointToNative(points[index]); } - final outCoordinates = points.isEmpty - ? nullptr.cast() - : arena(points.length); - _check( - raw.mln_map_lat_lngs_for_pixels( - _handle.raw, - nativePoints, - points.length, - outCoordinates, - ), + return raw.mln_map_lat_lngs_for_pixels( + _handle.raw, + nativePoints, + points.length, + completion, ); - return [ - for (var index = 0; index < points.length; index += 1) - native_struct.latLngFromNative(outCoordinates[index]), - ]; - }); - } + }, + (values, index) => + native_struct.latLngFromNative(values.cast()[index]), + ); - /// Creates a standalone projection helper from the current map transform. - MapProjectionHandle createProjection() { - return withNativeArena((arena) { - final outProjection = arena(); - outProjection.value = 0; - _check(raw.mln_map_projection_create(_handle.raw, outProjection)); - return MapProjectionHandle._(NativeMapProjection(outProjection.value)); - }); - } + Future> _coordinateListQuery( + int elementSize, + int Function(Arena, Pointer) start, + T Function(Pointer, int) convert, + ) => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: elementSize, + start: (completion) => withNativeArena((arena) => start(arena, completion)), + decode: (result) => [ + for (var index = 0; index < result.value_count; index += 1) + convert(result.value, index), + ], + ); + + /// Creates a projection helper without blocking the calling isolate. + /// + /// The projection copies this map's transform state after every earlier map + /// command and never observes map changes made after its creation. Every + /// later projection call is synchronous. + Future createProjection() => _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => + raw.mln_map_projection_create(_handle.raw, completion), + decode: (result) => MapProjectionHandle._( + NativeMapProjection(result.value.cast().value), + ), + ); /// Sets or replaces one runtime style image. - void setStyleImage( + Future setStyleImage( String imageId, PremultipliedRgba8Image image, { StyleImageOptions? options, }) { final resolvedOptions = options ?? StyleImageOptions(); - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(imageId, arena); final nativeImage = arena(); nativeImage.ref = _premultipliedRgba8ImageToNative(image, arena); @@ -2477,6 +2384,7 @@ final class MapHandle { nativeId.value, nativeImage, nativeOptions, + completion, ), ); }); @@ -2484,141 +2392,111 @@ final class MapHandle { /// Copies one runtime style image's stretchable intervals, or null when no /// image carries [imageId]. The record holds horizontal intervals first. - ({List stretchX, List stretchY})? + Future<({List stretchX, List stretchY})?> getStyleImageStretches(String imageId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(imageId, arena); - final outXCount = arena(); - final outYCount = arena(); - final outFound = arena(); - _check( - raw.mln_map_copy_style_image_stretches( - _handle.raw, - nativeId.value, - nullptr, - 0, - outXCount, - nullptr, - 0, - outYCount, - outFound, - ), - ); - if (!outFound.value) { - return null; - } - - final xCount = outXCount.value; - final yCount = outYCount.value; - final rawX = xCount == 0 - ? nullptr.cast() - : arena(xCount); - final rawY = yCount == 0 - ? nullptr.cast() - : arena(yCount); - _check( - raw.mln_map_copy_style_image_stretches( + return _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE_STRETCHES, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeId = nativeStringView(imageId, arena); + return raw.mln_map_copy_style_image_stretches( _handle.raw, nativeId.value, - rawX, - xCount, - outXCount, - rawY, - yCount, - outYCount, - outFound, - ), - ); - List read( - Pointer array, - int count, - ) => List.generate( - count, - (index) => ImageStretch(array[index].from, array[index].to), - ); - return (stretchX: read(rawX, xCount), stretchY: read(rawY, yCount)); - }); - } - - /// Removes one runtime style image and returns whether one was removed. - bool removeStyleImage(String imageId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(imageId, arena); - final outRemoved = arena(); - _check( - raw.mln_map_remove_style_image(_handle.raw, nativeId.value, outRemoved), - ); - return outRemoved.value; - }); + completion, + ); + }), + decode: (result) { + if (result.value_count == 0) return null; + final value = result.value + .cast() + .ref; + List read( + Pointer array, + int count, + ) => List.generate( + count, + (index) => ImageStretch(array[index].from, array[index].to), + ); + return ( + stretchX: read(value.stretch_x, value.stretch_x_count), + stretchY: read(value.stretch_y, value.stretch_y_count), + ); + }, + ); } - /// Reports whether one runtime style image exists. - bool styleImageExists(String imageId) { - return withNativeArena((arena) { + /// Removes one runtime style image. + /// + /// The command fails with [MaplibreStatus.notFound] when no runtime style + /// image has [imageId]. + Future removeStyleImage(String imageId) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(imageId, arena); - final outExists = arena(); _check( - raw.mln_map_style_image_exists(_handle.raw, nativeId.value, outExists), + raw.mln_map_remove_style_image(_handle.raw, nativeId.value, completion), ); - return outExists.value; }); } /// Copies fixed metadata for one runtime style image. - StyleImageInfo? getStyleImageInfo(String imageId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(imageId, arena); - final outInfo = arena(); - outInfo.ref = raw.mln_style_image_info_default(); - final outFound = arena(); - _check( - raw.mln_map_get_style_image_info( - _handle.raw, - nativeId.value, - outInfo, - outFound, - ), - ); - return outFound.value ? _styleImageInfoFromNative(outInfo.ref) : null; - }); + Future getStyleImageInfo(String imageId) { + return _startStyleImage(imageId).then((image) => image?.info); } - /// Copies one runtime style image as premultiplied RGBA8 pixels. - StyleImage? copyStyleImagePremultipliedRgba8(String imageId) { - final info = getStyleImageInfo(imageId); - if (info == null) { - return null; - } - return withNativeArena((arena) { + Future _startStyleImage(String imageId) => _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { final nativeId = nativeStringView(imageId, arena); - final pixels = info.byteLength == 0 - ? nullptr.cast() - : arena(info.byteLength); - final outByteLength = arena(); - final outFound = arena(); - _check( - raw.mln_map_copy_style_image_premultiplied_rgba8( - _handle.raw, - nativeId.value, - pixels, - info.byteLength, - outByteLength, - outFound, - ), + return raw.mln_map_get_style_image_info( + _handle.raw, + nativeId.value, + completion, ); - if (!outFound.value) { - return null; - } + }), + decode: (result) { + if (result.value_count == 0) return null; + final value = result.value.cast().ref; return StyleImage( - info: info, - bytes: Uint8List.fromList(pixels.asTypedList(outByteLength.value)), + info: _styleImageInfoFromNative(value.info), + bytes: _copyBufferView(value.pixels), ); - }); - } + }, + ); + + /// Copies one runtime style image as premultiplied RGBA8 pixels. + Future copyStyleImagePremultipliedRgba8(String imageId) => + _startStyleImage(imageId); + + Future _copyLayerText( + String layerId, + int Function(int, raw.mln_buffer_view, Pointer) start, + ) => _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeId = nativeStringView(layerId, arena); + return start(_handle.raw, nativeId.value, completion); + }), + decode: (result) => utf8.decode( + _copyBufferView(result.value.cast().ref), + ), + ); /// Adds one style source from a style-spec source JSON object. - void addStyleSourceJson(String sourceId, Uint8List sourceJson) { - withNativeArena((arena) { + /// + /// Resolves with the command's terminal disposition and snapshot generation. + Future addStyleSourceJson( + String sourceId, + Uint8List sourceJson, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeSourceJson = nativeBufferView(sourceJson, arena); _check( @@ -2626,18 +2504,19 @@ final class MapHandle { _handle.raw, nativeId.value, nativeSourceJson, + completion, ), ); }); } /// Adds a GeoJSON source that loads from [url]. - void addGeoJsonSourceUrl( + Future addGeoJsonSourceUrl( String sourceId, String url, { GeoJsonSourceOptions? options, }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); final nativeOptions = _nativeGeoJsonSourceOptions( @@ -2650,6 +2529,7 @@ final class MapHandle { nativeId.value, nativeUrl.value, nativeOptions, + completion, ), ); }); @@ -2657,25 +2537,30 @@ final class MapHandle { /// Adds a GeoJSON source with prepared inline [data]. /// - /// The call borrows [data]'s handle and the source retains the prepared - /// index, adopting the options the data was prepared with, fixed for the - /// lifetime of the source. - void addGeoJsonSourceData(String sourceId, GeoJsonSourceDataHandle data) { - withNativeArena((arena) { + /// The command borrows [data]'s handle before returning and the source + /// retains the prepared index, adopting the options the data was prepared + /// with, fixed for the lifetime of the source, so the data may be closed + /// as soon as this returns. + Future addGeoJsonSourceData( + String sourceId, + GeoJsonSourceDataHandle data, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_add_geojson_source_data( _handle.raw, nativeId.value, data._state.handle.raw, + completion, ), ); }); } /// Updates one GeoJSON source to load from [url]. - void setGeoJsonSourceUrl(String sourceId, String url) { - withNativeArena((arena) { + Future setGeoJsonSourceUrl(String sourceId, String url) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2683,6 +2568,7 @@ final class MapHandle { _handle.raw, nativeId.value, nativeUrl.value, + completion, ), ); }); @@ -2690,18 +2576,23 @@ final class MapHandle { /// Updates one GeoJSON source with prepared inline [data]. /// - /// The call borrows [data]'s handle. Installing is cheap: the heavy parse - /// and tiling already ran in [GeoJsonSourceDataHandle.prepare]. Data whose - /// baked-in options differ from the source's is rejected as invalid - /// argument, with cluster properties excepted from the comparison. - void setGeoJsonSourceData(String sourceId, GeoJsonSourceDataHandle data) { - withNativeArena((arena) { + /// The command borrows [data]'s handle before returning. Installing is + /// cheap: the heavy parse and tiling already ran in + /// [GeoJsonSourceDataHandle.prepare]. Data whose baked-in options differ + /// from the source's fails the command as an invalid argument, with cluster + /// properties excepted from the comparison. + Future setGeoJsonSourceData( + String sourceId, + GeoJsonSourceDataHandle data, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_set_geojson_source_data( _handle.raw, nativeId.value, data._state.handle.raw, + completion, ), ); }); @@ -2712,26 +2603,30 @@ final class MapHandle { /// Tiles are sliced inline during the update pass when either the source's /// baked-in [GeoJsonSourceOptions.synchronousTiling] option or this override /// enables it. - void setGeoJsonSourceSynchronousTiling(String sourceId, bool enabled) { - withNativeArena((arena) { + Future setGeoJsonSourceSynchronousTiling( + String sourceId, + bool enabled, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_set_geojson_source_synchronous_tiling( _handle.raw, nativeId.value, enabled, + completion, ), ); }); } /// Adds a vector source with a TileJSON URL. - void addVectorSourceUrl( + Future addVectorSourceUrl( String sourceId, String url, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2740,18 +2635,19 @@ final class MapHandle { nativeId.value, nativeUrl.value, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds a vector source with inline tile URL templates. - void addVectorSourceTiles( + Future addVectorSourceTiles( String sourceId, List tiles, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_add_vector_source_tiles( @@ -2760,18 +2656,19 @@ final class MapHandle { _stringViewArray(tiles, arena), tiles.length, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds a raster source with a TileJSON URL. - void addRasterSourceUrl( + Future addRasterSourceUrl( String sourceId, String url, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2780,18 +2677,19 @@ final class MapHandle { nativeId.value, nativeUrl.value, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds a raster source with inline tile URL templates. - void addRasterSourceTiles( + Future addRasterSourceTiles( String sourceId, List tiles, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_add_raster_source_tiles( @@ -2800,18 +2698,19 @@ final class MapHandle { _stringViewArray(tiles, arena), tiles.length, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds a raster DEM source with a TileJSON URL. - void addRasterDemSourceUrl( + Future addRasterDemSourceUrl( String sourceId, String url, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2820,18 +2719,19 @@ final class MapHandle { nativeId.value, nativeUrl.value, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds a raster DEM source with inline tile URL templates. - void addRasterDemSourceTiles( + Future addRasterDemSourceTiles( String sourceId, List tiles, { TileSourceOptions options = const TileSourceOptions(), }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_add_raster_dem_source_tiles( @@ -2840,18 +2740,19 @@ final class MapHandle { _stringViewArray(tiles, arena), tiles.length, _nativeTileSourceOptions(options, arena), + completion, ), ); }); } /// Adds an image source that loads its image from [url]. - void addImageSourceUrl( + Future addImageSourceUrl( String sourceId, List coordinates, String url, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2861,18 +2762,19 @@ final class MapHandle { _latLngArray(coordinates, arena), coordinates.length, nativeUrl.value, + completion, ), ); }); } /// Adds an image source with inline image pixels. - void addImageSourceImage( + Future addImageSourceImage( String sourceId, List coordinates, PremultipliedRgba8Image image, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeImage = arena(); nativeImage.ref = _premultipliedRgba8ImageToNative(image, arena); @@ -2883,14 +2785,15 @@ final class MapHandle { _latLngArray(coordinates, arena), coordinates.length, nativeImage, + completion, ), ); }); } /// Updates an image source to load from [url]. - void setImageSourceUrl(String sourceId, String url) { - withNativeArena((arena) { + Future setImageSourceUrl(String sourceId, String url) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeUrl = nativeStringView(url, arena); _check( @@ -2898,14 +2801,18 @@ final class MapHandle { _handle.raw, nativeId.value, nativeUrl.value, + completion, ), ); }); } /// Updates an image source with inline image pixels. - void setImageSourceImage(String sourceId, PremultipliedRgba8Image image) { - withNativeArena((arena) { + Future setImageSourceImage( + String sourceId, + PremultipliedRgba8Image image, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeImage = arena(); nativeImage.ref = _premultipliedRgba8ImageToNative(image, arena); @@ -2914,14 +2821,18 @@ final class MapHandle { _handle.raw, nativeId.value, nativeImage, + completion, ), ); }); } /// Updates image source coordinates. - void setImageSourceCoordinates(String sourceId, List coordinates) { - withNativeArena((arena) { + Future setImageSourceCoordinates( + String sourceId, + List coordinates, + ) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_set_image_source_coordinates( @@ -2929,35 +2840,35 @@ final class MapHandle { nativeId.value, _latLngArray(coordinates, arena), coordinates.length, + completion, ), ); }); } /// Copies image source coordinates, or null when the source is missing. - List? getImageSourceCoordinates(String sourceId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(sourceId, arena); - final outCoordinates = arena(4); - final outCount = arena(); - final outFound = arena(); - _check( - raw.mln_map_get_image_source_coordinates( + Future?> getImageSourceCoordinates(String sourceId) { + return _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { + final nativeId = nativeStringView(sourceId, arena); + return raw.mln_map_get_image_source_coordinates( _handle.raw, nativeId.value, - outCoordinates, - 4, - outCount, - outFound, - ), - ); - return outFound.value - ? [ - for (var index = 0; index < outCount.value; index += 1) - native_struct.latLngFromNative(outCoordinates[index]), - ] - : null; - }); + completion, + ); + }), + decode: (result) => result.value_count == 0 + ? null + : [ + for (var index = 0; index < result.value_count; index += 1) + native_struct.latLngFromNative( + result.value.cast()[index], + ), + ], + ); } /// Adds a custom geometry source with queued fetch/cancel notifications. @@ -2965,7 +2876,7 @@ final class MapHandle { /// The C API releases the source's callback root when the source is removed, /// when a style load drops it, or when this map is destroyed, so a host that /// adds a source subscribes to nothing to keep it alive. - void addCustomGeometrySource( + Future addCustomGeometrySource( String sourceId, CustomGeometrySourceOptions options, ) { @@ -2973,36 +2884,40 @@ final class MapHandle { options, () => _releaseCustomGeometryCallbacks(sourceId), ); - try { - withNativeArena((arena) { + final future = _runtime._startCommand( + (completion) => withNativeArena((arena) { final nativeId = nativeStringView(sourceId, arena); final nativeOptions = arena(); nativeOptions.ref = _customGeometrySourceOptionsToNative( options, callbackState, ); - _check( - raw.mln_map_add_custom_geometry_source( - _handle.raw, - nativeId.value, - nativeOptions, - ), + return raw.mln_map_add_custom_geometry_source( + _handle.raw, + nativeId.value, + nativeOptions, + completion, ); - }); - _customGeometryCallbacks[sourceId] = callbackState; - } catch (_) { - callbackState.close(); - rethrow; - } + }), + onRejected: callbackState.close, + ); + future.then((completion) { + if (completion.disposition == CommandDisposition.committed) { + _customGeometryCallbacks[sourceId] = callbackState; + } else { + callbackState.close(); + } + }, onError: (_, _) => callbackState.close()); + return future; } /// Sets custom geometry source data for one canonical tile. - void setCustomGeometrySourceTileData( + Future setCustomGeometrySourceTileData( String sourceId, CanonicalTileId tileId, Uint8List data, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeData = nativeBufferView(data, arena); _check( @@ -3011,40 +2926,43 @@ final class MapHandle { nativeId.value, _canonicalTileIdToNative(tileId), nativeData, + completion, ), ); }); } /// Invalidates custom geometry source data for one canonical tile. - void invalidateCustomGeometrySourceTile( + Future invalidateCustomGeometrySourceTile( String sourceId, CanonicalTileId tileId, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_invalidate_custom_geometry_source_tile( _handle.raw, nativeId.value, _canonicalTileIdToNative(tileId), + completion, ), ); }); } /// Invalidates custom geometry source data inside one geographic region. - void invalidateCustomGeometrySourceRegion( + Future invalidateCustomGeometrySourceRegion( String sourceId, LatLngBounds bounds, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_invalidate_custom_geometry_source_region( _handle.raw, nativeId.value, native_struct.latLngBoundsToNative(bounds), + completion, ), ); }); @@ -3055,7 +2973,7 @@ final class MapHandle { /// The C API releases the source's callback root when the source is removed, /// when a style load drops it, or when this map is destroyed, so a host that /// adds a source subscribes to nothing to keep it alive. - void addCustomMvtVectorSource( + Future addCustomMvtVectorSource( String sourceId, CustomMvtVectorSourceOptions options, ) { @@ -3063,36 +2981,40 @@ final class MapHandle { options, () => _releaseCustomMvtVectorCallbacks(sourceId), ); - try { - withNativeArena((arena) { + final future = _runtime._startCommand( + (completion) => withNativeArena((arena) { final nativeId = nativeStringView(sourceId, arena); final nativeOptions = arena(); nativeOptions.ref = _customMvtVectorSourceOptionsToNative( options, callbackState, ); - _check( - raw.mln_map_add_custom_mvt_vector_source( - _handle.raw, - nativeId.value, - nativeOptions, - ), + return raw.mln_map_add_custom_mvt_vector_source( + _handle.raw, + nativeId.value, + nativeOptions, + completion, ); - }); - _customMvtVectorCallbacks[sourceId] = callbackState; - } catch (_) { - callbackState.close(); - rethrow; - } + }), + onRejected: callbackState.close, + ); + future.then((completion) { + if (completion.disposition == CommandDisposition.committed) { + _customMvtVectorCallbacks[sourceId] = callbackState; + } else { + callbackState.close(); + } + }, onError: (_, _) => callbackState.close()); + return future; } /// Sets custom MVT vector source data for one canonical tile. - void setCustomMvtVectorSourceTileData( + Future setCustomMvtVectorSourceTileData( String sourceId, CanonicalTileId tileId, Uint8List data, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeData = nativeBufferView(data, arena); _check( @@ -3101,18 +3023,19 @@ final class MapHandle { nativeId.value, _canonicalTileIdToNative(tileId), nativeData, + completion, ), ); }); } /// Reports a custom MVT vector source error for one canonical tile. - void setCustomMvtVectorSourceTileError( + Future setCustomMvtVectorSourceTileError( String sourceId, CanonicalTileId tileId, String message, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); final nativeMessage = nativeStringView(message, arena); _check( @@ -3121,75 +3044,66 @@ final class MapHandle { nativeId.value, _canonicalTileIdToNative(tileId), nativeMessage.value, + completion, ), ); }); } /// Invalidates custom MVT vector source data for one canonical tile. - void invalidateCustomMvtVectorSourceTile( + Future invalidateCustomMvtVectorSourceTile( String sourceId, CanonicalTileId tileId, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); _check( raw.mln_map_invalidate_custom_mvt_vector_source_tile( _handle.raw, nativeId.value, _canonicalTileIdToNative(tileId), + completion, ), ); }); } - /// Reports whether a style source ID exists. - bool styleSourceExists(String sourceId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(sourceId, arena); - final outExists = arena(); - _check( - raw.mln_map_style_source_exists(_handle.raw, nativeId.value, outExists), - ); - return outExists.value; - }); - } - - /// Removes one style source by ID and returns whether one was removed. - bool removeStyleSource(String sourceId) { - return withNativeArena((arena) { + /// Removes one style source by ID. + /// + /// The command fails with [MaplibreStatus.notFound] when no style source + /// has [sourceId], and with [MaplibreStatus.invalidState] when a layer + /// still uses the source. + Future removeStyleSource(String sourceId) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(sourceId, arena); - final outRemoved = arena(); _check( raw.mln_map_remove_style_source( _handle.raw, nativeId.value, - outRemoved, + completion, ), ); - return outRemoved.value; }); } - /// Copies fixed style source metadata, or returns null when the source is absent. - SourceInfo? getStyleSourceInfo(String sourceId) { - return withNativeArena((arena) { + /// Copies fixed style source metadata after prior commands. + Future getStyleSourceInfo(String sourceId) => _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_STYLE_SOURCE, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { final nativeId = nativeStringView(sourceId, arena); - final outInfo = arena(); - outInfo.ref.size = sizeOf(); - final outFound = arena(); - _check( - raw.mln_map_get_style_source_info( - _handle.raw, - nativeId.value, - outInfo, - outFound, - ), - ); - if (!outFound.value) { - return null; - } - final info = outInfo.ref; + return raw.mln_map_get_style_source_info( + _handle.raw, + nativeId.value, + completion, + ); + }), + decode: (result) { + if (result.value_count == 0) return null; + final value = result.value.cast().ref; + final info = value.info; final hasUrl = info.fields & raw.mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_URL.value != @@ -3229,47 +3143,20 @@ final class MapHandle { .MLN_STYLE_SOURCE_INFO_RASTER_ENCODING .value != 0; - List? tileUrls; - if (hasTileJson) { - final outTileUrls = arena(); - outTileUrls.value = 0; - final outTileUrlsFound = arena(); - _check( - raw.mln_map_get_style_source_tile_urls( - _handle.raw, - nativeId.value, - outTileUrls, - outTileUrlsFound, - ), - ); - if (!outTileUrlsFound.value) { - return null; - } - tileUrls = _copyStyleStringList( - NativeStyleStringList(outTileUrls.value), - ); - } return SourceInfo( type: SourceType.fromRaw(info.type), id: sourceId, isVolatile: info.is_volatile, - attribution: _copyStyleSourceAttribution( - _handle, - nativeId.value, - info.has_attribution, - info.attribution_size, - arena, - ), - url: _copyStyleSourceUrl( - _handle, - nativeId.value, - hasUrl, - info.url_size, - arena, - ), + attribution: info.has_attribution + ? utf8.decode(_copyBufferView(value.attribution)) + : null, + url: hasUrl ? utf8.decode(_copyBufferView(value.url)) : null, tileJson: hasTileJson ? ParsedTileJson( - tileUrls: tileUrls!, + tileUrls: [ + for (var index = 0; index < value.tile_url_count; index += 1) + utf8.decode(_copyBufferView(value.tile_urls[index])), + ], minZoom: info.min_zoom, maxZoom: info.max_zoom, scheme: TileScheme.fromRaw(info.scheme), @@ -3286,26 +3173,48 @@ final class MapHandle { ? RasterDemEncoding.fromRaw(info.raster_encoding) : null, ); - }); - } + }, + ); - /// Copies style source IDs in style order. - List listStyleSourceIds() { - return withNativeArena((arena) { - final outList = arena(); - outList.value = 0; - _check(raw.mln_map_list_style_source_ids(_handle.raw, outList)); - return _copyStyleIdList(NativeStyleIdList(outList.value)); - }); - } + /// Copies style source IDs in style order after prior commands. + Future> listStyleSourceIds() => _startStringList( + (completion) => raw.mln_map_list_style_source_ids(_handle.raw, completion), + ); + + Future> _startStringList(NativeCompletionStart start) => + _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: start, + decode: (result) => [ + for (var index = 0; index < result.value_count; index += 1) + utf8.decode( + _copyBufferView(result.value.cast()[index]), + ), + ], + ); + + Future _startOptionalBuffer(NativeCompletionStart start) => + _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: start, + decode: (result) => result.value_count == 0 + ? null + : _copyBufferView(result.value.cast().ref), + ); /// Adds a hillshade layer for a raster DEM source. - void addHillshadeLayer( + Future addHillshadeLayer( String layerId, String sourceId, { String? beforeLayerId, }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeSourceId = nativeStringView(sourceId, arena); final nativeBeforeLayerId = nativeStringView(beforeLayerId ?? '', arena); @@ -3315,18 +3224,19 @@ final class MapHandle { nativeLayerId.value, nativeSourceId.value, nativeBeforeLayerId.value, + completion, ), ); }); } /// Adds a color-relief layer for a raster DEM source. - void addColorReliefLayer( + Future addColorReliefLayer( String layerId, String sourceId, { String? beforeLayerId, }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeSourceId = nativeStringView(sourceId, arena); final nativeBeforeLayerId = nativeStringView(beforeLayerId ?? '', arena); @@ -3336,14 +3246,18 @@ final class MapHandle { nativeLayerId.value, nativeSourceId.value, nativeBeforeLayerId.value, + completion, ), ); }); } /// Adds a source-free location indicator layer. - void addLocationIndicatorLayer(String layerId, {String? beforeLayerId}) { - withNativeArena((arena) { + Future addLocationIndicatorLayer( + String layerId, { + String? beforeLayerId, + }) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeBeforeLayerId = nativeStringView(beforeLayerId ?? '', arena); _check( @@ -3351,18 +3265,19 @@ final class MapHandle { _handle.raw, nativeLayerId.value, nativeBeforeLayerId.value, + completion, ), ); }); } /// Sets a location indicator layer location. - void setLocationIndicatorLocation( + Future setLocationIndicatorLocation( String layerId, LatLng coordinate, { double altitude = 0, }) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_location_indicator_location( @@ -3370,46 +3285,55 @@ final class MapHandle { nativeLayerId.value, native_struct.latLngToNative(coordinate), altitude, + completion, ), ); }); } /// Sets a location indicator layer bearing in degrees. - void setLocationIndicatorBearing(String layerId, double bearing) { - withNativeArena((arena) { + Future setLocationIndicatorBearing( + String layerId, + double bearing, + ) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_location_indicator_bearing( _handle.raw, nativeLayerId.value, bearing, + completion, ), ); }); } /// Sets a location indicator layer accuracy radius in meters. - void setLocationIndicatorAccuracyRadius(String layerId, double radius) { - withNativeArena((arena) { + Future setLocationIndicatorAccuracyRadius( + String layerId, + double radius, + ) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_location_indicator_accuracy_radius( _handle.raw, nativeLayerId.value, radius, + completion, ), ); }); } /// Sets one location indicator image-name property. - void setLocationIndicatorImageName( + Future setLocationIndicatorImageName( String layerId, LocationIndicatorImageKind imageKind, String imageId, ) { - withNativeArena((arena) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeImageId = nativeStringView(imageId, arena); _check( @@ -3418,14 +3342,18 @@ final class MapHandle { nativeLayerId.value, imageKind.rawValue, nativeImageId.value, + completion, ), ); }); } /// Adds one style layer from a full style-spec layer JSON object. - void addStyleLayerJson(Uint8List layerJson, {String? beforeLayerId}) { - withNativeArena((arena) { + Future addStyleLayerJson( + Uint8List layerJson, { + String? beforeLayerId, + }) { + return _startCommandInArena((arena, completion) { final nativeLayerJson = nativeBufferView(layerJson, arena); final nativeBeforeLayerId = nativeStringView(beforeLayerId ?? '', arena); _check( @@ -3433,44 +3361,46 @@ final class MapHandle { _handle.raw, nativeLayerJson, nativeBeforeLayerId.value, + completion, ), ); }); } /// Copies one style layer as a full style-spec layer JSON snapshot. - Uint8List? getStyleLayerJson(String layerId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(layerId, arena); - final outLayer = arena(); - outLayer.value = 0; - final outFound = arena(); - _check( - raw.mln_map_get_style_layer_json( + Future getStyleLayerJson(String layerId) { + return _startOptionalBuffer( + (completion) => withNativeArena((arena) { + final nativeId = nativeStringView(layerId, arena); + return raw.mln_map_get_style_layer_json( _handle.raw, nativeId.value, - outLayer, - outFound, - ), - ); - if (!outFound.value) { - return null; - } - return copyOwnedBuffer(NativeOwnedBufferHandle(outLayer.value)); - }); + completion, + ); + }), + ); } /// Sets the style light from a style-spec light JSON object. - void setStyleLightJson(Uint8List lightJson) { - withNativeArena((arena) { + Future setStyleLightJson(Uint8List lightJson) { + return _startCommandInArena((arena, completion) { final nativeLightJson = nativeBufferView(lightJson, arena); - _check(raw.mln_map_set_style_light_json(_handle.raw, nativeLightJson)); + _check( + raw.mln_map_set_style_light_json( + _handle.raw, + nativeLightJson, + completion, + ), + ); }); } /// Sets one style light property by style-spec property name. - void setStyleLightProperty(String propertyName, Uint8List value) { - withNativeArena((arena) { + Future setStyleLightProperty( + String propertyName, + Uint8List value, + ) { + return _startCommandInArena((arena, completion) { final nativePropertyName = nativeStringView(propertyName, arena); final nativeValue = nativeBufferView(value, arena); _check( @@ -3478,27 +3408,24 @@ final class MapHandle { _handle.raw, nativePropertyName.value, nativeValue, + completion, ), ); }); } /// Copies one style light property, or null when the property is undefined. - Uint8List? getStyleLightProperty(String propertyName) { - return withNativeArena((arena) { - final nativePropertyName = nativeStringView(propertyName, arena); - final outValue = arena(); - outValue.value = 0; - _check( - raw.mln_map_get_style_light_property( + Future getStyleLightProperty(String propertyName) { + return _startOptionalBuffer( + (completion) => withNativeArena((arena) { + final nativePropertyName = nativeStringView(propertyName, arena); + return raw.mln_map_get_style_light_property( _handle.raw, nativePropertyName.value, - outValue, - ), - ); - final buffer = NativeOwnedBufferHandle(outValue.value); - return buffer.isNull ? null : copyOwnedBuffer(buffer); - }); + completion, + ); + }), + ); } /// Sets the style's global transition options. @@ -3506,29 +3433,43 @@ final class MapHandle { /// This replaces the whole transition configuration rather than merging into /// it, and loading a style replaces it again with the style's own options, so /// apply an override after the style loads. - void setStyleTransitionOptions(StyleTransitionOptions options) { - withNativeArena((arena) { + Future setStyleTransitionOptions( + StyleTransitionOptions options, + ) { + return _startCommandInArena((arena, completion) { final nativeOptions = arena(); nativeOptions.ref = _styleTransitionOptionsToNative(options); _check( - raw.mln_map_set_style_transition_options(_handle.raw, nativeOptions), + raw.mln_map_set_style_transition_options( + _handle.raw, + nativeOptions, + completion, + ), ); }); } /// Copies the style's global transition options. - StyleTransitionOptions getStyleTransitionOptions() { - return withNativeArena((arena) { - final outOptions = arena(); - outOptions.ref = raw.mln_style_transition_options_default(); - _check(raw.mln_map_get_style_transition_options(_handle.raw, outOptions)); - return _styleTransitionOptionsFromNative(outOptions.ref); - }); + Future getStyleTransitionOptions() { + return _startMapValue( + copyKind: + raw.mln_adapter_completion_copy_kind.MLN_ADAPTER_COMPLETION_COPY_FLAT, + elementSize: sizeOf(), + start: (completion) => + raw.mln_map_get_style_transition_options(_handle.raw, completion), + decode: (result) => _styleTransitionOptionsFromNative( + result.value.cast().ref, + ), + ); } /// Sets one layer property by style-spec property name. - void setLayerProperty(String layerId, String propertyName, Uint8List value) { - withNativeArena((arena) { + Future setLayerProperty( + String layerId, + String propertyName, + Uint8List value, + ) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativePropertyName = nativeStringView(propertyName, arena); final nativeValue = nativeBufferView(value, arena); @@ -3538,34 +3479,31 @@ final class MapHandle { nativeLayerId.value, nativePropertyName.value, nativeValue, + completion, ), ); }); } /// Copies one layer property, or null when the property is undefined. - Uint8List? getLayerProperty(String layerId, String propertyName) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final nativePropertyName = nativeStringView(propertyName, arena); - final outValue = arena(); - outValue.value = 0; - _check( - raw.mln_map_get_layer_property( + Future getLayerProperty(String layerId, String propertyName) { + return _startOptionalBuffer( + (completion) => withNativeArena((arena) { + final nativeLayerId = nativeStringView(layerId, arena); + final nativePropertyName = nativeStringView(propertyName, arena); + return raw.mln_map_get_layer_property( _handle.raw, nativeLayerId.value, nativePropertyName.value, - outValue, - ), - ); - final buffer = NativeOwnedBufferHandle(outValue.value); - return buffer.isNull ? null : copyOwnedBuffer(buffer); - }); + completion, + ); + }), + ); } /// Sets or clears one layer filter. - void setLayerFilter(String layerId, Uint8List? filter) { - withNativeArena((arena) { + Future setLayerFilter(String layerId, Uint8List? filter) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeFilter = filter == null ? nullptr.cast() @@ -3576,34 +3514,34 @@ final class MapHandle { _handle.raw, nativeLayerId.value, nativeFilter, + completion, ), ); }); } /// Copies one layer filter, or null when the layer has no filter. - Uint8List? getLayerFilter(String layerId) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final outFilter = arena(); - outFilter.value = 0; - _check( - raw.mln_map_get_layer_filter( + Future getLayerFilter(String layerId) { + return _startOptionalBuffer( + (completion) => withNativeArena((arena) { + final nativeLayerId = nativeStringView(layerId, arena); + return raw.mln_map_get_layer_filter( _handle.raw, nativeLayerId.value, - outFilter, - ), - ); - final buffer = NativeOwnedBufferHandle(outFilter.value); - return buffer.isNull ? null : copyOwnedBuffer(buffer); - }); + completion, + ); + }), + ); } /// Sets one layer's source-layer ID. /// /// Layer types that take no source, such as background, are rejected. - void setLayerSourceLayer(String layerId, String sourceLayer) { - withNativeArena((arena) { + Future setLayerSourceLayer( + String layerId, + String sourceLayer, + ) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeSourceLayer = nativeStringView(sourceLayer, arena); _check( @@ -3611,26 +3549,23 @@ final class MapHandle { _handle.raw, nativeLayerId.value, nativeSourceLayer.value, + completion, ), ); }); } /// Copies one layer's source-layer ID, empty when the layer carries none. - String getLayerSourceLayer(String layerId) { - return _copyLayerText( - _handle, - layerId, - raw.mln_map_copy_layer_source_layer, - ); + Future getLayerSourceLayer(String layerId) { + return _copyLayerText(layerId, raw.mln_map_copy_layer_source_layer); } /// Sets one layer's source ID. /// /// Layer types that take no source, such as background, are rejected. The /// named source need not exist yet. - void setLayerSourceId(String layerId, String sourceId) { - withNativeArena((arena) { + Future setLayerSourceId(String layerId, String sourceId) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeSourceId = nativeStringView(sourceId, arena); _check( @@ -3638,150 +3573,122 @@ final class MapHandle { _handle.raw, nativeLayerId.value, nativeSourceId.value, + completion, ), ); }); } /// Copies one layer's source ID, empty when the layer carries none. - String getLayerSourceId(String layerId) { - return _copyLayerText(_handle, layerId, raw.mln_map_copy_layer_source_id); + Future getLayerSourceId(String layerId) { + return _copyLayerText(layerId, raw.mln_map_copy_layer_source_id); } /// Sets the lowest zoom at which one layer draws. /// /// Pass `double.negativeInfinity` for no lower bound. - void setLayerMinZoom(String layerId, double minZoom) { - withNativeArena((arena) { + Future setLayerMinZoom(String layerId, double minZoom) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_layer_min_zoom( _handle.raw, nativeLayerId.value, minZoom, + completion, ), ); }); } - /// Reads the lowest zoom at which one layer draws. - /// - /// A layer with no lower bound reports `double.negativeInfinity`. - double getLayerMinZoom(String layerId) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final outZoom = arena(); - _check( - raw.mln_map_get_layer_min_zoom( - _handle.raw, - nativeLayerId.value, - outZoom, - ), - ); - return outZoom.value; - }); - } - /// Sets the highest zoom at which one layer draws. /// /// Pass `double.infinity` for no upper bound. - void setLayerMaxZoom(String layerId, double maxZoom) { - withNativeArena((arena) { + Future setLayerMaxZoom(String layerId, double maxZoom) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_layer_max_zoom( _handle.raw, nativeLayerId.value, maxZoom, + completion, ), ); }); } - /// Reads the highest zoom at which one layer draws. - /// - /// A layer with no upper bound reports `double.infinity`. - double getLayerMaxZoom(String layerId) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final outZoom = arena(); - _check( - raw.mln_map_get_layer_max_zoom( - _handle.raw, - nativeLayerId.value, - outZoom, - ), - ); - return outZoom.value; - }); - } - /// Sets whether one layer draws. - void setLayerVisibility(String layerId, StyleLayerVisibility visibility) { - withNativeArena((arena) { + Future setLayerVisibility( + String layerId, + StyleLayerVisibility visibility, + ) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); _check( raw.mln_map_set_layer_visibility( _handle.raw, nativeLayerId.value, visibility.rawValue, + completion, ), ); }); } - /// Reads whether one layer draws. - StyleLayerVisibility getLayerVisibility(String layerId) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final outVisibility = arena(); - _check( - raw.mln_map_get_layer_visibility( - _handle.raw, - nativeLayerId.value, - outVisibility, - ), - ); - return StyleLayerVisibility.fromRawValue(outVisibility.value); - }); - } - - /// Reports whether a style layer ID exists. - bool styleLayerExists(String layerId) { - return withNativeArena((arena) { - final nativeId = nativeStringView(layerId, arena); - final outExists = arena(); - _check( - raw.mln_map_style_layer_exists(_handle.raw, nativeId.value, outExists), - ); - return outExists.value; - }); - } - - /// Borrows one style layer type string, or returns null when absent. - String? getStyleLayerType(String layerId) { - return withNativeArena((arena) { + /// Copies fixed style layer metadata, or null when no layer has [layerId]. + Future getStyleLayerInfo(String layerId) => _startMapValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_STYLE_LAYER, + elementSize: sizeOf(), + start: (completion) => withNativeArena((arena) { final nativeId = nativeStringView(layerId, arena); - final outLayerType = arena(); - final outFound = arena(); - _check( - raw.mln_map_get_style_layer_type( - _handle.raw, - nativeId.value, - outLayerType, - outFound, - ), + return raw.mln_map_get_style_layer_info( + _handle.raw, + nativeId.value, + completion, + ); + }), + decode: (result) { + if (result.value_count == 0) return null; + final value = result.value.cast().ref; + final info = value.info; + final hasSourceId = + info.fields & + raw + .mln_style_layer_info_field + .MLN_STYLE_LAYER_INFO_SOURCE_ID + .value != + 0; + final hasSourceLayer = + info.fields & + raw + .mln_style_layer_info_field + .MLN_STYLE_LAYER_INFO_SOURCE_LAYER + .value != + 0; + return LayerInfo( + type: _copyStringView(info.type) ?? '', + minZoom: info.min_zoom, + maxZoom: info.max_zoom, + visibility: StyleLayerVisibility.fromRawValue(info.visibility), + sourceId: hasSourceId + ? utf8.decode(_copyBufferView(value.source_id)) + : null, + sourceLayer: hasSourceLayer + ? utf8.decode(_copyBufferView(value.source_layer)) + : null, ); - if (!outFound.value) { - return null; - } - return _copyStringView(outLayerType.ref); - }); - } + }, + ); /// Moves one style layer before another layer or to the top. - void moveStyleLayer(String layerId, {String? beforeLayerId}) { - withNativeArena((arena) { + Future moveStyleLayer( + String layerId, { + String? beforeLayerId, + }) { + return _startCommandInArena((arena, completion) { final nativeLayerId = nativeStringView(layerId, arena); final nativeBeforeLayerId = nativeStringView(beforeLayerId ?? '', arena); _check( @@ -3789,47 +3696,53 @@ final class MapHandle { _handle.raw, nativeLayerId.value, nativeBeforeLayerId.value, + completion, ), ); }); } - /// Removes one style layer by ID and returns whether one was removed. - bool removeStyleLayer(String layerId) { - return withNativeArena((arena) { + /// Removes one style layer by ID. + /// + /// The command fails with [MaplibreStatus.notFound] when no style layer has + /// [layerId]. + Future removeStyleLayer(String layerId) { + return _startCommandInArena((arena, completion) { final nativeId = nativeStringView(layerId, arena); - final outRemoved = arena(); _check( - raw.mln_map_remove_style_layer(_handle.raw, nativeId.value, outRemoved), + raw.mln_map_remove_style_layer(_handle.raw, nativeId.value, completion), ); - return outRemoved.value; }); } /// Copies style layer IDs in style order. - List listStyleLayerIds() { - return withNativeArena((arena) { - final outList = arena(); - outList.value = 0; - _check(raw.mln_map_list_style_layer_ids(_handle.raw, outList)); - return _copyStyleIdList(NativeStyleIdList(outList.value)); - }); + Future> listStyleLayerIds() { + return _startStringList( + (completion) => raw.mln_map_list_style_layer_ids(_handle.raw, completion), + ); } - /// Explicitly destroys this map. + /// Releases this map's public native handle. /// - /// Destroying the map releases every custom-geometry source it still holds, - /// which retires those sources' callback roots. - void close() { + /// Callback roots remain alive until native teardown releases every + /// custom-geometry source that the map still owns. + Future close() async { final id = _state.handleId; - _state.close( - (handle) => raw.mln_map_destroy(handle.raw), - _c.threadLastErrorMessage, - ); + await _state.closeAsync((handle) async { + await _runtime._startUnit( + (completion) => raw.mln_map_release(handle.raw, completion), + ); + // Native release callbacks are listener callbacks. Yield on this isolate + // before retiring any root whose release message has not run yet. + await Future.delayed(Duration.zero); + for (final state in _customGeometryCallbacks.values.toList()) { + state._retire(); + } + await Future.delayed(Duration.zero); + }); _runtime._unregisterMapId(id); } - /// Drops [sourceId]'s callback root once the C API has released it. void _releaseCustomGeometryCallbacks(String sourceId) { _customGeometryCallbacks.remove(sourceId); } diff --git a/bindings/dart/lib/src/runtime/runtime_native_conversions.dart b/bindings/dart/lib/src/runtime/runtime_native_conversions.dart index a400648c4..696385966 100644 --- a/bindings/dart/lib/src/runtime/runtime_native_conversions.dart +++ b/bindings/dart/lib/src/runtime/runtime_native_conversions.dart @@ -195,13 +195,12 @@ final class _CustomGeometryCallbackState extends RetainedCallbackState { CustomGeometrySourceOptions options, void Function() onReleased, ) { - // The C API invokes this once on the map owner thread, which is this - // isolate's thread, from inside whichever call stopped referencing the - // source: a removal, a style load, or the map's destruction. + // The C API invokes this once when a removal, style load, or map close + // stops referencing the source. releaseUserData = NativeCallable< raw.mln_custom_geometry_source_release_callbackFunction - >.isolateLocal((Pointer _) { + >.listener((Pointer _) { onReleased(); _retire(); }); @@ -300,10 +299,12 @@ final class _CustomMvtVectorCallbackState extends RetainedCallbackState { CustomMvtVectorSourceOptions options, void Function() onReleased, ) { + // The C API invokes this once when a removal, style load, or map close + // stops referencing the source. releaseUserData = NativeCallable< raw.mln_custom_mvt_vector_source_release_callbackFunction - >.isolateLocal((Pointer _) { + >.listener((Pointer _) { onReleased(); _retire(); }); @@ -603,49 +604,6 @@ raw.mln_rendered_query_geometry _renderedQueryGeometryToNative( } } -Pointer _featureStateSelectorToNative( - FeatureStateSelector selector, - Allocator allocator, -) { - final nativeSelector = allocator(); - nativeSelector.ref.size = sizeOf(); - nativeSelector.ref.source_id = nativeStringView( - selector.sourceId, - allocator, - ).value; - final sourceLayerId = selector.sourceLayerId; - if (sourceLayerId != null) { - nativeSelector.ref.fields |= raw - .mln_feature_state_selector_field - .MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID - .value; - nativeSelector.ref.source_layer_id = nativeStringView( - sourceLayerId, - allocator, - ).value; - } - final featureId = selector.featureId; - if (featureId != null) { - nativeSelector.ref.fields |= raw - .mln_feature_state_selector_field - .MLN_FEATURE_STATE_SELECTOR_FEATURE_ID - .value; - nativeSelector.ref.feature_id = nativeStringView( - featureId, - allocator, - ).value; - } - final stateKey = selector.stateKey; - if (stateKey != null) { - nativeSelector.ref.fields |= raw - .mln_feature_state_selector_field - .MLN_FEATURE_STATE_SELECTOR_STATE_KEY - .value; - nativeSelector.ref.state_key = nativeStringView(stateKey, allocator).value; - } - return nativeSelector; -} - Pointer _renderedFeatureQueryOptionsToNative( RenderedFeatureQueryOptions options, @@ -965,6 +923,7 @@ raw.mln_vulkan_context_descriptor _vulkanContextDescriptorToNative( raw.mln_opengl_context_descriptor _openglContextDescriptorToNative( OpenGLContextDescriptor value, + Allocator arena, ) { final result = Struct.create(); result.size = sizeOf(); @@ -998,6 +957,17 @@ raw.mln_opengl_context_descriptor _openglContextDescriptorToNative( result.data.egl.get_proc_address = Pointer.fromAddress( value.getProcAddress.address, ); + case WebGLContextDescriptor(): + result.platformAsInt = raw + .mln_opengl_context_platform + .MLN_OPENGL_CONTEXT_PLATFORM_WEBGL + .value; + result.data.webgl.size = sizeOf(); + result.data.webgl.kind = value.isTransferredCanvas ? 1 : 0; + result.data.webgl.context = value.context; + result.data.webgl.canvas_selector = value.canvasSelector == null + ? Struct.create() + : nativeStringView(value.canvasSelector!, arena).value; } return result; } @@ -1024,10 +994,11 @@ raw.mln_vulkan_surface_descriptor _vulkanSurfaceDescriptorToNative( raw.mln_opengl_surface_descriptor _openglSurfaceDescriptorToNative( OpenGLSurfaceDescriptor value, + Allocator arena, ) { final result = raw.mln_opengl_surface_descriptor_default(); result.extent = _renderTargetExtentToNative(value.extent); - result.context = _openglContextDescriptorToNative(value.context); + result.context = _openglContextDescriptorToNative(value.context, arena); result.surface = Pointer.fromAddress(value.surface.address); return result; } @@ -1091,16 +1062,18 @@ _vulkanBorrowedTextureDescriptorToNative( raw.mln_opengl_owned_texture_descriptor _openglOwnedTextureDescriptorToNative( OpenGLOwnedTextureDescriptor value, + Allocator arena, ) { final result = raw.mln_opengl_owned_texture_descriptor_default(); result.extent = _renderTargetExtentToNative(value.extent); - result.context = _openglContextDescriptorToNative(value.context); + result.context = _openglContextDescriptorToNative(value.context, arena); return result; } raw.mln_opengl_borrowed_texture_descriptor _openglBorrowedTextureDescriptorToNative( OpenGLBorrowedTextureDescriptor value, + Allocator arena, ) { final result = raw.mln_opengl_borrowed_texture_descriptor_default(); result.extent = _renderTargetExtentToNative(value.extent); @@ -1112,12 +1085,64 @@ _openglBorrowedTextureDescriptorToNative( value.physicalHeight, 'physical texture height', ); - result.context = _openglContextDescriptorToNative(value.context); + result.context = _openglContextDescriptorToNative(value.context, arena); result.texture = value.texture; result.target = value.target; return result; } +raw.mln_webgpu_context_descriptor _webGPUContextDescriptorToNative( + WebGPUContextDescriptor value, +) { + final result = Struct.create(); + result.size = sizeOf(); + result.instance = Pointer.fromAddress(value.instance.address); + result.device = Pointer.fromAddress(value.device.address); + result.queue = Pointer.fromAddress(value.queue.address); + return result; +} + +raw.mln_webgpu_surface_descriptor _webGPUSurfaceDescriptorToNative( + WebGPUSurfaceDescriptor value, +) { + final result = raw.mln_webgpu_surface_descriptor_default(); + result.extent = _renderTargetExtentToNative(value.extent); + result.context = _webGPUContextDescriptorToNative(value.context); + result.surface = Pointer.fromAddress(value.surface.address); + result.format = value.format; + return result; +} + +raw.mln_webgpu_owned_texture_descriptor _webGPUOwnedTextureDescriptorToNative( + WebGPUOwnedTextureDescriptor value, +) { + final result = raw.mln_webgpu_owned_texture_descriptor_default(); + result.extent = _renderTargetExtentToNative(value.extent); + result.context = _webGPUContextDescriptorToNative(value.context); + return result; +} + +raw.mln_webgpu_borrowed_texture_descriptor +_webGPUBorrowedTextureDescriptorToNative( + WebGPUBorrowedTextureDescriptor value, +) { + final result = raw.mln_webgpu_borrowed_texture_descriptor_default(); + result.extent = _renderTargetExtentToNative(value.extent); + result.physical_width = _positiveUint32( + value.physicalWidth, + 'physical texture width', + ); + result.physical_height = _positiveUint32( + value.physicalHeight, + 'physical texture height', + ); + result.context = _webGPUContextDescriptorToNative(value.context); + result.texture = Pointer.fromAddress(value.texture.address); + result.texture_view = Pointer.fromAddress(value.textureView.address); + result.format = value.format; + return result; +} + Pointer _nativeCamera( CameraOptions camera, Allocator allocator, @@ -1145,226 +1170,73 @@ Pointer _nativeAnimation( return nativeAnimation; } -Pointer _nativeScreenPoint( - ScreenPoint? point, - Allocator allocator, -) { - if (point == null) { - return nullptr.cast(); - } - final nativePoint = allocator(); - nativePoint.ref = native_struct.screenPointToNative(point); - return nativePoint; -} - -/// Probes the required length, then copies. A null buffer with zero capacity is -/// a size probe the C API answers with OK. -String _copyMapText( - NativeMap map, - int Function(int, Pointer, int, Pointer) copy, -) { - return withNativeArena((arena) { - final outSize = arena(); - _check(copy(map.raw, nullptr.cast(), 0, outSize)); - final required = outSize.value; - if (required == 0) { - return ''; - } - - final buffer = arena(required); - final outCopied = arena(); - _check(copy(map.raw, buffer, required, outCopied)); - return buffer.cast().toDartString(length: outCopied.value); - }); -} - -Uint8List _copyMapData( - NativeMap map, - int Function(int, Pointer, int, Pointer) copy, -) { - return withNativeArena((arena) { - final outSize = arena(); - _check(copy(map.raw, nullptr.cast(), 0, outSize)); - final required = outSize.value; - if (required == 0) return Uint8List(0); - final buffer = arena(required); - final outCopied = arena(); - _check(copy(map.raw, buffer, required, outCopied)); - return Uint8List.fromList(buffer.asTypedList(outCopied.value)); - }); -} - -String _copyLayerText( - NativeMap map, - String layerId, - int Function(int, raw.mln_buffer_view, Pointer, int, Pointer) - copy, -) { - return withNativeArena((arena) { - final nativeLayerId = nativeStringView(layerId, arena); - final outSize = arena(); - _check( - copy(map.raw, nativeLayerId.value, nullptr.cast(), 0, outSize), - ); - final required = outSize.value; - if (required == 0) { - return ''; - } - - final buffer = arena(required); - final outCopied = arena(); - _check(copy(map.raw, nativeLayerId.value, buffer, required, outCopied)); - return buffer.cast().toDartString(length: outCopied.value); - }); -} - -String? _copyStyleSourceAttribution( - NativeMap map, - raw.mln_buffer_view sourceId, - bool hasAttribution, - int attributionSize, +Pointer _featureStateSelectorToNative( + FeatureStateSelector selector, Allocator allocator, ) { - if (!hasAttribution) { - return null; + final nativeSelector = allocator(); + nativeSelector.ref.size = sizeOf(); + nativeSelector.ref.source_id = nativeStringView( + selector.sourceId, + allocator, + ).value; + final sourceLayerId = selector.sourceLayerId; + if (sourceLayerId != null) { + nativeSelector.ref.fields |= raw + .mln_feature_state_selector_field + .MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID + .value; + nativeSelector.ref.source_layer_id = nativeStringView( + sourceLayerId, + allocator, + ).value; } - final buffer = attributionSize == 0 - ? nullptr.cast() - : allocator(attributionSize); - final outSize = allocator(); - final outFound = allocator(); - _check( - raw.mln_map_copy_style_source_attribution( - map.raw, - sourceId, - buffer, - attributionSize, - outSize, - outFound, - ), - ); - if (!outFound.value) { - return null; + final featureId = selector.featureId; + if (featureId != null) { + nativeSelector.ref.fields |= raw + .mln_feature_state_selector_field + .MLN_FEATURE_STATE_SELECTOR_FEATURE_ID + .value; + nativeSelector.ref.feature_id = nativeStringView( + featureId, + allocator, + ).value; } - if (outSize.value == 0) { - return ''; + final stateKey = selector.stateKey; + if (stateKey != null) { + nativeSelector.ref.fields |= raw + .mln_feature_state_selector_field + .MLN_FEATURE_STATE_SELECTOR_STATE_KEY + .value; + nativeSelector.ref.state_key = nativeStringView(stateKey, allocator).value; } - return buffer.cast().toDartString(length: outSize.value); + return nativeSelector; } -String? _copyStyleSourceUrl( - NativeMap map, - raw.mln_buffer_view sourceId, - bool hasUrl, - int urlSize, - Allocator allocator, -) { - if (!hasUrl) { - return null; - } - final buffer = urlSize == 0 ? nullptr.cast() : allocator(urlSize); - final outSize = allocator(); - final outFound = allocator(); - _check( - raw.mln_map_copy_style_source_url( - map.raw, - sourceId, - buffer, - urlSize, - outSize, - outFound, - ), +QueriedFeature _queriedFeatureFromNative(raw.mln_queried_feature hit) { + final hasSourceId = + hit.fields & + raw.mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_ID.value != + 0; + final hasSourceLayerId = + hit.fields & + raw + .mln_queried_feature_field + .MLN_QUERIED_FEATURE_SOURCE_LAYER_ID + .value != + 0; + final hasState = + hit.fields & + raw.mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE.value != + 0; + return QueriedFeature( + feature: _copyBufferViewBytes(hit.feature), + sourceId: hasSourceId ? _copyStringView(hit.source_id) : null, + sourceLayerId: hasSourceLayerId + ? _copyStringView(hit.source_layer_id) + : null, + state: hasState ? _copyBufferViewBytes(hit.state) : null, ); - if (!outFound.value) { - return null; - } - if (outSize.value == 0) { - return ''; - } - return buffer.cast().toDartString(length: outSize.value); -} - -List _copyStyleIdList(NativeStyleIdList list) { - try { - return withNativeArena((arena) { - final outCount = arena(); - _check(raw.mln_style_id_list_count(list.raw, outCount)); - final ids = []; - for (var index = 0; index < outCount.value; index += 1) { - final outId = arena(); - _check(raw.mln_style_id_list_get(list.raw, index, outId)); - ids.add(_copyStringView(outId.ref) ?? ''); - } - return ids; - }); - } finally { - raw.mln_style_id_list_destroy(list.raw); - } -} - -List _copyStyleStringList(NativeStyleStringList list) { - try { - return withNativeArena((arena) { - final outCount = arena(); - _check(raw.mln_style_string_list_count(list.raw, outCount)); - final values = []; - for (var index = 0; index < outCount.value; index += 1) { - final outValue = arena(); - _check(raw.mln_style_string_list_get(list.raw, index, outValue)); - values.add(_copyStringView(outValue.ref) ?? ''); - } - return values; - }); - } finally { - raw.mln_style_string_list_destroy(list.raw); - } -} - -List _copyQueriedFeatureList(NativeQueriedFeatureList list) { - try { - return withNativeArena((arena) { - final outCount = arena(); - _check(raw.mln_queried_feature_list_count(list.raw, outCount)); - final features = []; - final outFeature = arena(); - for (var index = 0; index < outCount.value; index += 1) { - outFeature.ref = raw.mln_queried_feature_default(); - _check(raw.mln_queried_feature_list_get(list.raw, index, outFeature)); - final hit = outFeature.ref; - final hasSourceId = - hit.fields & - raw - .mln_queried_feature_field - .MLN_QUERIED_FEATURE_SOURCE_ID - .value != - 0; - final hasSourceLayerId = - hit.fields & - raw - .mln_queried_feature_field - .MLN_QUERIED_FEATURE_SOURCE_LAYER_ID - .value != - 0; - final hasState = - hit.fields & - raw.mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE.value != - 0; - features.add( - QueriedFeature( - feature: _copyBufferViewBytes(hit.feature), - sourceId: hasSourceId ? _copyStringView(hit.source_id) : null, - sourceLayerId: hasSourceLayerId - ? _copyStringView(hit.source_layer_id) - : null, - state: hasState ? _copyBufferViewBytes(hit.state) : null, - ), - ); - } - return features; - }); - } finally { - raw.mln_queried_feature_list_destroy(list.raw); - } } String? _copyStringView(raw.mln_buffer_view view) => diff --git a/bindings/dart/lib/src/runtime/runtime_offline.dart b/bindings/dart/lib/src/runtime/runtime_offline.dart index feb5fff19..5a755ba71 100644 --- a/bindings/dart/lib/src/runtime/runtime_offline.dart +++ b/bindings/dart/lib/src/runtime/runtime_offline.dart @@ -1,238 +1,11 @@ part of 'runtime.dart'; -final class OfflineOperationHandle implements Finalizable { - OfflineOperationHandle._( - this._runtime, - this._id, - this._kind, - this._resultKind, - ) { - _runtime._registerOfflineOperation(this); - _leakReporter = NativeLeakReporter( - this, - 'OfflineOperationHandle', - NativeHandle(_id == 0 ? 1 : _id), - ); - } - - final RuntimeHandle _runtime; - final _OfflineOperationKind _kind; - final _OfflineOperationResultKind _resultKind; - - final int _id; - late final NativeLeakReporter _leakReporter; - - var _discarded = false; - - /// Whether this operation has been discarded by Dart. - bool get isDiscarded => _discarded; - - /// Takes a completed offline region create result. - OfflineRegionInfo takeCreatedRegion() { - _requireResult( - _OfflineOperationKind.regionCreate, - _OfflineOperationResultKind.region, - 'takeCreatedRegion', - ); - return withNativeArena((arena) { - final outRegion = arena(); - outRegion.value = 0; - _check( - raw.mln_runtime_offline_region_create_take_result( - _runtime._handle.raw, - _id, - outRegion, - ), - ); - _markDiscarded(); - return _copyOfflineRegionSnapshot( - NativeOfflineRegionSnapshot(outRegion.value), - ); - }); - } - - /// Takes a completed optional offline region get result. - OfflineRegionInfo? takeOptionalRegion() { - _requireResult( - _OfflineOperationKind.regionGet, - _OfflineOperationResultKind.optionalRegion, - 'takeOptionalRegion', - ); - return withNativeArena((arena) { - final outRegion = arena(); - outRegion.value = 0; - final outFound = arena(); - _check( - raw.mln_runtime_offline_region_get_take_result( - _runtime._handle.raw, - _id, - outRegion, - outFound, - ), - ); - _markDiscarded(); - return outFound.value - ? _copyOfflineRegionSnapshot( - NativeOfflineRegionSnapshot(outRegion.value), - ) - : null; - }); - } - - /// Takes a completed offline regions list result. - List takeRegionList() { - _requireResult( - _OfflineOperationKind.regionsList, - _OfflineOperationResultKind.regionList, - 'takeRegionList', - ); - return withNativeArena((arena) { - final outRegions = arena(); - outRegions.value = 0; - _check( - raw.mln_runtime_offline_regions_list_take_result( - _runtime._handle.raw, - _id, - outRegions, - ), - ); - _markDiscarded(); - return _copyOfflineRegionList(NativeOfflineRegionList(outRegions.value)); - }); - } - - /// Takes a completed offline regions merge result. - List takeMergedRegionList() { - _requireResult( - _OfflineOperationKind.regionsMergeDatabase, - _OfflineOperationResultKind.regionList, - 'takeMergedRegionList', - ); - return withNativeArena((arena) { - final outRegions = arena(); - outRegions.value = 0; - _check( - raw.mln_runtime_offline_regions_merge_database_take_result( - _runtime._handle.raw, - _id, - outRegions, - ), - ); - _markDiscarded(); - return _copyOfflineRegionList(NativeOfflineRegionList(outRegions.value)); - }); - } - - /// Takes a completed offline region metadata update result. - OfflineRegionInfo takeUpdatedRegionMetadata() { - _requireResult( - _OfflineOperationKind.regionUpdateMetadata, - _OfflineOperationResultKind.region, - 'takeUpdatedRegionMetadata', - ); - return withNativeArena((arena) { - final outRegion = arena(); - outRegion.value = 0; - _check( - raw.mln_runtime_offline_region_update_metadata_take_result( - _runtime._handle.raw, - _id, - outRegion, - ), - ); - _markDiscarded(); - return _copyOfflineRegionSnapshot( - NativeOfflineRegionSnapshot(outRegion.value), - ); - }); - } - - /// Takes a completed offline region status result. - OfflineRegionStatus takeRegionStatus() { - _requireResult( - _OfflineOperationKind.regionGetStatus, - _OfflineOperationResultKind.regionStatus, - 'takeRegionStatus', - ); - return withNativeArena((arena) { - final outStatus = arena(); - outStatus.ref.size = sizeOf(); - _check( - raw.mln_runtime_offline_region_get_status_take_result( - _runtime._handle.raw, - _id, - outStatus, - ), - ); - _markDiscarded(); - return _offlineRegionStatusFromNative(outStatus.ref); - }); - } - - /// Discards runtime-owned state for this operation. - void discard() { - if (_discarded) { - return; - } - _check( - raw.mln_runtime_offline_operation_discard(_runtime._handle.raw, _id), - ); - _markDiscarded(); - } - - void _markDiscarded() { - _discarded = true; - _runtime._unregisterOfflineOperationId(_id); - _leakReporter.close(); - } - - void _requireResult( - _OfflineOperationKind expectedKind, - _OfflineOperationResultKind expected, - String accessorName, - ) { - if (_discarded) { - throwInvalidState('offline operation has been discarded'); - } - if (_kind != expectedKind || _resultKind != expected) { - throwInvalidState( - '$accessorName cannot take ${_resultKind.name} result from ' - '${_kind.name} operation; expected ${expected.name} result from ' - '${expectedKind.name}', - ); - } +Uint8List _copyBufferView(raw.mln_buffer_view view) { + if (view.size == 0) return Uint8List(0); + if (view.data == nullptr) { + throwInvalidState('native completion returned an invalid buffer'); } -} - -enum _OfflineOperationKind { - ambientCache('ambient cache'), - regionCreate('region create'), - regionGet('region get'), - regionsList('regions list'), - regionsMergeDatabase('regions merge database'), - regionUpdateMetadata('region update metadata'), - regionGetStatus('region get status'), - regionSetObserved('region set observed'), - regionSetDownloadState('region set download state'), - regionInvalidate('region invalidate'), - regionDelete('region delete'), - setMaximumAmbientCacheSize('set maximum ambient cache size'); - - const _OfflineOperationKind(this.name); - - final String name; -} - -enum _OfflineOperationResultKind { - none('none'), - region('region'), - optionalRegion('optional region'), - regionList('region list'), - regionStatus('region status'); - - const _OfflineOperationResultKind(this.name); - - final String name; + return Uint8List.fromList(view.data.cast().asTypedList(view.size)); } raw.mln_runtime_options _runtimeOptionsToNative( @@ -391,47 +164,6 @@ Pointer _nativeBytes(Uint8List? bytes, Allocator allocator) { return nativeBytes; } -OfflineRegionInfo _copyOfflineRegionSnapshot( - NativeOfflineRegionSnapshot snapshot, -) { - try { - return withNativeArena((arena) { - final outInfo = arena(); - outInfo.ref.size = sizeOf(); - _check(raw.mln_offline_region_snapshot_get(snapshot.raw, outInfo)); - return _offlineRegionInfoFromNative(outInfo.ref); - }); - } finally { - raw.mln_offline_region_snapshot_destroy(snapshot.raw); - } -} - -List _copyOfflineRegionList(NativeOfflineRegionList list) { - try { - return withNativeArena((arena) { - final outCount = arena(); - _check(raw.mln_offline_region_list_count(list.raw, outCount)); - return [ - for (var index = 0; index < outCount.value; index += 1) - _copyOfflineRegionListEntry(list, index, arena), - ]; - }); - } finally { - raw.mln_offline_region_list_destroy(list.raw); - } -} - -OfflineRegionInfo _copyOfflineRegionListEntry( - NativeOfflineRegionList list, - int index, - Allocator allocator, -) { - final outInfo = allocator(); - outInfo.ref.size = sizeOf(); - _check(raw.mln_offline_region_list_get(list.raw, index, outInfo)); - return _offlineRegionInfoFromNative(outInfo.ref); -} - OfflineRegionInfo _offlineRegionInfoFromNative( raw.mln_offline_region_info info, ) { diff --git a/bindings/dart/lib/src/runtime/runtime_render_handles.dart b/bindings/dart/lib/src/runtime/runtime_render_handles.dart index 7d3980027..aa901e797 100644 --- a/bindings/dart/lib/src/runtime/runtime_render_handles.dart +++ b/bindings/dart/lib/src/runtime/runtime_render_handles.dart @@ -1,23 +1,15 @@ part of 'runtime.dart'; -final class _TextureFrameLease { - _TextureFrameLease(Object owner, this.release) - : owner = WeakReference(owner); - - _TextureFrameLease.orphaned(this.release) : owner = null; - - final WeakReference? owner; - final void Function() release; -} - -/// Any-thread standalone projection helper snapshot from a map transform. +/// Owned standalone projection snapshot created from a map. +/// +/// Every method is synchronous, runs on the calling isolate's thread, is +/// internally serialized, and may be called from any isolate. A projection +/// copies the map's transform state at creation and never observes map +/// changes made after that and remains usable after its source map and runtime +/// close. final class MapProjectionHandle { MapProjectionHandle._(NativeMapProjection handle) - : _state = NativeHandleState( - handle, - 'MapProjectionHandle', - threadAffine: false, - ); + : _state = NativeHandleState(handle, 'MapProjectionHandle'); final NativeHandleState _state; @@ -26,7 +18,7 @@ final class MapProjectionHandle { NativeMapProjection get _handle => _state.handle; - /// Copies the current projection camera options. + /// Copies the projection camera, observing every earlier projection setter. CameraOptions camera() { return withNativeArena((arena) { final outCamera = arena(); @@ -36,7 +28,8 @@ final class MapProjectionHandle { }); } - /// Applies camera fields to the projection helper. + /// Applies camera fields before returning, so a later read or conversion + /// observes them. The map's camera is unaffected. void setCamera(CameraOptions camera) { withNativeArena((arena) { final nativeCamera = arena(); @@ -48,7 +41,7 @@ final class MapProjectionHandle { }); } - /// Updates the camera so coordinates are visible within [padding]. + /// Applies a camera fit for [coordinates] before returning. void setVisibleCoordinates( List coordinates, { EdgeInsets padding = const EdgeInsets(), @@ -73,7 +66,7 @@ final class MapProjectionHandle { }); } - /// Updates the camera so geometry coordinates are visible within [padding]. + /// Applies a camera fit for GeoJSON Geometry bytes before returning. void setVisibleGeometry( Uint8List geometry, { EdgeInsets padding = const EdgeInsets(), @@ -90,7 +83,7 @@ final class MapProjectionHandle { }); } - /// Converts a geographic world coordinate to a screen point. + /// Converts a geographic coordinate to a logical-pixel screen point. ScreenPoint pixelForLatLng(LatLng coordinate) { return withNativeArena((arena) { final outPoint = arena(); @@ -105,7 +98,7 @@ final class MapProjectionHandle { }); } - /// Converts a screen point to a geographic world coordinate. + /// Converts a logical-pixel screen point to a geographic coordinate. LatLng latLngForPixel(ScreenPoint point) { return withNativeArena((arena) { final outCoordinate = arena(); @@ -120,44 +113,57 @@ final class MapProjectionHandle { }); } - /// Explicitly destroys this projection helper. + /// Closes the projection after waiting for projection calls already running + /// on other threads. void close() { _state.close( - (handle) => raw.mln_map_projection_destroy(handle.raw), + (handle) => raw.mln_map_projection_close(handle.raw), _c.threadLastErrorMessage, ); } } -/// Outcome reported by [RenderSessionHandle.renderUpdate]. +/// Outcome of one accepted frame demand. +/// +/// Each outcome has its own retry condition, so a host that paces its own +/// frame loop keeps them apart. final class RenderResult { const RenderResult._(this.rawValue, this.name); - /// The render target holds a new frame. + /// A frame was rendered for acquisition, presentation, or ordered readback. static const rendered = RenderResult._(0, 'rendered'); - /// The call produced no frame. + /// No newer map update was available. static const noUpdate = RenderResult._(1, 'noUpdate'); - /// The map has not applied the session's current size yet. + /// An ordered extent change had not reached the driver. static const sizePending = RenderResult._(2, 'sizePending'); - /// The render target had no frame to draw into. + /// The target could not produce a frame. static const targetNotReady = RenderResult._(3, 'targetNotReady'); - /// Creates a render result while preserving unknown native values. - factory RenderResult.fromRawValue(int rawValue) => switch (rawValue) { + /// A newer demand in the same coalescing boundary replaced this demand. + static const superseded = RenderResult._(4, 'superseded'); + + /// The demand's timeout elapsed before driver work began. + static const deadlineMissed = RenderResult._(5, 'deadlineMissed'); + + /// Returns the result for a native value, or an unknown result for a value + /// that this binding does not name. + factory RenderResult.fromRawValue(int value) => switch (value) { 0 => rendered, 1 => noUpdate, 2 => sizePending, 3 => targetNotReady, - _ => RenderResult._(rawValue, 'unknown($rawValue)'), + 4 => superseded, + 5 => deadlineMissed, + _ => RenderResult._(value, 'unknown($value)'), }; - /// Raw native value. + /// The value that the C API reported. final int rawValue; - /// Human-readable name. + /// Diagnostic name of this outcome. final String name; @override @@ -168,522 +174,810 @@ final class RenderResult { int get hashCode => rawValue.hashCode; } -/// Outcome of [RenderSessionHandle.renderUpdate]: the render result plus the -/// repaint request the map raised while rendering. -final class RenderUpdate { - const RenderUpdate._({required this.result, required this.needsRepaint}); +/// Render execution placement selected during attachment. +/// +/// A target that requires one placement rejects the other during attachment. +enum RenderDriver { + /// Native code owns a serial worker that initializes, drives, and tears down + /// transferable graphics state. + coreWorker(1), + + /// The host calls [RenderSessionHandle.serviceDriverWork] from the thread + /// where the target's graphics context is usable, and native code stores + /// work until it does. + callerGraphicsThread(2); + + const RenderDriver(this.rawValue); + + /// The value that the C API uses for this placement. + final int rawValue; +} + +/// Common policy for a render-session attachment. +final class RenderSessionAttachOptions { + /// Creates an attachment policy. + const RenderSessionAttachOptions({ + this.driver = RenderDriver.coreWorker, + this.requestedTextureRingDepth = 1, + }); + + /// Execution placement for the attached session. + final RenderDriver driver; + + /// Requested count of host-acquirable slots in a session-owned texture ring, + /// from one to three. + /// + /// A private texture target grants one slot whatever this value is, and a + /// target without a ring ignores it. The attached session reports the + /// granted count as [RenderSessionCapabilities.textureRingDepth]. + final int requestedTextureRingDepth; +} + +/// One nonblocking request for a rendered frame. +/// +/// Matching demands may coalesce before work starts, and every accepted demand +/// still produces one terminal [RenderFrameResult]. +final class FrameDemand { + /// Creates a frame demand. + const FrameDemand({ + this.renderIfNeeded = true, + this.present = false, + this.token = 0, + this.coalescingBoundary = 0, + this.timeoutNanoseconds = 0, + }); - /// What the call produced; names the wake to wait for before calling again. - final RenderResult result; + /// Whether to render only when a newer map update exists. + /// + /// A demand that finds nothing newer terminates as [RenderResult.noUpdate]. + final bool renderIfNeeded; + + /// Whether the target presents the rendered frame. + /// + /// A target presents only when + /// [RenderSessionCapabilities.supportsPresentation] is set. + final bool present; + + /// Host identity returned with the terminal frame result. + final int token; + + /// Boundary that limits coalescing: two demands coalesce only when this + /// value and their flags match. + final int coalescingBoundary; + + /// Time allowed before driver work begins, in nanoseconds; zero has no + /// limit. + /// + /// The timeout starts when native code accepts the demand, and a demand + /// whose timeout elapses first terminates as [RenderResult.deadlineMissed]. + /// It bounds latency rather than setting a cadence. + final int timeoutNanoseconds; +} + +/// Immutable terminal result for one accepted frame demand. +final class RenderFrameResult { + const RenderFrameResult._({ + required this.disposition, + required this.token, + required this.mapUpdateGeneration, + required this.extentGeneration, + required this.frameGeneration, + required this.needsRepaint, + }); + + factory RenderFrameResult._fromNative(raw.mln_render_frame_result value) => + RenderFrameResult._( + disposition: RenderResult.fromRawValue(value.disposition), + token: value.token, + mapUpdateGeneration: value.map_update_generation, + extentGeneration: value.extent_generation, + frameGeneration: value.frame_generation, + needsRepaint: value.needs_repaint, + ); + + /// Terminal outcome of the demand. + final RenderResult disposition; + + /// The [FrameDemand.token] of the demand that produced this result. + final int token; + + /// Map-update generation that the driver used. + final int mapUpdateGeneration; + + /// Extent generation that the driver used. + final int extentGeneration; + + /// Frame generation of the rendered frame, and zero unless [disposition] is + /// [RenderResult.rendered]. + final int frameGeneration; /// Whether the map asked for another frame while it rendered this one, as /// during an ongoing camera transition. /// - /// This is the same signal that - /// [RuntimeEventType.mapRenderFrameFinished] carries in its - /// [RuntimeEventRenderFrame.needsRepaint] field, delivered here without the - /// event round trip, so a host can re-arm its frame loop before it drains - /// events. It is true only when [result] is [RenderResult.rendered] and - /// reads false for every other outcome. + /// Meaningful only when [disposition] is [RenderResult.rendered]; it reads + /// false for every other outcome. final bool needsRepaint; } -/// Owner-thread render session handle attached to a retained map. -final class RenderSessionHandle { - RenderSessionHandle._(NativeRenderSession handle) - : _state = NativeHandleState(handle, 'RenderSessionHandle'); +/// Immutable capabilities fixed during attachment. +final class RenderSessionCapabilities { + const RenderSessionCapabilities._( + this.driver, + this.textureRingDepth, + this.flags, + ); - final NativeHandleState _state; - _TextureFrameLease? _activeTextureFrame; + /// Execution placement that this session uses. + final RenderDriver driver; - /// Whether this render session has been closed by the Dart binding. - bool get isClosed => _state.isClosed; + /// Granted host-acquirable slot count, and zero for a target without a + /// texture ring. + final int textureRingDepth; - /// The session belongs to the isolate that attached it, which need not be the - /// map's. It holds no Dart reference to the map; native reports an - /// invalid-state status for destroying a map that still has a session. - NativeRenderSession get _handle => _state.handle; + /// Capability bits as the C API reports them, decoded by the getters below. + final int flags; + + /// Whether [RenderSessionHandle.acquireFrame] can lease rendered texture + /// frames from this session. + bool get supportsFrameAcquisition => flags & 1 != 0; + + /// Whether [RenderSessionHandle.readPremultipliedRgba8] can copy the latest + /// rendered frame to the CPU. + bool get supportsReadback => flags & 2 != 0; + + /// Whether [AcquiredFrame.release] accepts backend synchronization rather + /// than CPU-complete synchronization alone. + bool get supportsConsumerSync => flags & 4 != 0; + + /// Whether the target presents rendered frames, which [FrameDemand.present] + /// requests. + bool get supportsPresentation => flags & 8 != 0; +} + +/// Latest any-thread render-session state and generations. +final class RenderSessionSnapshot { + const RenderSessionSnapshot._({ + required this.state, + required this.driver, + required this.latestResult, + required this.extent, + required this.generation, + required this.mapUpdateGeneration, + required this.renderedUpdateGeneration, + required this.extentGeneration, + required this.frameGeneration, + required this.latestDemandToken, + required this.pendingDemandCount, + required this.acquiredFrameCount, + required this.targetReady, + required this.pendingChanges, + }); + + /// Lifecycle state: 1 attaching, 2 attached, 3 detaching, 4 detached, + /// 5 target lost, and 6 abandoned. + final int state; + + /// Execution placement that this session uses. + final RenderDriver driver; + + /// Most recent terminal frame outcome. + final RenderResult latestResult; + + /// Logical extent that the session currently renders at. + final RenderTargetExtent extent; + + /// Session generation, which increases whenever the session changes state, + /// target, or extent. + final int generation; + + /// Latest map-update generation published to this session. + final int mapUpdateGeneration; + + /// Map-update generation of the most recently rendered frame. + final int renderedUpdateGeneration; + + /// Extent generation, which increases with each applied resize. + final int extentGeneration; + + /// Frame generation, which increases with each rendered frame. + final int frameGeneration; + + /// Token of the most recently accepted frame demand. + final int latestDemandToken; + + /// Count of accepted demands that have no terminal result yet. + final int pendingDemandCount; - /// Resizes an attached render session. + /// Count of texture-ring slots that the host currently holds. + final int acquiredFrameCount; + + /// Whether the target can produce a frame. + final bool targetReady; + + /// Whether the session holds changes that a render-if-needed demand would + /// render, such as a newer map update or a pending resize. + final bool pendingChanges; +} + +/// Result of irreversible target abandonment. +final class RenderAbandonResult { + const RenderAbandonResult._(this.quarantined, this.quarantinedResourceCount); + + /// Whether graphics resources could not be destroyed and were quarantined. + final bool quarantined; + + /// Count of backend resource groups retained until the process exits. + final int quarantinedResourceCount; +} + +/// Synchronization that protects an acquired texture frame. +final class GpuSync { + /// Creates synchronization for work that completed before the call that + /// carries it. + const GpuSync.cpuComplete() : kind = 0, object = null, value = 0; + + /// Creates synchronization from a backend object and its signal value. /// - /// Surface and session-owned texture targets resize in place. A caller-owned - /// texture is rejected here: allocate one at the new size and hand it over - /// with [setMetalBorrowedTextureTarget] or its Vulkan or OpenGL counterpart. + /// The backend object stays caller-owned. Metal is the exception: the + /// session retains a shared event. A Vulkan, OpenGL, or WebGPU object stays + /// borrowed until a later [RenderSessionHandle.barrier] or + /// [RenderSessionHandle.detach] completes. + const GpuSync.native({ + required this.kind, + required this.object, + required this.value, + }); + + /// Payload kind: 0 CPU-complete, 1 Metal shared event, 2 Vulkan timeline + /// semaphore, 3 OpenGL fence, and 4 WebGPU token. + final int kind; + + /// Backend synchronization object, and null for CPU-complete + /// synchronization. + final NativePointer? object; + + /// Signal value for a timeline object, such as a Metal shared event or a + /// Vulkan timeline semaphore. + final int value; +} + +/// An attachment that exposes the session while graphics initialization runs. +final class RenderSessionAttachment { + /// Creates an attachment from a session and its attachment completion. + const RenderSessionAttachment(this.session, this.completed); + + /// The session, which is usable for driver work and for state reads while + /// attachment runs. + final RenderSessionHandle session; + + /// Completes after the selected driver initializes the target. /// - /// The session keeps its renderer across a resize, along with the tile - /// pyramid, glyph and image atlases, and symbol placement. A scale factor - /// that differs from the session's current one retires the renderer instead, - /// because a renderer compiles its shaders for one pixel ratio; the same - /// applies to every `setTarget` method. Map-owned feature state survives - /// either way. - void resize(int width, int height, {double scaleFactor = 1}) { - _checkNoActiveTextureFrame('resize render session'); - _check( - raw.mln_render_session_resize(_handle.raw, width, height, scaleFactor), - ); - } + /// A caller-graphics-thread session completes attachment only after the host + /// services driver work. A failed attachment still requires + /// [RenderSessionHandle.detach] or [RenderSessionHandle.abandon] before + /// [RenderSessionHandle.close]. + final Future completed; +} + +/// A render session attached to one map and render target. +/// +/// A map has at most one live session, and its style, sources, layers, and +/// camera remain after the session detaches. The session advances through its +/// selected [RenderDriver] rather than through a runtime pump. +/// +/// The target setters below start ordered replacements of the session's +/// surface or caller-owned texture, as after a window recreation or a new host +/// allocation. A replacement keeps the session's rendering resources, +/// including loaded tiles, unless the scale factor changes, and map-owned +/// feature state survives either way. It changes the graphics resource only, +/// so the map viewport keeps following map creation and map resize. A setter +/// for a target kind that the session does not use reports an unsupported +/// status, and replacing a texture target while a frame is acquired reports an +/// invalid-state status. +final class RenderSessionHandle implements Finalizable { + RenderSessionHandle._(this._runtime, NativeRenderSession handle) + : _state = NativeHandleState(handle, 'RenderSessionHandle'); - /// Presents this attached surface session through a new Metal surface. + final RuntimeHandle _runtime; + final NativeHandleState _state; + final _frameResultsReady = StreamController.broadcast(); + final _driverWorkReady = StreamController.broadcast(); + + /// Notifications that frame results are available to drain. /// - /// Replacing the surface in place keeps this session's renderer along with - /// the tile pyramid, atlases, and symbol placement. Map-owned feature state - /// is unchanged. + /// Readiness is level-triggered and notifications may coalesce, so call + /// [drainFrameResults] until it returns an empty batch. Events arrive on the + /// isolate that attached the session. + Stream get frameResultsReady => _frameResultsReady.stream; + + /// Notifications that caller-graphics-thread work is ready to service. /// - /// [descriptor] names the same graphics context this session attached with, - /// and its extent applies as [resize] applies one. A descriptor whose context - /// device is neither null nor this session's device throws an - /// [InvalidArgumentException] and leaves this session rendering into the - /// surface it has. The session assigns the layer its own device and pixel - /// format. - void setMetalSurfaceTarget(MetalSurfaceDescriptor descriptor) { - _checkNoActiveTextureFrame('set Metal surface target'); - withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _metalSurfaceDescriptorToNative(descriptor); - _check(raw.mln_metal_surface_set_target(_handle.raw, nativeDescriptor)); - }); + /// Notifications may coalesce, so call [serviceDriverWork] until the mailbox + /// drains. Events arrive on the isolate that attached the session. + Stream get driverWorkReady => _driverWorkReady.stream; + + void _notifyFramesReady() => _frameResultsReady.add(null); + void _notifyDriverWorkReady() => _driverWorkReady.add(null); + + /// Whether this session has been closed by the Dart binding. + bool get isClosed => _state.isClosed; + + Future _completeWhileRetained(Future completion) async { + final retained = this; + try { + await completion; + } finally { + retained.isClosed; + } } - /// Presents this attached surface session through a new Vulkan surface. + NativeRenderSession get _handle => _state.handle; + + /// Copies the capabilities that attachment fixed for this session. + RenderSessionCapabilities get capabilities => withNativeArena((arena) { + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_render_session_get_capabilities(_handle.raw, out)); + return RenderSessionCapabilities._( + RenderDriver.values.firstWhere( + (value) => value.rawValue == out.ref.driver, + ), + out.ref.texture_ring_depth, + out.ref.flags, + ); + }); + + /// Copies the latest session state and generations. + RenderSessionSnapshot get snapshot => withNativeArena((arena) { + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_render_session_get_snapshot(_handle.raw, out)); + final value = out.ref; + return RenderSessionSnapshot._( + state: value.state, + driver: RenderDriver.values.firstWhere( + (driver) => driver.rawValue == value.driver, + ), + latestResult: RenderResult.fromRawValue(value.latest_result), + extent: RenderTargetExtent( + width: value.extent.width, + height: value.extent.height, + scaleFactor: value.extent.scale_factor, + ), + generation: value.generation, + mapUpdateGeneration: value.map_update_generation, + renderedUpdateGeneration: value.rendered_update_generation, + extentGeneration: value.extent_generation, + frameGeneration: value.frame_generation, + latestDemandToken: value.latest_demand_token, + pendingDemandCount: value.pending_demand_count, + acquiredFrameCount: value.acquired_frame_count, + targetReady: value.target_ready, + pendingChanges: value.pending_changes, + ); + }); + + /// Requests one frame and returns without waiting. /// - /// See [setMetalSurfaceTarget] for what replacing a surface preserves. The - /// outgoing `VkSurfaceKHR` must still be valid: this session holds a - /// swapchain built from it, and Vulkan destroys every swapchain before its - /// surface. - void setVulkanSurfaceTarget(VulkanSurfaceDescriptor descriptor) { - _checkNoActiveTextureFrame('set Vulkan surface target'); + /// A core-worker session wakes its graphics worker, and a + /// caller-graphics-thread session publishes driver work and notifies + /// [driverWorkReady]. The demand captures the current extent generation, and + /// its terminal result arrives through [drainFrameResults]. + void requestFrame([FrameDemand demand = const FrameDemand()]) { withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _vulkanSurfaceDescriptorToNative(descriptor); - _check(raw.mln_vulkan_surface_set_target(_handle.raw, nativeDescriptor)); + final native = arena() + ..ref = raw.mln_frame_demand_default(); + native.ref.flags = + (demand.renderIfNeeded ? 1 : 0) | (demand.present ? 2 : 0); + native.ref.token = demand.token; + native.ref.coalescing_boundary = demand.coalescingBoundary; + native.ref.timeout_ns = demand.timeoutNanoseconds; + _check(raw.mln_render_session_request_frame(_handle.raw, native)); }); } - /// Presents this attached surface session through a new OpenGL surface. + /// Drains every queued terminal frame result as one owned batch. /// - /// See [setMetalSurfaceTarget] for what replacing a surface preserves. The - /// new surface is made current on the next render, so a host may hand over a - /// replacement for one it has already destroyed, and an unusable surface is - /// reported by the next [renderUpdate] rather than by this call. - void setOpenGLSurfaceTarget(OpenGLSurfaceDescriptor descriptor) { - _checkNoActiveTextureFrame('set OpenGL surface target'); - withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _openglSurfaceDescriptorToNative(descriptor); - _check(raw.mln_opengl_surface_set_target(_handle.raw, nativeDescriptor)); + /// The C API hands over a batch whose records stay stable until release, and + /// this method copies the records into Dart values and releases the batch + /// before returning. Draining again after an empty result is valid. + List drainFrameResults() { + return withNativeArena((arena) { + final outBatch = arena()..value = 0; + _check(raw.mln_render_session_drain_frame_results(_handle.raw, outBatch)); + final batch = outBatch.value; + if (batch == 0) return const []; + try { + final count = arena(); + _check(raw.mln_render_frame_batch_count(batch, count)); + return List.generate(count.value, (index) { + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_render_frame_batch_get(batch, index, out)); + return RenderFrameResult._fromNative(out.ref); + }, growable: false); + } finally { + raw.mln_render_frame_batch_release(batch); + } }); } - /// Renders this attached texture session into a new caller-owned Metal - /// texture. + /// Services up to [maxWork] driver work items and returns the count + /// serviced; zero services every item currently queued. /// - /// Handing a reallocated texture over here keeps this session's renderer, - /// where [resize] rejects a caller-owned texture outright. + /// Call this on the thread where the target's graphics context is current. + /// The first successful call fixes the session's graphics-thread identity, + /// and a later call from another thread reports a wrong-thread status. + /// Attach, resize, target replacement, queries, readback, maintenance, + /// barriers, and detach all use this mailbox, so keep servicing it while + /// presentation is paused. A core-worker session reports an invalid-state status, because + /// its own worker owns execution. /// - /// The replacement belongs to the device this session attached with, which - /// throws an [InvalidArgumentException] otherwise, and carries the pixel - /// format it attached with, which throws an [UnsupportedFeatureException] - /// otherwise; both leave this session rendering into the texture it has. The - /// caller owns the replacement and keeps it valid until the next replacement, - /// [detach], or [close]. The outgoing texture is neither read nor released - /// here, so a host that already released it may still hand over a - /// replacement. - void setMetalBorrowedTextureTarget( - MetalBorrowedTextureDescriptor descriptor, - ) { - _checkNoActiveTextureFrame('set Metal borrowed texture target'); - withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _metalBorrowedTextureDescriptorToNative( - descriptor, - ); + /// [maxWork] must be zero or positive. + int serviceDriverWork({int maxWork = 0}) { + if (maxWork < 0) { + throwInvalidArgument('maxWork must not be negative'); + } + return withNativeArena((arena) { + final serviced = arena(); _check( - raw.mln_metal_borrowed_texture_set_target( + raw.mln_render_session_service_driver_work( _handle.raw, - nativeDescriptor, + maxWork, + serviced, ), ); + return serviced.value; }); } - /// Renders this attached texture session into a new caller-owned Vulkan - /// image. + /// Starts an ordered resize and completes after the driver applies the + /// extent and updates the map viewport. /// - /// See [setMetalBorrowedTextureTarget] for what replacing a target preserves. - /// The replacement carries the format and both layouts this session attached - /// with, since its render pass was built around them. - void setVulkanBorrowedTextureTarget( - VulkanBorrowedTextureDescriptor descriptor, - ) { - _checkNoActiveTextureFrame('set Vulkan borrowed texture target'); - withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _vulkanBorrowedTextureDescriptorToNative( - descriptor, - ); - _check( - raw.mln_vulkan_borrowed_texture_set_target( - _handle.raw, - nativeDescriptor, - ), - ); - }); - } - - /// Renders this attached texture session into a new caller-owned OpenGL - /// texture. + /// A surface or session-owned texture target resizes in place and keeps the + /// renderer, which carries the tile pyramid, the glyph and image atlases, + /// and symbol placement. A changed scale factor retires the renderer + /// instead, because its shaders are compiled for one pixel ratio. + /// Map-owned feature state survives either way and reaches the replacement + /// renderer on the next render update. /// - /// See [setMetalBorrowedTextureTarget] for what replacing a target preserves. - /// The replacement belongs to the context this session attached with, or one - /// in its share group, and that context must be current on this isolate's - /// thread. - void setOpenGLBorrowedTextureTarget( - OpenGLBorrowedTextureDescriptor descriptor, - ) { - _checkNoActiveTextureFrame('set OpenGL borrowed texture target'); - withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _openglBorrowedTextureDescriptorToNative( - descriptor, - ); - _check( - raw.mln_opengl_borrowed_texture_set_target( - _handle.raw, - nativeDescriptor, - ), + /// A caller-owned texture is sized by its owner, so resizing one reports an + /// unsupported status: allocate a texture at the new size and hand it over + /// with the backend's borrowed-texture target setter. A resize applied while + /// a texture frame is acquired fails with an invalid-state status. + Future resize(RenderTargetExtent extent) => _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _renderTargetExtentToNative(extent); + return raw.mln_render_session_resize(_handle.raw, native, out); + }), + ); + + /// Starts an ordered Metal surface replacement. + Future setMetalSurfaceTarget(MetalSurfaceDescriptor descriptor) => + _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _metalSurfaceDescriptorToNative(descriptor); + return raw.mln_metal_surface_set_target(_handle.raw, native, out); + }), ); - }); - } - /// Renders the latest map update into this session's render target. - /// - /// [RenderUpdate.result] names the wake to wait for before calling again: - /// - /// - [RenderResult.rendered]: the target holds a new frame. The map retains - /// its latest update, so a host redraws on demand after a resize or a - /// surface expose, and gates a frame loop on - /// [RuntimeEventType.mapRenderUpdateAvailable]. - /// - [RenderResult.noUpdate]: the call produced no frame. The map either has - /// no update yet, or the Metal backend has not created an owned texture - /// because content is not ready. Wait for - /// [RuntimeEventType.mapRenderUpdateAvailable]. - /// - [RenderResult.sizePending]: this session resized and the map, which - /// applies its size on its own thread, is still behind. The map publishes - /// an update for the new size on its own, so wait for the next - /// [RuntimeEventType.mapRenderUpdateAvailable]. - /// - [RenderResult.targetNotReady]: the render target had no frame - /// available, such as a Metal surface whose next drawable is nil. Wait for - /// a host event that changes the target, or back off and retry. - /// - /// [RenderUpdate.needsRepaint] reports whether the map asked for another - /// frame while it rendered this one, so a host can re-arm its frame loop - /// before it drains events. - RenderUpdate renderUpdate() { - _checkNoActiveTextureFrame('render update'); - return withNativeArena((arena) { - final result = arena(); - final needsRepaint = arena(); - _check( - raw.mln_render_session_render_update(_handle.raw, result, needsRepaint), + /// Starts an ordered Vulkan surface replacement. + Future setVulkanSurfaceTarget(VulkanSurfaceDescriptor descriptor) => + _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _vulkanSurfaceDescriptorToNative(descriptor); + return raw.mln_vulkan_surface_set_target(_handle.raw, native, out); + }), ); - return RenderUpdate._( - result: RenderResult.fromRawValue(result.value), - needsRepaint: needsRepaint.value, + + /// Starts an ordered OpenGL surface replacement. + Future setOpenGLSurfaceTarget(OpenGLSurfaceDescriptor descriptor) => + _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _openglSurfaceDescriptorToNative(descriptor, arena); + return raw.mln_opengl_surface_set_target(_handle.raw, native, out); + }), ); - }); - } - /// Detaches backend-bound render resources while keeping the handle live. - void detach() { - _checkNoActiveTextureFrame('detach render session'); - _check(raw.mln_render_session_detach(_handle.raw)); - } + /// Starts an ordered WebGPU surface replacement. + Future setWebGPUSurfaceTarget(WebGPUSurfaceDescriptor descriptor) => + _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _webGPUSurfaceDescriptorToNative(descriptor); + return raw.mln_webgpu_surface_set_target(_handle.raw, native, out); + }), + ); - /// Asks the session renderer to release cached resources where possible. - void reduceMemoryUse() { - _check(raw.mln_render_session_reduce_memory_use(_handle.raw)); - } + /// Starts an ordered caller-owned Metal texture replacement. + Future setMetalBorrowedTextureTarget( + MetalBorrowedTextureDescriptor descriptor, + ) => _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _metalBorrowedTextureDescriptorToNative(descriptor); + return raw.mln_metal_borrowed_texture_set_target( + _handle.raw, + native, + out, + ); + }), + ); - /// Clears renderer data for the session. - void clearData() { - _check(raw.mln_render_session_clear_data(_handle.raw)); - } + /// Starts an ordered caller-owned Vulkan texture replacement. + Future setVulkanBorrowedTextureTarget( + VulkanBorrowedTextureDescriptor descriptor, + ) => _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _vulkanBorrowedTextureDescriptorToNative(descriptor); + return raw.mln_vulkan_borrowed_texture_set_target( + _handle.raw, + native, + out, + ); + }), + ); - /// Dumps renderer debug logs through MapLibre Native logging. - void dumpDebugLogs() { - _check(raw.mln_render_session_dump_debug_logs(_handle.raw)); - } + /// Starts an ordered caller-owned OpenGL texture replacement. + Future setOpenGLBorrowedTextureTarget( + OpenGLBorrowedTextureDescriptor descriptor, + ) => _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _openglBorrowedTextureDescriptorToNative(descriptor, arena); + return raw.mln_opengl_borrowed_texture_set_target( + _handle.raw, + native, + out, + ); + }), + ); + + /// Starts an ordered caller-owned WebGPU texture replacement. + Future setWebGPUBorrowedTextureTarget( + WebGPUBorrowedTextureDescriptor descriptor, + ) => _voidOperation( + (out) => withNativeArena((arena) { + final native = arena() + ..ref = _webGPUBorrowedTextureDescriptorToNative(descriptor); + return raw.mln_webgpu_borrowed_texture_set_target( + _handle.raw, + native, + out, + ); + }), + ); - /// Queries rendered features from the latest render session state. - List queryRenderedFeatures( + /// Starts a barrier and completes after every render-session operation + /// accepted before it reaches a terminal result. + /// + /// A barrier requests no frame of its own, so it observes the work that is + /// already accepted rather than driving new work. + Future barrier() => + _voidOperation((out) => raw.mln_render_session_barrier(_handle.raw, out)); + + /// Starts a best-effort release of renderer caches and completes after the + /// driver runs it. + Future reduceMemoryUse() => _voidOperation( + (out) => raw.mln_render_session_reduce_memory_use(_handle.raw, out), + ); + + /// Starts renderer-data clearing and completes after the driver runs it. + Future clearData() => _voidOperation( + (out) => raw.mln_render_session_clear_data(_handle.raw, out), + ); + + /// Starts renderer diagnostic-log emission and completes after the driver + /// runs it. + Future dumpDebugLogs() => _voidOperation( + (out) => raw.mln_render_session_dump_debug_logs(_handle.raw, out), + ); + + /// Starts a rendered-feature query against the session's latest driver state + /// and completes with one copied hit per match. + /// + /// Every input is copied before this returns. A core-worker session runs the + /// query on its worker, and a caller-graphics-thread session publishes driver + /// work and completes the query only after the host services it. + /// + /// A [RenderedQueryBox] is normalized and clipped to the viewport, so a box + /// larger than the viewport queries everything visible and a box outside the + /// viewport matches nothing. A [RenderedQueryPoint] and a + /// [RenderedQueryLineString] are queried as given. + Future> queryRenderedFeatures( RenderedQueryGeometry geometry, { RenderedFeatureQueryOptions? options, - }) { - final resolvedOptions = options ?? RenderedFeatureQueryOptions(); - return withNativeArena((arena) { - final nativeGeometry = arena(); - nativeGeometry.ref = _renderedQueryGeometryToNative(geometry, arena); + }) => _queryFeatures( + (completion) => withNativeArena((arena) { + final nativeGeometry = arena() + ..ref = _renderedQueryGeometryToNative(geometry, arena); final nativeOptions = _renderedFeatureQueryOptionsToNative( - resolvedOptions, + options ?? RenderedFeatureQueryOptions(), arena, ); - final outResult = arena(); - outResult.value = 0; - _check( - raw.mln_render_session_query_rendered_features( - _handle.raw, - nativeGeometry, - nativeOptions, - outResult, - ), + return raw.mln_render_session_query_rendered_features( + _handle.raw, + nativeGeometry, + nativeOptions, + completion, ); - return _copyQueriedFeatureList(NativeQueriedFeatureList(outResult.value)); - }); - } + }), + ); - /// Queries source features from the latest render session state. - List querySourceFeatures( + /// Starts a source-feature query for the source with [sourceId] and + /// completes with one copied hit per match. + /// + /// The query runs against the session's latest driver state, like + /// [queryRenderedFeatures], and covers the features that the source holds + /// rather than the features that the style renders. + Future> querySourceFeatures( String sourceId, { SourceFeatureQueryOptions? options, - }) { - final resolvedOptions = options ?? SourceFeatureQueryOptions(); - return withNativeArena((arena) { - final nativeSourceId = nativeStringView(sourceId, arena); + }) => _queryFeatures( + (completion) => withNativeArena((arena) { final nativeOptions = _sourceFeatureQueryOptionsToNative( - resolvedOptions, + options ?? SourceFeatureQueryOptions(), arena, ); - final outResult = arena(); - outResult.value = 0; - _check( - raw.mln_render_session_query_source_features( - _handle.raw, - nativeSourceId.value, - nativeOptions, - outResult, - ), + return raw.mln_render_session_query_source_features( + _handle.raw, + nativeStringView(sourceId, arena).value, + nativeOptions, + completion, ); - return _copyQueriedFeatureList(NativeQueriedFeatureList(outResult.value)); - }); - } + }), + ); - /// Queries a feature extension from the latest render session state. - Uint8List queryFeatureExtensions({ + /// Starts a feature-extension query against the session's latest driver + /// state and completes with copied JSON bytes. + /// + /// [feature] is one UTF-8 GeoJSON Feature, and [extension] and + /// [extensionField] name the extension to evaluate for it. [arguments] is + /// optional UTF-8 JSON, and must be a JSON object when it is present. + Future queryFeatureExtensions({ required String sourceId, required Uint8List feature, required String extension, required String extensionField, Uint8List? arguments, - }) { - return withNativeArena((arena) { - final nativeSourceId = nativeStringView(sourceId, arena); - final nativeFeature = nativeBufferView(feature, arena); - final nativeExtension = nativeStringView(extension, arena); - final nativeExtensionField = nativeStringView(extensionField, arena); + }) => _bufferOperation( + (completion) => withNativeArena((arena) { final nativeArguments = arguments == null ? nullptr.cast() : (arena() ..ref = nativeBufferView(arguments, arena)); - final outResult = arena(); - outResult.value = 0; - _check( - raw.mln_render_session_query_feature_extensions( - _handle.raw, - nativeSourceId.value, - nativeFeature, - nativeExtension.value, - nativeExtensionField.value, - nativeArguments, - outResult, - ), + return raw.mln_render_session_query_feature_extensions( + _handle.raw, + nativeStringView(sourceId, arena).value, + nativeBufferView(feature, arena), + nativeStringView(extension, arena).value, + nativeStringView(extensionField, arena).value, + nativeArguments, + completion, ); - return copyOwnedBuffer(NativeOwnedBufferHandle(outResult.value)); - }); - } - - /// Reads the latest rendered session-owned texture as premultiplied RGBA8. - TextureImageInfo textureImageInfo() { - _checkNoActiveTextureFrame('read texture image info'); - return withNativeArena((arena) { - final info = arena(); - info.ref = raw.mln_texture_image_info_default(); - // A null buffer with zero capacity is a size probe that reports the - // required byte length. - _check( - raw.mln_texture_read_premultiplied_rgba8( - _handle.raw, - nullptr.cast(), - 0, - info, - ), + }), + ); + + Future _bufferOperation(NativeCompletionStart start) => + _runtime._startValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS, + elementSize: sizeOf(), + start: start, + decode: (result) => + _copyBufferView(result.value.cast().ref), ); - return TextureImageInfo._fromNative(info.ref); - }); - } - /// Reads the latest texture into caller-owned native [buffer]. - TextureImageInfo readPremultipliedRgba8Into(NativeBuffer buffer) { - _checkNoActiveTextureFrame('read texture image'); - final expectedInfo = textureImageInfo(); - if (buffer.byteLength < expectedInfo.byteLength) { - throwInvalidArgument( - 'native buffer length ${buffer.byteLength} is smaller than required ' - '${expectedInfo.byteLength}', - ); - } - return withNativeArena((arena) { - final info = arena(); - info.ref = raw.mln_texture_image_info_default(); - _check( - raw.mln_texture_read_premultiplied_rgba8( - _handle.raw, - Pointer.fromAddress(buffer.unsafePointer.address), - buffer.byteLength, - info, - ), + Future> _queryFeatures(NativeCompletionStart start) => + _runtime._startValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_QUERIED_FEATURES, + elementSize: sizeOf(), + start: start, + decode: (result) => [ + for (var index = 0; index < result.value_count; index += 1) + _queriedFeatureFromNative( + result.value.cast()[index], + ), + ], ); - return TextureImageInfo._fromNative(info.ref); - }); - } - /// Reads the latest rendered session-owned texture as premultiplied RGBA8. - TextureImage readPremultipliedRgba8() { - _checkNoActiveTextureFrame('read texture image'); - final info = textureImageInfo(); - return withNativeArena((arena) { - final data = arena(info.byteLength); - final readInfo = arena(); - readInfo.ref = raw.mln_texture_image_info_default(); - _check( - raw.mln_texture_read_premultiplied_rgba8( - _handle.raw, - data, - info.byteLength, - readInfo, - ), - ); - final copiedInfo = TextureImageInfo._fromNative(readInfo.ref); + /// Starts readback of the latest rendered texture frame and completes with + /// Dart-owned premultiplied RGBA8 bytes. + /// + /// The C API borrows the pixels for its completion only, and this method + /// copies them before the returned future completes. A session whose + /// [RenderSessionCapabilities.supportsReadback] is false reports an + /// unsupported status. + /// + /// Readback returns the frame that the driver already rendered for the + /// session's current generation, so request a frame and let it terminate + /// after each resize or target replacement. Reading back before that frame + /// exists reports an invalid-state status. + Future readPremultipliedRgba8() => _runtime._startValue( + copyKind: raw + .mln_adapter_completion_copy_kind + .MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK, + elementSize: sizeOf(), + start: (completion) => + raw.mln_texture_read_premultiplied_rgba8(_handle.raw, completion), + decode: (result) { + final value = result.value.cast().ref; return TextureImage( - info: copiedInfo, - bytes: Uint8List.fromList(data.asTypedList(copiedInfo.byteLength)), + info: TextureImageInfo._fromNative(value.info), + bytes: _copyBufferView(value.data), ); - }); - } + }, + ); - /// Acquires the latest Metal texture frame until [MetalOwnedTextureFrame.close]. - MetalOwnedTextureFrame acquireMetalTextureFrame() { - _checkNoActiveTextureFrame('acquire Metal texture frame'); - final outFrame = calloc(); - try { - outFrame.ref.size = sizeOf(); - _check(raw.mln_metal_owned_texture_acquire_frame(_handle.raw, outFrame)); - } catch (_) { - calloc.free(outFrame); - rethrow; - } - return _constructAcquiredTextureFrame( - outFrame, - () => raw.mln_metal_owned_texture_release_frame(_handle.raw, outFrame), - () => MetalOwnedTextureFrame._(this, outFrame), - ); - } + /// Leases the oldest rendered texture-ring slot that no lease already holds. + /// + /// The call is nonblocking, and the lease owns its slot until + /// [AcquiredFrame.release]. A session whose + /// [RenderSessionCapabilities.supportsFrameAcquisition] is false reports an + /// unsupported status, and a session with no rendered slot ready reports a + /// not-ready status, so pace acquisition with [drainFrameResults] or + /// [RenderSessionSnapshot.frameGeneration]. + AcquiredFrame acquireFrame() => withNativeArena((arena) { + final out = arena()..value = 0; + _check(raw.mln_render_session_acquire_frame(_handle.raw, out)); + return AcquiredFrame._(out.value); + }); - /// Acquires the latest Vulkan texture frame until [VulkanOwnedTextureFrame.close]. - VulkanOwnedTextureFrame acquireVulkanTextureFrame() { - _checkNoActiveTextureFrame('acquire Vulkan texture frame'); - final outFrame = calloc(); - try { - outFrame.ref.size = sizeOf(); - _check(raw.mln_vulkan_owned_texture_acquire_frame(_handle.raw, outFrame)); - } catch (_) { - calloc.free(outFrame); - rethrow; - } - return _constructAcquiredTextureFrame( - outFrame, - () => raw.mln_vulkan_owned_texture_release_frame(_handle.raw, outFrame), - () => VulkanOwnedTextureFrame._(this, outFrame), + /// Starts normal graphics teardown and map detachment, and completes after + /// the driver destroys the session's graphics resources. + /// + /// Every mailbox operation accepted earlier reaches a terminal result before + /// teardown runs, so the host's target, device, and borrowed synchronization + /// objects stay in use until the returned future completes. A session that + /// still holds an acquired frame reports an invalid-state status and stays + /// attached, so release every [AcquiredFrame] first. + Future detach() => + _voidOperation((out) => raw.mln_render_session_detach(_handle.raw, out)); + + /// Irreversibly closes this session's control and mailboxes without calling + /// the graphics driver. + /// + /// The call waits for the map's in-flight tile work, which can still reach + /// the host's graphics objects through quarantined renderer resources, so the + /// host may destroy its target and device as soon as this returns. Do not + /// call it from a MapLibre callback. Resources that could not be destroyed + /// are reported as quarantined. + RenderAbandonResult abandon() => withNativeArena((arena) { + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_render_session_abandon(_handle.raw, out)); + return RenderAbandonResult._( + out.ref.disposition == 1, + out.ref.quarantined_resource_count, ); - } + }); - /// Acquires the latest OpenGL texture frame until [OpenGLOwnedTextureFrame.close]. - OpenGLOwnedTextureFrame acquireOpenGLTextureFrame() { - _checkNoActiveTextureFrame('acquire OpenGL texture frame'); - final outFrame = calloc(); - try { - outFrame.ref.size = sizeOf(); - _check(raw.mln_opengl_owned_texture_acquire_frame(_handle.raw, outFrame)); - } catch (_) { - calloc.free(outFrame); - rethrow; - } - return _constructAcquiredTextureFrame( - outFrame, - () => raw.mln_opengl_owned_texture_release_frame(_handle.raw, outFrame), - () => OpenGLOwnedTextureFrame._(this, outFrame), - ); - } + Future _voidOperation(NativeCompletionStart start) => + _runtime._startUnit(start); - /// Explicitly destroys this render session. + /// Retires the native session handle and closes [frameResultsReady] and + /// [driverWorkReady]. + /// + /// Close a session after [detach] completes or after [abandon] returns. The + /// call is CPU-only and may run on any isolate, and it waits for a + /// core-worker session's worker to stop. A session that is still attached + /// reports an invalid-state status, as does a detached session that still + /// holds an acquired frame. void close() { - _checkNoActiveTextureFrame('close render session'); _state.close( (handle) => raw.mln_render_session_destroy(handle.raw), _c.threadLastErrorMessage, ); - } - - void _registerTextureFrame(Object frame, void Function() release) { - _checkNoActiveTextureFrame('acquire texture frame'); - _activeTextureFrame = _TextureFrameLease(frame, release); - } - - T _constructAcquiredTextureFrame( - Pointer descriptor, - int Function() release, - T Function() construct, - ) { - try { - return construct(); - } catch (error, stackTrace) { - cleanupFailedFrameConstruction( - release: release, - releaseSucceeded: () => calloc.free(descriptor), - releaseFailed: () { - _activeTextureFrame = _TextureFrameLease.orphaned(() { - _check(release()); - calloc.free(descriptor); - }); - }, - ); - Error.throwWithStackTrace(error, stackTrace); - } - } - - void _releaseTextureFrame(Object frame) { - final lease = _activeTextureFrame; - if (lease == null || !identical(lease.owner?.target, frame)) { - return; - } - lease.release(); - _activeTextureFrame = null; - } - - void _checkNoActiveTextureFrame(String operation) { - final lease = _activeTextureFrame; - if (lease == null) { - return; - } - if (lease.owner?.target != null) { - throwInvalidState( - '$operation requires releasing the active texture frame', - ); - } - lease.release(); - _activeTextureFrame = null; + _frameResultsReady.close(); + _driverWorkReady.close(); } } @@ -741,9 +1035,8 @@ extension type const ResourceRequestHandle._(NativeResourceRequest _handle) { } } -/// Exposes an attach reference's map id for tests that must reach the C API -/// with a raw id. -int mapAttachRefIdForTesting(MapAttachRef ref) => ref._mapId; +/// Exposes a map's handle id for tests that must reach the C API with a raw id. +int mapHandleIdForTesting(MapHandle map) => map._state.handleId; /// Exposes a runtime's handle id for tests that must reach the C API with a raw /// id. @@ -781,6 +1074,7 @@ final class TextureImageInfo { /// Dart-owned premultiplied RGBA8 texture readback bytes. final class TextureImage { + /// Creates a texture image, copying [bytes] into an unmodifiable view. TextureImage({required this.info, required Uint8List bytes}) : bytes = Uint8List.fromList(bytes).asUnmodifiableView(); @@ -791,612 +1085,729 @@ final class TextureImage { final Uint8List bytes; } -/// Scoped Metal texture frame borrowed from a session-owned texture target. -final class MetalOwnedTextureFrame implements Finalizable { - MetalOwnedTextureFrame._(this._session, this._frame) { - final frame = _frame.ref; - _generation = uint64FromNative(frame.generation); - _width = frame.width; - _height = frame.height; - _scaleFactor = frame.scale_factor; - _frameId = uint64FromNative(frame.frame_id); - _pixelFormat = uint64FromNative(frame.pixel_format); - _textureAddress = frame.texture.address; - _deviceAddress = frame.device.address; - _leakReporter = NativeLeakReporter( - this, - 'MetalOwnedTextureFrame', - NativeHandle(_frame.address), - ); - try { - final session = _session; - final descriptor = _frame; - _session._registerTextureFrame(this, () { - _check( - raw.mln_metal_owned_texture_release_frame( - session._handle.raw, - descriptor, - ), - ); - calloc.free(descriptor); - }); - } catch (_) { - _leakReporter.close(); - rethrow; +/// Scoped lease on one rendered texture-ring slot. +final class AcquiredFrame { + AcquiredFrame._(this._handle); + + int _handle; + + void _checkOpen() { + if (_handle == 0) { + throwInvalidState('acquired frame has already been released'); } } - final RenderSessionHandle _session; - final Pointer _frame; - late final BigInt _generation; - late final int _width; - late final int _height; - late final double _scaleFactor; - late final BigInt _frameId; - late final BigInt _pixelFormat; - late final int _textureAddress; - late final int _deviceAddress; - late final NativeLeakReporter _leakReporter; - var _closed = false; - - /// Session-owned target generation that produced this frame. - BigInt get generation { + /// Copies the frame result of the demand that produced this frame. + RenderFrameResult get result => withNativeArena((arena) { _checkOpen(); - return _generation; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_result(_handle, out)); + return RenderFrameResult._fromNative(out.ref); + }); - /// Physical texture width in device pixels. - int get width { + /// Copies the synchronization that the session signals when the rendered + /// contents of this frame are ready for the consumer. + /// + /// Wait on it before the consumer reads the frame's texture. + GpuSync get producerSync => withNativeArena((arena) { _checkOpen(); - return _width; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_producer_sync(_handle, out)); + return GpuSync.native( + kind: out.ref.kind, + object: out.ref.object == nullptr + ? null + : NativePointer(out.ref.object.address), + value: out.ref.value, + ); + }); - /// Physical texture height in device pixels. - int get height { + /// Copies WebGPU-native metadata for this frame. + /// + /// A frame from another render backend reports an unsupported status. + WebGPUOwnedTextureFrame get webGPUTexture => withNativeArena((arena) { _checkOpen(); - return _height; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_webgpu_texture(_handle, out)); + return WebGPUOwnedTextureFrame._fromNative(out.ref, this); + }); - /// UI-to-device pixel scale used for this frame. - double get scaleFactor { + /// Copies Metal-native metadata for this frame. + /// + /// A frame from another render backend reports an unsupported status. + MetalOwnedTextureFrame get metalTexture => withNativeArena((arena) { _checkOpen(); - return _scaleFactor; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_metal_texture(_handle, out)); + return MetalOwnedTextureFrame._fromNative(out.ref, this); + }); - /// Backend frame identity within [generation]. - BigInt get frameId { + /// Copies Vulkan-native metadata for this frame. + /// + /// A frame from another render backend reports an unsupported status. + VulkanOwnedTextureFrame get vulkanTexture => withNativeArena((arena) { _checkOpen(); - return _frameId; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_vulkan_texture(_handle, out)); + return VulkanOwnedTextureFrame._fromNative(out.ref, this); + }); - /// Backend-native Metal pixel format value. - BigInt get pixelFormat { + /// Copies OpenGL-native metadata for this frame. + /// + /// Read it on the thread where the session's OpenGL context is current. A + /// frame from another render backend reports an unsupported status. + OpenGLOwnedTextureFrame get openGLTexture => withNativeArena((arena) { _checkOpen(); - return _pixelFormat; - } + final out = arena() + ..ref.size = sizeOf(); + _check(raw.mln_acquired_frame_get_opengl_texture(_handle, out)); + return OpenGLOwnedTextureFrame._fromNative(out.ref, this); + }); - /// Unsafe borrowed `id` / `MTL::Texture*` pointer. + /// Returns the leased slot to the session after consumer work on [sync]. /// - /// The pointer is valid only until [close] releases this frame. - ScopedNativePointer get unsafeTexture => - _borrowedPointer(_textureAddress, 'Metal texture'); + /// The session retires the slot through its driver before it renders into + /// the slot again, and every pointer that this lease exposed becomes invalid + /// here. Release is one-shot, and a second call throws an invalid-state + /// error. A backend synchronization kind that the session does not accept + /// reports an unsupported status and keeps the lease, so the host may retry + /// with [GpuSync.cpuComplete]. + void release({GpuSync sync = const GpuSync.cpuComplete()}) { + withNativeArena((arena) { + _checkOpen(); + final frame = arena()..value = _handle; + final nativeSync = arena() + ..ref = raw.mln_gpu_sync_default() + ..ref.kind = sync.kind + ..ref.object = Pointer.fromAddress(sync.object?.address ?? 0) + ..ref.value = sync.value; + _check(raw.mln_acquired_frame_release(frame, nativeSync)); + _handle = 0; + }); + } +} - /// Unsafe borrowed `id` / `MTL::Device*` pointer. - /// - /// The pointer is valid only until [close] releases this frame. - ScopedNativePointer get unsafeDevice => - _borrowedPointer(_deviceAddress, 'Metal device'); +/// Scoped Metal metadata from an [AcquiredFrame]. +final class MetalOwnedTextureFrame { + const MetalOwnedTextureFrame._( + this.generation, + this.width, + this.height, + this.scaleFactor, + this.frameId, + this.pixelFormat, + this._textureAddress, + this._deviceAddress, + this._owner, + ); + + factory MetalOwnedTextureFrame._fromNative( + raw.mln_metal_owned_texture_frame value, + AcquiredFrame owner, + ) => MetalOwnedTextureFrame._( + value.generation, + value.width, + value.height, + value.scale_factor, + value.frame_id, + value.pixel_format, + value.texture.address, + value.device.address, + owner, + ); + + /// Session generation that produced this frame. + final int generation; + + /// Physical Metal texture width in device pixels. + final int width; - /// Releases this frame. The unsafe backend pointers become invalid. - void close() { - if (_closed) { - return; - } - _session._releaseTextureFrame(this); - _leakReporter.close(); - _closed = true; - } + /// Physical Metal texture height in device pixels. + final int height; - ScopedNativePointer _borrowedPointer(int address, String name) { - _checkOpen(); - return ScopedNativePointer( - address, - checkValid: _checkOpen, - debugName: name, - ); - } + /// UI-to-device pixel scale used for this frame. + final double scaleFactor; - void _checkOpen() { - if (_closed) { - throwInvalidArgument('Metal texture frame has already been released'); - } - final _ = _session._handle.raw; - } -} + /// Opaque frame identity that the session uses to reject a stale release. + final int frameId; -/// Scoped Vulkan texture frame borrowed from a session-owned texture target. -final class VulkanOwnedTextureFrame implements Finalizable { - VulkanOwnedTextureFrame._(this._session, this._frame) { - final frame = _frame.ref; - _generation = uint64FromNative(frame.generation); - _width = frame.width; - _height = frame.height; - _scaleFactor = frame.scale_factor; - _frameId = uint64FromNative(frame.frame_id); - _format = frame.format; - _layout = frame.layout; - _imageAddress = frame.image.address; - _imageViewAddress = frame.image_view.address; - _deviceAddress = frame.device.address; - _leakReporter = NativeLeakReporter( - this, - 'VulkanOwnedTextureFrame', - NativeHandle(_frame.address), - ); - try { - final session = _session; - final descriptor = _frame; - _session._registerTextureFrame(this, () { - _check( - raw.mln_vulkan_owned_texture_release_frame( - session._handle.raw, - descriptor, - ), - ); - calloc.free(descriptor); - }); - } catch (_) { - _leakReporter.close(); - rethrow; - } - } + /// Backend-native `MTLPixelFormat` value. + final int pixelFormat; - final RenderSessionHandle _session; - final Pointer _frame; - late final BigInt _generation; - late final int _width; - late final int _height; - late final double _scaleFactor; - late final BigInt _frameId; - late final int _format; - late final int _layout; - late final int _imageAddress; - late final int _imageViewAddress; - late final int _deviceAddress; - late final NativeLeakReporter _leakReporter; - var _closed = false; - - /// Session-owned target generation that produced this frame. - BigInt get generation { - _checkOpen(); - return _generation; - } + final int _textureAddress; + final int _deviceAddress; + final AcquiredFrame _owner; - /// Physical image width in device pixels. - int get width { - _checkOpen(); - return _width; - } + /// Borrowed `id`, valid until the owning frame is released. + ScopedNativePointer get unsafeTexture => + _scopedPointer(_textureAddress, 'Metal texture', _owner); - /// Physical image height in device pixels. - int get height { - _checkOpen(); - return _height; - } + /// Borrowed `id`, valid until the owning frame is released. + ScopedNativePointer get unsafeDevice => + _scopedPointer(_deviceAddress, 'Metal device', _owner); +} + +/// Scoped Vulkan metadata from an [AcquiredFrame]. +final class VulkanOwnedTextureFrame { + const VulkanOwnedTextureFrame._( + this.generation, + this.width, + this.height, + this.scaleFactor, + this.frameId, + this.format, + this.layout, + this._imageAddress, + this._imageViewAddress, + this._deviceAddress, + this._owner, + ); + + factory VulkanOwnedTextureFrame._fromNative( + raw.mln_vulkan_owned_texture_frame value, + AcquiredFrame owner, + ) => VulkanOwnedTextureFrame._( + value.generation, + value.width, + value.height, + value.scale_factor, + value.frame_id, + value.format, + value.layout, + value.image.address, + value.image_view.address, + value.device.address, + owner, + ); + + /// Session generation that produced this frame. + final int generation; + + /// Physical Vulkan image width in device pixels. + final int width; + + /// Physical Vulkan image height in device pixels. + final int height; /// UI-to-device pixel scale used for this frame. - double get scaleFactor { - _checkOpen(); - return _scaleFactor; - } + final double scaleFactor; - /// Backend frame identity within [generation]. - BigInt get frameId { - _checkOpen(); - return _frameId; - } + /// Opaque frame identity that the session uses to reject a stale release. + final int frameId; - /// Backend-native Vulkan format value. - int get format { - _checkOpen(); - return _format; - } + /// Backend-native `VkFormat` value. + final int format; - /// Backend-native Vulkan image layout value. - int get layout { - _checkOpen(); - return _layout; - } + /// Backend-native `VkImageLayout` value that the image is left in, which + /// allows the host to sample the frame. + final int layout; - /// Unsafe borrowed VkImage pointer. - /// - /// The pointer is valid only until [close] releases this frame. + final int _imageAddress; + final int _imageViewAddress; + final int _deviceAddress; + final AcquiredFrame _owner; + + /// Borrowed `VkImage`, valid until the owning frame is released. ScopedNativePointer get unsafeImage => - _borrowedPointer(_imageAddress, 'Vulkan image'); + _scopedPointer(_imageAddress, 'Vulkan image', _owner); - /// Unsafe borrowed VkImageView pointer. - /// - /// The pointer is valid only until [close] releases this frame. + /// Borrowed `VkImageView`, valid until the owning frame is released. ScopedNativePointer get unsafeImageView => - _borrowedPointer(_imageViewAddress, 'Vulkan image view'); + _scopedPointer(_imageViewAddress, 'Vulkan image view', _owner); - /// Unsafe borrowed VkDevice pointer. - /// - /// The pointer is valid only until [close] releases this frame. + /// Borrowed `VkDevice`, valid until the owning frame is released. ScopedNativePointer get unsafeDevice => - _borrowedPointer(_deviceAddress, 'Vulkan device'); + _scopedPointer(_deviceAddress, 'Vulkan device', _owner); +} - /// Releases this frame. The unsafe backend pointers become invalid. - void close() { - if (_closed) { - return; - } - _session._releaseTextureFrame(this); - _leakReporter.close(); - _closed = true; - } +/// Scoped WebGPU metadata from an [AcquiredFrame]. +final class WebGPUOwnedTextureFrame { + const WebGPUOwnedTextureFrame._( + this.generation, + this.width, + this.height, + this.scaleFactor, + this.frameId, + this.format, + this._textureAddress, + this._textureViewAddress, + this._deviceAddress, + this._owner, + ); + + factory WebGPUOwnedTextureFrame._fromNative( + raw.mln_webgpu_owned_texture_frame value, + AcquiredFrame owner, + ) => WebGPUOwnedTextureFrame._( + value.generation, + value.width, + value.height, + value.scale_factor, + value.frame_id, + value.format, + value.texture.address, + value.texture_view.address, + value.device.address, + owner, + ); + + /// Session generation that produced this frame. + final int generation; + + /// Physical WebGPU texture width in device pixels. + final int width; - ScopedNativePointer _borrowedPointer(int address, String name) { - _checkOpen(); - return ScopedNativePointer( - address, - checkValid: _checkOpen, - debugName: name, - ); - } + /// Physical WebGPU texture height in device pixels. + final int height; - void _checkOpen() { - if (_closed) { - throwInvalidArgument('Vulkan texture frame has already been released'); - } - final _ = _session._handle.raw; - } -} + /// UI-to-device pixel scale used for this frame. + final double scaleFactor; -/// Scoped OpenGL texture frame borrowed from a session-owned texture target. -final class OpenGLOwnedTextureFrame implements Finalizable { - OpenGLOwnedTextureFrame._(this._session, this._frame) { - final frame = _frame.ref; - _generation = uint64FromNative(frame.generation); - _width = frame.width; - _height = frame.height; - _scaleFactor = frame.scale_factor; - _frameId = uint64FromNative(frame.frame_id); - _texture = frame.texture; - _target = frame.target; - _internalFormat = frame.internal_format; - _format = frame.format; - _type = frame.type; - _leakReporter = NativeLeakReporter( - this, - 'OpenGLOwnedTextureFrame', - NativeHandle(_frame.address), - ); - try { - final session = _session; - final descriptor = _frame; - _session._registerTextureFrame(this, () { - _check( - raw.mln_opengl_owned_texture_release_frame( - session._handle.raw, - descriptor, - ), - ); - calloc.free(descriptor); - }); - } catch (_) { - _leakReporter.close(); - rethrow; - } - } + /// Opaque frame identity that the session uses to reject a stale release. + final int frameId; - final RenderSessionHandle _session; - final Pointer _frame; - late final BigInt _generation; - late final int _width; - late final int _height; - late final double _scaleFactor; - late final BigInt _frameId; - late final int _texture; - late final int _target; - late final int _internalFormat; - late final int _format; - late final int _type; - late final NativeLeakReporter _leakReporter; - var _closed = false; - - /// Session-owned target generation that produced this frame. - BigInt get generation { - _checkOpen(); - return _generation; - } + /// Backend-native `WGPUTextureFormat` value. + final int format; - /// Physical texture width in device pixels. - int get width { - _checkOpen(); - return _width; - } + final int _textureAddress; + final int _textureViewAddress; + final int _deviceAddress; + final AcquiredFrame _owner; - /// Physical texture height in device pixels. - int get height { - _checkOpen(); - return _height; - } + /// Borrowed `WGPUTexture`, valid until the owning frame is released. + ScopedNativePointer get unsafeTexture => + _scopedPointer(_textureAddress, 'WebGPU texture', _owner); + + /// Borrowed `WGPUTextureView`, valid until the owning frame is released. + ScopedNativePointer get unsafeTextureView => + _scopedPointer(_textureViewAddress, 'WebGPU texture view', _owner); + + /// Borrowed `WGPUDevice`, valid until the owning frame is released. + ScopedNativePointer get unsafeDevice => + _scopedPointer(_deviceAddress, 'WebGPU device', _owner); +} + +/// Scoped OpenGL metadata from an [AcquiredFrame]. +final class OpenGLOwnedTextureFrame { + const OpenGLOwnedTextureFrame._( + this.generation, + this.width, + this.height, + this.scaleFactor, + this.frameId, + this.texture, + this.target, + this.internalFormat, + this.format, + this.type, + this._owner, + ); + + factory OpenGLOwnedTextureFrame._fromNative( + raw.mln_opengl_owned_texture_frame value, + AcquiredFrame owner, + ) => OpenGLOwnedTextureFrame._( + value.generation, + value.width, + value.height, + value.scale_factor, + value.frame_id, + value.texture, + value.target, + value.internal_format, + value.format, + value.type, + owner, + ); + + /// Session generation that produced this frame. + final int generation; + + /// Physical OpenGL texture width in device pixels. + final int width; + + /// Physical OpenGL texture height in device pixels. + final int height; /// UI-to-device pixel scale used for this frame. - double get scaleFactor { - _checkOpen(); - return _scaleFactor; - } + final double scaleFactor; - /// Backend frame identity within [generation]. - BigInt get frameId { - _checkOpen(); - return _frameId; - } + /// Opaque frame identity that the session uses to reject a stale release. + final int frameId; - /// Borrowed OpenGL texture name. - ScopedNativeInt get texture { - _checkOpen(); - return ScopedNativeInt( - _texture, - checkValid: _checkOpen, - debugName: 'OpenGL texture', - ); - } + /// Borrowed OpenGL texture object name, valid until the owning frame is + /// released. + final int texture; - /// Backend-native OpenGL texture target. - int get target { - _checkOpen(); - return _target; - } + /// OpenGL texture target, which is expected to be `GL_TEXTURE_2D`. + final int target; - /// Backend-native OpenGL internal format. - int get internalFormat { - _checkOpen(); - return _internalFormat; - } + /// OpenGL internal format, such as `GL_RGBA8`. + final int internalFormat; - /// Backend-native OpenGL pixel format. - int get format { - _checkOpen(); - return _format; - } + /// OpenGL pixel format, such as `GL_RGBA`. + final int format; - /// Backend-native OpenGL pixel type. - int get type { - _checkOpen(); - return _type; - } + /// OpenGL pixel type, such as `GL_UNSIGNED_BYTE`. + final int type; - /// Releases this frame. The borrowed texture name becomes invalid. - void close() { - if (_closed) { - return; - } - _session._releaseTextureFrame(this); - _leakReporter.close(); - _closed = true; - } + final AcquiredFrame _owner; - void _checkOpen() { - if (_closed) { - throwInvalidArgument('OpenGL texture frame has already been released'); - } - final _ = _session._handle.raw; - } + /// Throws when the owning frame is already released, so a host can check + /// [texture] before it uses the name. + void ensureValid() => _owner._checkOpen(); } -/// A reference to a map for attaching a render session, safe to send to another -/// isolate. -/// -/// Produced by [MapHandle.attachRef]. Every attach function lives here rather -/// than on [MapHandle], because attaching is the one map operation that runs on -/// the render session's isolate instead of the map's. +ScopedNativePointer _scopedPointer( + int address, + String name, + AcquiredFrame owner, +) => + ScopedNativePointer(address, checkValid: owner._checkOpen, debugName: name); + +/// Render-session attachment operations on an any-thread map handle. /// -/// This carries only the map's handle id and does not keep the map alive. -/// Attaching against a closed map is rejected as stale rather than binding the -/// session to a later map. +/// Every attachment copies its descriptor and its +/// [RenderSessionAttachOptions] before returning, and returns a +/// [RenderSessionAttachment] whose session is usable at once. The session +/// finishes initializing its target when the attachment's completion resolves. +/// The isolate that attaches a session receives its +/// [RenderSessionHandle.frameResultsReady] and +/// [RenderSessionHandle.driverWorkReady] notifications. /// -/// Every attach opens with [ensureAbiVersion], because an attach is routinely -/// the first native call on its isolate and a session created by a mismatched -/// library would leave the map attached with nothing able to detach it. -final class MapAttachRef { - const MapAttachRef._(this._mapId); +/// A target that requires one [RenderDriver] rejects the other during +/// attachment, which is why the defaults below differ per backend. +extension MapRenderAttachments on MapHandle { + /// Starts attachment of a Metal surface target. + /// + /// A core-worker session retains the Metal layer and device on its worker. A + /// caller-graphics-thread session initializes the target when the host + /// services driver work with the Metal context usable on that thread. + RenderSessionAttachment attachMetalSurface( + MetalSurfaceDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _metalSurfaceDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_metal_surface_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - final int _mapId; + /// Starts attachment of a Vulkan surface target. + RenderSessionAttachment attachVulkanSurface( + VulkanSurfaceDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _vulkanSurfaceDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_vulkan_surface_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - NativeMap get _mapHandle => NativeMap(_mapId); + /// Starts attachment of an OpenGL surface target. + /// + /// WGL, EGL, and existing WebGL contexts require + /// [RenderDriver.callerGraphicsThread]. A transferred WebGL canvas also + /// supports [RenderDriver.coreWorker]. Context ownership stays with the + /// host. + RenderSessionAttachment attachOpenGLSurface( + OpenGLSurfaceDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _openglSurfaceDescriptorToNative(descriptor, arena); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_opengl_surface_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Metal native surface render target to the map. - RenderSessionHandle attachMetalSurface(MetalSurfaceDescriptor descriptor) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _metalSurfaceDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_metal_surface_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a session-owned Metal texture ring. + RenderSessionAttachment attachMetalOwnedTexture( + MetalOwnedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _metalOwnedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_metal_owned_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Vulkan native surface render target to the map. - RenderSessionHandle attachVulkanSurface(VulkanSurfaceDescriptor descriptor) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _vulkanSurfaceDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_vulkan_surface_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a caller-owned Metal texture target. + /// + /// The host owns the texture and keeps it valid until the session detaches + /// or closes. + RenderSessionAttachment attachMetalBorrowedTexture( + MetalBorrowedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _metalBorrowedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_metal_borrowed_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches an OpenGL native surface render target to the map. - RenderSessionHandle attachOpenGLSurface(OpenGLSurfaceDescriptor descriptor) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _openglSurfaceDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_opengl_surface_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a session-owned Vulkan texture ring. + RenderSessionAttachment attachVulkanOwnedTexture( + VulkanOwnedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _vulkanOwnedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_vulkan_owned_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Metal texture render target owned by the render session. - RenderSessionHandle attachMetalOwnedTexture( - MetalOwnedTextureDescriptor descriptor, - ) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _metalOwnedTextureDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_metal_owned_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a caller-owned Vulkan texture target. + /// + /// The host owns the image, its view, and the context, and keeps them valid + /// until the session detaches or closes. + RenderSessionAttachment attachVulkanBorrowedTexture( + VulkanBorrowedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions(), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _vulkanBorrowedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_vulkan_borrowed_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Metal caller-owned texture render target to the map. - RenderSessionHandle attachMetalBorrowedTexture( - MetalBorrowedTextureDescriptor descriptor, - ) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _metalBorrowedTextureDescriptorToNative( - descriptor, - ); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_metal_borrowed_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a session-owned OpenGL texture ring. + /// + /// Shared WGL, EGL, and existing WebGL contexts require + /// [RenderDriver.callerGraphicsThread] and grant frame acquisition, + /// readback, and consumer synchronization. Dedicated EGL and transferred + /// WebGL contexts require [RenderDriver.coreWorker] and grant readback alone + /// with a ring depth of one. + /// + /// The host keeps every backend handle that the descriptor names valid until + /// detach completes, including an initialized `EGLDisplay`. + RenderSessionAttachment attachOpenGLOwnedTexture( + OpenGLOwnedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _openglOwnedTextureDescriptorToNative(descriptor, arena); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_opengl_owned_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Vulkan texture render target owned by the render session. - RenderSessionHandle attachVulkanOwnedTexture( - VulkanOwnedTextureDescriptor descriptor, - ) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _vulkanOwnedTextureDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_vulkan_owned_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a caller-owned OpenGL texture target. + /// + /// The texture belongs to the descriptor's context or to a context in the + /// same share group, and the host keeps both valid until the session + /// detaches or closes. + RenderSessionAttachment attachOpenGLBorrowedTexture( + OpenGLBorrowedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _openglBorrowedTextureDescriptorToNative(descriptor, arena); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_opengl_borrowed_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches a Vulkan caller-owned texture render target to the map. - RenderSessionHandle attachVulkanBorrowedTexture( - VulkanBorrowedTextureDescriptor descriptor, - ) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _vulkanBorrowedTextureDescriptorToNative( - descriptor, - ); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_vulkan_borrowed_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a WebGPU surface target. + /// + /// Browser targets require [RenderDriver.callerGraphicsThread], because + /// WebGPU objects stay in the agent that created them. + RenderSessionAttachment attachWebGPUSurface( + WebGPUSurfaceDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _webGPUSurfaceDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_webgpu_surface_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches an OpenGL texture render target owned by the render session. - RenderSessionHandle attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor descriptor, - ) { - ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = arena(); - nativeDescriptor.ref = _openglOwnedTextureDescriptorToNative(descriptor); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_opengl_owned_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), - ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); - } + /// Starts attachment of a session-owned WebGPU texture ring. + /// + /// Browser targets require [RenderDriver.callerGraphicsThread]. + RenderSessionAttachment attachWebGPUOwnedTexture( + WebGPUOwnedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _webGPUOwnedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_webgpu_owned_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); - /// Attaches an OpenGL caller-owned texture render target to the map. - RenderSessionHandle attachOpenGLBorrowedTexture( - OpenGLBorrowedTextureDescriptor descriptor, + /// Starts attachment of a caller-owned WebGPU texture target. + /// + /// The descriptor's device creates the texture and its view, and the host + /// keeps them valid until the session detaches or closes. + RenderSessionAttachment attachWebGPUBorrowedTexture( + WebGPUBorrowedTextureDescriptor descriptor, { + RenderSessionAttachOptions options = const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + ), + }) => withNativeArena((arena) { + final native = arena() + ..ref = _webGPUBorrowedTextureDescriptorToNative(descriptor); + return _startRenderAttachment( + options, + (policy, session, operation) => raw.mln_webgpu_borrowed_texture_attach( + _handle.raw, + native, + policy, + session, + operation, + ), + ); + }); + + RenderSessionAttachment _startRenderAttachment( + RenderSessionAttachOptions options, + int Function( + Pointer, + Pointer, + Pointer, + ) + start, ) { ensureAbiVersion(); - return withNativeArena((arena) { - final nativeDescriptor = - arena(); - nativeDescriptor.ref = _openglBorrowedTextureDescriptorToNative( - descriptor, + RenderSessionHandle? renderSession; + final frameWake = NativeWakeState(() { + final target = renderSession; + if (target != null && !target.isClosed) target._notifyFramesReady(); + }); + final driverWorkWake = NativeWakeState(() { + final target = renderSession; + if (target != null && !target.isClosed) target._notifyDriverWorkReady(); + }); + try { + final completed = _runtime._startUnit( + (completion) => withNativeArena((arena) { + final policy = arena() + ..ref = raw.mln_render_session_attach_options_default(); + policy.ref.driver = options.driver.rawValue; + policy.ref.requested_texture_ring_depth = + options.requestedTextureRingDepth; + frameWake.writeTo(policy.ref.frame_wake); + driverWorkWake.writeTo(policy.ref.driver_work_wake); + final session = arena()..value = 0; + final status = start(policy, session, completion); + if (status == nativeStatusOk) { + renderSession = RenderSessionHandle._( + _runtime, + NativeRenderSession(session.value), + ); + } + return status; + }), ); - final outSession = arena(); - outSession.value = 0; - _check( - raw.mln_opengl_borrowed_texture_attach( - _mapHandle.raw, - nativeDescriptor, - outSession, - ), + final session = renderSession!; + return RenderSessionAttachment( + session, + session._completeWhileRetained(completed), ); - return RenderSessionHandle._(NativeRenderSession(outSession.value)); - }); + } catch (_) { + frameWake.reject(); + driverWorkWake.reject(); + rethrow; + } } } diff --git a/bindings/dart/lib/src/runtime/runtime_resource_callbacks.dart b/bindings/dart/lib/src/runtime/runtime_resource_callbacks.dart index 4fbd4f97e..82f41cc13 100644 --- a/bindings/dart/lib/src/runtime/runtime_resource_callbacks.dart +++ b/bindings/dart/lib/src/runtime/runtime_resource_callbacks.dart @@ -189,25 +189,32 @@ int _resourceRouteFlags(ResourceProviderRoute route) { } final class _ResourceProviderCallbackState extends RetainedCallbackState { - _ResourceProviderCallbackState(ResourceProvider provider) { + _ResourceProviderCallbackState(ResourceProvider provider) + : _callback = provider.callback { for (final route in provider.routes) { _checkNativeCString(route.url); } - callback = - NativeCallable< - raw.mln_adapter_queued_resource_request_listenerFunction - >.listener((Pointer request) { - if (request == nullptr) { - close(); - return; - } - final ran = runUpcall( - () => _invokeQueuedResourceProvider(provider.callback, request), - ); - if (!ran) { - _dropQueuedResourceProviderRequest(request); - } - }); + listener = NativeCallable.listener(( + Pointer _, + ) { + runUpcall(drain); + }); + final outQueue = calloc(); + final wake = calloc(); + try { + wake.ref.size = sizeOf(); + wake.ref.callback = listener.nativeFunction; + wake.ref.user_data = nullptr; + wake.ref.release_user_data = nullptr; + _check(raw.mln_adapter_resource_request_queue_create(wake, outQueue)); + queue = outQueue.value; + } catch (_) { + listener.close(); + rethrow; + } finally { + calloc.free(wake); + calloc.free(outQueue); + } pointer = calloc(); pointer.ref.route_count = provider.routes.length; pointer.ref.routes = provider.routes.isEmpty @@ -222,26 +229,44 @@ final class _ResourceProviderCallbackState extends RetainedCallbackState { pointer.ref.routes[index].flags = _resourceRouteFlags(route); pointer.ref.routes[index].url = _nativeOwnedCString(route.url); } - pointer.ref.listener = callback.nativeFunction; + pointer.ref.queue = queue; } + final ResourceProviderCallback _callback; + late final NativeCallable listener; + late final int queue; late final Pointer pointer; - late final NativeCallable< - raw.mln_adapter_queued_resource_request_listenerFunction - > - callback; - var _retirementQueued = false; - void retire() { - if (_retirementQueued) { - return; + void drain() { + while (true) { + final outRequest = + calloc>(); + try { + _check( + raw.mln_adapter_resource_request_queue_acquire(queue, outRequest), + ); + final request = outRequest.value.cast(); + if (request == nullptr) { + return; + } + final ran = runUpcall( + () => _invokeQueuedResourceProvider(_callback, request), + ); + if (!ran) { + _dropQueuedResourceProviderRequest(request); + } + } finally { + calloc.free(outRequest); + } } - _retirementQueued = true; - raw.mln_adapter_queued_resource_provider_retire(pointer); } + void retire() => closeSynchronously(); + @override void closeResources() { + drain(); + raw.mln_adapter_resource_request_queue_close(queue); final routes = pointer.ref.routes; for (var index = 0; index < pointer.ref.route_count; index += 1) { calloc.free(routes[index].url); @@ -250,7 +275,7 @@ final class _ResourceProviderCallbackState extends RetainedCallbackState { calloc.free(routes); } calloc.free(pointer); - callback.close(); + listener.close(); } } diff --git a/bindings/dart/lib/src/style/style.dart b/bindings/dart/lib/src/style/style.dart index 94c5b532b..1c81ee007 100644 --- a/bindings/dart/lib/src/style/style.dart +++ b/bindings/dart/lib/src/style/style.dart @@ -352,6 +352,42 @@ final class GeoJsonSourceOptions { ); } +/// Layer metadata copied from retained native style state. +/// +/// [sourceId] and [sourceLayer] are present when the layer carries them. The +/// value remains valid after its layer or map closes. +final class LayerInfo { + /// Creates copied layer metadata. + const LayerInfo({ + required this.type, + required this.minZoom, + required this.maxZoom, + required this.visibility, + this.sourceId, + this.sourceLayer, + }); + + /// Style-spec layer type string, for example `"fill"` or `"background"`. + final String type; + + /// Lowest zoom at which the layer draws; negative infinity with no lower + /// bound. + final double minZoom; + + /// Highest zoom at which the layer draws; positive infinity with no upper + /// bound. + final double maxZoom; + + /// Whether the layer draws. + final StyleLayerVisibility visibility; + + /// Source ID, when the layer carries one. + final String? sourceId; + + /// Source-layer ID, when the layer carries one. + final String? sourceLayer; +} + /// Whether a style layer draws. final class StyleLayerVisibility { const StyleLayerVisibility._(this.rawValue, this.name); diff --git a/bindings/dart/mise.toml b/bindings/dart/mise.toml index 112863de4..1adb95861 100644 --- a/bindings/dart/mise.toml +++ b/bindings/dart/mise.toml @@ -130,18 +130,3 @@ run_windows = ''' export PATH="$(cygpath -u "$MISE_MONOREPO_ROOT/build/$usage_preset/install/bin")${PATH:+:${PATH}}" dart test ''' - -[tasks."test:download"] -description = "Test the build hook against a downloaded native snapshot archive." -# Runs one file rather than the suite, because the rest holds native handles -# across isolate boundaries. Removing the pointer forces the download; it goes -# back afterwards so a local prefix survives this task. -run = ''' -pointer=.dart_tool/maplibre_native_install_dir -saved="$(mktemp)" -trap 'if [[ -s "$saved" ]]; then mv "$saved" "$pointer"; else rm -f "$saved"; fi' EXIT -if [[ -f "$pointer" ]]; then - mv "$pointer" "$saved" -fi -dart test test/native_library_test.dart -''' diff --git a/bindings/dart/test/handle_identity_test.dart b/bindings/dart/test/handle_identity_test.dart index 2ce99471e..4808df5c2 100644 --- a/bindings/dart/test/handle_identity_test.dart +++ b/bindings/dart/test/handle_identity_test.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:maplibre_native_ffi/maplibre_native_ffi.dart'; import 'package:maplibre_native_ffi/src/runtime/runtime.dart' - show mapAttachRefIdForTesting; + show mapHandleIdForTesting; import 'package:maplibre_native_ffi/src/internal/c/maplibre_native_c.dart'; import 'package:maplibre_native_ffi/src/internal/c/maplibre_native_c.g.dart' as raw; @@ -13,16 +13,45 @@ import 'package:test/test.dart'; final _c = MaplibreNativeCApi.open(); -/// Calls the C size accessor with a raw map id, so a test can replay a released -/// id or use one from another thread. The safe API expresses neither. -void _mapSizeById(NativeMap map) { +void _discardCompletion( + Pointer _, + Pointer _, +) {} + +void _discardCompletionState(Pointer _) {} + +/// Calls the C snapshot accessor with a raw map ID. +void _mapSnapshotById(NativeMap map) { + final arena = Arena(); + try { + final snapshot = arena(); + snapshot.ref.size = sizeOf(); + checkNativeStatus( + raw.mln_map_snapshot_get(map.raw, snapshot), + _c.threadLastErrorMessage, + ); + } finally { + arena.releaseAll(); + } +} + +void _runtimeBarrierById(int handle) { final arena = Arena(); try { - final width = arena(); - final height = arena(); - final scaleFactor = arena(); + final completion = raw.mln_completion.$allocate( + arena, + size: sizeOf(), + callback: Pointer.fromFunction( + _discardCompletion, + ), + user_data: nullptr, + release_user_data: + Pointer.fromFunction( + _discardCompletionState, + ), + ); checkNativeStatus( - raw.mln_map_get_size(map.raw, width, height, scaleFactor), + raw.mln_runtime_barrier(handle, completion), _c.threadLastErrorMessage, ); } finally { @@ -31,55 +60,58 @@ void _mapSizeById(NativeMap map) { } void main() { - test('a released map id replayed after a new map is reported stale', () { - final runtime = RuntimeHandle.create(); - final first = runtime.createMap(); - final released = NativeMap(mapAttachRefIdForTesting(first.attachRef())); - first.close(); + test( + 'a released map id replayed after a new map is reported stale', + () async { + final runtime = RuntimeHandle.create(); + final first = await runtime.createMap(); + final released = NativeMap(mapHandleIdForTesting(first)); + await first.close(); - // The released slot is the one the next map takes, so the replayed id - // names a retired generation of a slot that is live again. - final second = runtime.createMap(); - addTearDown(() { - second.close(); - runtime.close(); - }); + // The released slot is the one the next map takes, so the replayed id + // names a retired generation of a slot that is live again. + final second = await runtime.createMap(); + addTearDown(() async { + await second.close(); + await runtime.close(); + }); - expect( - () => _mapSizeById(released), - throwsA( - isA().having( - (error) => error.diagnostic, - 'diagnostic', - contains('stale'), + expect( + () => _mapSnapshotById(released), + throwsA( + isA().having( + (error) => error.diagnostic, + 'diagnostic', + contains('stale'), + ), ), - ), - ); + ); - // The live map is unaffected by the replay. - _mapSizeById(NativeMap(mapAttachRefIdForTesting(second.attachRef()))); - }); + // The live map is unaffected by the replay. + _mapSnapshotById(NativeMap(mapHandleIdForTesting(second))); + }, + ); - test('a map id passed to a runtime operation is rejected on its kind', () { - final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - addTearDown(() { - map.close(); - runtime.close(); - }); + test( + 'a map id passed to a runtime operation is rejected on its kind', + () async { + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + addTearDown(() async { + await map.close(); + await runtime.close(); + }); - // The generated bindings spell both as `int`, so this call needs the raw - // id; the C API rejects it on its kind tag. - expect( - () => checkNativeStatus( - raw.mln_runtime_pump(mapAttachRefIdForTesting(map.attachRef()), 0, -1), - _c.threadLastErrorMessage, - ), - throwsA( - isA() - .having((e) => e.diagnostic, 'diagnostic', contains('map')) - .having((e) => e.diagnostic, 'diagnostic', contains('runtime')), - ), - ); - }); + // The generated bindings spell both as `int`, so this call needs the raw + // id; the C API rejects it on its kind tag. + expect( + () => _runtimeBarrierById(mapHandleIdForTesting(map)), + throwsA( + isA() + .having((e) => e.diagnostic, 'diagnostic', contains('map')) + .having((e) => e.diagnostic, 'diagnostic', contains('runtime')), + ), + ); + }, + ); } diff --git a/bindings/dart/test/internal_support_test.dart b/bindings/dart/test/internal_support_test.dart index 7fce6a68e..a4b26e406 100644 --- a/bindings/dart/test/internal_support_test.dart +++ b/bindings/dart/test/internal_support_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:ffi'; -import 'dart:isolate'; import 'package:maplibre_native_ffi/src/error/maplibre_exception.dart'; import 'package:maplibre_native_ffi/src/internal/c/maplibre_native_c.dart'; @@ -67,7 +66,6 @@ void main() { final cases = <(int, Type)>[ (nativeStatusInvalidArgument, InvalidArgumentException), (nativeStatusInvalidState, InvalidStateException), - (nativeStatusWrongThread, WrongThreadException), (nativeStatusUnsupported, UnsupportedFeatureException), (nativeStatusNativeError, NativeErrorException), ]; @@ -204,50 +202,6 @@ void main() { expect(state.isClosed, isTrue); }); - test('owner isolate mismatch rejects use before native calls', () { - final state = NativeHandleState<_FakeNativeHandle>( - _fakeHandle, - 'fake_handle', - ownerIsolateHash: Isolate.current.hashCode + 1, - leakReporting: false, - ); - - expect(() => state.handle, throwsA(isA())); - expect( - () => state.close((_) => nativeStatusOk, () => 'unused'), - throwsA(isA()), - ); - }); - - test('owner native thread mismatch reports the isolate moved', () { - // The isolate check still passes when the VM moves an isolate off its - // original native thread, so the thread check has to catch it. - final state = NativeHandleState<_FakeNativeHandle>( - _fakeHandle, - 'fake_handle', - ownerThreadToken: -1, - leakReporting: false, - ); - - expect( - () => state.handle, - throwsA( - isA().having( - (error) => error.diagnostic, - 'diagnostic', - allOf( - contains('native thread its isolate has since left'), - contains('awaited I/O'), - ), - ), - ), - ); - expect( - () => state.close((_) => nativeStatusOk, () => 'unused'), - throwsA(isA()), - ); - }); - test('failed close leaves handle live for retry', () { final state = NativeHandleState<_FakeNativeHandle>( _fakeHandle, @@ -280,7 +234,7 @@ void main() { expect(retained, 0); }); - test('failed cleanup preserves descriptor for owner-thread retry', () { + test('failed cleanup preserves descriptor for a later retry', () { var released = 0; var retained = 0; @@ -295,135 +249,148 @@ void main() { }); }); - test('a drained batch is indexed by its stride and copied field by field', () { - final runtime = RuntimeHandle.create(); - // A stride wider than this binding's own record is what a C API version - // that added a payload member reports, so the decoder indexes by it. - final eventSize = sizeOf() + 8; - final payloadOffset = - sizeOf() - - sizeOf(); - final events = calloc(eventSize * 3); - final messageBytes = utf8.encode('copied messagetile-source'); - final messages = calloc(messageBytes.length); - final batch = calloc(); - try { - // The library this binding runs against reports the record size this - // binding compiled, so a later mismatch is an ABI change rather than a - // decode bug. - batch.ref = raw.mln_runtime_event_batch_default(); - expect( - raw.mln_runtime_drain_events( - runtimeHandleIdForTesting(runtime), - 0, - batch, - ), - nativeStatusOk, - ); - expect(batch.ref.event_size, sizeOf()); - - messages.asTypedList(messageBytes.length).setAll(0, messageBytes); - - final unknown = (events + 0).cast().ref; - unknown.type = 0xfeed; - unknown.source_type = 0xbeef; - unknown.source = 0xcafe; - unknown.code = 17; - unknown.payload_type = 0xf00d; - unknown.message_offset = 0; - unknown.message_size = 14; - final unknownWindow = (events + payloadOffset).asTypedList( - eventSize - payloadOffset, - ); - for (var index = 0; index < unknownWindow.length; index += 1) { - unknownWindow[index] = index + 1; - } - - final renderMap = (events + eventSize).cast().ref; - renderMap.type = 16; - renderMap.source_type = 1; - // A map id this build cannot resolve to a wrapper still names one object - // for the life of the process, so the raw id has to survive the decode: - // it is the only identity a host can route or correlate the event on. - renderMap.source = 0xfeed; - renderMap.code = 0; - renderMap.payload_type = 2; - renderMap.payload.render_map.mode = 1; - - final transition = (events + 2 * eventSize) - .cast() - .ref; - transition.type = 23; - transition.source_type = 0; - transition.code = 0; - transition.payload_type = 9; - transition.payload.camera_transition_finished.transition_id = -1; - transition.message_offset = 15; - transition.message_size = 11; - - batch.ref.size = sizeOf(); - batch.ref.event_size = eventSize; - batch.ref.events = events.cast(); - batch.ref.event_count = 3; - batch.ref.messages = messages.cast(); - batch.ref.messages_size = messageBytes.length; - batch.ref.remaining_count = 7; - - final decoded = decodeRuntimeEventBatchForTesting(batch.ref, runtime); - // Every field is copied before the drain returns, so overwriting the - // arena cannot change what the host already holds. - unknownWindow.fillRange(0, unknownWindow.length, 9); - messages[0] = 'X'.codeUnitAt(0); - - expect(decoded.remainingCount, 7); - expect(decoded.events, hasLength(3)); - - final unknownEvent = decoded.events[0]; - expect(unknownEvent.eventType.rawValue, 0xfeed); - expect(unknownEvent.code, 17); - final unknownSource = unknownEvent.source as UnknownRuntimeEventSource; - expect(unknownSource.sourceType.rawValue, 0xbeef); - expect(unknownSource.sourceId, 0xcafe); - expect(unknownEvent.message, 'copied message'); - final unknownPayload = unknownEvent.payload; - expect(unknownPayload, isA()); - expect(unknownPayload.rawPayloadType, 0xf00d); - expect( - (unknownPayload as RuntimeEventPayloadUnknown).bytes, - List.generate(eventSize - payloadOffset, (index) => index + 1), - ); - - // The second and third events decode only when the walk steps by the - // reported stride rather than by this binding's own record size. - final renderMapEvent = decoded.events[1]; - expect(renderMapEvent.eventType, RuntimeEventType.mapRenderMapFinished); - final renderMapSource = renderMapEvent.source as MapRuntimeEventSource; - expect(renderMapSource.map, isNull); - expect(renderMapSource.sourceId, 0xfeed); - // A zero message size is the absent message, not the empty one. - expect(renderMapEvent.message, isNull); - expect( - (renderMapEvent.payload as RuntimeEventRenderMap).mode, - RenderMode.full, - ); + test( + 'a drained batch is indexed by its stride and copied field by field', + () async { + final runtime = RuntimeHandle.create(); + // A stride wider than this binding's own record is what a C API version + // that added a payload member reports, so the decoder indexes by it. + final eventSize = sizeOf() + 8; + final payloadOffset = + sizeOf() - + sizeOf(); + final events = calloc(eventSize * 3); + final messageBytes = utf8.encode('copied messagetile-source'); + final messages = calloc(messageBytes.length); + final batch = calloc(); + try { + // The library this binding runs against reports the record size this + // binding compiled, so a later mismatch is an ABI change rather than a + // decode bug. + withNativeArena((arena) { + final outBatch = arena(); + final view = arena(); + view.ref.size = sizeOf(); + expect( + raw.mln_runtime_drain_events( + runtimeHandleIdForTesting(runtime), + outBatch, + ), + nativeStatusOk, + ); + try { + expect( + raw.mln_event_batch_get(outBatch.value, view), + nativeStatusOk, + ); + expect(view.ref.event_size, sizeOf()); + } finally { + raw.mln_event_batch_release(outBatch.value); + } + }); + + messages.asTypedList(messageBytes.length).setAll(0, messageBytes); + + final unknown = (events + 0).cast().ref; + unknown.type = 0xfeed; + unknown.source_type = 0xbeef; + unknown.source = 0xcafe; + unknown.code = 17; + unknown.payload_type = 0xf00d; + unknown.message_offset = 0; + unknown.message_size = 14; + final unknownWindow = (events + payloadOffset).asTypedList( + eventSize - payloadOffset, + ); + for (var index = 0; index < unknownWindow.length; index += 1) { + unknownWindow[index] = index + 1; + } - final transitionEvent = decoded.events[2]; - expect( - transitionEvent.eventType, - RuntimeEventType.mapCameraTransitionFinished, - ); - expect(transitionEvent.source, isA()); - expect(transitionEvent.message, 'tile-source'); - expect( - (transitionEvent.payload as RuntimeEventCameraTransitionFinished) - .transitionId, - (BigInt.one << 64) - BigInt.one, - ); - } finally { - calloc.free(batch); - calloc.free(messages); - calloc.free(events); - runtime.close(); - } - }); + final renderMap = (events + eventSize) + .cast() + .ref; + renderMap.type = 16; + renderMap.source_type = 1; + // A map id this build cannot resolve to a wrapper still names one object + // for the life of the process, so the raw id has to survive the decode: + // it is the only identity a host can route or correlate the event on. + renderMap.source = 0xfeed; + renderMap.code = 0; + renderMap.payload_type = 2; + renderMap.payload.render_map.mode = 1; + + final transition = (events + 2 * eventSize) + .cast() + .ref; + transition.type = 22; + transition.source_type = 0; + transition.code = 0; + transition.payload_type = 8; + transition.payload.camera_transition_finished.transition_id = -1; + transition.message_offset = 15; + transition.message_size = 11; + + batch.ref.size = sizeOf(); + batch.ref.event_size = eventSize; + batch.ref.events = events.cast(); + batch.ref.event_count = 3; + batch.ref.messages = messages.cast(); + batch.ref.messages_size = messageBytes.length; + final decoded = decodeRuntimeEventBatchForTesting(batch.ref, runtime); + // Every field is copied before the drain returns, so overwriting the + // arena cannot change what the host already holds. + unknownWindow.fillRange(0, unknownWindow.length, 9); + messages[0] = 'X'.codeUnitAt(0); + + expect(decoded.events, hasLength(3)); + + final unknownEvent = decoded.events[0]; + expect(unknownEvent.eventType.rawValue, 0xfeed); + expect(unknownEvent.code, 17); + final unknownSource = unknownEvent.source as UnknownRuntimeEventSource; + expect(unknownSource.sourceType.rawValue, 0xbeef); + expect(unknownSource.sourceId, 0xcafe); + expect(unknownEvent.message, 'copied message'); + final unknownPayload = unknownEvent.payload; + expect(unknownPayload, isA()); + expect(unknownPayload.rawPayloadType, 0xf00d); + expect( + (unknownPayload as RuntimeEventPayloadUnknown).bytes, + List.generate(eventSize - payloadOffset, (index) => index + 1), + ); + + // The second and third events decode only when the walk steps by the + // reported stride rather than by this binding's own record size. + final renderMapEvent = decoded.events[1]; + expect(renderMapEvent.eventType, RuntimeEventType.mapRenderMapFinished); + final renderMapSource = renderMapEvent.source as MapRuntimeEventSource; + expect(renderMapSource.map, isNull); + expect(renderMapSource.sourceId, 0xfeed); + // A zero message size is the absent message, not the empty one. + expect(renderMapEvent.message, isNull); + expect( + (renderMapEvent.payload as RuntimeEventRenderMap).mode, + RenderMode.full, + ); + + final transitionEvent = decoded.events[2]; + expect( + transitionEvent.eventType, + RuntimeEventType.mapCameraTransitionFinished, + ); + expect(transitionEvent.source, isA()); + expect(transitionEvent.message, 'tile-source'); + expect( + (transitionEvent.payload as RuntimeEventCameraTransitionFinished) + .transitionId, + (BigInt.one << 64) - BigInt.one, + ); + } finally { + calloc.free(batch); + calloc.free(messages); + calloc.free(events); + await runtime.close(); + } + }, + ); } diff --git a/bindings/dart/test/isolate_migration_test.dart b/bindings/dart/test/isolate_migration_test.dart new file mode 100644 index 000000000..1d8ecbfe0 --- /dev/null +++ b/bindings/dart/test/isolate_migration_test.dart @@ -0,0 +1,54 @@ +import 'dart:isolate'; + +import 'package:maplibre_native_ffi/maplibre_native_ffi.dart'; +import 'package:test/test.dart'; + +void main() { + test( + 'runtime and map remain usable after isolate execution and await', + () async { + final runtime = RuntimeHandle.create( + options: const RuntimeOptions(cachePath: ':memory:'), + ); + final map = await runtime.createMap( + options: const MapOptions(width: 64, height: 64), + ); + + await Isolate.run(() {}); + + final before = map.snapshot(); + final completion = map.requestRepaint(); + final camera = await map.queryCamera(); + expect((await completion).disposition, CommandDisposition.committed); + expect(camera.generation, greaterThanOrEqualTo(before.generation)); + + await map.close(); + await runtime.close(); + expect(map.isClosed, isTrue); + expect(runtime.isClosed, isTrue); + }, + ); + + test( + 'native execution progresses while the isolate stays responsive', + () async { + final runtime = RuntimeHandle.create( + options: const RuntimeOptions(cachePath: ':memory:'), + ); + final map = await runtime.createMap(); + try { + var timerRan = false; + Future.delayed(Duration.zero, () { + timerRan = true; + }); + final camera = await map.queryCamera(); + + expect(timerRan, isTrue); + expect(camera.generation, greaterThanOrEqualTo(BigInt.zero)); + } finally { + await map.close(); + await runtime.close(); + } + }, + ); +} diff --git a/bindings/dart/test/maplibre_native_ffi_test.dart b/bindings/dart/test/maplibre_native_ffi_test.dart index 0679957b6..29d09dbed 100644 --- a/bindings/dart/test/maplibre_native_ffi_test.dart +++ b/bindings/dart/test/maplibre_native_ffi_test.dart @@ -1,6 +1,5 @@ import 'dart:ffi'; import 'dart:convert'; -import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; @@ -27,6 +26,17 @@ final _maskWithoutStyleLoaded = RuntimeEventMask( Uint8List _jsonBytes(String value) => Uint8List.fromList(utf8.encode(value)); +Future _expectCommandFailure( + Future future, + MaplibreStatus status, +) async { + final completion = await future; + expect(completion.disposition, CommandDisposition.failed); + expect(completion.status, status); + expect(completion.diagnostic, isNotEmpty); + return completion; +} + /// Dispatches one record through the registered adapter log callback, the way /// MapLibre's logging threads do, and reports the consume value native code /// sees. @@ -52,17 +62,20 @@ int _dispatchLogRecord( } void main() { - test('map options carry FastPFOR decoding to native', () { + test('map options carry FastPFOR decoding to native', () async { expect(const MapOptions().fastPforEnabled, isFalse); expect(const MapOptions(fastPforEnabled: true), isNot(const MapOptions())); final runtime = RuntimeHandle.create(); - final map = runtime.createMap( + final map = await runtime.createMap( options: const MapOptions(width: 64, height: 64, fastPforEnabled: true), ); - expect(map.size(), const MapSize(width: 64, height: 64, scaleFactor: 1)); - map.close(); - runtime.close(); + expect( + map.snapshot().size, + const MapSize(width: 64, height: 64, scaleFactor: 1), + ); + await map.close(); + await runtime.close(); }); test('one drain takes every event a style load queued', () async { final runtime = RuntimeHandle.create(); @@ -71,17 +84,14 @@ void main() { // A fresh runtime has nothing queued. final empty = runtime.drainEvents(); expect(empty.events, isEmpty); - expect(empty.remainingCount, 0); - final map = runtime.createMap(); + final map = await runtime.createMap(); addTearDown(map.close); // Start the style load with an empty queue. - _quiesce(runtime); map.setStyleJson(_jsonBytes(_emptyStyleJson)); late RuntimeEventBatch batch; await _waitUntil(() { - runtime.pump(); batch = runtime.drainEvents(); return batch.events.any( (event) => event.eventType == RuntimeEventType.mapStyleLoaded, @@ -90,29 +100,16 @@ void main() { // A style load reports several event types, and one drain takes them all. expect(batch.events.length, greaterThan(1)); - expect(batch.remainingCount, 0); expect( batch.events.map((event) => event.eventType), contains(RuntimeEventType.mapRenderUpdateAvailable), ); - - // A bounded drain reports what stayed queued, and an unbounded one reaches - // the end of the queue. - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - late RuntimeEventBatch bounded; - await _waitUntil(() { - runtime.pump(); - bounded = runtime.drainEvents(maxEvents: 1); - return bounded.remainingCount > 0; - }); - expect(bounded.events, hasLength(1)); - expect(runtime.drainEvents().remainingCount, 0); }); - test('both handles report and narrow their event masks', () { + test('both handles report and narrow their event masks', () async { final runtime = RuntimeHandle.create(); addTearDown(runtime.close); - final map = runtime.createMap(); + final map = await runtime.createMap(); addTearDown(map.close); // The default options select every event type. @@ -152,7 +149,7 @@ void main() { () async { final runtime = RuntimeHandle.create(); addTearDown(runtime.close); - final map = runtime.createMap(); + final map = await runtime.createMap(); addTearDown(map.close); map.setEventMask( RuntimeEventMask.mapStyleLoaded | RuntimeEventMask.mapLoadingFailed, @@ -161,7 +158,6 @@ void main() { map.setStyleJson(_jsonBytes(_emptyStyleJson)); final types = {}; await _waitUntil(() { - runtime.pump(); types.addAll( runtime.drainEvents().events.map((event) => event.eventType), ); @@ -180,13 +176,12 @@ void main() { const sourceId = 'dart-released-source'; final runtime = RuntimeHandle.create(); addTearDown(runtime.close); - final map = runtime.createMap( + final map = await runtime.createMap( options: MapOptions(eventMask: _maskWithoutStyleLoaded), ); addTearDown(map.close); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - _quiesce(runtime); - map.addCustomGeometrySource( + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); + await map.addCustomGeometrySource( sourceId, CustomGeometrySourceOptions(fetchTile: (_) {}), ); @@ -196,10 +191,9 @@ void main() { // nothing of its own. expect(map.eventMask, _maskWithoutStyleLoaded); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); final types = {}; await _waitUntil(() { - runtime.pump(); types.addAll( runtime.drainEvents().events.map((event) => event.eventType), ); @@ -212,18 +206,18 @@ void main() { ); test( - 'a style replacement releases a custom MVT vector source with style loads unselected', + 'a style replacement releases a custom MVT vector source with style loads ' + 'unselected', () async { const sourceId = 'dart-released-mvt-source'; final runtime = RuntimeHandle.create(); addTearDown(runtime.close); - final map = runtime.createMap( + final map = await runtime.createMap( options: MapOptions(eventMask: _maskWithoutStyleLoaded), ); addTearDown(map.close); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - _quiesce(runtime); - map.addCustomMvtVectorSource( + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); + await map.addCustomMvtVectorSource( sourceId, CustomMvtVectorSourceOptions(fetchTile: (_) {}), ); @@ -231,10 +225,9 @@ void main() { expect(map.eventMask, _maskWithoutStyleLoaded); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); final types = {}; await _waitUntil(() { - runtime.pump(); types.addAll( runtime.drainEvents().events.map((event) => event.eventType), ); @@ -246,7 +239,7 @@ void main() { }, ); - test('process-global APIs cross the native C ABI', () { + test('process-global APIs cross the native C ABI', () async { expect(Maplibre.cVersion(), greaterThanOrEqualTo(0)); final backends = Maplibre.supportedRenderBackends(); expect(backends.bits, greaterThanOrEqualTo(0)); @@ -288,15 +281,18 @@ void main() { Maplibre.clearLogCallback(); }); - test('render target extents report their physical size through native', () { - final size = const RenderTargetExtent( - width: 65, - height: 33, - scaleFactor: 1.5, - ).physicalSize(); - expect(size.width, 98); - expect(size.height, 50); - }); + test( + 'render target extents report their physical size through native', + () async { + final size = const RenderTargetExtent( + width: 65, + height: 33, + scaleFactor: 1.5, + ).physicalSize(); + expect(size.width, 98); + expect(size.height, 50); + }, + ); test('process-global log callbacks retire across isolates', () async { Maplibre.setLogCallback((_) {}); @@ -372,10 +368,10 @@ void main() { ), ), ]); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - final event = await _pumpUntilEvent( + final event = await _waitUntilEvent( runtime, (candidate) => candidate.eventType == RuntimeEventType.mapStyleLoaded && @@ -383,8 +379,8 @@ void main() { ); expect((event.source as MapRuntimeEventSource).map, same(map)); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }, ); @@ -406,18 +402,18 @@ void main() { }, ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(unmatchedUrl); - final event = await _pumpUntilEvent( + final event = await _waitUntilEvent( runtime, (candidate) => candidate.eventType == RuntimeEventType.mapLoadingFailed, ); expect(event.source, isA()); expect(providerCalls, 0); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); test('queued resource provider callbacks cross the native C ABI', () async { @@ -452,9 +448,9 @@ void main() { ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => requests.isNotEmpty); + await _waitUntilCondition(runtime, () => requests.isNotEmpty); ownerToken.waitUntilRetired(); expect( @@ -462,8 +458,8 @@ void main() { throwsA(isA()), ); ownerToken.close(); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); expect(await completion, isTrue); }); @@ -496,9 +492,9 @@ void main() { ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(aliasUrl); - await _pumpUntil(runtime, () => requests.isNotEmpty); + await _waitUntilCondition(runtime, () => requests.isNotEmpty); expect(requests.first.requestedUrl, aliasUrl); expect( @@ -506,8 +502,8 @@ void main() { 'https://demotiles.maplibre.org/style.json', ); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); // BND-156: a glob route claims a URL family whose members are known only @@ -535,9 +531,9 @@ void main() { ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl('${origin}unenumerated/style.json'); - await _pumpUntilEvent( + await _waitUntilEvent( runtime, (candidate) => candidate.eventType == RuntimeEventType.mapStyleLoaded, ); @@ -545,15 +541,15 @@ void main() { // A pattern matches the complete URL, so a URL that merely contains the // route's origin stays with native loading. map.setStyleUrl('custom://elsewhere/${origin}style.json'); - await _pumpUntilEvent( + await _waitUntilEvent( runtime, (candidate) => candidate.eventType == RuntimeEventType.mapLoadingFailed, ); expect(claimed, ['${origin}unenumerated/style.json']); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); // BND-157: a route picks which of the request's two URLs it compares, so a @@ -583,11 +579,11 @@ void main() { }, ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(aliasUrl); - await _pumpUntil(runtime, () => requests.isNotEmpty); - map.close(); - runtime.close(); + await _waitUntilCondition(runtime, () => requests.isNotEmpty); + await map.close(); + await runtime.close(); return requests.single; } @@ -626,17 +622,17 @@ void main() { ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => token != null); + await _waitUntilCondition(runtime, () => token != null); final liveToken = token!; final waiter = Isolate.run(() { liveToken.waitUntilRetired(); return true; }); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); await _waitUntil(liveToken.cancelled); expect( () => liveToken.complete( @@ -662,9 +658,9 @@ void main() { }, ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => token != null); + await _waitUntilCondition(runtime, () => token != null); final liveToken = token!; expect( @@ -690,8 +686,8 @@ void main() { throwsA(isA()), ); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); test('transferred token aliases have one terminal winner', () async { @@ -709,9 +705,9 @@ void main() { }, ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => token != null); + await _waitUntilCondition(runtime, () => token != null); final liveToken = token!; final waiter = Isolate.run(() { @@ -725,8 +721,8 @@ void main() { Isolate.run(() => _completeTokenAlias(liveToken)), Isolate.run(() => _completeTokenAlias(liveToken)), ]); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); final results = await race; // Teardown may retire the request before either alias reaches it, so the @@ -757,12 +753,12 @@ void main() { ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => calls > 0); + await _waitUntilCondition(runtime, () => calls > 0); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); test('closed resource request handles reject further use', () async { @@ -798,32 +794,76 @@ void main() { }, ), ); - final map = runtime.createMap(); + final map = await runtime.createMap(); map.setStyleUrl(styleUrl); - await _pumpUntil(runtime, () => callbackFinished); + await _waitUntilCondition(runtime, () => callbackFinished); expect(cancelledRejected, isTrue); expect(completionRejected, isTrue); expect(repeatedCloseSucceeded, isTrue); - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); - test('projection remains usable after its source map closes', () async { - final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - final projection = map.createProjection(); - - map.close(); - runtime.close(); - await Future.delayed(Duration.zero); - expect(projection.pixelForLatLng(const LatLng(0, 0)).x.isFinite, isTrue); - projection.setCamera(const CameraOptions(center: LatLng(1, 1), zoom: 2)); - expect(projection.camera().zoom, closeTo(2, 0.0001)); - - projection.close(); - }); + test( + 'projection is synchronous and observes only earlier commands', + () async { + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap( + options: const MapOptions(width: 256, height: 256), + ); + // A projection created after a camera command observes that command. + map.updateCamera(const CameraOptions(center: LatLng(10, 20), zoom: 3)); + final projection = await map.createProjection(); + try { + final created = projection.camera(); + expect(created.zoom, closeTo(3, 0.0001)); + expect(created.center!.latitude, closeTo(10, 0.0001)); + expect(created.center!.longitude, closeTo(20, 0.0001)); + + // A synchronous conversion round-trip returns to the same coordinate. + const coordinate = LatLng(10, 20); + final pixel = projection.pixelForLatLng(coordinate); + expect(pixel.x.isFinite, isTrue); + final roundTrip = projection.latLngForPixel(pixel); + expect(roundTrip.latitude, closeTo(coordinate.latitude, 1e-6)); + expect(roundTrip.longitude, closeTo(coordinate.longitude, 1e-6)); + + // A setter is applied before it returns, so it changes later + // conversions, and the map's own camera stays untouched. + final before = projection.pixelForLatLng(const LatLng(0, 0)); + projection.setCamera( + const CameraOptions(center: LatLng(1, 1), zoom: 2), + ); + expect(projection.camera().zoom, closeTo(2, 0.0001)); + final after = projection.pixelForLatLng(const LatLng(0, 0)); + expect(after == before, isFalse); + expect((await map.queryCamera()).camera.zoom, closeTo(3, 0.0001)); + + // A later map command never reaches an existing projection. + map.updateCamera(const CameraOptions(zoom: 9)); + await map.queryCamera(); + expect(projection.camera().zoom, closeTo(2, 0.0001)); + + // Projection calls remain valid when Dart resumes the isolate on + // another native thread. + await Isolate.run(() {}); + projection.setVisibleCoordinates(const [LatLng(-1, -1), LatLng(1, 1)]); + projection.setVisibleGeometry( + _jsonBytes('{"type":"Point","coordinates":[0,0]}'), + ); + await map.close(); + await runtime.close(); + expect(projection.camera().center, isNotNull); + } finally { + projection.close(); + expect(projection.isClosed, isTrue); + await map.close(); + await runtime.close(); + } + }, + ); test('custom geometry tile callbacks reach their isolate', () async { final deliveredTiles = []; @@ -853,91 +893,181 @@ void main() { callback.close(); }); - test('loaded style document and URL read back what was loaded', () { + test('loaded style document and URL read back what was loaded', () async { final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - - // Nothing parsed and nothing requested yet. - expect(map.getLoadedStyleJson(), isEmpty); - expect(map.getStyleUrl(), ''); + final map = await runtime.createMap(); + try { + // Nothing parsed and nothing requested yet. + expect(await map.getLoadedStyleJson(), isEmpty); + expect(await map.getStyleUrl(), ''); - // The document reads back byte-for-byte, so it can be reloaded unchanged. - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - expect(map.getLoadedStyleJson(), _jsonBytes(_emptyStyleJson)); - // Inline JSON clears the URL. - expect(map.getStyleUrl(), ''); - - // The URL is request state, recorded before the load can succeed, while the - // document still reports the style that last parsed. - map.setStyleUrl('https://example.com/style.json'); - expect(map.getStyleUrl(), 'https://example.com/style.json'); - expect(map.getLoadedStyleJson(), _jsonBytes(_emptyStyleJson)); - - map.close(); - runtime.close(); + // The document reads back byte-for-byte, so it can be reloaded unchanged. + map.setStyleJson(_jsonBytes(_emptyStyleJson)); + expect(await map.getLoadedStyleJson(), _jsonBytes(_emptyStyleJson)); + // Inline JSON clears the URL. + expect(await map.getStyleUrl(), ''); + + // The URL is request state, recorded before the load can succeed, while + // the document still reports the style that last parsed. + map.setStyleUrl('https://example.com/style.json'); + expect(await map.getStyleUrl(), 'https://example.com/style.json'); + expect(await map.getLoadedStyleJson(), _jsonBytes(_emptyStyleJson)); + } finally { + await map.close(); + await runtime.close(); + } }); test('a removal and a map close each release a callback root', () async { const sourceId = 'dart-lifecycle-source'; final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); + final map = await runtime.createMap(); + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); - map.addCustomGeometrySource( + await map.addCustomGeometrySource( sourceId, CustomGeometrySourceOptions(fetchTile: (_) {}), ); final removedProbe = customGeometryCallbackProbeForTesting(map, sourceId)!; - expect(map.removeStyleSource(sourceId), isTrue); - expect(removedProbe.retirementQueued, isTrue); + expect( + await _waitForCommandDisposition( + runtime, + map.removeStyleSource(sourceId), + ), + CommandDisposition.committed, + ); + await _waitUntil(() => removedProbe.retirementQueued); expect(customGeometryCallbackProbeForTesting(map, sourceId), isNull); - map.addCustomGeometrySource( + await map.addCustomGeometrySource( sourceId, CustomGeometrySourceOptions(fetchTile: (_) {}), ); final closeProbe = customGeometryCallbackProbeForTesting(map, sourceId)!; - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); expect(closeProbe.retirementQueued, isTrue); await _waitUntil(() => removedProbe.closed && closeProbe.closed); }); - test( - 'a custom MVT vector source removal and map close each release a callback root', - () async { - const sourceId = 'dart-mvt-lifecycle-source'; - final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); + test('a custom MVT vector source removal and map close each release a ' + 'callback root', () async { + const sourceId = 'dart-mvt-lifecycle-source'; + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); - map.addCustomMvtVectorSource( - sourceId, - CustomMvtVectorSourceOptions(fetchTile: (_) {}), + await map.addCustomMvtVectorSource( + sourceId, + CustomMvtVectorSourceOptions(fetchTile: (_) {}), + ); + final removedProbe = customMvtVectorCallbackProbeForTesting(map, sourceId)!; + expect( + await _waitForCommandDisposition( + runtime, + map.removeStyleSource(sourceId), + ), + CommandDisposition.committed, + ); + await _waitUntil(() => removedProbe.retirementQueued); + expect(customMvtVectorCallbackProbeForTesting(map, sourceId), isNull); + + await map.addCustomMvtVectorSource( + sourceId, + CustomMvtVectorSourceOptions(fetchTile: (_) {}), + ); + final closeProbe = customMvtVectorCallbackProbeForTesting(map, sourceId)!; + await map.close(); + await runtime.close(); + expect(closeProbe.retirementQueued, isTrue); + await _waitUntil(() => removedProbe.closed && closeProbe.closed); + }); + + test('feature state round-trips through the map store', () async { + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + try { + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); + const selector = FeatureStateSelector( + sourceId: 'dart-feature-state-source', + featureId: 'feature-1', ); - final removedProbe = customMvtVectorCallbackProbeForTesting( - map, - sourceId, - )!; - expect(map.removeStyleSource(sourceId), isTrue); - expect(removedProbe.retirementQueued, isTrue); - expect(customMvtVectorCallbackProbeForTesting(map, sourceId), isNull); - map.addCustomMvtVectorSource( - sourceId, - CustomMvtVectorSourceOptions(fetchTile: (_) {}), + // The map store answers reads without a render session or a loaded + // source, and missing state reads back as an empty object. + expect( + jsonDecode(utf8.decode(await map.getFeatureState(selector))), + {}, ); - final closeProbe = customMvtVectorCallbackProbeForTesting(map, sourceId)!; - map.close(); - runtime.close(); - expect(closeProbe.retirementQueued, isTrue); - await _waitUntil(() => removedProbe.closed && closeProbe.closed); - }, - ); - test('nine-patch style images round-trip through the native C ABI', () { + await _expectCommandCommitted( + runtime, + map.setFeatureState(selector, _jsonBytes('{"hover":true,"rank":2}')), + ); + expect(jsonDecode(utf8.decode(await map.getFeatureState(selector))), { + 'hover': true, + 'rank': 2, + }); + + // Removing one key leaves the rest of the feature's state. + await _expectCommandCommitted( + runtime, + map.removeFeatureState( + const FeatureStateSelector( + sourceId: 'dart-feature-state-source', + featureId: 'feature-1', + stateKey: 'hover', + ), + ), + ); + expect(jsonDecode(utf8.decode(await map.getFeatureState(selector))), { + 'rank': 2, + }); + + // A source-wide removal clears the remaining state. + await _expectCommandCommitted( + runtime, + map.removeFeatureState( + const FeatureStateSelector(sourceId: 'dart-feature-state-source'), + ), + ); + expect( + jsonDecode(utf8.decode(await map.getFeatureState(selector))), + {}, + ); + + // A selector without a feature ID cannot name state to set, and a state + // key without a feature ID cannot name state to remove. + expect( + () => map.setFeatureState( + const FeatureStateSelector(sourceId: 'dart-feature-state-source'), + _jsonBytes('{"hover":true}'), + ), + throwsA(isA()), + ); + expect( + () => map.removeFeatureState( + const FeatureStateSelector( + sourceId: 'dart-feature-state-source', + stateKey: 'hover', + ), + ), + throwsA(isA()), + ); + // Feature state must be one JSON object. + expect( + () => map.setFeatureState(selector, _jsonBytes('[1,2]')), + throwsA(isA()), + ); + } finally { + await map.close(); + await runtime.close(); + } + }); + + test('nine-patch style images round-trip through the native C ABI', () async { final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); + final map = await runtime.createMap(); try { map.setStyleJson(_jsonBytes(_emptyStyleJson)); final image = PremultipliedRgba8Image( @@ -962,7 +1092,7 @@ void main() { ), ); - final info = map.getStyleImageInfo('patch'); + final info = await map.getStyleImageInfo('patch'); expect(info, isNotNull); expect(info!.stretchXCount, 1); expect(info.stretchYCount, 2); @@ -971,14 +1101,14 @@ void main() { expect(info.textFitWidth, isNull); expect(info.textFitHeight, StyleImageTextFit.proportional); - final stretches = map.getStyleImageStretches('patch'); + final stretches = await map.getStyleImageStretches('patch'); expect(stretches, isNotNull); expect(stretches!.stretchX, [const ImageStretch(0, 1)]); expect(stretches.stretchY, [ const ImageStretch(0, 1), const ImageStretch(1, 2), ]); - expect(map.getStyleImageStretches('missing'), isNull); + expect(await map.getStyleImageStretches('missing'), isNull); // A backwards interval is rejected by C. expect( @@ -996,16 +1126,16 @@ void main() { callerStretches.add(const ImageStretch(1, 2)); expect(snapshotted.stretchX, [const ImageStretch(0, 1)]); } finally { - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); } }); - test('layer base accessors round-trip through the native C ABI', () { + test('layer base accessors round-trip through the native C ABI', () async { final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); + final map = await runtime.createMap(); try { - map.setStyleJson( + await map.setStyleJson( _jsonBytes( '{"version":8,"sources":{"geo":{"type":"geojson","data":' '{"type":"FeatureCollection","features":[]}}},"layers":[' @@ -1014,101 +1144,121 @@ void main() { ), ); - expect(map.getLayerSourceLayer('fill'), ''); - map.setLayerSourceLayer('fill', 'roads'); - expect(map.getLayerSourceLayer('fill'), 'roads'); - expect(map.getLayerSourceId('fill'), 'geo'); + expect(await map.getLayerSourceLayer('fill'), ''); + await map.setLayerSourceLayer('fill', 'roads'); + expect(await map.getLayerSourceLayer('fill'), 'roads'); + expect(await map.getLayerSourceId('fill'), 'geo'); - // A layer type that takes no source is rejected, not silently ignored. - expect( - () => map.setLayerSourceLayer('bg', 'roads'), - throwsA(isA()), + // A layer type that takes no source rejects a source-layer command and + // preserves its previous value. + await _expectCommandFailure( + map.setLayerSourceLayer('bg', 'roads'), + MaplibreStatus.invalidArgument, ); - expect(map.getLayerSourceId('bg'), ''); + expect(await map.getLayerSourceId('bg'), ''); + + // An unset zoom range crosses the boundary as infinities, and the + // reported source ID and source-layer sizes feed the copy operations. + var fillInfo = (await map.getStyleLayerInfo('fill'))!; + expect(fillInfo.type, 'fill'); + expect(fillInfo.minZoom, double.negativeInfinity); + expect(fillInfo.maxZoom, double.infinity); + expect(fillInfo.visibility, StyleLayerVisibility.visible); + expect(fillInfo.sourceId, 'geo'); + expect(fillInfo.sourceLayer, 'roads'); - // An unset zoom range crosses the boundary as infinities. - expect(map.getLayerMinZoom('fill'), double.negativeInfinity); - expect(map.getLayerMaxZoom('fill'), double.infinity); map.setLayerMinZoom('fill', 4); map.setLayerMaxZoom('fill', 12.5); - expect(map.getLayerMinZoom('fill'), 4); - expect(map.getLayerMaxZoom('fill'), 12.5); - - expect(map.getLayerVisibility('fill'), StyleLayerVisibility.visible); map.setLayerVisibility('fill', StyleLayerVisibility.none); - expect(map.getLayerVisibility('fill'), StyleLayerVisibility.none); - - expect( - () => map.getLayerMinZoom('missing'), - throwsA(isA()), - ); + fillInfo = (await map.getStyleLayerInfo('fill'))!; + expect(fillInfo.minZoom, 4); + expect(fillInfo.maxZoom, 12.5); + expect(fillInfo.visibility, StyleLayerVisibility.none); + + // A layer that carries no source reports absent source fields. + final bgInfo = (await map.getStyleLayerInfo('bg'))!; + expect(bgInfo.type, 'background'); + expect(bgInfo.sourceId, isNull); + expect(bgInfo.sourceLayer, isNull); + + // A missing layer reports null rather than metadata. + expect(await map.getStyleLayerInfo('missing'), isNull); } finally { - map.close(); - runtime.close(); + await map.close(); + await runtime.close(); } }); - test('style transition options round-trip through the native C ABI', () { - const transitionStyleJson = - '{"version":8,"transition":{"duration":750,"delay":100},' - '"sources":{},"layers":[]}'; - final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - try { - // A map with no style yet reports no duration or delay. The placement - // flag always reports, because native always holds a value for it. - final empty = map.getStyleTransitionOptions(); - expect(empty.durationMs, isNull); - expect(empty.delayMs, isNull); - expect(empty.enablePlacementTransitions, isTrue); - - // The style parser fills in its own 300ms duration for a style that - // declares no transition. - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - final parsed = map.getStyleTransitionOptions(); - expect(parsed.durationMs, 300); - expect(parsed.delayMs, isNull); - - map.setStyleJson(_jsonBytes(transitionStyleJson)); - final declared = map.getStyleTransitionOptions(); - expect(declared.durationMs, 750); - expect(declared.delayMs, 100); - expect(declared.enablePlacementTransitions, isTrue); - - // A present zero stays distinguishable from an absent field, and an - // absent field clears what the style declared rather than merging. - const options = StyleTransitionOptions( - durationMs: 0, - enablePlacementTransitions: false, - ); - map.setStyleTransitionOptions(options); - expect(map.getStyleTransitionOptions(), options); - expect(map.getStyleTransitionOptions().hashCode, options.hashCode); + test( + 'style transition options round-trip through the native C ABI', + () async { + const transitionStyleJson = + '{"version":8,"transition":{"duration":750,"delay":100},' + '"sources":{},"layers":[]}'; + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + try { + // A map with no style yet reports no duration or delay. The placement + // flag always reports, because native always holds a value for it. + final empty = await map.getStyleTransitionOptions(); + expect(empty.durationMs, isNull); + expect(empty.delayMs, isNull); + expect(empty.enablePlacementTransitions, isTrue); + + // The style parser fills in its own 300ms duration for a style that + // declares no transition. + map.setStyleJson(_jsonBytes(_emptyStyleJson)); + final parsed = await map.getStyleTransitionOptions(); + expect(parsed.durationMs, 300); + expect(parsed.delayMs, isNull); + + map.setStyleJson(_jsonBytes(transitionStyleJson)); + final declared = await map.getStyleTransitionOptions(); + expect(declared.durationMs, 750); + expect(declared.delayMs, 100); + expect(declared.enablePlacementTransitions, isTrue); + + // A present zero stays distinguishable from an absent field, and an + // absent field clears what the style declared rather than merging. + const options = StyleTransitionOptions( + durationMs: 0, + enablePlacementTransitions: false, + ); + map.setStyleTransitionOptions(options); + expect(await map.getStyleTransitionOptions(), options); + expect( + (await map.getStyleTransitionOptions()).hashCode, + options.hashCode, + ); - // Omitting the flag leaves the cross-fade on rather than clearing it. - map.setStyleTransitionOptions( - const StyleTransitionOptions(durationMs: 250), - ); - expect( - map.getStyleTransitionOptions().enablePlacementTransitions, - isTrue, - ); + // Omitting the flag leaves the cross-fade on rather than clearing it. + map.setStyleTransitionOptions( + const StyleTransitionOptions(durationMs: 250), + ); + expect( + (await map.getStyleTransitionOptions()).enablePlacementTransitions, + isTrue, + ); - // Loading a style replaces the override with what that style declares. - map.setStyleJson(_jsonBytes(transitionStyleJson)); - expect(map.getStyleTransitionOptions(), declared); + // Loading a style replaces the override with what that style declares. + map.setStyleJson(_jsonBytes(transitionStyleJson)); + expect(await map.getStyleTransitionOptions(), declared); - expect( - () => map.setStyleTransitionOptions( + final rejectedCommand = map.setStyleTransitionOptions( const StyleTransitionOptions(delayMs: -1), - ), - throwsA(isA()), - ); - } finally { - map.close(); - runtime.close(); - } - }); + ); + await _expectCommandFailure( + rejectedCommand, + MaplibreStatus.invalidArgument, + ); + // The next ordered read observes the last committed options unchanged. + expect(await map.getStyleTransitionOptions(), declared); + } finally { + await map.close(); + await runtime.close(); + } + }, + ); test('runtime and map handles use the native C ABI', () async { final cacheSizeRuntime = RuntimeHandle.create( @@ -1119,13 +1269,8 @@ void main() { () => cacheSizeRuntime.setMaximumAmbientCacheSize(BigInt.from(-1)), throwsA(isA()), ); - final cacheSizeOperation = cacheSizeRuntime.setMaximumAmbientCacheSize( - BigInt.zero, - ); - expect(cacheSizeOperation.isDiscarded, isFalse); - cacheSizeOperation.discard(); - expect(cacheSizeOperation.isDiscarded, isTrue); - cacheSizeRuntime.close(); + await cacheSizeRuntime.setMaximumAmbientCacheSize(BigInt.zero); + await cacheSizeRuntime.close(); final runtime = RuntimeHandle.create(); expect(runtime.isClosed, isFalse); @@ -1217,8 +1362,8 @@ void main() { ), throwsA(isA()), ); - expect( - () => runtime.createMap(options: const MapOptions(width: -1)), + await expectLater( + runtime.createMap(options: const MapOptions(width: -1)), throwsA(isA()), ); runtime.setResourceUrlRewriteRules([ @@ -1258,40 +1403,9 @@ void main() { }, ), ); - final offlineOperation = runtime.runAmbientCacheOperation( - AmbientCacheOperation.clear, - ); - expect(offlineOperation.isDiscarded, isFalse); - expect(() => runtime.close(), throwsA(isA())); - RuntimeEventOfflineOperationCompleted? offlineCompletion; - await _waitUntil(() { - runtime.pump(); - for (final event in runtime.drainEvents().events) { - final payload = event.payload; - if (payload is RuntimeEventOfflineOperationCompleted && - identical(payload.operation, offlineOperation)) { - offlineCompletion = payload; - } - } - return offlineCompletion != null; - }); - expect(offlineCompletion!.operation, same(offlineOperation)); - expect(offlineCompletion!.resultStatus, MaplibreStatus.ok); - expect(runtime.drainEvents().events, isEmpty); - offlineOperation.discard(); - expect(offlineOperation.isDiscarded, isTrue); - final offlineListOperation = runtime.listOfflineRegions(); - expect(offlineListOperation.isDiscarded, isFalse); - expect( - () => offlineListOperation.takeRegionStatus(), - throwsA(isA()), - ); - offlineListOperation.discard(); - expect( - () => offlineListOperation.takeRegionList(), - throwsA(isA()), - ); - final offlineCreateOperation = runtime.createOfflineRegion( + await runtime.runAmbientCacheOperation(AmbientCacheOperation.clear); + expect(await runtime.listOfflineRegions(), isEmpty); + final offlineRegion = await runtime.createOfflineRegion( const OfflineTilePyramidRegionDefinition( styleUrl: 'https://example.com/style.json', bounds: LatLngBounds( @@ -1304,12 +1418,16 @@ void main() { ), metadata: Uint8List.fromList([1, 2, 3]), ); - expect(offlineCreateOperation.isDiscarded, isFalse); - offlineCreateOperation.discard(); + expect(offlineRegion.metadata, [1, 2, 3]); - final map = runtime.createMap(); + final map = await runtime.createMap( + options: const MapOptions(mapMode: MapMode.staticMap), + ); expect(map.isClosed, isFalse); - expect(map.size(), const MapSize(width: 256, height: 256, scaleFactor: 1)); + expect( + map.snapshot().size, + const MapSize(width: 256, height: 256, scaleFactor: 1), + ); runtime.setResourceProviderRules(const []); runtime.setResourceProvider( ResourceProvider( @@ -1318,12 +1436,11 @@ void main() { ), ); runtime.clearResourceProvider(); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); - map.requestRepaint(); - expect(() => map.requestStillImage(), throwsA(isA())); - map.setDebugOptions(MapDebugOptions.tileBorders); - expect(map.debugOptions().contains(MapDebugOptions.tileBorders), isTrue); - map.setDebugOptions(MapDebugOptions.none); + await map.setStyleJson(_jsonBytes(_emptyStyleJson)); + await _expectCommandFailure( + map.requestRepaint(), + MaplibreStatus.invalidState, + ); var throwingLogCalls = 0; Maplibre.setLogCallback((_) { throwingLogCalls += 1; @@ -1332,6 +1449,36 @@ void main() { map.dumpDebugLogs(); await _waitUntil(() => throwingLogCalls > 0); Maplibre.clearLogCallback(); + final copiedEvents = runtime.drainEvents().events; + final styleLoadedEvent = copiedEvents.firstWhere( + (event) => event.eventType == RuntimeEventType.mapStyleLoaded, + ); + expect(styleLoadedEvent.source, isA()); + expect((styleLoadedEvent.source as MapRuntimeEventSource).map, same(map)); + expect(runtime.drainEvents().events, isEmpty); + + // A committed command reports the published snapshot generation, and a + // snapshot at or past that generation observes the commit. + final debugCommand = map.setDebugOptions(MapDebugOptions.tileBorders); + final debugFinished = await _waitForCommandCompletion( + runtime, + debugCommand, + ); + expect(debugFinished.disposition, CommandDisposition.committed); + expect(debugFinished.generation, greaterThan(BigInt.zero)); + final debugSnapshot = map.snapshot(); + expect( + debugSnapshot.generation, + greaterThanOrEqualTo(debugFinished.generation), + ); + expect( + debugSnapshot.debugOptions.contains(MapDebugOptions.tileBorders), + isTrue, + ); + map.setDebugOptions(MapDebugOptions.none); + + // Style image metadata answers existence, and a removal is a command that + // commits once and then fails with not-found. map.setStyleImage( 'dart-image', PremultipliedRgba8Image( @@ -1342,36 +1489,43 @@ void main() { ), options: StyleImageOptions(pixelRatio: 2, sdf: true), ); - expect(map.styleImageExists('dart-image'), isTrue); - final styleImageInfo = map.getStyleImageInfo('dart-image'); + final styleImageInfo = await map.getStyleImageInfo('dart-image'); expect(styleImageInfo, isNotNull); expect(styleImageInfo!.width, 1); expect(styleImageInfo.height, 1); expect(styleImageInfo.pixelRatio, closeTo(2, 0.0001)); expect(styleImageInfo.sdf, isTrue); - final styleImage = map.copyStyleImagePremultipliedRgba8('dart-image'); + final styleImage = await map.copyStyleImagePremultipliedRgba8('dart-image'); expect(styleImage, isNotNull); expect(styleImage!.bytes, [255, 0, 0, 255]); - expect(map.removeStyleImage('dart-image'), isTrue); - expect(map.styleImageExists('dart-image'), isFalse); - runtime.pump(); - final copiedEvents = runtime.drainEvents().events; - final styleLoadedEvent = copiedEvents.firstWhere( - (event) => event.eventType == RuntimeEventType.mapStyleLoaded, + expect( + await _waitForCommandDisposition( + runtime, + map.removeStyleImage('dart-image'), + ), + CommandDisposition.committed, ); - expect(styleLoadedEvent.source, isA()); - expect((styleLoadedEvent.source as MapRuntimeEventSource).map, same(map)); - expect(runtime.drainEvents().events, isEmpty); - map.jumpTo(const CameraOptions(center: LatLng(0, 0), zoom: 1)); - final camera = map.camera(); - expect(camera.center, const LatLng(0, 0)); - expect(camera.zoom, closeTo(1, 0.0001)); + expect(await map.getStyleImageInfo('dart-image'), isNull); + await _expectCommandFailure( + map.removeStyleImage('dart-image'), + MaplibreStatus.notFound, + ); + + final jumpCommand = map.updateCamera( + const CameraOptions(center: LatLng(0, 0), zoom: 1), + ); + final camera = await map.queryCamera(); + expect((await jumpCommand).disposition, CommandDisposition.committed); + expect(camera.camera.center, const LatLng(0, 0)); + expect(camera.camera.zoom, closeTo(1, 0.0001)); runtime.drainEvents(); - final transitionId = (BigInt.one << 64) - BigInt.one; - map.easeTo( + final transitionId = (BigInt.one << 63) - BigInt.one; + map.updateCamera( const CameraOptions(zoom: 2), + mode: CameraUpdateMode.ease, animation: AnimationOptions(durationMs: 0, transitionId: transitionId), ); + await runtime.barrier(); final cameraEvents = runtime.drainEvents().events; final transitionEvent = cameraEvents.firstWhere( (event) => @@ -1390,75 +1544,88 @@ void main() { .map((event) => CameraChangeMode.fromRawValue(event.code)), contains(CameraChangeMode.immediate), ); - map.setRenderingStatsViewEnabled(true); - expect(map.renderingStatsViewEnabled(), isTrue); + // Each new snapshot field round-trips through its set command. + final statsFinished = await _waitForCommandCompletion( + runtime, + map.setRenderingStatsViewEnabled(true), + ); + final statsSnapshot = map.snapshot(); + expect(statsSnapshot.renderingStatsViewEnabled, isTrue); + expect( + statsSnapshot.generation, + greaterThanOrEqualTo(statsFinished.generation), + ); map.setRenderingStatsViewEnabled(false); - expect(map.isFullyLoaded(), isA()); + await _waitForCommandCompletion( + runtime, + map.setViewportOptions( + const MapViewportOptions(viewportMode: ViewportMode.flippedY), + ), + ); + expect(map.snapshot().viewportOptions.viewportMode, ViewportMode.flippedY); map.setViewportOptions( const MapViewportOptions(viewportMode: ViewportMode.defaultMode), ); - expect(map.viewportOptions().viewportMode, isNotNull); - map.setTileOptions(const MapTileOptions(prefetchZoomDelta: 0)); - expect(map.tileOptions().prefetchZoomDelta, isNotNull); + await _waitForCommandCompletion( + runtime, + map.setTileOptions(const MapTileOptions(prefetchZoomDelta: 0)), + ); + expect(map.snapshot().tileOptions.prefetchZoomDelta, 0); const cameraBounds = LatLngBounds( southwest: LatLng(-10, -20), northeast: LatLng(10, 20), ); - map.setBounds( - const BoundOptions( - bounds: BoundsConstraint.bounded(cameraBounds), - minZoom: 0, - maxZoom: 24, + await _waitForCommandCompletion( + runtime, + map.setBounds( + const BoundOptions( + bounds: BoundsConstraint.bounded(cameraBounds), + minZoom: 0, + maxZoom: 24, + ), ), ); - expect(map.bounds().bounds, const BoundsConstraint.bounded(cameraBounds)); - map.setBounds(const BoundOptions(bounds: BoundsConstraint.unbounded())); - expect(map.bounds().bounds, const BoundsConstraint.unbounded()); + expect( + map.snapshot().bounds.bounds, + const BoundsConstraint.bounded(cameraBounds), + ); + await _waitForCommandCompletion( + runtime, + map.setBounds(const BoundOptions(bounds: BoundsConstraint.unbounded())), + ); + expect(map.snapshot().bounds.bounds, const BoundsConstraint.unbounded()); final projectionMode = map.projectionMode(); expect(projectionMode.axonometric, isNotNull); map.setProjectionMode(const ProjectionModeOptions(axonometric: false)); - expect(map.freeCameraOptions(), isA()); + await _waitForCommandCompletion( + runtime, + map.setFreeCameraOptions( + const FreeCameraOptions(orientation: Quaternion(0, 0, 0, 1)), + ), + ); + expect(map.snapshot().freeCameraOptions.orientation, isNotNull); expect( - map - .cameraForLatLngBounds( - const LatLngBounds( - southwest: LatLng(-1, -1), - northeast: LatLng(1, 1), - ), - ) - .zoom, + (await map.cameraForLatLngBounds( + const LatLngBounds(southwest: LatLng(-1, -1), northeast: LatLng(1, 1)), + )).zoom, isNotNull, ); expect( - map.cameraForLatLngs(const [LatLng(-1, -1), LatLng(1, 1)]).zoom, + (await map.cameraForLatLngs(const [LatLng(-1, -1), LatLng(1, 1)])).zoom, isNotNull, ); expect( - map - .latLngBoundsForCamera(const CameraOptions(center: LatLng(0, 0))) - .southwest - .latitude - .isFinite, + (await map.latLngBoundsForCamera( + const CameraOptions(center: LatLng(0, 0)), + )).southwest.latitude.isFinite, isTrue, ); - map.moveBy(1, 1); - map.scaleBy(1.01, anchor: const ScreenPoint(128, 128)); - map.rotateBy(const ScreenPoint(0, 0), const ScreenPoint(1, 1)); - map.pitchBy(0); - map.cancelTransitions(); - expect(map.isGestureInProgress(), isFalse); - map.setGestureInProgress(true); - map.moveBy(8, -4); - expect(map.isGestureInProgress(), isTrue); - map.setGestureInProgress(false); - expect(map.isGestureInProgress(), isFalse); - expect(() => map.scaleBy(-1), throwsA(isA())); - final centerPixel = map.pixelForLatLng(const LatLng(0, 0)); + final centerPixel = await map.pixelForLatLng(const LatLng(0, 0)); expect(centerPixel.x.isFinite, isTrue); - expect(map.latLngForPixel(centerPixel).latitude.isFinite, isTrue); - expect(map.pixelsForLatLngs(const [LatLng(0, 0)]), hasLength(1)); - expect(map.latLngsForPixels([centerPixel]), hasLength(1)); - final projection = map.createProjection(); + expect((await map.latLngForPixel(centerPixel)).latitude.isFinite, isTrue); + expect(await map.pixelsForLatLngs(const [LatLng(0, 0)]), hasLength(1)); + expect(await map.latLngsForPixels([centerPixel]), hasLength(1)); + final projection = await map.createProjection(); final projectionCamera = projection.camera(); expect(projectionCamera.center, isNotNull); expect(projection.pixelForLatLng(const LatLng(0, 0)).x.isFinite, isTrue); @@ -1471,7 +1638,7 @@ void main() { projection.close(); expect(projection.isClosed, isTrue); expect( - () => map.attachRef().attachMetalSurface( + () => map.attachMetalSurface( const MetalSurfaceDescriptor( extent: RenderTargetExtent(width: -1, height: 16), context: MetalContextDescriptor(device: NativePointer.nullPointer), @@ -1481,7 +1648,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachMetalSurface( + () => map.attachMetalSurface( const MetalSurfaceDescriptor( extent: RenderTargetExtent(width: 16, height: 16), context: MetalContextDescriptor(device: NativePointer.nullPointer), @@ -1491,7 +1658,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachMetalOwnedTexture( + () => map.attachMetalOwnedTexture( const MetalOwnedTextureDescriptor( extent: RenderTargetExtent(width: 16, height: 16), context: MetalContextDescriptor(device: NativePointer.nullPointer), @@ -1500,7 +1667,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachOpenGLOwnedTexture( + () => map.attachOpenGLOwnedTexture( const OpenGLOwnedTextureDescriptor( extent: RenderTargetExtent(width: -1, height: 16), context: EglContextDescriptor( @@ -1513,7 +1680,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachOpenGLOwnedTexture( + () => map.attachOpenGLOwnedTexture( const OpenGLOwnedTextureDescriptor( extent: RenderTargetExtent(width: 16, height: 16), context: EglContextDescriptor( @@ -1526,7 +1693,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachOpenGLBorrowedTexture( + () => map.attachOpenGLBorrowedTexture( const OpenGLBorrowedTextureDescriptor( extent: RenderTargetExtent(width: 16, height: 16), physicalWidth: 16, @@ -1543,7 +1710,7 @@ void main() { throwsA(isA()), ); expect( - () => map.attachRef().attachOpenGLSurface( + () => map.attachOpenGLSurface( const OpenGLSurfaceDescriptor( extent: RenderTargetExtent(width: 16, height: 16), context: EglContextDescriptor( @@ -1557,30 +1724,41 @@ void main() { throwsA(isA()), ); - final sourceIds = map.listStyleSourceIds(); + final sourceIds = await map.listStyleSourceIds(); expect(sourceIds, contains('org.maplibre.annotations')); expect( - map.listStyleLayerIds(), + await map.listStyleLayerIds(), contains('org.maplibre.annotations.points'), ); - expect(map.styleSourceExists('missing-source'), isFalse); - expect(map.styleLayerExists('missing-layer'), isFalse); - expect(map.removeStyleSource('missing-source'), isFalse); - expect(map.removeStyleLayer('missing-layer'), isFalse); + // Existence is answered by the info getters' found flag, and removing a + // missing object fails with not-found. + expect(await map.getStyleSourceInfo('missing-source'), isNull); + expect(await map.getStyleLayerInfo('missing-layer'), isNull); + await _expectCommandFailure( + map.removeStyleSource('missing-source'), + MaplibreStatus.notFound, + ); + await _expectCommandFailure( + map.removeStyleLayer('missing-layer'), + MaplibreStatus.notFound, + ); map.addGeoJsonSourceUrl( 'dart-geojson-url-source', 'https://example.com/a.geojson', ); expect( - map.getStyleSourceInfo('dart-geojson-url-source')!.type, + (await map.getStyleSourceInfo('dart-geojson-url-source'))!.type, SourceType.geoJson, ); map.setGeoJsonSourceUrl( 'dart-geojson-url-source', 'https://example.com/b.geojson', ); - expect(map.removeStyleSource('dart-geojson-url-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-geojson-url-source'), + ); expect( () => GeoJsonSourceDataHandle.prepare( _jsonBytes('{"type":"FeatureCollection","features":[]}'), @@ -1604,36 +1782,62 @@ void main() { ), options: GeoJsonSourceOptions(cluster: true, clusterRadius: 60), ); - map.addGeoJsonSourceData('dart-clustered-geojson-source', clusteredData); - // Installing rejects data whose baked-in options differ from the source's. + await _expectCommandCommitted( + runtime, + map.addGeoJsonSourceData('dart-clustered-geojson-source', clusteredData), + ); + // The map thread rejects data whose baked-in options differ from the + // source's, reported through the command's terminal event. final plainData = GeoJsonSourceDataHandle.prepare( _jsonBytes('{"type":"Point","coordinates":[0,0]}'), ); - expect( - () => - map.setGeoJsonSourceData('dart-clustered-geojson-source', plainData), - throwsA(isA()), + final mismatchedInstallCommand = map.setGeoJsonSourceData( + 'dart-clustered-geojson-source', + plainData, ); + // A prepared handle may close as soon as the install command is submitted. plainData.close(); - map.setGeoJsonSourceSynchronousTiling( - 'dart-clustered-geojson-source', - true, + await _expectCommandFailure( + mismatchedInstallCommand, + MaplibreStatus.invalidArgument, ); - map.setGeoJsonSourceSynchronousTiling( - 'dart-clustered-geojson-source', - false, + await _expectCommandCommitted( + runtime, + map.setGeoJsonSourceSynchronousTiling( + 'dart-clustered-geojson-source', + true, + ), ); - expect( - () => map.setGeoJsonSourceSynchronousTiling('missing-source', true), - throwsA(isA()), + await _expectCommandCommitted( + runtime, + map.setGeoJsonSourceSynchronousTiling( + 'dart-clustered-geojson-source', + false, + ), + ); + await _expectCommandFailure( + map.setGeoJsonSourceSynchronousTiling('missing-source', true), + MaplibreStatus.invalidArgument, ); // One prepared handle installs on any number of sources. - map.addGeoJsonSourceData('dart-clustered-geojson-copy', clusteredData); + await _expectCommandCommitted( + runtime, + map.addGeoJsonSourceData('dart-clustered-geojson-copy', clusteredData), + ); clusteredData.close(); // Closing the handle never invalidates a source it was installed on. - expect(map.styleSourceExists('dart-clustered-geojson-source'), isTrue); - expect(map.removeStyleSource('dart-clustered-geojson-copy'), isTrue); - expect(map.removeStyleSource('dart-clustered-geojson-source'), isTrue); + expect( + await map.getStyleSourceInfo('dart-clustered-geojson-source'), + isNotNull, + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-clustered-geojson-copy'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-clustered-geojson-source'), + ); clusteredData.close(); expect(clusteredData.isClosed, isTrue); expect( @@ -1645,10 +1849,13 @@ void main() { 'https://example.com/vector.json', ); expect( - map.getStyleSourceInfo('dart-vector-source')!.type, + (await map.getStyleSourceInfo('dart-vector-source'))!.type, SourceType.vector, ); - expect(map.removeStyleSource('dart-vector-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-vector-source'), + ); expect( () => map.addVectorSourceTiles( 'dart-vector-invalid-tiles-source', @@ -1663,18 +1870,24 @@ void main() { options: const TileSourceOptions(minZoom: 0, maxZoom: 14), ); expect( - map.getStyleSourceInfo('dart-vector-tiles-source')!.type, + (await map.getStyleSourceInfo('dart-vector-tiles-source'))!.type, SourceType.vector, ); - expect(map.removeStyleSource('dart-vector-tiles-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-vector-tiles-source'), + ); map.addRasterSourceTiles('dart-raster-tiles-source', const [ 'https://example.com/{z}/{x}/{y}.png', ], options: const TileSourceOptions(tileSize: 256)); expect( - map.getStyleSourceInfo('dart-raster-tiles-source')!.type, + (await map.getStyleSourceInfo('dart-raster-tiles-source'))!.type, SourceType.raster, ); - expect(map.removeStyleSource('dart-raster-tiles-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-raster-tiles-source'), + ); map.addRasterDemSourceTiles( 'dart-raster-dem-tiles-source', const ['https://example.com/{z}/{x}/{y}.png'], @@ -1684,28 +1897,46 @@ void main() { ), ); expect( - map.getStyleSourceInfo('dart-raster-dem-tiles-source')!.type, + (await map.getStyleSourceInfo('dart-raster-dem-tiles-source'))!.type, SourceType.rasterDem, ); map.addHillshadeLayer( 'dart-hillshade-layer', 'dart-raster-dem-tiles-source', ); - expect(map.getStyleLayerType('dart-hillshade-layer'), 'hillshade'); + expect( + (await map.getStyleLayerInfo('dart-hillshade-layer'))!.type, + 'hillshade', + ); map.addColorReliefLayer( 'dart-color-relief-layer', 'dart-raster-dem-tiles-source', ); - expect(map.getStyleLayerType('dart-color-relief-layer'), 'color-relief'); + expect( + (await map.getStyleLayerInfo('dart-color-relief-layer'))!.type, + 'color-relief', + ); map.moveStyleLayer( 'dart-color-relief-layer', beforeLayerId: 'dart-hillshade-layer', ); - expect(map.removeStyleLayer('dart-color-relief-layer'), isTrue); - expect(map.removeStyleLayer('dart-hillshade-layer'), isTrue); - expect(map.removeStyleSource('dart-raster-dem-tiles-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleLayer('dart-color-relief-layer'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleLayer('dart-hillshade-layer'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-raster-dem-tiles-source'), + ); map.addLocationIndicatorLayer('dart-location-layer'); - expect(map.getStyleLayerType('dart-location-layer'), 'location-indicator'); + expect( + (await map.getStyleLayerInfo('dart-location-layer'))!.type, + 'location-indicator', + ); map.setLocationIndicatorLocation( 'dart-location-layer', const LatLng(37.7749, -122.4194), @@ -1713,7 +1944,10 @@ void main() { final location = jsonDecode( utf8.decode( - map.getLayerProperty('dart-location-layer', 'location')!, + (await map.getLayerProperty( + 'dart-location-layer', + 'location', + ))!, ), ) as List; @@ -1729,7 +1963,10 @@ void main() { LocationIndicatorImageKind.top, 'dart-location-image', ); - expect(map.removeStyleLayer('dart-location-layer'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleLayer('dart-location-layer'), + ); const imageSourceCoordinates = [ LatLng(1, -1), LatLng(1, 1), @@ -1746,9 +1983,12 @@ void main() { bytes: Uint8List.fromList([0, 255, 0, 255]), ), ); - expect(map.getStyleSourceInfo('dart-image-source')!.type, SourceType.image); expect( - map.getImageSourceCoordinates('dart-image-source'), + (await map.getStyleSourceInfo('dart-image-source'))!.type, + SourceType.image, + ); + expect( + await map.getImageSourceCoordinates('dart-image-source'), imageSourceCoordinates, ); map.setImageSourceUrl('dart-image-source', 'https://example.com/image.png'); @@ -1757,10 +1997,13 @@ void main() { imageSourceCoordinates.reversed.toList(), ); expect( - map.getImageSourceCoordinates('dart-image-source'), + await map.getImageSourceCoordinates('dart-image-source'), imageSourceCoordinates.reversed.toList(), ); - expect(map.removeStyleSource('dart-image-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-image-source'), + ); final fetchedTiles = []; expect( @@ -1796,7 +2039,7 @@ void main() { CustomGeometrySourceOptions(fetchTile: fetchedTiles.add), ); expect( - map.getStyleSourceInfo('dart-custom-source')!.type, + (await map.getStyleSourceInfo('dart-custom-source'))!.type, SourceType.customVector, ); map.setCustomGeometrySourceTileData( @@ -1819,18 +2062,24 @@ void main() { 'dart-custom-source', const LatLngBounds(southwest: LatLng(-1, -1), northeast: LatLng(1, 1)), ); - expect(map.removeStyleSource('dart-custom-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-custom-source'), + ); map.addCustomGeometrySource( 'dart-custom-source', CustomGeometrySourceOptions(fetchTile: fetchedTiles.add), ); - expect(map.removeStyleSource('dart-custom-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-custom-source'), + ); map.addCustomMvtVectorSource( 'dart-custom-mvt-source', CustomMvtVectorSourceOptions(fetchTile: fetchedTiles.add), ); expect( - map.getStyleSourceInfo('dart-custom-mvt-source')!.type, + (await map.getStyleSourceInfo('dart-custom-mvt-source'))!.type, SourceType.customMvtVector, ); map.setCustomMvtVectorSourceTileData( @@ -1847,7 +2096,10 @@ void main() { 'dart-custom-mvt-source', const CanonicalTileId(z: 0, x: 0, y: 0), ); - expect(map.removeStyleSource('dart-custom-mvt-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-custom-mvt-source'), + ); final geoJsonData = GeoJsonSourceDataHandle.prepare( _jsonBytes( @@ -1855,15 +2107,17 @@ void main() { '"properties":{"kind":"dart"}}', ), ); - map.addGeoJsonSourceData('dart-geojson-source', geoJsonData); + await _expectCommandCommitted( + runtime, + map.addGeoJsonSourceData('dart-geojson-source', geoJsonData), + ); geoJsonData.close(); - expect(map.styleSourceExists('dart-geojson-source'), isTrue); - final info = map.getStyleSourceInfo('dart-geojson-source'); + final info = await map.getStyleSourceInfo('dart-geojson-source'); expect(info, isNotNull); expect(info!.type, SourceType.geoJson); expect(info.id, 'dart-geojson-source'); expect(info.attribution, isNull); - expect(map.listStyleSourceIds(), contains('dart-geojson-source')); + expect(await map.listStyleSourceIds(), contains('dart-geojson-source')); final updatedGeoJsonData = GeoJsonSourceDataHandle.prepare( _jsonBytes('{"type":"Point","coordinates":[2,1]}'), @@ -1875,10 +2129,12 @@ void main() { '{"id":"dart-circle-layer","type":"circle","source":"dart-geojson-source"}', ), ); - expect(map.styleLayerExists('dart-circle-layer'), isTrue); - expect(map.getStyleLayerType('dart-circle-layer'), 'circle'); - expect(map.listStyleLayerIds(), contains('dart-circle-layer')); - final layerJson = map.getStyleLayerJson('dart-circle-layer'); + final circleInfo = await map.getStyleLayerInfo('dart-circle-layer'); + expect(circleInfo, isNotNull); + expect(circleInfo!.type, 'circle'); + expect(circleInfo.sourceId, 'dart-geojson-source'); + expect(await map.listStyleLayerIds(), contains('dart-circle-layer')); + final layerJson = await map.getStyleLayerJson('dart-circle-layer'); expect( jsonDecode(utf8.decode(layerJson!)), containsPair('id', 'dart-circle-layer'), @@ -1890,7 +2146,7 @@ void main() { _jsonBytes('6.5'), ); expect( - map.getLayerProperty('dart-circle-layer', 'circle-radius'), + await map.getLayerProperty('dart-circle-layer', 'circle-radius'), _jsonBytes('6.5'), ); map.setLayerFilter( @@ -1898,22 +2154,32 @@ void main() { _jsonBytes('["==",["get","kind"],"dart"]'), ); expect( - map.getLayerFilter('dart-circle-layer'), + await map.getLayerFilter('dart-circle-layer'), _jsonBytes('["==",["get","kind"],"dart"]'), ); map.setLayerFilter('dart-circle-layer', null); - expect(map.getLayerFilter('dart-circle-layer'), isNull); + expect(await map.getLayerFilter('dart-circle-layer'), isNull); - expect(map.removeStyleLayer('dart-circle-layer'), isTrue); - expect(map.removeStyleSource('dart-geojson-source'), isTrue); + await _expectCommandCommitted( + runtime, + map.removeStyleLayer('dart-circle-layer'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('dart-geojson-source'), + ); - map.close(); + await map.close(); expect(map.isClosed, isTrue); - runtime.close(); + final operationAfterClose = runtime.runAmbientCacheOperation( + AmbientCacheOperation.clear, + ); + await runtime.close(); expect(runtime.isClosed, isTrue); + await operationAfterClose; }); - test('native pointer preserves address value semantics', () { + test('native pointer preserves address value semantics', () async { const pointer = NativePointer(0x1234); expect(pointer.address, 0x1234); @@ -1924,115 +2190,130 @@ void main() { expect(NativePointer.nullPointer.isNull, isTrue); }); - test('BND-109 source inspection returns independent copied metadata', () { - final runtime = RuntimeHandle.create(); - final map = runtime.createMap(); - map.setStyleJson(_jsonBytes(_emptyStyleJson)); + test( + 'BND-109 source inspection returns independent copied metadata', + () async { + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + map.setStyleJson(_jsonBytes(_emptyStyleJson)); - const tileUrls = [ - 'https://a.example.com/{z}/{x}/{y}.mvt', - 'https://b.example.com/{z}/{x}/{y}.mvt', - ]; - const bounds = LatLngBounds( - southwest: LatLng(-12, -34), - northeast: LatLng(56, 78), - ); - map.addVectorSourceTiles( - 'inline-vector', - tileUrls, - options: const TileSourceOptions( - minZoom: 0, - maxZoom: 12, - attribution: 'Inline attribution', - scheme: TileScheme.tms, - bounds: bounds, - tileSize: 512, - vectorEncoding: VectorTileEncoding.mlt, - ), - ); + const tileUrls = [ + 'https://a.example.com/{z}/{x}/{y}.mvt', + 'https://b.example.com/{z}/{x}/{y}.mvt', + ]; + const bounds = LatLngBounds( + southwest: LatLng(-12, -34), + northeast: LatLng(56, 78), + ); + map.addVectorSourceTiles( + 'inline-vector', + tileUrls, + options: const TileSourceOptions( + minZoom: 0, + maxZoom: 12, + attribution: 'Inline attribution', + scheme: TileScheme.tms, + bounds: bounds, + tileSize: 512, + vectorEncoding: VectorTileEncoding.mlt, + ), + ); - final inline = map.getStyleSourceInfo('inline-vector')!; - expect(inline.type, SourceType.vector); - expect(inline.url, isNull); - expect(inline.attribution, 'Inline attribution'); - expect(inline.tileSize, 512); - expect(inline.vectorEncoding, VectorTileEncoding.mlt); - expect(inline.rasterDemEncoding, isNull); - expect(inline.tileJson, isNotNull); - expect(inline.tileJson!.tileUrls, tileUrls); - expect(inline.tileJson!.minZoom, 0); - expect(inline.tileJson!.maxZoom, 12); - expect(inline.tileJson!.scheme, TileScheme.tms); - expect(inline.tileJson!.bounds, bounds); - expect( - () => inline.tileJson!.tileUrls.add('https://example.com/extra'), - throwsUnsupportedError, - ); + final inline = (await map.getStyleSourceInfo('inline-vector'))!; + expect(inline.type, SourceType.vector); + expect(inline.url, isNull); + expect(inline.attribution, 'Inline attribution'); + expect(inline.tileSize, 512); + expect(inline.vectorEncoding, VectorTileEncoding.mlt); + expect(inline.rasterDemEncoding, isNull); + expect(inline.tileJson, isNotNull); + expect(inline.tileJson!.tileUrls, tileUrls); + expect(inline.tileJson!.minZoom, 0); + expect(inline.tileJson!.maxZoom, 12); + expect(inline.tileJson!.scheme, TileScheme.tms); + expect(inline.tileJson!.bounds, bounds); + expect( + () => inline.tileJson!.tileUrls.add('https://example.com/extra'), + throwsUnsupportedError, + ); - map.addVectorSourceUrl( - 'url-vector', - 'https://example.com/vector-tilejson.json', - ); - final urlBacked = map.getStyleSourceInfo('url-vector')!; - expect(urlBacked.url, 'https://example.com/vector-tilejson.json'); - expect(urlBacked.tileJson, isNull); + map.addVectorSourceUrl( + 'url-vector', + 'https://example.com/vector-tilejson.json', + ); + final urlBacked = (await map.getStyleSourceInfo('url-vector'))!; + expect(urlBacked.url, 'https://example.com/vector-tilejson.json'); + expect(urlBacked.tileJson, isNull); + + map.addRasterDemSourceTiles( + 'inline-dem', + const ['https://example.com/{z}/{x}/{y}.png'], + options: const TileSourceOptions( + tileSize: 256, + rasterDemEncoding: RasterDemEncoding.terrarium, + ), + ); + final rasterDem = (await map.getStyleSourceInfo('inline-dem'))!; + expect(rasterDem.tileSize, 256); + expect(rasterDem.rasterDemEncoding, RasterDemEncoding.terrarium); + expect(rasterDem.vectorEncoding, isNull); - map.addRasterDemSourceTiles( - 'inline-dem', - const ['https://example.com/{z}/{x}/{y}.png'], - options: const TileSourceOptions( - tileSize: 256, - rasterDemEncoding: RasterDemEncoding.terrarium, - ), - ); - final rasterDem = map.getStyleSourceInfo('inline-dem')!; - expect(rasterDem.tileSize, 256); - expect(rasterDem.rasterDemEncoding, RasterDemEncoding.terrarium); - expect(rasterDem.vectorEncoding, isNull); - - expect(map.removeStyleSource('inline-vector'), isTrue); - expect(map.removeStyleSource('url-vector'), isTrue); - expect(map.removeStyleSource('inline-dem'), isTrue); - map.close(); - runtime.close(); - - expect(inline.id, 'inline-vector'); - expect(inline.tileJson!.tileUrls, tileUrls); - expect(urlBacked.url, 'https://example.com/vector-tilejson.json'); - expect(TileScheme.fromRaw(91).rawValue, 91); - expect(VectorTileEncoding.fromRaw(92).rawValue, 92); - expect(RasterDemEncoding.fromRaw(93).rawValue, 93); - }); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('inline-vector'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('url-vector'), + ); + await _expectCommandCommitted( + runtime, + map.removeStyleSource('inline-dem'), + ); + await map.close(); + await runtime.close(); + + expect(inline.id, 'inline-vector'); + expect(inline.tileJson!.tileUrls, tileUrls); + expect(urlBacked.url, 'https://example.com/vector-tilejson.json'); + expect(TileScheme.fromRaw(91).rawValue, 91); + expect(VectorTileEncoding.fromRaw(92).rawValue, 92); + expect(RasterDemEncoding.fromRaw(93).rawValue, 93); + }, + ); - test('scoped native values validate before exposing borrowed values', () { - var live = true; - void checkLive() { - if (!live) { - throw StateError('scope closed'); + test( + 'scoped native values validate before exposing borrowed values', + () async { + var live = true; + void checkLive() { + if (!live) { + throw StateError('scope closed'); + } } - } - final pointer = ScopedNativePointer( - 0x1234, - checkValid: checkLive, - debugName: 'test pointer', - ); - final value = ScopedNativeInt( - 7, - checkValid: checkLive, - debugName: 'test value', - ); + final pointer = ScopedNativePointer( + 0x1234, + checkValid: checkLive, + debugName: 'test pointer', + ); + final value = ScopedNativeInt( + 7, + checkValid: checkLive, + debugName: 'test value', + ); - expect(pointer.address, 0x1234); - expect(pointer.toNativePointer(), const NativePointer(0x1234)); - expect(value.value, 7); + expect(pointer.address, 0x1234); + expect(pointer.toNativePointer(), const NativePointer(0x1234)); + expect(value.value, 7); - live = false; - expect(() => pointer.address, throwsStateError); - expect(() => value.value, throwsStateError); - }); + live = false; + expect(() => pointer.address, throwsStateError); + expect(() => value.value, throwsStateError); + }, + ); - test('native buffer owns reusable native byte storage', () { + test('native buffer owns reusable native byte storage', () async { final buffer = NativeBuffer(4); try { buffer.writeBytes(Uint8List.fromList([42])); @@ -2051,13 +2332,12 @@ void main() { expect(() => NativeBuffer(0), throwsArgumentError); }); - test('runtime value wrappers preserve unknown raw values', () { + test('runtime value wrappers preserve unknown raw values', () async { final eventType = RuntimeEventType.fromRawValue(0xfeed); final sourceType = RuntimeEventSourceType.fromRawValue(0xbeef); final renderMode = RenderMode.fromRawValue(42); final renderResult = RenderResult.fromRawValue(42); - final operationKind = OfflineOperationKind.fromRawValue(99); - final resultKind = OfflineOperationResultKind.fromRawValue(100); + const unknownDefinition = UnknownOfflineRegionDefinition(101); final ownership = OpenGLContextOwnership.fromRawValue(102); final clientApi = OpenGLClientApi.fromRawValue(103); @@ -2068,8 +2348,7 @@ void main() { expect(renderMode.name, 'unknown(42)'); expect(renderResult, RenderResult.fromRawValue(42)); expect(renderResult.name, 'unknown(42)'); - expect(operationKind.rawValue, 99); - expect(resultKind.rawValue, 100); + expect(unknownDefinition.rawType, 101); expect(ownership, OpenGLContextOwnership.fromRawValue(102)); expect(ownership.name, 'unknown(102)'); @@ -2077,170 +2356,64 @@ void main() { }); test( - 'a parked owner isolate wakes for native work and for a signal', + 'runtime and map survive isolate execution and await resumption', () async { - final ready = ReceivePort(); - final signalled = ReceivePort(); - await Isolate.spawn(_signalWakeSource, [ - ready.sendPort, - signalled.sendPort, - ]); - final worker = await ready.first as SendPort; - - // No await below this line: the VM may resume an isolate on another OS - // thread, and runtime calls are owner-thread affine. The worker - // handshake finishes above so this isolate owns the runtime throughout. final runtime = RuntimeHandle.create( options: const RuntimeOptions(cachePath: ':memory:'), ); - final map = runtime.createMap(); - _quiesce(runtime); - - // The scheme is unsupported, so native reports the failure from its own - // threads and that failure reaches the parked owner isolate. - map.setStyleUrl('unsupported://style.json'); - var loadingFailed = false; - final loadStarted = Stopwatch()..start(); - for (var attempt = 0; attempt < 20 && !loadingFailed; attempt += 1) { - runtime.pump(timeout: _parkTimeout); - expect( - loadStarted.elapsed, - lessThan(_promptReturn), - reason: 'parks sat out their timeouts while loading was pending', - ); - for (final event in runtime.drainEvents().events) { - if (event.eventType == RuntimeEventType.mapLoadingFailed) { - loadingFailed = true; - } - } - } - expect(loadingFailed, isTrue); + final map = await runtime.createMap( + options: const MapOptions(width: 64, height: 64), + ); - // A source signalled from another isolate matches a host's submission - // path, and the park it releases has no other work to end it. - final source = runtime.acquireWakeSource(); - _quiesce(runtime); - worker.send(source); + await Isolate.run(() {}); - final parkStarted = Stopwatch()..start(); - runtime.pump(timeout: _parkTimeout); - expect( - parkStarted.elapsed, - lessThan(_promptReturn), - reason: - 'the parked owner isolate timed out instead of taking the signal', - ); + final before = map.snapshot(); + final completion = map.requestRepaint(); + final camera = await map.queryCamera(); + expect((await completion).disposition, CommandDisposition.committed); + expect(camera.generation, greaterThanOrEqualTo(before.generation)); - // A wake source stays usable after its runtime closes, so hosts tear the - // two down in either order. - map.close(); - runtime.close(); - source.signal(); - source.close(); - expect(source.isClosed, isTrue); - expect(source.close, returnsNormally); - expect(source.signal, throwsA(isA())); - - await signalled.first; - ready.close(); - signalled.close(); + await map.close(); + await runtime.close(); + expect(map.isClosed, isTrue); + expect(runtime.isClosed, isTrue); }, ); - test('a pump clears the wake flag it returns on', () { - final runtime = RuntimeHandle.create( - options: const RuntimeOptions(cachePath: ':memory:'), - ); - final source = runtime.acquireWakeSource(); - _quiesce(runtime); - - source.signal(); - final signalledStarted = Stopwatch()..start(); - runtime.pump(timeout: _parkTimeout); - expect( - signalledStarted.elapsed, - lessThan(_promptReturn), - reason: 'a pump waited even though the wake flag was set', - ); + test('native execution progresses without blocking the isolate', () async { + final runtime = RuntimeHandle.create(); + final map = await runtime.createMap(); + map.setStyleUrl('unsupported://autonomous-progress.json'); - // The pump above cleared the wake flag, so this one waits its full timeout. - final idleStarted = Stopwatch()..start(); - runtime.pump(timeout: const Duration(milliseconds: 200)); - expect( - idleStarted.elapsed, - greaterThanOrEqualTo(const Duration(milliseconds: 100)), - reason: 'the first pump left the wake flag set', + final event = await _waitUntilEvent( + runtime, + (candidate) => candidate.eventType == RuntimeEventType.mapLoadingFailed, ); + expect(event.eventType, RuntimeEventType.mapLoadingFailed); - source.close(); - runtime.close(); + await map.close(); + await runtime.close(); }); - test('an attach reference reaches native from another isolate', () async { + test('runtime close waits for native teardown', () async { final runtime = RuntimeHandle.create(); - final map = runtime.createMap( - options: const MapOptions(width: 64, height: 64), - ); - // A MapHandle cannot cross isolates, so the reference carries the address. - final attachRef = map.attachRef(); - - // Close before awaiting: the isolate may resume on a different OS thread - // after an await, which would make these handles unusable. The reference - // outliving them is the point here. - map.close(); - runtime.close(); + final map = await runtime.createMap(); + map.setStyleUrl('unsupported://runtime-teardown.json'); - // The extent is valid, so the binding's own checks pass and the attach - // reaches native, which validates the retired map id before the descriptor. - final diagnostic = await Isolate.run(() { - try { - attachRef.attachMetalSurface( - const MetalSurfaceDescriptor( - extent: RenderTargetExtent(width: 16, height: 16), - context: MetalContextDescriptor(device: NativePointer.nullPointer), - layer: NativePointer.nullPointer, - ), - ); - return 'attached'; - } on MaplibreException catch (error) { - return error.diagnostic; - } - }); - // BND-196: the C API rejects the released id as stale rather than binding - // the session to whatever map is created next. - expect(diagnostic, contains('stale')); - }); -} - -/// Long enough that a park only ends early because something woke it. -const _parkTimeout = Duration(seconds: 10); - -/// Well below [_parkTimeout], and far above the scheduling noise a loaded CI -/// machine adds to a wake. -const _promptReturn = Duration(seconds: 5); + // The live map rejects the release before native teardown starts. + await expectLater(runtime.close(), throwsA(isA())); + expect(runtime.isClosed, isFalse); -/// Pumps until the runtime is idle, so a park that follows is released by the -/// signal the test raises rather than by leftover work. -void _quiesce(RuntimeHandle runtime) { - for (var attempt = 0; attempt < 100; attempt += 1) { - runtime.pump(); - if (runtime.drainEvents().events.isEmpty) { - return; - } - } - fail('the runtime kept producing events while idle'); -} + await map.close(); + // This future resolves only after the runtime's threads and its released + // map's teardown are gone. + await runtime.close(); + expect(runtime.isClosed, isTrue); -/// Signals a wake source transferred from the owner isolate, once that isolate -/// has had time to enter its park. -Future _signalWakeSource(List ports) async { - final inbox = ReceivePort(); - ports[0].send(inbox.sendPort); - final source = await inbox.first as WakeSource; - sleep(const Duration(milliseconds: 20)); - source.signal(); - ports[1].send(null); - inbox.close(); + // A second close no-ops rather than releasing again. + await runtime.close(); + expect(runtime.isClosed, isTrue); + }); } Future _completeTransferredRequest(ResourceRequestHandle token) { @@ -2288,25 +2461,40 @@ void _clearLogCallback() { Maplibre.clearLogCallback(); } -Future _pumpUntil( +Future _waitUntilCondition( RuntimeHandle runtime, bool Function() condition, { Duration timeout = const Duration(seconds: 5), }) async { await _waitUntil(() { - runtime.pump(); runtime.drainEvents(); return condition(); }, timeout: timeout); } -Future _pumpUntilEvent( +Future _waitForCommandCompletion( + RuntimeHandle runtime, + Future completion, +) => completion; + +Future _waitForCommandDisposition( + RuntimeHandle runtime, + Future completion, +) async => (await completion).disposition; + +Future _expectCommandCommitted( + RuntimeHandle runtime, + Future completion, +) async { + expect((await completion).disposition, CommandDisposition.committed); +} + +Future _waitUntilEvent( RuntimeHandle runtime, bool Function(RuntimeEvent event) predicate, ) async { RuntimeEvent? matched; await _waitUntil(() { - runtime.pump(); for (final event in runtime.drainEvents().events) { if (predicate(event)) { matched = event; diff --git a/bindings/dart/test/native_library_test.dart b/bindings/dart/test/native_library_test.dart index 726c8c7c0..fc3f5be47 100644 --- a/bindings/dart/test/native_library_test.dart +++ b/bindings/dart/test/native_library_test.dart @@ -2,11 +2,9 @@ import 'package:maplibre_native_ffi/maplibre_native_ffi.dart'; import 'package:test/test.dart'; void main() { - /// Resolves and loads the code asset `hook/build.dart` declares. This runs in - /// CI where the rest of the suite does not, so it is what proves a downloaded - /// artifact works. It goes through the public API so the ABI-version gate - /// runs too. Every artifact compiles exactly one renderer, so the mask is - /// never empty. + /// Resolves and loads the code asset that `hook/build.dart` declares. This + /// goes through the public API, so the ABI-version gate runs too. Every + /// artifact compiles exactly one renderer, so the mask is never empty. test('loads the native library', () { expect(Maplibre.supportedRenderBackends().bits, isNonZero); }); diff --git a/bindings/dart/test/render_driver_test.dart b/bindings/dart/test/render_driver_test.dart new file mode 100644 index 000000000..46881cd4a --- /dev/null +++ b/bindings/dart/test/render_driver_test.dart @@ -0,0 +1,215 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:maplibre_native_ffi/maplibre_native_ffi.dart'; +import 'package:test/test.dart'; + +typedef _CreateDeviceNative = Pointer Function(); +typedef _CreateDevice = Pointer Function(); +typedef _ReleaseObjectNative = Void Function(Pointer); +typedef _ReleaseObject = void Function(Pointer); + +final String? _metalTestSkipReason = !Platform.isMacOS + ? 'requires a macOS Metal host' + : !Maplibre.supportedRenderBackends().contains(RenderBackendMask.metal) + ? 'requires a native library built with the Metal renderer' + : null; + +final class _MetalTestContext { + _MetalTestContext._(this._objectiveCRuntime, this.device); + + static _MetalTestContext? create() { + final metal = DynamicLibrary.open( + '/System/Library/Frameworks/Metal.framework/Metal', + ); + final create = metal.lookupFunction<_CreateDeviceNative, _CreateDevice>( + 'MTLCreateSystemDefaultDevice', + ); + final objectiveCRuntime = DynamicLibrary.open('/usr/lib/libobjc.A.dylib'); + final device = create(); + return device == nullptr + ? null + : _MetalTestContext._(objectiveCRuntime, device); + } + + final DynamicLibrary _objectiveCRuntime; + final Pointer device; + + void close() => + _objectiveCRuntime.lookupFunction<_ReleaseObjectNative, _ReleaseObject>( + 'objc_release', + )(device); +} + +Future _serviceAndComplete( + RenderSessionHandle session, + Future operation, +) async { + expect(session.serviceDriverWork(), greaterThan(0)); + await operation; +} + +void main() { + test('transferred WebGL canvases select dedicated ownership', () { + const descriptor = WebGLContextDescriptor.transferredCanvas('#map'); + expect(descriptor.ownership, OpenGLContextOwnership.dedicated); + }); + + test( + 'public core-worker render workflow renders, reads, and queries', + () async { + final context = _MetalTestContext.create(); + if (context == null) { + fail('MTLCreateSystemDefaultDevice returned nil'); + } + + final runtime = RuntimeHandle.create(); + final map = await MapHandle.create( + runtime, + options: const MapOptions(width: 32, height: 32), + ); + RenderSessionHandle? session; + try { + final style = Uint8List.fromList( + utf8.encode(''' +{"version":8,"sources":{"points":{"type":"geojson","data":{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"origin"},"geometry":{"type":"Point","coordinates":[0,0]}}]}}},"layers":[{"id":"points","type":"circle","source":"points","paint":{"circle-radius":8,"circle-color":"#ff0000"}}]} +'''), + ); + map.setStyleJson(style); + await runtime.barrier(); + + final attachment = map.attachMetalOwnedTexture( + MetalOwnedTextureDescriptor( + extent: const RenderTargetExtent(width: 32, height: 32), + context: MetalContextDescriptor( + device: NativePointer(context.device.address), + ), + ), + options: const RenderSessionAttachOptions( + driver: RenderDriver.coreWorker, + requestedTextureRingDepth: 2, + ), + ); + session = attachment.session; + await attachment.completed; + expect(session.capabilities.textureRingDepth, inInclusiveRange(1, 3)); + + session.requestFrame( + const FrameDemand(renderIfNeeded: false, token: 17), + ); + await session.barrier(); + final results = session.drainFrameResults(); + expect(results, hasLength(1)); + expect(results.single.token, 17); + expect(results.single.disposition, same(RenderResult.rendered)); + expect(results.single.frameGeneration, greaterThan(0)); + + final frame = session.acquireFrame(); + expect(frame.result.frameGeneration, results.single.frameGeneration); + expect(frame.producerSync.kind, 0); + expect(frame.metalTexture.unsafeTexture.isNull, isFalse); + frame.release(); + + final image = await session.readPremultipliedRgba8(); + expect(image.info.width, 32); + expect(image.info.height, 32); + expect(image.bytes, hasLength(image.info.byteLength)); + + final query = await session.queryRenderedFeatures( + const RenderedQueryPoint(ScreenPoint(16, 16)), + ); + for (final hit in query) { + expect( + jsonDecode(utf8.decode(hit.feature)), + isA>(), + ); + } + + await session.detach(); + session.close(); + session = null; + } finally { + if (session != null) { + try { + session.abandon(); + session.close(); + } catch (_) {} + } + await map.close(); + await runtime.close(); + context.close(); + } + }, + skip: _metalTestSkipReason, + ); + + test('caller driver services work and abandons cleanly', () async { + final context = _MetalTestContext.create(); + if (context == null) { + fail('MTLCreateSystemDefaultDevice returned nil'); + } + + final runtime = RuntimeHandle.create(); + final map = await MapHandle.create( + runtime, + options: const MapOptions(width: 16, height: 16), + ); + RenderSessionHandle? session; + try { + final attachment = map.attachMetalOwnedTexture( + MetalOwnedTextureDescriptor( + extent: const RenderTargetExtent(width: 16, height: 16), + context: MetalContextDescriptor( + device: NativePointer(context.device.address), + ), + ), + options: const RenderSessionAttachOptions( + driver: RenderDriver.callerGraphicsThread, + requestedTextureRingDepth: 2, + ), + ); + session = attachment.session; + final attachWorkReady = session.driverWorkReady.first; + await attachWorkReady.timeout(const Duration(seconds: 5)); + await _serviceAndComplete(session, attachment.completed); + expect(session.capabilities.driver, RenderDriver.callerGraphicsThread); + + map.setStyleJson( + Uint8List.fromList( + utf8.encode('{"version":8,"sources":{},"layers":[]}'), + ), + ); + await runtime.barrier(); + + final frameResultsReady = session.frameResultsReady.first; + session.requestFrame(const FrameDemand(renderIfNeeded: false, token: 23)); + expect(session.serviceDriverWork(), greaterThan(0)); + await frameResultsReady.timeout(const Duration(seconds: 5)); + final result = session.drainFrameResults().single; + expect(result.token, 23); + expect(result.disposition, same(RenderResult.rendered)); + + final frame = session.acquireFrame(); + frame.release(); + expect(session.serviceDriverWork(), greaterThan(0)); + expect(() => frame.result, throwsA(isA())); + + final abandoned = session.abandon(); + expect(abandoned.quarantinedResourceCount, greaterThanOrEqualTo(0)); + session.close(); + session = null; + } finally { + if (session != null) { + try { + session.abandon(); + session.close(); + } catch (_) {} + } + await map.close(); + await runtime.close(); + context.close(); + } + }, skip: _metalTestSkipReason); +} diff --git a/bindings/dotnet/Maplibre.NativeFfi.C.Local.props b/bindings/dotnet/Maplibre.NativeFfi.C.Local.props index 1dea0c459..658049084 100644 --- a/bindings/dotnet/Maplibre.NativeFfi.C.Local.props +++ b/bindings/dotnet/Maplibre.NativeFfi.C.Local.props @@ -5,13 +5,15 @@ - + %(Filename)%(Extension) - PreserveNewest - PreserveNewest + Always + Always diff --git a/bindings/dotnet/mise.toml b/bindings/dotnet/mise.toml index a46a14895..bd6e6e95d 100644 --- a/bindings/dotnet/mise.toml +++ b/bindings/dotnet/mise.toml @@ -34,18 +34,32 @@ extends = "ffi:preset" # Visual Studio exports Platform=x64 on Windows; the .NET solution uses Any CPU. dir = "../.." run = ''' +case "$usage_preset" in + ios-* | tvos-*) + echo "The .NET binding has no iOS or tvOS target." >&2 + exit 2 + ;; +esac MaplibreNativeCInstallDir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" \ dotnet build \ Maplibre.NativeFfi.slnx \ - -p:Platform="Any CPU" + -p:Platform="Any CPU" \ + -p:BaseOutputPath="$MISE_MONOREPO_ROOT/build/dotnet/$usage_preset/" ''' [tasks.test] extends = "ffi:preset" dir = "../.." run = ''' +case "$usage_preset" in + ios-* | tvos-*) + echo "The .NET binding has no iOS or tvOS target." >&2 + exit 2 + ;; +esac MaplibreNativeCInstallDir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" \ dotnet test \ --project bindings/dotnet/tests/Maplibre.NativeFfi.Tests/Maplibre.NativeFfi.Tests.csproj \ - -p:Platform="Any CPU" + -p:Platform="Any CPU" \ + -p:BaseOutputPath="$MISE_MONOREPO_ROOT/build/dotnet/$usage_preset/" ''' diff --git a/bindings/dotnet/scripts/generate-clangsharp.py b/bindings/dotnet/scripts/generate-clangsharp.py index 305892ff4..7b6fca835 100755 --- a/bindings/dotnet/scripts/generate-clangsharp.py +++ b/bindings/dotnet/scripts/generate-clangsharp.py @@ -23,6 +23,8 @@ "diagnostics", "logging", "runtime", + "completion", + "wake", "map", "camera", "projection", diff --git a/bindings/dotnet/scripts/generate-clangsharp.rsp b/bindings/dotnet/scripts/generate-clangsharp.rsp index ad5e7cf85..d56643c62 100644 --- a/bindings/dotnet/scripts/generate-clangsharp.rsp +++ b/bindings/dotnet/scripts/generate-clangsharp.rsp @@ -1,5 +1,5 @@ --include-directory -../../include/maplibre_native_c +../../include --language c -std @@ -21,7 +21,9 @@ mln_map=MlnMap mln_map_projection=MlnMapProjection mln_geojson_source_data=MlnGeoJsonSourceData mln_render_session=MlnRenderSession -mln_wake_source=MlnWakeSource +mln_render_frame_batch=MlnRenderFrameBatch +mln_acquired_frame=MlnAcquiredFrame +mln_event_batch=MlnEventBatch mln_resource_request_handle=MlnResourceRequest mln_offline_region_snapshot=MlnOfflineRegionSnapshot mln_offline_region_list=MlnOfflineRegionList diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Camera/CameraOptions.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Camera/CameraOptions.cs index 744d4b57c..24348ee41 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Camera/CameraOptions.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Camera/CameraOptions.cs @@ -36,6 +36,52 @@ public sealed record CameraOptions public double? FieldOfView { get; set; } } +public enum CameraUpdateMode : uint +{ + Jump = 0, + Ease = 1, + Fly = 2, +} + +public enum GesturePhase : uint +{ + None = 0, + Begin = 1, + Update = 2, + End = 3, + Cancel = 4, +} + +/// One copied camera command submitted to a map. +public sealed record CameraUpdate +{ + public CameraUpdateMode Mode { get; set; } + public CameraOptions Camera { get; set; } = new(); + public AnimationOptions Animation { get; set; } = new(); + public GesturePhase GesturePhase { get; set; } +} + +public enum CameraDeltaKind : uint +{ + Move = 0, + Scale = 1, + Bearing = 2, + Pitch = 3, +} + +/// One relative camera operation. +public sealed record CameraDelta +{ + public CameraDeltaKind Kind { get; set; } + public ScreenPoint Offset { get; set; } + public double Amount { get; set; } + public ScreenPoint? Anchor { get; set; } + public AnimationOptions Animation { get; set; } = new(); +} + +/// A camera snapshot and its committed generation. +public readonly record struct CameraSnapshot(CameraOptions Camera, ulong Generation); + /// Camera animation descriptor. /// /// Compares and hashes by property value; keep an instance unmodified while it is a key in a diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Error/MaplibreStatus.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Error/MaplibreStatus.cs index ca544f399..491ef9c6c 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Error/MaplibreStatus.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Error/MaplibreStatus.cs @@ -21,6 +21,21 @@ public enum MaplibreStatus /// A native MapLibre error or C++ exception was converted to status. NativeError = -5, + /// The operation reached its terminal cancelled disposition. + Cancelled = -6, + + /// A conflicting driver call or lifecycle transition is in flight. + Busy = -7, + + /// The render target or graphics receiver was irreversibly lost. + TargetLost = -8, + + /// A nonblocking acquisition or service call has no result yet. + NotReady = -9, + + /// No style object carries the requested ID. + NotFound = -10, + /// The loaded C ABI version is incompatible with this binding. AbiMismatch = -1000, diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Error/WrongThreadException.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Error/WrongThreadException.cs index 216557ca7..df0392b22 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Error/WrongThreadException.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Error/WrongThreadException.cs @@ -1,6 +1,6 @@ namespace Maplibre.NativeFfi.Error; -/// Exception for owner-thread-affine operations called from another thread. +/// Exception for graphics-thread-affine operations called from another thread. public sealed class WrongThreadException : MaplibreException { public WrongThreadException( diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/base.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/base.g.cs index f188405b9..923a623f7 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/base.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/base.g.cs @@ -11,6 +11,11 @@ internal enum mln_status MLN_STATUS_WRONG_THREAD = -3, MLN_STATUS_UNSUPPORTED = -4, MLN_STATUS_NATIVE_ERROR = -5, + MLN_STATUS_CANCELLED = -6, + MLN_STATUS_BUSY = -7, + MLN_STATUS_TARGET_LOST = -8, + MLN_STATUS_NOT_READY = -9, + MLN_STATUS_NOT_FOUND = -10, } [NativeTypeName("uint32_t")] diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/camera.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/camera.g.cs index c2a0ae3bd..89eb878ac 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/camera.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/camera.g.cs @@ -10,6 +10,12 @@ internal static unsafe partial class NativeMethods [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_animation_options mln_animation_options_default(); + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_camera_delta mln_camera_delta_default(); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_camera_update mln_camera_update_default(); + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_camera_fit_options mln_camera_fit_options_default(); @@ -29,123 +35,66 @@ internal static unsafe partial class NativeMethods public static extern mln_map_tile_options mln_map_tile_options_default(); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_debug_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint32_t")] uint options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_debug_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint32_t *")] uint* out_options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_rendering_stats_view_enabled([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("bool")] byte enabled); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_rendering_stats_view_enabled([NativeTypeName("mln_map")] MlnMap map, bool* out_enabled); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_is_fully_loaded([NativeTypeName("mln_map")] MlnMap map, bool* out_loaded); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_dump_debug_logs([NativeTypeName("mln_map")] MlnMap map); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_viewport_options([NativeTypeName("mln_map")] MlnMap map, mln_map_viewport_options* out_options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_viewport_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_map_viewport_options *")] mln_map_viewport_options* options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_tile_options([NativeTypeName("mln_map")] MlnMap map, mln_map_tile_options* out_options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_tile_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_map_tile_options *")] mln_map_tile_options* options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_camera([NativeTypeName("mln_map")] MlnMap map, mln_camera_options* out_camera); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_jump_to([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_ease_to([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_fly_to([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_move_by([NativeTypeName("mln_map")] MlnMap map, double delta_x, double delta_y); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_move_by_animated([NativeTypeName("mln_map")] MlnMap map, double delta_x, double delta_y, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_scale_by([NativeTypeName("mln_map")] MlnMap map, double scale, [NativeTypeName("const mln_screen_point *")] mln_screen_point* anchor); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_scale_by_animated([NativeTypeName("mln_map")] MlnMap map, double scale, [NativeTypeName("const mln_screen_point *")] mln_screen_point* anchor, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_rotate_by([NativeTypeName("mln_map")] MlnMap map, mln_screen_point first, mln_screen_point second); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_rotate_by_animated([NativeTypeName("mln_map")] MlnMap map, mln_screen_point first, mln_screen_point second, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); + public static extern mln_status mln_map_set_debug_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint32_t")] uint options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_pitch_by([NativeTypeName("mln_map")] MlnMap map, double pitch); + public static extern mln_status mln_map_set_rendering_stats_view_enabled([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("bool")] byte enabled, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_pitch_by_animated([NativeTypeName("mln_map")] MlnMap map, double pitch, [NativeTypeName("const mln_animation_options *")] mln_animation_options* animation); + public static extern mln_status mln_map_dump_debug_logs([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_cancel_transitions([NativeTypeName("mln_map")] MlnMap map); + public static extern mln_status mln_map_set_viewport_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_map_viewport_options *")] mln_map_viewport_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_gesture_in_progress([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("bool")] byte in_progress); + public static extern mln_status mln_map_set_tile_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_map_tile_options *")] mln_map_tile_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_is_gesture_in_progress([NativeTypeName("mln_map")] MlnMap map, bool* out_in_progress); + public static extern mln_status mln_map_camera_snapshot_get([NativeTypeName("mln_map")] MlnMap map, mln_camera_options* out_camera, [NativeTypeName("uint64_t *")] ulong* out_generation); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_camera_for_lat_lng_bounds([NativeTypeName("mln_map")] MlnMap map, mln_lat_lng_bounds bounds, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, mln_camera_options* out_camera); + public static extern mln_status mln_map_update_camera([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_update *")] mln_camera_update* update, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_camera_for_lat_lngs([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, mln_camera_options* out_camera); + public static extern mln_status mln_map_apply_camera_delta([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_delta *")] mln_camera_delta* delta, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_camera_for_geometry([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view geometry, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, mln_camera_options* out_camera); + public static extern mln_status mln_map_camera_query([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_lat_lng_bounds_for_camera([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, mln_lat_lng_bounds* out_bounds); + public static extern mln_status mln_map_camera_for_lat_lng_bounds([NativeTypeName("mln_map")] MlnMap map, mln_lat_lng_bounds bounds, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_lat_lng_bounds_for_camera_unwrapped([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, mln_lat_lng_bounds* out_bounds); + public static extern mln_status mln_map_camera_for_lat_lngs([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_bounds([NativeTypeName("mln_map")] MlnMap map, mln_bound_options* out_options); + public static extern mln_status mln_map_camera_for_geometry([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view geometry, [NativeTypeName("const mln_camera_fit_options *")] mln_camera_fit_options* fit_options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_bounds([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_bound_options *")] mln_bound_options* options); + public static extern mln_status mln_map_lat_lng_bounds_for_camera([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_free_camera_options([NativeTypeName("mln_map")] MlnMap map, mln_free_camera_options* out_options); + public static extern mln_status mln_map_lat_lng_bounds_for_camera_unwrapped([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_camera_options *")] mln_camera_options* camera, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_free_camera_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_free_camera_options *")] mln_free_camera_options* options); + public static extern mln_status mln_map_set_bounds([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_bound_options *")] mln_bound_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_projection_mode([NativeTypeName("mln_map")] MlnMap map, mln_projection_mode* out_mode); + public static extern mln_status mln_map_set_free_camera_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_free_camera_options *")] mln_free_camera_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_projection_mode([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_projection_mode *")] mln_projection_mode* mode); + public static extern mln_status mln_map_set_projection_mode([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_projection_mode *")] mln_projection_mode* mode, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_pixel_for_lat_lng([NativeTypeName("mln_map")] MlnMap map, mln_lat_lng coordinate, mln_screen_point* out_point); + public static extern mln_status mln_map_pixel_for_lat_lng([NativeTypeName("mln_map")] MlnMap map, mln_lat_lng coordinate, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_lat_lng_for_pixel([NativeTypeName("mln_map")] MlnMap map, mln_screen_point point, mln_lat_lng* out_coordinate); + public static extern mln_status mln_map_lat_lng_for_pixel([NativeTypeName("mln_map")] MlnMap map, mln_screen_point point, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_pixels_for_lat_lngs([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, mln_screen_point* out_points); + public static extern mln_status mln_map_pixels_for_lat_lngs([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_lat_lngs_for_pixels([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_screen_point *")] mln_screen_point* points, [NativeTypeName("size_t")] nuint point_count, mln_lat_lng* out_coordinates); + public static extern mln_status mln_map_lat_lngs_for_pixels([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_screen_point *")] mln_screen_point* points, [NativeTypeName("size_t")] nuint point_count, [NativeTypeName("const mln_completion *")] mln_completion* completion); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/completion.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/completion.g.cs new file mode 100644 index 000000000..345e76ea5 --- /dev/null +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/completion.g.cs @@ -0,0 +1,50 @@ +namespace Maplibre.NativeFfi.Internal.C +{ + [NativeTypeName("uint32_t")] + internal enum mln_command_disposition : uint + { + MLN_COMMAND_DISPOSITION_COMMITTED = 0, + MLN_COMMAND_DISPOSITION_SUPERSEDED = 1, + MLN_COMMAND_DISPOSITION_FAILED = 2, + MLN_COMMAND_DISPOSITION_CANCELLED = 3, + } + + internal unsafe partial struct mln_completion_result + { + [NativeTypeName("uint32_t")] + public uint size; + + public mln_status status; + + [NativeTypeName("uint32_t")] + public uint disposition; + + [NativeTypeName("uint32_t")] + public uint reserved; + + [NativeTypeName("uint64_t")] + public ulong generation; + + public mln_buffer_view diagnostic; + + [NativeTypeName("const void *")] + public void* value; + + [NativeTypeName("size_t")] + public nuint value_count; + } + + internal unsafe partial struct mln_completion + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("mln_completion_callback")] + public delegate* unmanaged[Cdecl] callback; + + public void* user_data; + + [NativeTypeName("mln_completion_release")] + public delegate* unmanaged[Cdecl] release_user_data; + } +} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/diagnostics.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/diagnostics.g.cs index d338138e4..69c237618 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/diagnostics.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/diagnostics.g.cs @@ -7,9 +7,5 @@ internal static unsafe partial class NativeMethods [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [return: NativeTypeName("const char *")] public static extern sbyte* mln_thread_last_error_message(); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - [return: NativeTypeName("uint64_t")] - public static extern ulong mln_thread_token(); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/logging.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/logging.g.cs index c093933ec..41de8fc2f 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/logging.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/logging.g.cs @@ -46,7 +46,7 @@ internal enum mln_log_event : uint internal static unsafe partial class NativeMethods { [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_log_set_callback([NativeTypeName("mln_log_callback")] delegate* unmanaged[Cdecl] callback, void* user_data); + public static extern mln_status mln_log_set_callback([NativeTypeName("mln_log_callback")] delegate* unmanaged[Cdecl] callback, void* user_data, [NativeTypeName("mln_log_callback_release")] delegate* unmanaged[Cdecl] release_user_data); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_log_clear_callback(); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/map.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/map.g.cs index 0d0784c77..d94a281a8 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/map.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/map.g.cs @@ -132,11 +132,8 @@ internal enum mln_map_mode : uint MLN_MAP_MODE_TILE = 2, } - internal partial struct mln_map_options + internal partial struct mln_logical_extent { - [NativeTypeName("uint32_t")] - public uint size; - [NativeTypeName("uint32_t")] public uint width; @@ -144,6 +141,14 @@ internal partial struct mln_map_options public uint height; public double scale_factor; + } + + internal partial struct mln_map_options + { + [NativeTypeName("uint32_t")] + public uint size; + + public mln_logical_extent initial_extent; [NativeTypeName("uint32_t")] public uint map_mode; @@ -233,6 +238,72 @@ internal partial struct mln_animation_options public ulong transition_id; } + [NativeTypeName("uint32_t")] + internal enum mln_camera_delta_kind : uint + { + MLN_CAMERA_DELTA_MOVE = 0, + MLN_CAMERA_DELTA_SCALE = 1, + MLN_CAMERA_DELTA_BEARING = 2, + MLN_CAMERA_DELTA_PITCH = 3, + } + + internal partial struct mln_camera_delta + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint kind; + + public mln_screen_point offset; + + public double amount; + + [NativeTypeName("bool")] + public byte has_anchor; + + public mln_screen_point anchor; + + public mln_animation_options animation; + } + + [NativeTypeName("uint32_t")] + internal enum mln_camera_update_mode : uint + { + MLN_CAMERA_UPDATE_MODE_JUMP = 0, + MLN_CAMERA_UPDATE_MODE_EASE = 1, + MLN_CAMERA_UPDATE_MODE_FLY = 2, + } + + [NativeTypeName("uint32_t")] + internal enum mln_gesture_phase : uint + { + MLN_GESTURE_PHASE_NONE = 0, + MLN_GESTURE_PHASE_BEGIN = 1, + MLN_GESTURE_PHASE_UPDATE = 2, + MLN_GESTURE_PHASE_END = 3, + MLN_GESTURE_PHASE_CANCEL = 4, + } + + internal partial struct mln_camera_update + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint mode; + + public mln_camera_options camera; + + public mln_animation_options animation; + + [NativeTypeName("uint32_t")] + public uint gesture_phase; + + [NativeTypeName("uint32_t")] + public uint reserved; + } + internal partial struct mln_camera_fit_options { [NativeTypeName("uint32_t")] @@ -387,7 +458,7 @@ internal partial struct mln_offline_region_definition [NativeTypeName("uint32_t")] public uint type; - [NativeTypeName("__AnonymousRecord_map_L425_C3")] + [NativeTypeName("__AnonymousRecord_map_L460_C3")] public _data_e__Union data; [StructLayout(LayoutKind.Explicit)] @@ -484,55 +555,95 @@ internal partial struct mln_map_tile_options public uint lod_mode; } - internal static unsafe partial class NativeMethods + internal partial struct mln_map_snapshot { - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_create_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_offline_region_definition *")] mln_offline_region_definition* definition, [NativeTypeName("const uint8_t *")] byte* metadata, [NativeTypeName("size_t")] nuint metadata_size, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + [NativeTypeName("uint32_t")] + public uint size; - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_get_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + [NativeTypeName("uint32_t")] + public uint debug_options; - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_regions_list_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + [NativeTypeName("uint64_t")] + public ulong generation; - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_regions_merge_database_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const char *")] sbyte* side_database_path, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public mln_camera_options camera; - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_update_metadata_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const uint8_t *")] byte* metadata, [NativeTypeName("size_t")] nuint metadata_size, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public mln_logical_extent logical_extent; - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_get_status_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public mln_projection_mode projection_mode; + + public mln_map_viewport_options viewport; + + [NativeTypeName("bool")] + public byte fully_loaded; + + [NativeTypeName("bool")] + public byte rendering_stats_view_enabled; + + [NativeTypeName("bool")] + public byte repaint_demand; + [NativeTypeName("uint8_t")] + public byte reserved_flags; + + [NativeTypeName("uint64_t")] + public ulong event_mask; + + [NativeTypeName("uint64_t")] + public ulong latest_render_update_generation; + + public mln_map_tile_options tile; + + public mln_bound_options bounds; + + public mln_free_camera_options free_camera; + } + + internal partial struct mln_camera_query_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + [NativeTypeName("uint64_t")] + public ulong generation; + + public mln_camera_options camera; + } + + internal static unsafe partial class NativeMethods + { [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_set_observed_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("bool")] byte observed, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_offline_region_create([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_offline_region_definition *")] mln_offline_region_definition* definition, [NativeTypeName("const uint8_t *")] byte* metadata, [NativeTypeName("size_t")] nuint metadata_size, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_set_download_state_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("uint32_t")] uint state, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_offline_region_get([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_invalidate_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_offline_regions_list([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_delete_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_offline_regions_merge_database([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const char *")] sbyte* side_database_path, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_create_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, [NativeTypeName("mln_offline_region_snapshot *")] MlnOfflineRegionSnapshot* out_region); + public static extern mln_status mln_runtime_offline_region_update_metadata([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const uint8_t *")] byte* metadata, [NativeTypeName("size_t")] nuint metadata_size, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_get_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, [NativeTypeName("mln_offline_region_snapshot *")] MlnOfflineRegionSnapshot* out_region, bool* out_found); + public static extern mln_status mln_runtime_offline_region_get_status([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_regions_list_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, [NativeTypeName("mln_offline_region_list *")] MlnOfflineRegionList* out_regions); + public static extern mln_status mln_runtime_offline_region_set_observed([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("bool")] byte observed, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_regions_merge_database_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, [NativeTypeName("mln_offline_region_list *")] MlnOfflineRegionList* out_regions); + public static extern mln_status mln_runtime_offline_region_set_download_state([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("uint32_t")] uint state, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_update_metadata_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, [NativeTypeName("mln_offline_region_snapshot *")] MlnOfflineRegionSnapshot* out_region); + public static extern mln_status mln_runtime_offline_region_invalidate([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_region_get_status_take_result([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id, mln_offline_region_status* out_status); + public static extern mln_status mln_runtime_offline_region_delete([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_region_id")] long region_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_offline_region_snapshot_get([NativeTypeName("mln_offline_region_snapshot")] MlnOfflineRegionSnapshot snapshot, mln_offline_region_info* out_info); @@ -553,45 +664,45 @@ internal static unsafe partial class NativeMethods public static extern mln_map_options mln_map_options_default(); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_create([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_map_options *")] mln_map_options* options, [NativeTypeName("mln_map *")] MlnMap* out_map); + public static extern mln_status mln_map_create([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_map_options *")] mln_map_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_size([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint32_t *")] uint* out_width, [NativeTypeName("uint32_t *")] uint* out_height, double* out_scale_factor); + public static extern mln_status mln_map_snapshot_get([NativeTypeName("mln_map")] MlnMap map, mln_map_snapshot* out_snapshot); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_request_repaint([NativeTypeName("mln_map")] MlnMap map); + public static extern mln_status mln_map_resize([NativeTypeName("mln_map")] MlnMap map, mln_logical_extent extent, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector, mln_buffer_view state); + public static extern mln_status mln_map_request_repaint([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector, [NativeTypeName("mln_buffer *")] MlnBuffer* out_state); + public static extern mln_status mln_map_set_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector, mln_buffer_view state, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_remove_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector); + public static extern mln_status mln_map_get_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_request_still_image([NativeTypeName("mln_map")] MlnMap map); + public static extern mln_status mln_map_remove_feature_state([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_feature_state_selector *")] mln_feature_state_selector* selector, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_destroy([NativeTypeName("mln_map")] MlnMap map); + public static extern mln_status mln_map_request_still_image([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_url([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const char *")] sbyte* url); + public static extern mln_status mln_map_release([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view json); + public static extern mln_status mln_map_set_style_url([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const char *")] sbyte* url, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_loaded_style_json([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint8_t *")] byte* out_json, [NativeTypeName("size_t")] nuint json_capacity, [NativeTypeName("size_t *")] nuint* out_json_size); + public static extern mln_status mln_map_set_style_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view json, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_style_url([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("char *")] sbyte* out_url, [NativeTypeName("size_t")] nuint url_capacity, [NativeTypeName("size_t *")] nuint* out_url_size); + public static extern mln_status mln_map_loaded_style_json([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_event_mask([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint64_t")] ulong mask); + public static extern mln_status mln_map_style_url([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_event_mask([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint64_t *")] ulong* out_mask); + public static extern mln_status mln_map_set_event_mask([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("uint64_t")] ulong mask, [NativeTypeName("const mln_completion *")] mln_completion* completion); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/projection.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/projection.g.cs index e4753e9cb..2018cd4c6 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/projection.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/projection.g.cs @@ -5,10 +5,10 @@ namespace Maplibre.NativeFfi.Internal.C internal static unsafe partial class NativeMethods { [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_projection_create([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("mln_map_projection *")] MlnMapProjection* out_projection); + public static extern mln_status mln_map_projection_create([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_projection_destroy([NativeTypeName("mln_map_projection")] MlnMapProjection projection); + public static extern mln_status mln_map_projection_close([NativeTypeName("mln_map_projection")] MlnMapProjection projection); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_map_projection_get_camera([NativeTypeName("mln_map_projection")] MlnMapProjection projection, mln_camera_options* out_camera); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/query.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/query.g.cs index 69cebc75d..198fa6250 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/query.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/query.g.cs @@ -34,7 +34,7 @@ internal partial struct mln_rendered_query_geometry [NativeTypeName("uint32_t")] public uint type; - [NativeTypeName("__AnonymousRecord_query_L51_C3")] + [NativeTypeName("__AnonymousRecord_query_L52_C3")] public _data_e__Union data; [StructLayout(LayoutKind.Explicit)] @@ -154,12 +154,12 @@ internal static unsafe partial class NativeMethods public static extern void mln_queried_feature_list_destroy([NativeTypeName("mln_queried_feature_list")] MlnQueriedFeatureList list); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_query_rendered_features([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_rendered_query_geometry *")] mln_rendered_query_geometry* geometry, [NativeTypeName("const mln_rendered_feature_query_options *")] mln_rendered_feature_query_options* options, [NativeTypeName("mln_queried_feature_list *")] MlnQueriedFeatureList* out_result); + public static extern mln_status mln_render_session_query_rendered_features([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_rendered_query_geometry *")] mln_rendered_query_geometry* geometry, [NativeTypeName("const mln_rendered_feature_query_options *")] mln_rendered_feature_query_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_query_source_features([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_buffer_view source_id, [NativeTypeName("const mln_source_feature_query_options *")] mln_source_feature_query_options* options, [NativeTypeName("mln_queried_feature_list *")] MlnQueriedFeatureList* out_result); + public static extern mln_status mln_render_session_query_source_features([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_buffer_view source_id, [NativeTypeName("const mln_source_feature_query_options *")] mln_source_feature_query_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_query_feature_extensions([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* arguments, [NativeTypeName("mln_buffer *")] MlnBuffer* out_result); + public static extern mln_status mln_render_session_query_feature_extensions([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* arguments, [NativeTypeName("const mln_completion *")] mln_completion* completion); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_session.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_session.g.cs index ea76e426b..e901bb71a 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_session.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_session.g.cs @@ -6,32 +6,205 @@ namespace Maplibre.NativeFfi.Internal.C internal enum mln_render_result : uint { MLN_RENDER_RESULT_RENDERED = 0, - MLN_RENDER_RESULT_NO_UPDATE, - MLN_RENDER_RESULT_SIZE_PENDING, - MLN_RENDER_RESULT_TARGET_NOT_READY, + MLN_RENDER_RESULT_NO_UPDATE = 1, + MLN_RENDER_RESULT_SIZE_PENDING = 2, + MLN_RENDER_RESULT_TARGET_NOT_READY = 3, + MLN_RENDER_RESULT_SUPERSEDED = 4, + MLN_RENDER_RESULT_DEADLINE_MISSED = 5, + } + + [NativeTypeName("uint32_t")] + internal enum mln_render_session_state : uint + { + MLN_RENDER_SESSION_STATE_ATTACHING = 1U, + MLN_RENDER_SESSION_STATE_ATTACHED = 2U, + MLN_RENDER_SESSION_STATE_DETACHING = 3U, + MLN_RENDER_SESSION_STATE_DETACHED = 4U, + MLN_RENDER_SESSION_STATE_TARGET_LOST = 5U, + MLN_RENDER_SESSION_STATE_ABANDONED = 6U, + } + + [NativeTypeName("uint32_t")] + internal enum mln_frame_demand_flag : uint + { + MLN_FRAME_DEMAND_IF_NEEDED = 1U << 0, + MLN_FRAME_DEMAND_PRESENT = 1U << 1, + } + + internal partial struct mln_frame_demand + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint flags; + + [NativeTypeName("uint64_t")] + public ulong token; + + [NativeTypeName("uint64_t")] + public ulong coalescing_boundary; + + [NativeTypeName("uint64_t")] + public ulong timeout_ns; + } + + internal partial struct mln_render_frame_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint disposition; + + [NativeTypeName("uint64_t")] + public ulong token; + + [NativeTypeName("uint64_t")] + public ulong map_update_generation; + + [NativeTypeName("uint64_t")] + public ulong extent_generation; + + [NativeTypeName("uint64_t")] + public ulong frame_generation; + + [NativeTypeName("bool")] + public byte needs_repaint; + } + + internal partial struct mln_render_session_snapshot + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint state; + + [NativeTypeName("uint32_t")] + public uint driver; + + [NativeTypeName("uint32_t")] + public uint latest_result; + + public mln_render_target_extent extent; + + [NativeTypeName("uint64_t")] + public ulong generation; + + [NativeTypeName("uint64_t")] + public ulong map_update_generation; + + [NativeTypeName("uint64_t")] + public ulong rendered_update_generation; + + [NativeTypeName("uint64_t")] + public ulong extent_generation; + + [NativeTypeName("uint64_t")] + public ulong frame_generation; + + [NativeTypeName("uint64_t")] + public ulong latest_demand_token; + + [NativeTypeName("uint32_t")] + public uint pending_demand_count; + + [NativeTypeName("uint32_t")] + public uint acquired_frame_count; + + [NativeTypeName("bool")] + public byte target_ready; + + [NativeTypeName("bool")] + public byte pending_changes; + } + + [NativeTypeName("uint32_t")] + internal enum mln_render_abandon_disposition : uint + { + MLN_RENDER_ABANDON_DISPOSITION_CLEAN = 0U, + MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED = 1U, + } + + internal partial struct mln_render_abandon_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint disposition; + + [NativeTypeName("uint32_t")] + public uint quarantined_resource_count; + + [NativeTypeName("uint32_t")] + public uint reserved; } internal static unsafe partial class NativeMethods { [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_resize([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("uint32_t")] uint width, [NativeTypeName("uint32_t")] uint height, double scale_factor); + public static extern mln_frame_demand mln_frame_demand_default(); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_render_update([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_render_result* out_result, bool* out_needs_repaint); + public static extern mln_status mln_render_session_get_capabilities([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_render_session_capabilities* out_capabilities); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_detach([NativeTypeName("mln_render_session")] MlnRenderSession session); + public static extern mln_status mln_render_session_get_snapshot([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_render_session_snapshot* out_snapshot); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_destroy([NativeTypeName("mln_render_session")] MlnRenderSession session); + public static extern mln_status mln_render_session_request_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_frame_demand *")] mln_frame_demand* demand); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_drain_frame_results([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("mln_render_frame_batch *")] MlnRenderFrameBatch* out_batch); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_frame_batch_count([NativeTypeName("mln_render_frame_batch")] MlnRenderFrameBatch batch, [NativeTypeName("size_t *")] nuint* out_count); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_frame_batch_get([NativeTypeName("mln_render_frame_batch")] MlnRenderFrameBatch batch, [NativeTypeName("size_t")] nuint index, mln_render_frame_result* out_result); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void mln_render_frame_batch_release([NativeTypeName("mln_render_frame_batch")] MlnRenderFrameBatch batch); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_acquire_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("mln_acquired_frame *")] MlnAcquiredFrame* out_frame); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_acquired_frame_get_result([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_render_frame_result* out_result); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_acquired_frame_get_producer_sync([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_gpu_sync* out_sync); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_acquired_frame_release([NativeTypeName("mln_acquired_frame *")] MlnAcquiredFrame* frame, [NativeTypeName("const mln_gpu_sync *")] mln_gpu_sync* consumer_completion); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_resize([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_render_target_extent *")] mln_render_target_extent* extent, [NativeTypeName("const mln_completion *")] mln_completion* completion); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_barrier([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_reduce_memory_use([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_clear_data([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_reduce_memory_use([NativeTypeName("mln_render_session")] MlnRenderSession session); + public static extern mln_status mln_render_session_dump_debug_logs([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_clear_data([NativeTypeName("mln_render_session")] MlnRenderSession session); + public static extern mln_status mln_render_session_service_driver_work([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("size_t")] nuint max_work, [NativeTypeName("size_t *")] nuint* out_serviced); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_render_session_dump_debug_logs([NativeTypeName("mln_render_session")] MlnRenderSession session); + public static extern mln_status mln_render_session_detach([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_abandon([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_render_abandon_result* out_result); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_status mln_render_session_destroy([NativeTypeName("mln_render_session")] MlnRenderSession session); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_target.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_target.g.cs index 1caf03552..1c9bee8e9 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_target.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/render_target.g.cs @@ -16,6 +16,80 @@ internal partial struct mln_render_target_extent public double scale_factor; } + [NativeTypeName("uint32_t")] + internal enum mln_render_driver_kind : uint + { + MLN_RENDER_DRIVER_CORE_WORKER = 1U, + MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD = 2U, + } + + [NativeTypeName("uint32_t")] + internal enum mln_render_session_capability_flag : uint + { + MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION = 1U << 0, + MLN_RENDER_SESSION_CAPABILITY_READBACK = 1U << 1, + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC = 1U << 2, + MLN_RENDER_SESSION_CAPABILITY_PRESENTATION = 1U << 3, + } + + internal partial struct mln_render_session_attach_options + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint driver; + + [NativeTypeName("uint32_t")] + public uint requested_texture_ring_depth; + + [NativeTypeName("uint32_t")] + public uint reserved; + + public mln_wake frame_wake; + + public mln_wake driver_work_wake; + } + + internal partial struct mln_render_session_capabilities + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint driver; + + [NativeTypeName("uint32_t")] + public uint texture_ring_depth; + + [NativeTypeName("uint32_t")] + public uint flags; + } + + [NativeTypeName("uint32_t")] + internal enum mln_gpu_sync_kind : uint + { + MLN_GPU_SYNC_CPU_COMPLETE = 0U, + MLN_GPU_SYNC_METAL_SHARED_EVENT = 1U, + MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE = 2U, + MLN_GPU_SYNC_OPENGL_FENCE = 3U, + MLN_GPU_SYNC_WEBGPU_TOKEN = 4U, + } + + internal unsafe partial struct mln_gpu_sync + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint kind; + + public void* @object; + + [NativeTypeName("uint64_t")] + public ulong value; + } + internal unsafe partial struct mln_metal_context_descriptor { [NativeTypeName("uint32_t")] @@ -117,13 +191,25 @@ internal unsafe partial struct mln_egl_context_descriptor public void* get_proc_address; } + [NativeTypeName("uint32_t")] + internal enum mln_webgl_context_kind : uint + { + MLN_WEBGL_CONTEXT_EXISTING = 0U, + MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS = 1U, + } + internal partial struct mln_webgl_context_descriptor { [NativeTypeName("uint32_t")] public uint size; + [NativeTypeName("uint32_t")] + public uint kind; + [NativeTypeName("int32_t")] public int context; + + public mln_buffer_view canvas_selector; } internal partial struct mln_opengl_context_descriptor @@ -135,7 +221,7 @@ internal partial struct mln_opengl_context_descriptor public mln_opengl_context_ownership ownership; - [NativeTypeName("__AnonymousRecord_render_target_L179_C3")] + [NativeTypeName("__AnonymousRecord_render_target_L287_C3")] public _data_e__Union data; [StructLayout(LayoutKind.Explicit)] @@ -154,6 +240,12 @@ internal partial struct _data_e__Union internal static unsafe partial class NativeMethods { + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_gpu_sync mln_gpu_sync_default(); + + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern mln_render_session_attach_options mln_render_session_attach_options_default(); + [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_render_target_extent_physical_size([NativeTypeName("const mln_render_target_extent *")] mln_render_target_extent* extent, [NativeTypeName("uint32_t *")] uint* out_width, [NativeTypeName("uint32_t *")] uint* out_height); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/runtime.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/runtime.g.cs index e23df519f..967592724 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/runtime.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/runtime.g.cs @@ -33,33 +33,6 @@ internal enum mln_offline_region_download_state : uint MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE = 1, } - [NativeTypeName("uint32_t")] - internal enum mln_offline_operation_kind : uint - { - MLN_OFFLINE_OPERATION_AMBIENT_CACHE = 1, - MLN_OFFLINE_OPERATION_REGION_CREATE = 2, - MLN_OFFLINE_OPERATION_REGION_GET = 3, - MLN_OFFLINE_OPERATION_REGIONS_LIST = 4, - MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE = 5, - MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA = 6, - MLN_OFFLINE_OPERATION_REGION_GET_STATUS = 7, - MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED = 8, - MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE = 9, - MLN_OFFLINE_OPERATION_REGION_INVALIDATE = 10, - MLN_OFFLINE_OPERATION_REGION_DELETE = 11, - MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE = 12, - } - - [NativeTypeName("uint32_t")] - internal enum mln_offline_operation_result_kind : uint - { - MLN_OFFLINE_OPERATION_RESULT_NONE = 0, - MLN_OFFLINE_OPERATION_RESULT_REGION = 1, - MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION = 2, - MLN_OFFLINE_OPERATION_RESULT_REGION_LIST = 3, - MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS = 4, - } - internal partial struct mln_offline_region_status { [NativeTypeName("uint32_t")] @@ -117,8 +90,7 @@ internal enum mln_runtime_event_type : uint MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED = 19, MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR = 20, MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = 21, - MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED = 22, - MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 23, + MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 22, } [NativeTypeName("uint64_t")] @@ -147,9 +119,8 @@ internal enum mln_runtime_event_mask : ulong MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED = 1UL << (int)(MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED), MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR = 1UL << (int)(MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR), MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = 1UL << (int)(MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED), - MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED = 1UL << (int)(MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED), MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_WILL_CHANGE | MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_IS_CHANGING | MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_DID_CHANGE | MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED | MLN_RUNTIME_EVENT_MASK_MAP_LOADING_STARTED | MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FINISHED | MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED | MLN_RUNTIME_EVENT_MASK_MAP_IDLE | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_ERROR | MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FINISHED | MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FAILED | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_STARTED | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_MAP_STARTED | MLN_RUNTIME_EVENT_MASK_MAP_RENDER_MAP_FINISHED | MLN_RUNTIME_EVENT_MASK_MAP_STYLE_IMAGE_MISSING | MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION | MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED, - MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED | MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR | MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED | MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED, + MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED | MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR | MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED, MLN_RUNTIME_EVENT_MASK_ALL = MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS | MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS, } @@ -170,8 +141,7 @@ internal enum mln_runtime_event_payload_type : uint MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS = 5, MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR = 6, MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT = 7, - MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED = 8, - MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 9, + MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 8, } [NativeTypeName("uint32_t")] @@ -287,6 +257,8 @@ internal unsafe partial struct mln_runtime_options [NativeTypeName("uint64_t")] public ulong event_mask; + + public mln_wake event_wake; } internal partial struct mln_rendering_stats @@ -383,24 +355,6 @@ internal partial struct mln_runtime_event_offline_region_tile_count_limit public ulong limit; } - internal partial struct mln_runtime_event_offline_operation_completed - { - [NativeTypeName("mln_offline_operation_id")] - public ulong operation_id; - - [NativeTypeName("uint32_t")] - public uint operation_kind; - - [NativeTypeName("uint32_t")] - public uint result_kind; - - [NativeTypeName("int32_t")] - public int result_status; - - [NativeTypeName("bool")] - public byte found; - } - [StructLayout(LayoutKind.Explicit)] internal partial struct mln_runtime_event_payload { @@ -422,9 +376,6 @@ internal partial struct mln_runtime_event_payload [FieldOffset(0)] public mln_runtime_event_offline_region_tile_count_limit offline_region_tile_count_limit; - [FieldOffset(0)] - public mln_runtime_event_offline_operation_completed offline_operation_completed; - [FieldOffset(0)] public mln_runtime_event_camera_transition_finished camera_transition_finished; } @@ -446,8 +397,8 @@ internal partial struct mln_runtime_event [NativeTypeName("uint32_t")] public uint payload_type; - [NativeTypeName("uint32_t")] - public uint message_offset; + [NativeTypeName("uint64_t")] + public ulong message_offset; [NativeTypeName("uint32_t")] public uint message_size; @@ -455,7 +406,7 @@ internal partial struct mln_runtime_event public mln_runtime_event_payload payload; } - internal unsafe partial struct mln_runtime_event_batch + internal unsafe partial struct mln_runtime_event_batch_view { [NativeTypeName("uint32_t")] public uint size; @@ -474,9 +425,6 @@ internal unsafe partial struct mln_runtime_event_batch [NativeTypeName("size_t")] public nuint messages_size; - - [NativeTypeName("size_t")] - public nuint remaining_count; } internal unsafe partial struct mln_resource_transform_response @@ -499,6 +447,9 @@ internal unsafe partial struct mln_resource_transform public delegate* unmanaged[Cdecl] callback; public void* user_data; + + [NativeTypeName("mln_runtime_callback_release")] + public delegate* unmanaged[Cdecl] release_user_data; } internal unsafe partial struct mln_http_header_transform_response @@ -518,6 +469,9 @@ internal unsafe partial struct mln_http_header_transform public delegate* unmanaged[Cdecl] callback; public void* user_data; + + [NativeTypeName("mln_runtime_callback_release")] + public delegate* unmanaged[Cdecl] release_user_data; } internal unsafe partial struct mln_resource_request @@ -631,6 +585,9 @@ internal unsafe partial struct mln_resource_provider public delegate* unmanaged[Cdecl] callback; public void* user_data; + + [NativeTypeName("mln_runtime_callback_release")] + public delegate* unmanaged[Cdecl] release_user_data; } internal static unsafe partial class NativeMethods @@ -654,10 +611,10 @@ internal static unsafe partial class NativeMethods public static extern mln_status mln_runtime_create([NativeTypeName("const mln_runtime_options *")] mln_runtime_options* options, [NativeTypeName("mln_runtime *")] MlnRuntime* out_runtime); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_set_resource_provider([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_resource_provider *")] mln_resource_provider* provider); + public static extern mln_status mln_runtime_set_resource_provider([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_resource_provider *")] mln_resource_provider* provider, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_clear_resource_provider([NativeTypeName("mln_runtime")] MlnRuntime runtime); + public static extern mln_status mln_runtime_clear_resource_provider([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_resource_request_complete([NativeTypeName("mln_resource_request_handle")] MlnResourceRequest handle, [NativeTypeName("const mln_resource_response *")] mln_resource_response* response); @@ -672,46 +629,37 @@ internal static unsafe partial class NativeMethods public static extern mln_status mln_resource_request_wait_until_retired([NativeTypeName("mln_resource_request_handle")] MlnResourceRequest handle); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_set_resource_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_resource_transform *")] mln_resource_transform* transform); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_clear_resource_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_set_http_header_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_http_header_transform *")] mln_http_header_transform* transform); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_clear_http_header_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime); + public static extern mln_status mln_runtime_set_resource_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_resource_transform *")] mln_resource_transform* transform, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_run_ambient_cache_operation_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("uint32_t")] uint operation, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_clear_resource_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_set_maximum_ambient_cache_size_start([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("uint64_t")] ulong size, [NativeTypeName("mln_offline_operation_id *")] ulong* out_operation_id); + public static extern mln_status mln_runtime_set_http_header_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_http_header_transform *")] mln_http_header_transform* transform, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_offline_operation_discard([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_offline_operation_id")] ulong operation_id); + public static extern mln_status mln_runtime_clear_http_header_transform([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_destroy([NativeTypeName("mln_runtime")] MlnRuntime runtime); + public static extern mln_status mln_runtime_run_ambient_cache_operation([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("uint32_t")] uint operation, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_pump([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("int64_t")] long timeout_ms, [NativeTypeName("int64_t")] long budget_ms); + public static extern mln_status mln_runtime_set_maximum_ambient_cache_size([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("uint64_t")] ulong size, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_wake_source_acquire([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_wake_source *")] MlnWakeSource* out_source); + public static extern mln_status mln_runtime_barrier([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_wake_source_signal([NativeTypeName("mln_wake_source")] MlnWakeSource source); + public static extern mln_status mln_runtime_release([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern void mln_wake_source_destroy([NativeTypeName("mln_wake_source")] MlnWakeSource source); + public static extern mln_status mln_runtime_drain_events([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("mln_event_batch *")] MlnEventBatch* out_batch); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_runtime_event_batch mln_runtime_event_batch_default(); + public static extern mln_status mln_event_batch_get([NativeTypeName("mln_event_batch")] MlnEventBatch batch, mln_runtime_event_batch_view* out_view); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_runtime_drain_events([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("size_t")] nuint max_events, mln_runtime_event_batch* out_batch); + public static extern void mln_event_batch_release([NativeTypeName("mln_event_batch")] MlnEventBatch batch); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_runtime_set_event_mask([NativeTypeName("mln_runtime")] MlnRuntime runtime, [NativeTypeName("uint64_t")] ulong mask); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/style.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/style.g.cs index bcef43af6..beffe8571 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/style.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/style.g.cs @@ -28,6 +28,13 @@ internal enum mln_style_source_info_field : uint MLN_STYLE_SOURCE_INFO_RASTER_ENCODING = 1U << 5, } + [NativeTypeName("uint32_t")] + internal enum mln_style_layer_info_field : uint + { + MLN_STYLE_LAYER_INFO_SOURCE_ID = 1U << 0, + MLN_STYLE_LAYER_INFO_SOURCE_LAYER = 1U << 1, + } + [NativeTypeName("uint32_t")] internal enum mln_style_tile_source_option_field : uint { @@ -69,6 +76,27 @@ internal partial struct mln_image_stretch public float to; } + internal unsafe partial struct mln_style_image_stretches_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + [NativeTypeName("const mln_image_stretch *")] + public mln_image_stretch* stretch_x; + + [NativeTypeName("size_t")] + public nuint stretch_x_count; + + [NativeTypeName("const mln_image_stretch *")] + public mln_image_stretch* stretch_y; + + [NativeTypeName("size_t")] + public nuint stretch_y_count; + } + internal partial struct mln_image_content { public float left; @@ -207,6 +235,66 @@ internal partial struct mln_style_source_info public uint raster_encoding; } + internal unsafe partial struct mln_style_source_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + public mln_style_source_info info; + + public mln_buffer_view attribution; + + public mln_buffer_view url; + + [NativeTypeName("const mln_buffer_view *")] + public mln_buffer_view* tile_urls; + + [NativeTypeName("size_t")] + public nuint tile_url_count; + } + + internal partial struct mln_style_layer_info + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint fields; + + public mln_buffer_view type; + + public double min_zoom; + + public double max_zoom; + + [NativeTypeName("uint32_t")] + public uint visibility; + + [NativeTypeName("size_t")] + public nuint source_id_size; + + [NativeTypeName("size_t")] + public nuint source_layer_size; + } + + internal partial struct mln_style_layer_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + public mln_style_layer_info info; + + public mln_buffer_view source_id; + + public mln_buffer_view source_layer; + } + internal partial struct mln_style_tile_source_options { [NativeTypeName("uint32_t")] @@ -451,6 +539,31 @@ internal partial struct mln_style_image_info public byte has_text_fit_height; } + internal unsafe partial struct mln_style_image_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + public mln_style_image_info info; + + public mln_buffer_view pixels; + + [NativeTypeName("const mln_image_stretch *")] + public mln_image_stretch* stretch_x; + + [NativeTypeName("size_t")] + public nuint stretch_x_count; + + [NativeTypeName("const mln_image_stretch *")] + public mln_image_stretch* stretch_y; + + [NativeTypeName("size_t")] + public nuint stretch_y_count; + } + internal partial struct mln_style_transition_options { [NativeTypeName("uint32_t")] @@ -512,34 +625,28 @@ internal static unsafe partial class NativeMethods public static extern void mln_style_string_list_destroy([NativeTypeName("mln_style_string_list")] MlnStyleStringList list); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_style_source_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view source_json); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_remove_style_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, bool* out_removed); + public static extern mln_status mln_map_add_style_source_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view source_json, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_style_source_exists([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, bool* out_exists); + public static extern mln_status mln_map_remove_style_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_source_type([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("uint32_t *")] uint* out_source_type, bool* out_found); + public static extern mln_status mln_map_get_style_source_info([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_source_info([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_style_source_info* out_info, bool* out_found); + public static extern mln_status mln_map_copy_style_source_attribution([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_style_source_attribution([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("char *")] sbyte* out_attribution, [NativeTypeName("size_t")] nuint attribution_capacity, [NativeTypeName("size_t *")] nuint* out_attribution_size, bool* out_found); + public static extern mln_status mln_map_copy_style_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_style_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("char *")] sbyte* out_url, [NativeTypeName("size_t")] nuint url_capacity, [NativeTypeName("size_t *")] nuint* out_url_size, bool* out_found); + public static extern mln_status mln_map_get_style_source_tile_urls([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_source_tile_urls([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("mln_style_string_list *")] MlnStyleStringList* out_tile_urls, bool* out_found); + public static extern mln_status mln_map_list_style_source_ids([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_list_style_source_ids([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("mln_style_id_list *")] MlnStyleIdList* out_source_ids); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_geojson_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_geojson_source_options *")] mln_geojson_source_options* options); + public static extern mln_status mln_map_add_geojson_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_geojson_source_options *")] mln_geojson_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] public static extern mln_status mln_geojson_source_data_create(mln_buffer_view data, [NativeTypeName("const mln_geojson_source_options *")] mln_geojson_source_options* options, [NativeTypeName("mln_geojson_source_data *")] MlnGeoJsonSourceData* out_data); @@ -548,192 +655,177 @@ internal static unsafe partial class NativeMethods public static extern void mln_geojson_source_data_destroy([NativeTypeName("mln_geojson_source_data")] MlnGeoJsonSourceData data); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_geojson_source_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("mln_geojson_source_data")] MlnGeoJsonSourceData data); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_geojson_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_geojson_source_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("mln_geojson_source_data")] MlnGeoJsonSourceData data); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_geojson_source_synchronous_tiling([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("bool")] byte enabled); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_vector_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_vector_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); + public static extern mln_status mln_map_add_geojson_source_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("mln_geojson_source_data")] MlnGeoJsonSourceData data, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_raster_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); + public static extern mln_status mln_map_set_geojson_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_raster_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); + public static extern mln_status mln_map_set_geojson_source_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("mln_geojson_source_data")] MlnGeoJsonSourceData data, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_raster_dem_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); + public static extern mln_status mln_map_set_geojson_source_synchronous_tiling([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("bool")] byte enabled, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_raster_dem_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options); + public static extern mln_status mln_map_add_vector_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_custom_geometry_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_custom_geometry_source_options *")] mln_custom_geometry_source_options* options); + public static extern mln_status mln_map_add_vector_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_custom_geometry_source_tile_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data); + public static extern mln_status mln_map_add_raster_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_invalidate_custom_geometry_source_tile([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id); + public static extern mln_status mln_map_add_raster_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_invalidate_custom_geometry_source_region([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_lat_lng_bounds bounds); + public static extern mln_status mln_map_add_raster_dem_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_custom_mvt_vector_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_custom_mvt_vector_source_options *")] mln_custom_mvt_vector_source_options* options); + public static extern mln_status mln_map_add_raster_dem_source_tiles([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* tiles, [NativeTypeName("size_t")] nuint tile_count, [NativeTypeName("const mln_style_tile_source_options *")] mln_style_tile_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_custom_mvt_vector_source_tile_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data); + public static extern mln_status mln_map_add_custom_geometry_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_custom_geometry_source_options *")] mln_custom_geometry_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_custom_mvt_vector_source_tile_error([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message); + public static extern mln_status mln_map_set_custom_geometry_source_tile_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_invalidate_custom_mvt_vector_source_tile([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id); + public static extern mln_status mln_map_invalidate_custom_geometry_source_tile([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image, [NativeTypeName("const mln_style_image_options *")] mln_style_image_options* options); + public static extern mln_status mln_map_invalidate_custom_geometry_source_region([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_remove_style_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, bool* out_removed); + public static extern mln_status mln_map_add_custom_mvt_vector_source([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_custom_mvt_vector_source_options *")] mln_custom_mvt_vector_source_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_style_image_exists([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, bool* out_exists); + public static extern mln_status mln_map_set_custom_mvt_vector_source_tile_data([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_image_info([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, mln_style_image_info* out_info, bool* out_found); + public static extern mln_status mln_map_set_custom_mvt_vector_source_tile_error([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_style_image_premultiplied_rgba8([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("uint8_t *")] byte* out_pixels, [NativeTypeName("size_t")] nuint pixel_capacity, [NativeTypeName("size_t *")] nuint* out_byte_length, bool* out_found); + public static extern mln_status mln_map_invalidate_custom_mvt_vector_source_tile([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_style_image_stretches([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, mln_image_stretch* out_stretch_x, [NativeTypeName("size_t")] nuint stretch_x_capacity, [NativeTypeName("size_t *")] nuint* out_stretch_x_count, mln_image_stretch* out_stretch_y, [NativeTypeName("size_t")] nuint stretch_y_capacity, [NativeTypeName("size_t *")] nuint* out_stretch_y_count, bool* out_found); + public static extern mln_status mln_map_set_style_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image, [NativeTypeName("const mln_style_image_options *")] mln_style_image_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_image_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, mln_buffer_view url); + public static extern mln_status mln_map_remove_style_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_image_source_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image); + public static extern mln_status mln_map_get_style_image_info([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_image_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url); + public static extern mln_status mln_map_copy_style_image_premultiplied_rgba8([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_image_source_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image); + public static extern mln_status mln_map_copy_style_image_stretches([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view image_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_image_source_coordinates([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count); + public static extern mln_status mln_map_add_image_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, mln_buffer_view url, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_image_source_coordinates([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_lat_lng* out_coordinates, [NativeTypeName("size_t")] nuint coordinate_capacity, [NativeTypeName("size_t *")] nuint* out_coordinate_count, bool* out_found); + public static extern mln_status mln_map_add_image_source_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_hillshade_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id); + public static extern mln_status mln_map_set_image_source_url([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, mln_buffer_view url, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_color_relief_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id); + public static extern mln_status mln_map_set_image_source_image([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_premultiplied_rgba8_image *")] mln_premultiplied_rgba8_image* image, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_location_indicator_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view before_layer_id); + public static extern mln_status mln_map_set_image_source_coordinates([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_lat_lng *")] mln_lat_lng* coordinates, [NativeTypeName("size_t")] nuint coordinate_count, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_location_indicator_location([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude); + public static extern mln_status mln_map_get_image_source_coordinates([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_location_indicator_bearing([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double bearing); + public static extern mln_status mln_map_add_hillshade_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_location_indicator_accuracy_radius([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double radius); + public static extern mln_status mln_map_add_color_relief_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_location_indicator_image_name([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("uint32_t")] uint image_kind, mln_buffer_view image_id); + public static extern mln_status mln_map_add_location_indicator_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_add_style_layer_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_json, mln_buffer_view before_layer_id); + public static extern mln_status mln_map_set_location_indicator_location([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_remove_style_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, bool* out_removed); + public static extern mln_status mln_map_set_location_indicator_bearing([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double bearing, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_style_layer_exists([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, bool* out_exists); + public static extern mln_status mln_map_set_location_indicator_accuracy_radius([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double radius, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_layer_type([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view* out_layer_type, bool* out_found); + public static extern mln_status mln_map_set_location_indicator_image_name([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("uint32_t")] uint image_kind, mln_buffer_view image_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_list_style_layer_ids([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("mln_style_id_list *")] MlnStyleIdList* out_layer_ids); + public static extern mln_status mln_map_add_style_layer_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_move_style_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view before_layer_id); + public static extern mln_status mln_map_remove_style_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_layer_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("mln_buffer *")] MlnBuffer* out_layer, bool* out_found); + public static extern mln_status mln_map_get_style_layer_info([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_light_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view light_json); + public static extern mln_status mln_map_list_style_layer_ids([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_light_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view property_name, mln_buffer_view value); + public static extern mln_status mln_map_move_style_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_light_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view property_name, [NativeTypeName("mln_buffer *")] MlnBuffer* out_value); + public static extern mln_status mln_map_get_style_layer_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_style_transition_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_style_transition_options *")] mln_style_transition_options* options); + public static extern mln_status mln_map_set_style_light_json([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view light_json, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_style_transition_options([NativeTypeName("mln_map")] MlnMap map, mln_style_transition_options* out_options); + public static extern mln_status mln_map_set_style_light_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view property_name, mln_buffer_view value, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value); + public static extern mln_status mln_map_get_style_light_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view property_name, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_layer_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view property_name, [NativeTypeName("mln_buffer *")] MlnBuffer* out_value); + public static extern mln_status mln_map_set_style_transition_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_style_transition_options *")] mln_style_transition_options* options, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_filter([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* filter); + public static extern mln_status mln_map_get_style_transition_options([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_layer_filter([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("mln_buffer *")] MlnBuffer* out_filter); + public static extern mln_status mln_map_set_layer_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_source_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_layer); + public static extern mln_status mln_map_get_layer_property([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view property_name, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_layer_source_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("char *")] sbyte* out_source_layer, [NativeTypeName("size_t")] nuint source_layer_capacity, [NativeTypeName("size_t *")] nuint* out_source_layer_size); + public static extern mln_status mln_map_set_layer_filter([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_buffer_view *")] mln_buffer_view* filter, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_source_id([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id); + public static extern mln_status mln_map_get_layer_filter([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_copy_layer_source_id([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("char *")] sbyte* out_source_id, [NativeTypeName("size_t")] nuint source_id_capacity, [NativeTypeName("size_t *")] nuint* out_source_id_size); + public static extern mln_status mln_map_set_layer_source_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_layer, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_min_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double min_zoom); + public static extern mln_status mln_map_copy_layer_source_layer([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_layer_min_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double* out_min_zoom); + public static extern mln_status mln_map_set_layer_source_id([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, mln_buffer_view source_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_max_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double max_zoom); + public static extern mln_status mln_map_copy_layer_source_id([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_layer_max_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double* out_max_zoom); + public static extern mln_status mln_map_set_layer_min_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double min_zoom, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_set_layer_visibility([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("uint32_t")] uint visibility); + public static extern mln_status mln_map_set_layer_max_zoom([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, double max_zoom, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_map_get_layer_visibility([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("uint32_t *")] uint* out_visibility); + public static extern mln_status mln_map_set_layer_visibility([NativeTypeName("mln_map")] MlnMap map, mln_buffer_view layer_id, [NativeTypeName("uint32_t")] uint visibility, [NativeTypeName("const mln_completion *")] mln_completion* completion); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/surface.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/surface.g.cs index 3c90dc1fe..798a1adb8 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/surface.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/surface.g.cs @@ -68,27 +68,27 @@ internal static unsafe partial class NativeMethods public static extern mln_webgpu_surface_descriptor mln_webgpu_surface_descriptor_default(); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_surface_descriptor *")] mln_metal_surface_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_metal_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_surface_descriptor *")] mln_metal_surface_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_surface_descriptor *")] mln_vulkan_surface_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_vulkan_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_surface_descriptor *")] mln_vulkan_surface_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_surface_descriptor *")] mln_opengl_surface_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_opengl_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_surface_descriptor *")] mln_opengl_surface_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_surface_descriptor *")] mln_webgpu_surface_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_webgpu_surface_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_surface_descriptor *")] mln_webgpu_surface_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_metal_surface_descriptor *")] mln_metal_surface_descriptor* descriptor); + public static extern mln_status mln_metal_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_metal_surface_descriptor *")] mln_metal_surface_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_vulkan_surface_descriptor *")] mln_vulkan_surface_descriptor* descriptor); + public static extern mln_status mln_vulkan_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_vulkan_surface_descriptor *")] mln_vulkan_surface_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_opengl_surface_descriptor *")] mln_opengl_surface_descriptor* descriptor); + public static extern mln_status mln_opengl_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_opengl_surface_descriptor *")] mln_opengl_surface_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_webgpu_surface_descriptor *")] mln_webgpu_surface_descriptor* descriptor); + public static extern mln_status mln_webgpu_surface_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_webgpu_surface_descriptor *")] mln_webgpu_surface_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/texture.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/texture.g.cs index a2037ed9c..8ad2d2d29 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/texture.g.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/texture.g.cs @@ -273,6 +273,19 @@ internal partial struct mln_texture_image_info public nuint byte_length; } + internal partial struct mln_texture_readback_result + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("uint32_t")] + public uint reserved; + + public mln_buffer_view data; + + public mln_texture_image_info info; + } + internal static unsafe partial class NativeMethods { [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] @@ -303,66 +316,54 @@ internal static unsafe partial class NativeMethods public static extern mln_texture_image_info mln_texture_image_info_default(); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_owned_texture_descriptor *")] mln_metal_owned_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_borrowed_texture_descriptor *")] mln_metal_borrowed_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_owned_texture_descriptor *")] mln_vulkan_owned_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_borrowed_texture_descriptor *")] mln_vulkan_borrowed_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); - - [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_owned_texture_descriptor *")] mln_opengl_owned_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_metal_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_owned_texture_descriptor *")] mln_metal_owned_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_borrowed_texture_descriptor *")] mln_opengl_borrowed_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_metal_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_metal_borrowed_texture_descriptor *")] mln_metal_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_metal_borrowed_texture_descriptor *")] mln_metal_borrowed_texture_descriptor* descriptor); + public static extern mln_status mln_vulkan_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_owned_texture_descriptor *")] mln_vulkan_owned_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_vulkan_borrowed_texture_descriptor *")] mln_vulkan_borrowed_texture_descriptor* descriptor); + public static extern mln_status mln_vulkan_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_vulkan_borrowed_texture_descriptor *")] mln_vulkan_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_opengl_borrowed_texture_descriptor *")] mln_opengl_borrowed_texture_descriptor* descriptor); + public static extern mln_status mln_opengl_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_owned_texture_descriptor *")] mln_opengl_owned_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_texture_read_premultiplied_rgba8([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("uint8_t *")] byte* out_data, [NativeTypeName("size_t")] nuint out_data_capacity, mln_texture_image_info* out_info); + public static extern mln_status mln_opengl_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_opengl_borrowed_texture_descriptor *")] mln_opengl_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_owned_texture_acquire_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_metal_owned_texture_frame* out_frame); + public static extern mln_status mln_webgpu_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_owned_texture_descriptor *")] mln_webgpu_owned_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_metal_owned_texture_release_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_metal_owned_texture_frame *")] mln_metal_owned_texture_frame* frame); + public static extern mln_status mln_webgpu_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_borrowed_texture_descriptor *")] mln_webgpu_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_render_session_attach_options *")] mln_render_session_attach_options* options, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_owned_texture_acquire_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_vulkan_owned_texture_frame* out_frame); + public static extern mln_status mln_metal_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_metal_borrowed_texture_descriptor *")] mln_metal_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_vulkan_owned_texture_release_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_vulkan_owned_texture_frame *")] mln_vulkan_owned_texture_frame* frame); + public static extern mln_status mln_vulkan_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_vulkan_borrowed_texture_descriptor *")] mln_vulkan_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_owned_texture_acquire_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_opengl_owned_texture_frame* out_frame); + public static extern mln_status mln_opengl_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_opengl_borrowed_texture_descriptor *")] mln_opengl_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_opengl_owned_texture_release_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_opengl_owned_texture_frame *")] mln_opengl_owned_texture_frame* frame); + public static extern mln_status mln_webgpu_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_webgpu_borrowed_texture_descriptor *")] mln_webgpu_borrowed_texture_descriptor* descriptor, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_owned_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_owned_texture_descriptor *")] mln_webgpu_owned_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_texture_read_premultiplied_rgba8([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_completion *")] mln_completion* completion); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_borrowed_texture_attach([NativeTypeName("mln_map")] MlnMap map, [NativeTypeName("const mln_webgpu_borrowed_texture_descriptor *")] mln_webgpu_borrowed_texture_descriptor* descriptor, [NativeTypeName("mln_render_session *")] MlnRenderSession* out_session); + public static extern mln_status mln_acquired_frame_get_metal_texture([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_metal_owned_texture_frame* out_frame); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_borrowed_texture_set_target([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_webgpu_borrowed_texture_descriptor *")] mln_webgpu_borrowed_texture_descriptor* descriptor); + public static extern mln_status mln_acquired_frame_get_vulkan_texture([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_vulkan_owned_texture_frame* out_frame); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_owned_texture_acquire_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, mln_webgpu_owned_texture_frame* out_frame); + public static extern mln_status mln_acquired_frame_get_opengl_texture([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_opengl_owned_texture_frame* out_frame); [DllImport("maplibre-native-c", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern mln_status mln_webgpu_owned_texture_release_frame([NativeTypeName("mln_render_session")] MlnRenderSession session, [NativeTypeName("const mln_webgpu_owned_texture_frame *")] mln_webgpu_owned_texture_frame* frame); + public static extern mln_status mln_acquired_frame_get_webgpu_texture([NativeTypeName("mln_acquired_frame")] MlnAcquiredFrame frame, mln_webgpu_owned_texture_frame* out_frame); } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Generated/wake.g.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/wake.g.cs new file mode 100644 index 000000000..bfec0fa1e --- /dev/null +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Generated/wake.g.cs @@ -0,0 +1,16 @@ +namespace Maplibre.NativeFfi.Internal.C +{ + internal unsafe partial struct mln_wake + { + [NativeTypeName("uint32_t")] + public uint size; + + [NativeTypeName("mln_wake_callback")] + public delegate* unmanaged[Cdecl] callback; + + public void* user_data; + + [NativeTypeName("mln_wake_release")] + public delegate* unmanaged[Cdecl] release_user_data; + } +} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/C/Handles.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/C/Handles.cs index afab9f972..5cfdc6478 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/C/Handles.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/C/Handles.cs @@ -53,7 +53,21 @@ internal readonly struct MlnRenderSession(ulong value) : IMlnHandle public bool IsNull => Value == 0; } -internal readonly struct MlnWakeSource(ulong value) : IMlnHandle +internal readonly struct MlnRenderFrameBatch(ulong value) : IMlnHandle +{ + public ulong Value { get; } = value; + + public bool IsNull => Value == 0; +} + +internal readonly struct MlnAcquiredFrame(ulong value) : IMlnHandle +{ + public ulong Value { get; } = value; + + public bool IsNull => Value == 0; +} + +internal readonly struct MlnEventBatch(ulong value) : IMlnHandle { public ulong Value { get; } = value; diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/CustomGeometrySourceState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/CustomGeometrySourceState.cs index e6821d94b..da5c5dbc7 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/CustomGeometrySourceState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/CustomGeometrySourceState.cs @@ -123,9 +123,8 @@ private static void CancelTile(void* userData, mln_canonical_tile_id tileId) state?.InvokeCancel(FromNative(tileId)); } - // The C API invokes this once, on the map owner thread, after MapLibre stops referencing this - // state, so the stubs go with the source whether it was removed, dropped by a style load, or - // retired with the map. + // The C API invokes this once after MapLibre stops referencing this state, so the stubs go + // with the source whether it was removed, dropped by a style load, or retired with the map. [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] private static void ReleaseUserData(void* userData) { diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/HttpHeaderTransformState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/HttpHeaderTransformState.cs index 3b642d2ef..84b5ecf9a 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/HttpHeaderTransformState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/HttpHeaderTransformState.cs @@ -23,6 +23,7 @@ internal HttpHeaderTransformState(HttpHeaderTransformCallback callback) size = (uint)sizeof(mln_http_header_transform), callback = &OnTransform, user_data = (void*)handle, + release_user_data = &Release, }; [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] @@ -88,6 +89,16 @@ private static mln_status OnTransform( } } + [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static void Release(void* userData) + { + try + { + ((HttpHeaderTransformState?)GCHandle.FromIntPtr((nint)userData).Target)?.Dispose(); + } + catch { } + } + public void Dispose() { var current = System.Threading.Interlocked.Exchange(ref handle, 0); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/LogCallbackState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/LogCallbackState.cs index fb30f532d..d6c4f2f89 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/LogCallbackState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/LogCallbackState.cs @@ -7,149 +7,89 @@ namespace Maplibre.NativeFfi.Internal.Callback; -internal unsafe delegate mln_status LogSetCallback( - delegate* unmanaged[Cdecl] callback, - void* userData -); - -internal delegate mln_status LogClearCallback(); - -internal sealed unsafe class LogCallbackState : IDisposable +internal sealed unsafe class LogCallbackState { - private static readonly LogSetCallback DefaultSetCallback = static (callback, userData) => - NativeMethods.mln_log_set_callback(callback, userData); - private static readonly LogClearCallback DefaultClearCallback = static () => - NativeMethods.mln_log_clear_callback(); - private static readonly Lock Gate = new(); - private static readonly Lock RegistryGate = new(); - private static readonly Dictionary Registry = []; - private static LogCallbackState? current; - private static nint nextToken; - - [ThreadStatic] - private static LogSetCallback? setCallbackForTest; - - [ThreadStatic] - private static LogClearCallback? clearCallbackForTest; - - private readonly nint token; private readonly LogCallback callback; - private bool retired; private LogCallbackState(LogCallback callback) { this.callback = callback; - lock (RegistryGate) - { - token = ++nextToken; - if (token == 0) - { - token = ++nextToken; - } - - Registry.Add(token, this); - } } internal static void Set(LogCallback callback) { ArgumentNullException.ThrowIfNull(callback); NativeLibraryLoader.EnsureLoaded(); - var replacement = new LogCallbackState(callback); - lock (Gate) + var root = GCHandle.Alloc(new LogCallbackState(callback)); + try { - try - { - NativeStatus.Check(SetCallback(&OnLog, replacement.UserData)); - var old = current; - current = replacement; - old?.Retire(); - } - catch (Exception error) - { - RetireAndSuppress(error, replacement); - throw; - } + NativeStatus.Check( + NativeMethods.mln_log_set_callback(&OnLog, (void*)GCHandle.ToIntPtr(root), &Release) + ); + } + catch + { + root.Free(); + throw; } } internal static void Clear() { NativeLibraryLoader.EnsureLoaded(); - lock (Gate) - { - NativeStatus.Check(ClearCallback()); - var old = current; - current = null; - old?.Retire(); - } + NativeStatus.Check(NativeMethods.mln_log_clear_callback()); } - internal static LogCallbackState? CurrentForTest => current; - - internal bool IsRetiredForTest => retired; - - internal static uint EmitForTest(uint severity, uint @event, long code, string message) + internal static uint EmitForTest( + LogCallback callback, + uint severity, + uint @event, + long code, + string message + ) { using var nativeMessage = NativeUtf8String.FromNullableString(message, nameof(message)); - var state = current; - return state is null ? 0 : state.Invoke(severity, @event, code, nativeMessage.Pointer); + return new LogCallbackState(callback).Invoke(severity, @event, code, nativeMessage.Pointer); } - internal static LogCallbackState? StateForTokenForTest(nint token) + [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static uint OnLog(void* userData, uint severity, uint @event, long code, sbyte* message) { - lock (RegistryGate) + try { - return Registry.GetValueOrDefault(token); - } - } - - internal static IDisposable UseCallbackMethodsForTest( - LogSetCallback setCallback, - LogClearCallback clearCallback - ) - { - var previousSet = setCallbackForTest; - var previousClear = clearCallbackForTest; - setCallbackForTest = setCallback; - clearCallbackForTest = clearCallback; - return new RestoreCallbackMethods(previousSet, previousClear); - } - - private static LogSetCallback SetCallback => setCallbackForTest ?? DefaultSetCallback; - - private static LogClearCallback ClearCallback => clearCallbackForTest ?? DefaultClearCallback; + if (userData is null) + { + return 0; + } - private sealed class RestoreCallbackMethods( - LogSetCallback? previousSet, - LogClearCallback? previousClear - ) : IDisposable - { - public void Dispose() + return ((LogCallbackState?)GCHandle.FromIntPtr((nint)userData).Target)?.Invoke( + severity, + @event, + code, + message + ) ?? 0; + } + catch { - setCallbackForTest = previousSet; - clearCallbackForTest = previousClear; + return 0; } } - private void* UserData => (void*)token; - [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - private static uint OnLog(void* userData, uint severity, uint @event, long code, sbyte* message) + private static void Release(void* userData) { - try + if (userData is null) { - var state = Enter((nint)userData); - if (state is null) - { - return 0; - } + return; + } - return state.Invoke(severity, @event, code, message); + try + { + GCHandle.FromIntPtr((nint)userData).Free(); } catch { - return 0; + // A release callback must not unwind across the C boundary. } } @@ -171,48 +111,4 @@ private uint Invoke(uint severity, uint @event, long code, sbyte* message) return 0; } } - - private static LogCallbackState? Enter(nint token) - { - lock (RegistryGate) - { - if (!Registry.TryGetValue(token, out var state) || state.retired) - { - return null; - } - - return state; - } - } - - public void Dispose() - { - Retire(); - } - - private void Retire() - { - lock (RegistryGate) - { - if (retired) - { - return; - } - - retired = true; - Registry.Remove(token); - } - } - - private static void RetireAndSuppress(Exception error, LogCallbackState state) - { - try - { - state.Retire(); - } - catch (Exception cleanup) - { - error.Data["SuppressedCleanupException"] = cleanup; - } - } } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceProviderState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceProviderState.cs index a6999fc51..b0c736684 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceProviderState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceProviderState.cs @@ -22,6 +22,7 @@ internal ResourceProviderState(ResourceProviderCallback callback) size = (uint)sizeof(mln_resource_provider), callback = &OnRequest, user_data = (void*)handle, + release_user_data = &Release, }; internal bool IsHandleAllocatedForTest => handle != 0; @@ -53,6 +54,16 @@ MlnResourceRequest requestHandle } } + [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static void Release(void* userData) + { + try + { + ((ResourceProviderState?)GCHandle.FromIntPtr((nint)userData).Target)?.Dispose(); + } + catch { } + } + private static uint Invoke( ResourceProviderState? state, mln_resource_request* request, diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceTransformState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceTransformState.cs index 059e19e89..f583a78b5 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceTransformState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Callback/ResourceTransformState.cs @@ -23,6 +23,7 @@ internal ResourceTransformState(ResourceTransformCallback callback) size = (uint)sizeof(mln_resource_transform), callback = &OnTransform, user_data = (void*)handle, + release_user_data = &Release, }; internal bool IsHandleAllocatedForTest => handle != 0; @@ -58,6 +59,16 @@ private static mln_status OnTransform( } } + [UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static void Release(void* userData) + { + try + { + ((ResourceTransformState?)GCHandle.FromIntPtr((nint)userData).Target)?.Dispose(); + } + catch { } + } + private static mln_status Invoke( ResourceTransformState? state, uint kind, diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Handle/NativeHandleState.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Handle/NativeHandleState.cs index 7e0df417f..052649de3 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Handle/NativeHandleState.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Handle/NativeHandleState.cs @@ -24,7 +24,6 @@ internal sealed class NativeHandleState private T handle; private bool closed; private bool releaseInProgress; - private int activeUses; internal NativeHandleState(T handle, StatusDestroy destroy, string typeName) { @@ -109,37 +108,15 @@ private T HandleLocked() } /// - /// Runs with the handle and with release held off until it returns. + /// Runs after checking that this wrapper still owns the handle. /// /// - /// Handles the host may use and release from different threads go through this, so a release - /// that begins mid-call waits for the call to finish and a losing race reports this wrapper's - /// closed-handle error rather than the C API's rejection of a retired id. Owner-thread-only - /// handles get that ordering from the owner-thread rule and can read - /// directly. runs outside the lock; calling from - /// inside it on the same thread deadlocks. + /// The C API leases the native object for each entry point, so concurrent release does not need + /// a second binding-side active-use lease. /// internal TResult WithLive(Func use) { - T live; - lock (gate) - { - live = HandleLocked(); - activeUses++; - } - - try - { - return use(live); - } - finally - { - lock (gate) - { - activeUses--; - Monitor.PulseAll(gate); - } - } + return use(Handle); } internal void WithLive(Action use) @@ -240,12 +217,6 @@ private bool BeginReleaseLocked(out T live) releaseInProgress = true; - // Uses that already read the handle still hold it; wait for them before destroying it. - while (activeUses > 0) - { - Monitor.Wait(gate); - } - return true; } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Status/NativeStatus.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Status/NativeStatus.cs index f516427db..dc3e48ae1 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Status/NativeStatus.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Status/NativeStatus.cs @@ -24,6 +24,14 @@ internal static void Check(int rawStatus) throw CreateException(rawStatus, CaptureDiagnostic()); } + internal static void Check(int rawStatus, string diagnostic) + { + if (rawStatus != (int)MaplibreStatus.Ok) + { + throw CreateException(rawStatus, diagnostic); + } + } + private static MaplibreException CreateException(int rawStatus, string diagnostic) { var status = StatusFromRaw(rawStatus); @@ -70,7 +78,7 @@ internal static IDisposable UseDiagnosticProviderForTest(Func provider) return new RestoreDiagnosticProvider(previous); } - private static MaplibreStatus StatusFromRaw(int rawStatus) => + internal static MaplibreStatus StatusFromRaw(int rawStatus) => rawStatus switch { 0 => MaplibreStatus.Ok, @@ -79,6 +87,11 @@ private static MaplibreStatus StatusFromRaw(int rawStatus) => -3 => MaplibreStatus.WrongThread, -4 => MaplibreStatus.Unsupported, -5 => MaplibreStatus.NativeError, + -6 => MaplibreStatus.Cancelled, + -7 => MaplibreStatus.Busy, + -8 => MaplibreStatus.TargetLost, + -9 => MaplibreStatus.NotReady, + -10 => MaplibreStatus.NotFound, _ => MaplibreStatus.Unknown, }; diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/MapStructs.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/MapStructs.cs index aac3a72e0..16e0022aa 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/MapStructs.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/MapStructs.cs @@ -169,6 +169,33 @@ internal static mln_animation_options ToNative(AnimationOptions options) return native; } + internal static mln_camera_update ToNative(CameraUpdate update) + { + ArgumentNullException.ThrowIfNull(update); + var native = NativeMethods.mln_camera_update_default(); + native.mode = (uint)update.Mode; + native.camera = ToNative(update.Camera); + native.animation = ToNative(update.Animation); + native.gesture_phase = (uint)update.GesturePhase; + return native; + } + + internal static mln_camera_delta ToNative(CameraDelta delta) + { + ArgumentNullException.ThrowIfNull(delta); + var native = NativeMethods.mln_camera_delta_default(); + native.kind = (uint)delta.Kind; + native.offset = ToNative(delta.Offset); + native.amount = delta.Amount; + if (delta.Anchor is { } anchor) + { + native.has_anchor = 1; + native.anchor = ToNative(anchor); + } + native.animation = ToNative(delta.Animation); + return native; + } + internal static CameraOptions CameraOptionsFromNative(mln_camera_options native) { var options = new CameraOptions(); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RenderStructs.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RenderStructs.cs index 18ef0255b..588a457e9 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RenderStructs.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RenderStructs.cs @@ -43,6 +43,27 @@ internal static mln_vulkan_context_descriptor ToNative(VulkanContextDescriptor? : (void*)context.GetDeviceProcAddr.Address, }; + internal static mln_webgpu_context_descriptor ToNative(WebGpuContextDescriptor? context) => + new() + { + size = (uint)sizeof(mln_webgpu_context_descriptor), + instance = context is null ? null : (void*)context.Instance.Address, + device = context is null ? null : (void*)context.Device.Address, + queue = context is null ? null : (void*)context.Queue.Address, + }; + + internal static mln_render_session_attach_options ToNative(RenderSessionAttachOptions? options) + { + NativeLibraryLoader.EnsureLoaded(); + var native = NativeMethods.mln_render_session_attach_options_default(); + if (options is not null) + { + native.driver = (uint)options.Driver; + native.requested_texture_ring_depth = options.RequestedTextureRingDepth; + } + return native; + } + internal static mln_opengl_context_descriptor ToNative(OpenGLContextDescriptor context) { ArgumentNullException.ThrowIfNull(context); @@ -76,6 +97,15 @@ internal static mln_opengl_context_descriptor ToNative(OpenGLContextDescriptor c get_proc_address = (void*)egl.GetProcAddress.Address, }; break; + case WebGLContextDescriptor webgl: + native.platform = mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + native.data.webgl = new mln_webgl_context_descriptor + { + size = (uint)sizeof(mln_webgl_context_descriptor), + kind = (uint)webgl.Kind, + context = webgl.Context, + }; + break; default: throw new InvalidArgumentException( MaplibreStatus.InvalidArgument, @@ -218,6 +248,61 @@ OpenGLBorrowedTextureDescriptor descriptor return native; } + internal static mln_webgpu_surface_descriptor ToNative(WebGpuSurfaceDescriptor descriptor) + { + ArgumentNullException.ThrowIfNull(descriptor); + var native = NativeMethods.mln_webgpu_surface_descriptor_default(); + native.extent = ToNative(descriptor.Extent); + native.context = ToNative(descriptor.Context); + native.surface = (void*)descriptor.Surface.Address; + native.format = descriptor.Format; + return native; + } + + internal static mln_webgpu_owned_texture_descriptor ToNative( + WebGpuOwnedTextureDescriptor descriptor + ) + { + ArgumentNullException.ThrowIfNull(descriptor); + var native = NativeMethods.mln_webgpu_owned_texture_descriptor_default(); + native.extent = ToNative(descriptor.Extent); + native.context = ToNative(descriptor.Context); + return native; + } + + internal static mln_webgpu_borrowed_texture_descriptor ToNative( + WebGpuBorrowedTextureDescriptor descriptor + ) + { + ArgumentNullException.ThrowIfNull(descriptor); + var native = NativeMethods.mln_webgpu_borrowed_texture_descriptor_default(); + native.extent = ToNative(descriptor.Extent); + native.physical_width = descriptor.PhysicalWidth; + native.physical_height = descriptor.PhysicalHeight; + native.context = ToNative(descriptor.Context); + native.texture = (void*)descriptor.Texture.Address; + native.texture_view = (void*)descriptor.TextureView.Address; + native.format = descriptor.Format; + return native; + } + + internal static WebGpuOwnedTextureFrame FromNative( + mln_webgpu_owned_texture_frame frame, + FrameScope scope + ) => + new( + scope, + frame.generation, + frame.width, + frame.height, + frame.scale_factor, + frame.frame_id, + NativePointer.FromNativeAddress((nint)frame.texture), + NativePointer.FromNativeAddress((nint)frame.texture_view), + NativePointer.FromNativeAddress((nint)frame.device), + frame.format + ); + internal static TextureImageInfo FromNative(mln_texture_image_info info) => new(info.width, info.height, info.stride, (ulong)info.byte_length); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RuntimeStructs.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RuntimeStructs.cs index cf057abe0..7831255ea 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RuntimeStructs.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Internal/Struct/RuntimeStructs.cs @@ -18,16 +18,16 @@ internal static unsafe class RuntimeStructs private static readonly uint PayloadOffset = (uint) Marshal.OffsetOf(nameof(mln_runtime_event.payload)); - internal static List ReadBatch(in mln_runtime_event_batch batch) => + internal static List ReadBatch(in mln_runtime_event_batch_view batch) => ReadBatch(batch, null, static _ => null); - /// Copies every event of a borrowed batch, in queue order. + /// Copies every event from an owned batch view, in queue order. /// /// Events are indexed by the stride the batch reports rather than by this binding's own record /// size, so a later C API version that widens the payload union stays readable. /// internal static List ReadBatch( - in mln_runtime_event_batch batch, + in mln_runtime_event_batch_view batch, RuntimeHandle? runtimeSource, Func mapSource ) @@ -94,8 +94,6 @@ private static RuntimeEventPayload ReadPayload(mln_runtime_event* record, uint e ReadOfflineRegionResponseError(record->payload.offline_region_response_error), mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT => ReadOfflineRegionTileCountLimit(record->payload.offline_region_tile_count_limit), - mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED => - ReadOfflineOperationCompleted(record->payload.offline_operation_completed), mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED => ReadCameraTransitionFinished(record->payload.camera_transition_finished), _ => new RuntimeEventPayload.Unknown( @@ -135,19 +133,6 @@ private static RuntimeEventPayload.OfflineRegionTileCountLimit ReadOfflineRegion in mln_runtime_event_offline_region_tile_count_limit payload ) => new(payload.region_id, payload.limit); - private static RuntimeEventPayload.OfflineOperationCompleted ReadOfflineOperationCompleted( - in mln_runtime_event_offline_operation_completed payload - ) => - new( - payload.operation_id, - (OfflineOperationKind)payload.operation_kind, - payload.operation_kind, - (OfflineOperationResultKind)payload.result_kind, - payload.result_kind, - payload.result_status, - payload.found != 0 - ); - private static RuntimeEventPayload.CameraTransitionFinished ReadCameraTransitionFinished( in mln_runtime_event_camera_transition_finished payload ) => new(payload.transition_id); diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapDescriptors.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapDescriptors.cs index 4000bc82d..7b9a3b37a 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapDescriptors.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapDescriptors.cs @@ -1,4 +1,5 @@ using Maplibre.NativeFfi.Camera; +using Maplibre.NativeFfi.Runtime; namespace Maplibre.NativeFfi.Map; @@ -42,6 +43,32 @@ public sealed record ProjectionModeOptions public double? YSkew { get; set; } } +public readonly record struct LogicalExtent(uint Width, uint Height, double ScaleFactor); + +/// A synchronous copy of the map's committed state. +/// +/// Every committed map command publishes a new snapshot and reports its generation in the +/// completion, so a snapshot whose is at or past that value observes +/// the commit. +/// +/// True once every requested style and tile resource finished loading. +public readonly record struct MapSnapshot( + ulong Generation, + DebugOptions DebugOptions, + CameraOptions Camera, + LogicalExtent LogicalExtent, + ProjectionModeOptions ProjectionMode, + ViewportOptions Viewport, + bool FullyLoaded, + bool RenderingStatsViewEnabled, + bool RepaintDemand, + RuntimeEventMask EventMask, + ulong LatestRenderUpdateGeneration, + TileOptions Tile, + BoundOptions Bounds, + FreeCameraOptions FreeCamera +); + /// Rendering statistics snapshot. public readonly record struct RenderingStats( double EncodingTime, diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.Async.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.Async.cs new file mode 100644 index 000000000..151cdcd46 --- /dev/null +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.Async.cs @@ -0,0 +1,187 @@ +using Maplibre.NativeFfi.Internal.C; +using Maplibre.NativeFfi.Internal.Memory; +using Maplibre.NativeFfi.Internal.Struct; +using Maplibre.NativeFfi.Render; +using Maplibre.NativeFfi.Runtime; +using Maplibre.NativeFfi.Style; + +namespace Maplibre.NativeFfi.Map; + +public sealed unsafe partial class MapHandle +{ + public Task StyleSourceInfoAsync( + string sourceId, + CancellationToken cancellationToken = default + ) + { + using var source = NativeStringView.From(sourceId, nameof(sourceId)); + return NativeCompletion + .Submit( + completion => + NativeMethods.mln_map_get_style_source_info(Handle, source.Value, completion), + result => ReadStyleSource(sourceId, result) + ) + .WaitAsync(cancellationToken); + } + + public Task StyleLayerInfoAsync( + string layerId, + CancellationToken cancellationToken = default + ) + { + using var layer = NativeStringView.From(layerId, nameof(layerId)); + return NativeCompletion + .Submit( + completion => + NativeMethods.mln_map_get_style_layer_info(Handle, layer.Value, completion), + result => ReadStyleLayer(layerId, result) + ) + .WaitAsync(cancellationToken); + } + + public Task StyleImageAsync( + string imageId, + CancellationToken cancellationToken = default + ) + { + using var image = NativeStringView.From(imageId, nameof(imageId)); + return NativeCompletion + .Submit( + completion => + NativeMethods.mln_map_get_style_image_info(Handle, image.Value, completion), + ReadStyleImage + ) + .WaitAsync(cancellationToken); + } + + private static SourceInfo? ReadStyleSource(string sourceId, mln_completion_result* completion) + { + if (completion->value_count == 0) + { + return null; + } + var value = NativeCompletion.Value(completion); + var info = value.info; + var fields = (mln_style_source_info_field)info.fields; + string? attribution = + info.has_attribution != 0 + ? RuntimeStructs.CopyUtf8((sbyte*)value.attribution.data, value.attribution.size) + : null; + string? url = fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_URL) + ? RuntimeStructs.CopyUtf8((sbyte*)value.url.data, value.url.size) + : null; + TileJson? tileJson = null; + if (fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_TILEJSON)) + { + var urls = new string[checked((int)value.tile_url_count)]; + for (nuint index = 0; index < value.tile_url_count; index++) + { + var view = value.tile_urls[index]; + urls[(int)index] = RuntimeStructs.CopyUtf8((sbyte*)view.data, view.size); + } + tileJson = new TileJson( + urls, + info.min_zoom, + info.max_zoom, + (TileScheme)info.scheme, + info.scheme, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_BOUNDS) + ? MapStructs.FromNative(info.bounds) + : null + ); + } + return new SourceInfo( + sourceId, + (SourceType)info.type, + info.type, + info.is_volatile != 0, + attribution, + url, + tileJson, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_TILE_SIZE) + ? info.tile_size + : null, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) + ? (VectorTileEncoding)info.vector_encoding + : null, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) + ? info.vector_encoding + : null, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) + ? (RasterDemEncoding)info.raster_encoding + : null, + fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) + ? info.raster_encoding + : null + ); + } + + private static LayerInfo? ReadStyleLayer(string layerId, mln_completion_result* completion) + { + if (completion->value_count == 0) + { + return null; + } + var value = NativeCompletion.Value(completion); + var info = value.info; + var fields = (mln_style_layer_info_field)info.fields; + return new LayerInfo( + layerId, + RuntimeStructs.CopyUtf8((sbyte*)info.type.data, info.type.size), + info.min_zoom, + info.max_zoom, + (StyleLayerVisibility)info.visibility, + info.visibility, + fields.HasFlag(mln_style_layer_info_field.MLN_STYLE_LAYER_INFO_SOURCE_ID) + ? RuntimeStructs.CopyUtf8((sbyte*)value.source_id.data, value.source_id.size) + : null, + fields.HasFlag(mln_style_layer_info_field.MLN_STYLE_LAYER_INFO_SOURCE_LAYER) + ? RuntimeStructs.CopyUtf8((sbyte*)value.source_layer.data, value.source_layer.size) + : null + ); + } + + private static StyleImage? ReadStyleImage(mln_completion_result* completion) + { + if (completion->value_count == 0) + { + return null; + } + var value = NativeCompletion.Value(completion); + var info = StyleStructs.FromNative(value.info); + var pixels = ValueStructs.CopyBufferView(value.pixels); + return new StyleImage( + new PremultipliedRgba8Image( + pixels, + new TextureImageInfo(info.Width, info.Height, info.Stride, (ulong)pixels.Length) + ), + new StyleImageOptions + { + PixelRatio = info.PixelRatio, + Sdf = info.Sdf, + StretchX = CopyStretches(value.stretch_x, value.stretch_x_count), + StretchY = CopyStretches(value.stretch_y, value.stretch_y_count), + Content = info.Content, + TextFitWidth = info.TextFitWidth, + TextFitHeight = info.TextFitHeight, + } + ); + } + + private static IReadOnlyList? CopyStretches( + mln_image_stretch* values, + nuint count + ) + { + if (count == 0) + { + return null; + } + var copied = new ImageStretch[checked((int)count)]; + for (nuint index = 0; index < count; index++) + { + copied[(int)index] = new ImageStretch(values[index].from, values[index].to); + } + return copied; + } +} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.cs index ffcf65fec..86ee36a76 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapHandle.cs @@ -16,29 +16,33 @@ namespace Maplibre.NativeFfi.Map; internal unsafe delegate mln_status MapAddCustomGeometrySource( MlnMap map, mln_buffer_view sourceId, - mln_custom_geometry_source_options* options + mln_custom_geometry_source_options* options, + mln_completion* completion ); internal unsafe delegate mln_status MapAddCustomMvtVectorSource( MlnMap map, mln_buffer_view sourceId, - mln_custom_mvt_vector_source_options* options + mln_custom_mvt_vector_source_options* options, + mln_completion* completion ); -/// Owner-thread map handle bound to a runtime. -public sealed unsafe class MapHandle : IDisposable +/// Any-thread map handle bound to a runtime. +public sealed unsafe partial class MapHandle : IDisposable, IAsyncDisposable { private static readonly MapAddCustomGeometrySource DefaultAddCustomGeometrySource = static ( map, sourceId, - options - ) => NativeMethods.mln_map_add_custom_geometry_source(map, sourceId, options); + options, + completion + ) => NativeMethods.mln_map_add_custom_geometry_source(map, sourceId, options, completion); private static readonly MapAddCustomMvtVectorSource DefaultAddCustomMvtVectorSource = static ( map, sourceId, - options - ) => NativeMethods.mln_map_add_custom_mvt_vector_source(map, sourceId, options); + options, + completion + ) => NativeMethods.mln_map_add_custom_mvt_vector_source(map, sourceId, options, completion); [ThreadStatic] private static MapAddCustomGeometrySource? addCustomGeometrySourceForTest; @@ -49,993 +53,755 @@ public sealed unsafe class MapHandle : IDisposable private readonly RuntimeHandle runtime; private readonly ulong nativeId; private readonly NativeHandleState state; + private Task teardown = Task.CompletedTask; private MapHandle(RuntimeHandle runtime, MlnMap handle) { this.runtime = runtime; nativeId = handle.Value; - state = new NativeHandleState( - handle, - static handle => NativeMethods.mln_map_destroy(handle), - nameof(MapHandle) - ); + state = new NativeHandleState(handle, StartRelease, nameof(MapHandle)); } - /// Creates a map from a runtime on the runtime owner thread. - public static MapHandle Create(RuntimeHandle runtime, MapOptions options) + /// Creates a map asynchronously. + public static Task CreateAsync( + RuntimeHandle runtime, + MapOptions options, + CancellationToken cancellationToken = default + ) { ArgumentNullException.ThrowIfNull(runtime); ArgumentNullException.ThrowIfNull(options); - var nativeOptions = options.ToNative(); - MlnMap map = default; - - NativeStatus.Check(NativeMethods.mln_map_create(runtime.Handle, &nativeOptions, &map)); - var handle = new MapHandle(runtime, map); - runtime.RegisterMap(handle); - return handle; + cancellationToken.ThrowIfCancellationRequested(); + return NativeCompletion + .Submit( + completion => + { + var nativeOptions = options.ToNative(); + return NativeMethods.mln_map_create(runtime.Handle, &nativeOptions, completion); + }, + result => + { + var map = NativeCompletion.Value(result); + var handle = new MapHandle(runtime, map); + runtime.RegisterMap(handle); + return handle; + } + ) + .WaitAsync(cancellationToken); } internal MlnMap Handle => state.Handle; + internal RuntimeHandle Runtime => runtime; + /// The issued native handle id, readable after close. internal ulong NativeId => nativeId; /// Whether this wrapper has successfully closed its native handle. public bool IsClosed => state.IsClosed; - /// Requests a repaint for a continuous map. - public void RequestRepaint() + /// Requests a repaint. + public Task RequestRepaintAsync() { - NativeStatus.Check(NativeMethods.mln_map_request_repaint(Handle)); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check(NativeMethods.mln_map_request_repaint(Handle, completion)); + }); } - /// Requests an asynchronous still-image render for a static map. - public void RequestStillImage() + /// Requests a noncoalescing still-image render. + public Task RequestStillImageAsync(CancellationToken cancellationToken = default) { - NativeStatus.Check(NativeMethods.mln_map_request_still_image(Handle)); + return NativeCompletion + .SubmitUnit(completion => NativeMethods.mln_map_request_still_image(Handle, completion)) + .WaitAsync(cancellationToken); } /// Sets native debug drawing options. - public void SetDebugOptions(DebugOptions options) - { - NativeStatus.Check(NativeMethods.mln_map_set_debug_options(Handle, (uint)options)); - } - - /// Gets native debug drawing options. - public DebugOptions GetDebugOptions() + /// The committed mask is visible as . + public Task SetDebugOptionsAsync(DebugOptions options) { - uint options = 0; - NativeStatus.Check(NativeMethods.mln_map_get_debug_options(Handle, &options)); - return (DebugOptions)options; + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_debug_options(Handle, (uint)options, completion) + ); + }); } /// Shows or hides the built-in rendering statistics overlay. - public void SetRenderingStatsViewEnabled(bool enabled) + /// + /// The committed value is visible as . + /// + public Task SetRenderingStatsViewEnabledAsync(bool enabled) { - NativeStatus.Check( - NativeMethods.mln_map_set_rendering_stats_view_enabled( - Handle, - enabled ? (byte)1 : (byte)0 - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_rendering_stats_view_enabled( + Handle, + enabled ? (byte)1 : (byte)0, + completion + ) + ); + }); } - /// Whether the built-in rendering statistics overlay is enabled. - public bool GetRenderingStatsViewEnabled() + /// Asks the native map to write debug logs. + public Task DumpDebugLogsAsync() { - bool enabled = false; - NativeStatus.Check( - NativeMethods.mln_map_get_rendering_stats_view_enabled(Handle, &enabled) + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check(NativeMethods.mln_map_dump_debug_logs(Handle, completion)); + }); + } + + /// Gets a synchronous copy of the committed map state. + public MapSnapshot GetSnapshot() + { + var snapshot = new mln_map_snapshot { size = (uint)sizeof(mln_map_snapshot) }; + NativeStatus.Check(NativeMethods.mln_map_snapshot_get(Handle, &snapshot)); + return new MapSnapshot( + snapshot.generation, + (DebugOptions)snapshot.debug_options, + MapStructs.CameraOptionsFromNative(snapshot.camera), + new LogicalExtent( + snapshot.logical_extent.width, + snapshot.logical_extent.height, + snapshot.logical_extent.scale_factor + ), + MapStructs.ProjectionModeOptionsFromNative(snapshot.projection_mode), + MapStructs.ViewportOptionsFromNative(snapshot.viewport), + snapshot.fully_loaded != 0, + snapshot.rendering_stats_view_enabled != 0, + snapshot.repaint_demand != 0, + (RuntimeEventMask)snapshot.event_mask, + snapshot.latest_render_update_generation, + MapStructs.TileOptionsFromNative(snapshot.tile), + MapStructs.BoundOptionsFromNative(snapshot.bounds), + MapStructs.FreeCameraOptionsFromNative(snapshot.free_camera) ); - return enabled; } - /// Whether the native map reports all required resources loaded. - public bool IsFullyLoaded() + /// Submits a logical extent change. + public Task ResizeAsync(LogicalExtent extent) { - bool loaded = false; - NativeStatus.Check(NativeMethods.mln_map_is_fully_loaded(Handle, &loaded)); - return loaded; + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_resize( + Handle, + new mln_logical_extent + { + width = extent.Width, + height = extent.Height, + scale_factor = extent.ScaleFactor, + }, + completion + ) + ); + }); } - /// Asks the native map to write debug logs through the native log system. - public void DumpDebugLogs() + /// Sets viewport options. + /// The committed options are visible as . + public Task SetViewportOptionsAsync(ViewportOptions options) { - NativeStatus.Check(NativeMethods.mln_map_dump_debug_logs(Handle)); + return NativeCompletion.SubmitCommandChecked(completion => + { + var nativeOptions = MapStructs.ToNative(options); + NativeStatus.Check( + NativeMethods.mln_map_set_viewport_options(Handle, &nativeOptions, completion) + ); + }); } - /// - /// Gets the map's logical viewport size in UI pixels and its pixel ratio. The scale factor is - /// fixed for the lifetime of the map and is independent of any render target's scale factor. - /// - public (uint Width, uint Height, double ScaleFactor) GetSize() + /// Sets tile tuning options. + /// The committed options are visible as . + public Task SetTileOptionsAsync(TileOptions options) { - uint width; - uint height; - double scaleFactor; - NativeStatus.Check(NativeMethods.mln_map_get_size(Handle, &width, &height, &scaleFactor)); - return (width, height, scaleFactor); + return NativeCompletion.SubmitCommandChecked(completion => + { + var nativeOptions = MapStructs.ToNative(options); + NativeStatus.Check( + NativeMethods.mln_map_set_tile_options(Handle, &nativeOptions, completion) + ); + }); } - /// Gets the map's viewport options. - public ViewportOptions GetViewportOptions() + /// Gets a synchronous camera snapshot. + public CameraSnapshot GetCameraSnapshot() { - var options = NativeMethods.mln_map_viewport_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_viewport_options(Handle, &options)); - return MapStructs.ViewportOptionsFromNative(options); + var camera = NativeMethods.mln_camera_options_default(); + ulong generation = 0; + NativeStatus.Check(NativeMethods.mln_map_camera_snapshot_get(Handle, &camera, &generation)); + return new CameraSnapshot(MapStructs.CameraOptionsFromNative(camera), generation); } - /// Sets viewport options, applying only non-null descriptor fields. - public void SetViewportOptions(ViewportOptions options) + /// Submits a copied camera update. + public Task UpdateCameraAsync(CameraUpdate update) { - var nativeOptions = MapStructs.ToNative(options); - NativeStatus.Check(NativeMethods.mln_map_set_viewport_options(Handle, &nativeOptions)); + return NativeCompletion.SubmitCommandChecked(completion => + { + var native = MapStructs.ToNative(update); + NativeStatus.Check(NativeMethods.mln_map_update_camera(Handle, &native, completion)); + }); } - /// Gets tile tuning options. - public TileOptions GetTileOptions() + /// Submits one relative camera operation. + public Task ApplyCameraDeltaAsync(CameraDelta delta) { - var options = NativeMethods.mln_map_tile_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_tile_options(Handle, &options)); - return MapStructs.TileOptionsFromNative(options); + return NativeCompletion.SubmitCommandChecked(completion => + { + var native = MapStructs.ToNative(delta); + NativeStatus.Check( + NativeMethods.mln_map_apply_camera_delta(Handle, &native, completion) + ); + }); } - /// Sets tile tuning options, applying only non-null descriptor fields. - public void SetTileOptions(TileOptions options) + /// Reads the camera in runtime order. + public Task QueryCameraAsync(CancellationToken cancellationToken = default) { - var nativeOptions = MapStructs.ToNative(options); - NativeStatus.Check(NativeMethods.mln_map_set_tile_options(Handle, &nativeOptions)); + return NativeCompletion + .Submit( + completion => NativeMethods.mln_map_camera_query(Handle, completion), + result => + { + var value = NativeCompletion.Value(result); + return new CameraSnapshot( + MapStructs.CameraOptionsFromNative(value.camera), + value.generation + ); + } + ) + .WaitAsync(cancellationToken); } - /// Gets the current camera descriptor. - public CameraOptions GetCamera() + public Task CameraForLatLngBoundsAsync( + LatLngBounds bounds, + CameraFitOptions? fitOptions, + CancellationToken cancellationToken = default + ) { - var camera = NativeMethods.mln_camera_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_camera(Handle, &camera)); - return MapStructs.CameraOptionsFromNative(camera); + return RunMapOperationAsync( + completion => + { + var nativeBounds = MapStructs.ToNative(bounds); + var nativeFitOptions = fitOptions is null + ? default + : MapStructs.ToNative(fitOptions); + return NativeMethods.mln_map_camera_for_lat_lng_bounds( + Handle, + nativeBounds, + fitOptions is null ? null : &nativeFitOptions, + completion + ); + }, + ReadCamera, + cancellationToken + ); } - /// Moves immediately to the camera descriptor, applying only non-null fields. - public void JumpTo(CameraOptions camera) + public Task CameraForLatLngsAsync( + IReadOnlyList coordinates, + CameraFitOptions? fitOptions, + CancellationToken cancellationToken = default + ) { - var nativeCamera = MapStructs.ToNative(camera); - NativeStatus.Check(NativeMethods.mln_map_jump_to(Handle, &nativeCamera)); - } - - /// Eases to the camera descriptor with animation options. - /// - /// A , or one with no - /// Duration, uses a duration of zero. Set Duration to animate. - /// - public void EaseTo(CameraOptions camera, AnimationOptions? animation) - { - var nativeCamera = MapStructs.ToNative(camera); - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_ease_to( - Handle, - &nativeCamera, - animation is null ? null : &nativeAnimation - ) + ArgumentNullException.ThrowIfNull(coordinates); + var nativeCoordinates = new mln_lat_lng[coordinates.Count]; + for (var index = 0; index < coordinates.Count; index++) + { + nativeCoordinates[index] = CoreStructs.ToNative(coordinates[index]); + } + return RunMapOperationAsync( + completion => + { + var nativeFitOptions = fitOptions is null + ? default + : MapStructs.ToNative(fitOptions); + fixed (mln_lat_lng* pointer = nativeCoordinates) + { + return NativeMethods.mln_map_camera_for_lat_lngs( + Handle, + nativeCoordinates.Length == 0 ? null : pointer, + (nuint)nativeCoordinates.Length, + fitOptions is null ? null : &nativeFitOptions, + completion + ); + } + }, + ReadCamera, + cancellationToken ); } - /// Flies to the camera descriptor with animation options. - /// - /// Unlike the other camera commands, a - /// , or one with no Duration, derives a - /// duration from the distance travelled at 1.2 rho-screenfuls per second. - /// - public void FlyTo(CameraOptions camera, AnimationOptions? animation) - { - var nativeCamera = MapStructs.ToNative(camera); - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_fly_to( - Handle, - &nativeCamera, - animation is null ? null : &nativeAnimation - ) + public Task CameraForGeometryAsync( + byte[] geometry, + CameraFitOptions? fitOptions, + CancellationToken cancellationToken = default + ) + { + using var nativeGeometry = NativeStringView.From(geometry, nameof(geometry)); + return RunMapOperationAsync( + completion => + { + var nativeFitOptions = fitOptions is null + ? default + : MapStructs.ToNative(fitOptions); + return NativeMethods.mln_map_camera_for_geometry( + Handle, + nativeGeometry.Value, + fitOptions is null ? null : &nativeFitOptions, + completion + ); + }, + ReadCamera, + cancellationToken ); } - /// Moves the map by a screen delta. - public void MoveBy(double deltaX, double deltaY) + public Task LatLngBoundsForCameraAsync( + CameraOptions camera, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_map_move_by(Handle, deltaX, deltaY)); - } - - /// Moves the map by a screen delta with animation options. - /// - /// A , or one with no - /// Duration, applies the change instantly; see . - /// - public void MoveByAnimated(double deltaX, double deltaY, AnimationOptions? animation) - { - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_move_by_animated( - Handle, - deltaX, - deltaY, - animation is null ? null : &nativeAnimation - ) + return RunMapOperationAsync( + completion => + { + var nativeCamera = MapStructs.ToNative(camera); + return NativeMethods.mln_map_lat_lng_bounds_for_camera( + Handle, + &nativeCamera, + completion + ); + }, + ReadBounds, + cancellationToken ); } - /// Scales the map around a screen anchor. - public void ScaleBy(double scale, ScreenPoint? anchor) + public Task LatLngBoundsForCameraUnwrappedAsync( + CameraOptions camera, + CancellationToken cancellationToken = default + ) { - var nativeAnchor = anchor is { } value ? MapStructs.ToNative(value) : default; - NativeStatus.Check( - NativeMethods.mln_map_scale_by(Handle, scale, anchor.HasValue ? &nativeAnchor : null) + return RunMapOperationAsync( + completion => + { + var nativeCamera = MapStructs.ToNative(camera); + return NativeMethods.mln_map_lat_lng_bounds_for_camera_unwrapped( + Handle, + &nativeCamera, + completion + ); + }, + ReadBounds, + cancellationToken ); } - /// Scales the map around a screen anchor with animation options. - /// - /// A , or one with no - /// Duration, applies the change instantly; see . - /// - public void ScaleByAnimated(double scale, ScreenPoint? anchor, AnimationOptions? animation) - { - var nativeAnchor = anchor is { } anchorValue ? MapStructs.ToNative(anchorValue) : default; - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_scale_by_animated( - Handle, - scale, - anchor.HasValue ? &nativeAnchor : null, - animation is null ? null : &nativeAnimation - ) - ); + /// Sets map bounds constraints. + /// The committed constraints are visible as . + public Task SetBoundsAsync(BoundOptions options) + { + return NativeCompletion.SubmitCommandChecked(completion => + { + var nativeOptions = MapStructs.ToNative(options); + NativeStatus.Check( + NativeMethods.mln_map_set_bounds(Handle, &nativeOptions, completion) + ); + }); } - /// Rotates around two screen points. - public void RotateBy(ScreenPoint first, ScreenPoint second) + /// Sets free-camera options. + /// The committed options are visible as . + public Task SetFreeCameraOptionsAsync(FreeCameraOptions options) { - var nativeFirst = MapStructs.ToNative(first); - var nativeSecond = MapStructs.ToNative(second); - NativeStatus.Check(NativeMethods.mln_map_rotate_by(Handle, nativeFirst, nativeSecond)); + return NativeCompletion.SubmitCommandChecked(completion => + { + var nativeOptions = MapStructs.ToNative(options); + NativeStatus.Check( + NativeMethods.mln_map_set_free_camera_options(Handle, &nativeOptions, completion) + ); + }); } - /// Rotates around two screen points with animation options. - /// - /// A , or one with no - /// Duration, applies the change instantly; see . - /// - public void RotateByAnimated(ScreenPoint first, ScreenPoint second, AnimationOptions? animation) - { - var nativeFirst = MapStructs.ToNative(first); - var nativeSecond = MapStructs.ToNative(second); - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_rotate_by_animated( - Handle, - nativeFirst, - nativeSecond, - animation is null ? null : &nativeAnimation - ) + public Task PixelForLatLngAsync( + LatLng coordinate, + CancellationToken cancellationToken = default + ) + { + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_pixel_for_lat_lng( + Handle, + CoreStructs.ToNative(coordinate), + completion + ), + result => MapStructs.FromNative(NativeCompletion.Value(result)), + cancellationToken ); } - /// Pitches the map by a delta in degrees. - public void PitchBy(double pitch) + public Task LatLngForPixelAsync( + ScreenPoint point, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_map_pitch_by(Handle, pitch)); - } - - /// Pitches the map by a delta in degrees with animation options. - /// - /// A , or one with no - /// Duration, applies the change instantly; see . - /// - public void PitchByAnimated(double pitch, AnimationOptions? animation) - { - var nativeAnimation = animation is null ? default : MapStructs.ToNative(animation); - NativeStatus.Check( - NativeMethods.mln_map_pitch_by_animated( - Handle, - pitch, - animation is null ? null : &nativeAnimation - ) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_lat_lng_for_pixel( + Handle, + MapStructs.ToNative(point), + completion + ), + result => CoreStructs.FromNative(NativeCompletion.Value(result)), + cancellationToken ); } - /// Cancels in-flight camera transitions. - public void CancelTransitions() + public Task PixelsForLatLngsAsync( + IReadOnlyList coordinates, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_map_cancel_transitions(Handle)); + ArgumentNullException.ThrowIfNull(coordinates); + var native = coordinates.Select(value => CoreStructs.ToNative(value)).ToArray(); + return RunMapOperationAsync( + completion => + { + fixed (mln_lat_lng* pointer = native) + { + return NativeMethods.mln_map_pixels_for_lat_lngs( + Handle, + native.Length == 0 ? null : pointer, + (nuint)native.Length, + completion + ); + } + }, + ReadPoints, + cancellationToken + ); } - /// Marks whether a host-driven gesture is in progress. - /// - /// The flag stays set until the host clears it: pair every - /// with a . - /// - public void SetGestureInProgress(bool inProgress) + public Task LatLngsForPixelsAsync( + IReadOnlyList points, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check( - NativeMethods.mln_map_set_gesture_in_progress(Handle, inProgress ? (byte)1 : (byte)0) + ArgumentNullException.ThrowIfNull(points); + var native = points.Select(value => MapStructs.ToNative(value)).ToArray(); + return RunMapOperationAsync( + completion => + { + fixed (mln_screen_point* pointer = native) + { + return NativeMethods.mln_map_lat_lngs_for_pixels( + Handle, + native.Length == 0 ? null : pointer, + (nuint)native.Length, + completion + ); + } + }, + ReadCoordinates, + cancellationToken ); } - /// Whether a host-driven gesture is currently in progress. - public bool IsGestureInProgress() + /// Creates a standalone projection in runtime order. + public Task CreateProjectionAsync( + CancellationToken cancellationToken = default + ) => MapProjectionHandle.CreateAsync(this, cancellationToken); + + /// Sets projection mode options. + public Task SetProjectionModeAsync(ProjectionModeOptions mode) { - bool inProgress = false; - NativeStatus.Check(NativeMethods.mln_map_is_gesture_in_progress(Handle, &inProgress)); - return inProgress; + return NativeCompletion.SubmitCommandChecked(completion => + { + var nativeMode = MapStructs.ToNative(mode); + NativeStatus.Check( + NativeMethods.mln_map_set_projection_mode(Handle, &nativeMode, completion) + ); + }); } - /// Calculates a camera that fits geographic bounds and fit options. - public CameraOptions CameraForLatLngBounds(LatLngBounds bounds, CameraFitOptions? fitOptions) + /// Loads a style URL. + public Task SetStyleUrlAsync(string url) { - var nativeBounds = MapStructs.ToNative(bounds); - var nativeFitOptions = fitOptions is null ? default : MapStructs.ToNative(fitOptions); - var camera = NativeMethods.mln_camera_options_default(); - NativeStatus.Check( - NativeMethods.mln_map_camera_for_lat_lng_bounds( - Handle, - nativeBounds, - fitOptions is null ? null : &nativeFitOptions, - &camera - ) - ); - return MapStructs.CameraOptionsFromNative(camera); + ArgumentNullException.ThrowIfNull(url); + using var nativeUrl = NativeUtf8String.FromNullableString(url, nameof(url)); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_style_url(Handle, nativeUrl.Pointer, completion) + ); + }); } - /// Calculates a camera that fits geographic coordinates and fit options. - public CameraOptions CameraForLatLngs( - IReadOnlyList coordinates, - CameraFitOptions? fitOptions - ) + /// Loads inline style JSON. + public Task SetStyleJsonAsync(byte[] json) { - ArgumentNullException.ThrowIfNull(coordinates); - var nativeCoordinates = new mln_lat_lng[coordinates.Count]; - for (var index = 0; index < coordinates.Count; index++) + ArgumentNullException.ThrowIfNull(json); + using var nativeJson = NativeStringView.From(json, nameof(json)); + return NativeCompletion.SubmitCommandChecked(completion => { - nativeCoordinates[index] = CoreStructs.ToNative(coordinates[index]); - } + NativeStatus.Check( + NativeMethods.mln_map_set_style_json(Handle, nativeJson.Value, completion) + ); + }); + } - var nativeFitOptions = fitOptions is null ? default : MapStructs.ToNative(fitOptions); - var camera = NativeMethods.mln_camera_options_default(); - fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + /// Sets per-feature state on this map. + public Task SetFeatureStateAsync(FeatureStateSelector selector, byte[] state) + { + ArgumentNullException.ThrowIfNull(selector); + ArgumentNullException.ThrowIfNull(state); + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeSelector = NativeFeatureStateSelector.From(selector); + using var nativeState = NativeStringView.From(state, nameof(state)); + var selectorValue = nativeSelector.Value; NativeStatus.Check( - NativeMethods.mln_map_camera_for_lat_lngs( + NativeMethods.mln_map_set_feature_state( Handle, - nativeCoordinates.Length == 0 ? null : coordinatesPointer, - (nuint)nativeCoordinates.Length, - fitOptions is null ? null : &nativeFitOptions, - &camera + &selectorValue, + nativeState.Value, + completion ) ); - } - return MapStructs.CameraOptionsFromNative(camera); + }); } - /// Calculates a camera that fits geographic geometry and fit options. - public CameraOptions CameraForGeometry(byte[] geometry, CameraFitOptions? fitOptions) + /// Copies per-feature state from this map. + public Task GetFeatureStateAsync( + FeatureStateSelector selector, + CancellationToken cancellationToken = default + ) { - using var nativeGeometry = NativeStringView.From(geometry, nameof(geometry)); - var nativeFitOptions = fitOptions is null ? default : MapStructs.ToNative(fitOptions); - var camera = NativeMethods.mln_camera_options_default(); - NativeStatus.Check( - NativeMethods.mln_map_camera_for_geometry( - Handle, - nativeGeometry.Value, - fitOptions is null ? null : &nativeFitOptions, - &camera - ) + ArgumentNullException.ThrowIfNull(selector); + return RunMapOperationAsync( + completion => + { + using var nativeSelector = NativeFeatureStateSelector.From(selector); + var selectorValue = nativeSelector.Value; + return NativeMethods.mln_map_get_feature_state(Handle, &selectorValue, completion); + }, + ReadBuffer, + cancellationToken ); - return MapStructs.CameraOptionsFromNative(camera); } - /// Computes geographic bounds for a camera from two viewport corners. - /// - /// The box is the hull of the top-left and bottom-right screen corners for that camera in the - /// current viewport. When bearing and pitch are zero, the box equals the visible area. Those - /// corners are the northwest and southeast of the viewport. Longitudes stay in -180 to 180. - /// - public LatLngBounds LatLngBoundsForCamera(CameraOptions camera) + /// Removes per-feature state from this map. + public Task RemoveFeatureStateAsync(FeatureStateSelector selector) { - var nativeCamera = MapStructs.ToNative(camera); - mln_lat_lng_bounds bounds = default; - NativeStatus.Check( - NativeMethods.mln_map_lat_lng_bounds_for_camera(Handle, &nativeCamera, &bounds) - ); - return MapStructs.FromNative(bounds); + ArgumentNullException.ThrowIfNull(selector); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSelector = NativeFeatureStateSelector.From(selector); + var selectorValue = nativeSelector.Value; + NativeStatus.Check( + NativeMethods.mln_map_remove_feature_state(Handle, &selectorValue, completion) + ); + }); } - /// Computes geographic bounds for a camera from the four viewport corners. - /// - /// The axis-aligned hull of all four screen corners and the center encompasses the projected - /// viewport. Longitudes unwrap onto the shortest path through the center. A viewport that - /// crosses the antimeridian reports values outside -180 to 180. - /// - public LatLngBounds LatLngBoundsForCameraUnwrapped(CameraOptions camera) - { - var nativeCamera = MapStructs.ToNative(camera); - mln_lat_lng_bounds bounds = default; - NativeStatus.Check( - NativeMethods.mln_map_lat_lng_bounds_for_camera_unwrapped( - Handle, - &nativeCamera, - &bounds - ) + /// Gets the style document that this map's style was last parsed from. + public Task GetLoadedStyleJsonAsync(CancellationToken cancellationToken = default) => + RunMapOperationAsync( + completion => NativeMethods.mln_map_loaded_style_json(Handle, completion), + ReadBuffer, + cancellationToken ); - return MapStructs.FromNative(bounds); - } - /// Gets map bounds constraints. - public BoundOptions GetBounds() - { - var options = NativeMethods.mln_bound_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_bounds(Handle, &options)); - return MapStructs.BoundOptionsFromNative(options); - } + /// Gets the URL that this map's style was last requested from. + public Task GetStyleUrlAsync(CancellationToken cancellationToken = default) => + RunMapOperationAsync( + completion => NativeMethods.mln_map_style_url(Handle, completion), + result => System.Text.Encoding.UTF8.GetString(ReadBuffer(result)), + cancellationToken + ); - /// Sets map bounds constraints, applying only non-null descriptor fields. - public void SetBounds(BoundOptions options) + /// Selects which map-originated event types this map queues. + public Task SetEventMaskAsync(RuntimeEventMask mask) { - var nativeOptions = MapStructs.ToNative(options); - NativeStatus.Check(NativeMethods.mln_map_set_bounds(Handle, &nativeOptions)); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_event_mask(Handle, (ulong)mask, completion) + ); + }); } - /// Gets free-camera options. - public FreeCameraOptions GetFreeCameraOptions() - { - var options = NativeMethods.mln_free_camera_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_free_camera_options(Handle, &options)); - return MapStructs.FreeCameraOptionsFromNative(options); - } + /// Reports the event mask in the latest committed map snapshot. + public RuntimeEventMask GetEventMask() => GetSnapshot().EventMask; - /// Sets free-camera options, applying only non-null descriptor fields. - public void SetFreeCameraOptions(FreeCameraOptions options) + public Task AddStyleSourceJsonAsync(string sourceId, byte[] sourceJson) { - var nativeOptions = MapStructs.ToNative(options); - NativeStatus.Check(NativeMethods.mln_map_set_free_camera_options(Handle, &nativeOptions)); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeJson = NativeStringView.From(sourceJson, nameof(sourceJson)); + NativeStatus.Check( + NativeMethods.mln_map_add_style_source_json( + Handle, + nativeSourceId.Value, + nativeJson.Value, + completion + ) + ); + }); } - /// Converts a geographic coordinate to a screen pixel using the current map projection. - public ScreenPoint PixelForLatLng(LatLng coordinate) + public Task RemoveStyleSourceAsync(string sourceId) { - var nativeCoordinate = CoreStructs.ToNative(coordinate); - mln_screen_point point = default; - NativeStatus.Check( - NativeMethods.mln_map_pixel_for_lat_lng(Handle, nativeCoordinate, &point) - ); - return MapStructs.FromNative(point); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + NativeStatus.Check( + NativeMethods.mln_map_remove_style_source(Handle, nativeSourceId.Value, completion) + ); + }); } - /// Converts a screen pixel to a geographic coordinate using the current map projection. - public LatLng LatLngForPixel(ScreenPoint point) - { - var nativePoint = MapStructs.ToNative(point); - mln_lat_lng coordinate = default; - NativeStatus.Check( - NativeMethods.mln_map_lat_lng_for_pixel(Handle, nativePoint, &coordinate) + /// Lists style source IDs in style order. + public Task StyleSourceIdsAsync(CancellationToken cancellationToken = default) => + RunMapOperationAsync( + completion => NativeMethods.mln_map_list_style_source_ids(Handle, completion), + ReadStrings, + cancellationToken ); - return CoreStructs.FromNative(coordinate); - } - /// Converts geographic coordinates to screen pixels using the current map projection. - public ScreenPoint[] PixelsForLatLngs(IReadOnlyList coordinates) + /// Adds a GeoJSON source that loads data from a URL. + /// + /// is fixed when the source is created; + /// and keep it. + /// + public Task AddGeoJsonSourceUrlAsync( + string sourceId, + string url, + GeoJsonSourceOptions? options + ) { - ArgumentNullException.ThrowIfNull(coordinates); - if (coordinates.Count == 0) - { - return []; - } - - var nativeCoordinates = new mln_lat_lng[coordinates.Count]; - var points = new mln_screen_point[coordinates.Count]; - for (var index = 0; index < coordinates.Count; index++) - { - nativeCoordinates[index] = CoreStructs.ToNative(coordinates[index]); - } - - fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) - fixed (mln_screen_point* pointsPointer = points) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); + using var nativeOptions = options is null + ? null + : NativeGeoJsonSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; NativeStatus.Check( - NativeMethods.mln_map_pixels_for_lat_lngs( + NativeMethods.mln_map_add_geojson_source_url( Handle, - coordinatesPointer, - (nuint)nativeCoordinates.Length, - pointsPointer + nativeSourceId.Value, + nativeUrl.Value, + nativeOptions is null ? null : &optionsValue, + completion ) ); - } - - var result = new ScreenPoint[points.Length]; - for (var index = 0; index < result.Length; index++) - { - result[index] = MapStructs.FromNative(points[index]); - } - return result; + }); } - /// Converts screen pixels to geographic coordinates using the current map projection. - public LatLng[] LatLngsForPixels(IReadOnlyList points) + /// Updates a GeoJSON source to load data from a URL. + public Task SetGeoJsonSourceUrlAsync(string sourceId, string url) { - ArgumentNullException.ThrowIfNull(points); - if (points.Count == 0) - { - return []; - } - - var nativePoints = new mln_screen_point[points.Count]; - var coordinates = new mln_lat_lng[points.Count]; - for (var index = 0; index < points.Count; index++) - { - nativePoints[index] = MapStructs.ToNative(points[index]); - } - - fixed (mln_screen_point* pointsPointer = nativePoints) - fixed (mln_lat_lng* coordinatesPointer = coordinates) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); NativeStatus.Check( - NativeMethods.mln_map_lat_lngs_for_pixels( + NativeMethods.mln_map_set_geojson_source_url( Handle, - pointsPointer, - (nuint)nativePoints.Length, - coordinatesPointer + nativeSourceId.Value, + nativeUrl.Value, + completion ) ); - } - - var result = new LatLng[coordinates.Length]; - for (var index = 0; index < result.Length; index++) - { - result[index] = CoreStructs.FromNative(coordinates[index]); - } - return result; - } - - /// Creates a standalone projection snapshot from the map's current camera state. - public MapProjectionHandle CreateProjection() - { - return MapProjectionHandle.Create(this); - } - - /// Gets projection mode options. - public ProjectionModeOptions GetProjectionMode() - { - var mode = NativeMethods.mln_projection_mode_default(); - NativeStatus.Check(NativeMethods.mln_map_get_projection_mode(Handle, &mode)); - return MapStructs.ProjectionModeOptionsFromNative(mode); + }); } - /// Sets projection mode options, applying only non-null descriptor fields. - public void SetProjectionMode(ProjectionModeOptions mode) - { - var nativeMode = MapStructs.ToNative(mode); - NativeStatus.Check(NativeMethods.mln_map_set_projection_mode(Handle, &nativeMode)); - } - - /// Loads a style URL through MapLibre Native style APIs. + /// Adds a GeoJSON source backed by prepared data. /// - /// Loading is asynchronous: a style that is missing, unreachable, or malformed - /// returns normally here and reports through a - /// runtime event. A style - /// MapLibre rejects semantically produces neither an exception nor an event. + /// The call borrows ; the source adopts the options baked into it + /// when the data was prepared and keeps its own reference, so the handle may be released + /// afterward. /// - public void SetStyleUrl(string url) + public Task AddGeoJsonSourceDataAsync( + string sourceId, + GeoJsonSourceDataHandle data + ) { - ArgumentNullException.ThrowIfNull(url); - using var nativeUrl = NativeUtf8String.FromNullableString(url, nameof(url)); - NativeStatus.Check(NativeMethods.mln_map_set_style_url(Handle, nativeUrl.Pointer)); - } - - /// Loads inline style JSON through MapLibre Native style APIs. - /// - /// Malformed JSON is reported twice: this call throws the parse error, and the - /// same message also arrives as a - /// runtime event. A style - /// MapLibre rejects semantically produces neither an exception nor an event. - /// - public void SetStyleJson(byte[] json) - { - ArgumentNullException.ThrowIfNull(json); - using var nativeJson = NativeStringView.From(json, nameof(json)); - NativeStatus.Check(NativeMethods.mln_map_set_style_json(Handle, nativeJson.Value)); - } - - /// Sets per-feature state on this map. - public void SetFeatureState(FeatureStateSelector selector, byte[] value) - { - using var nativeSelector = NativeFeatureStateSelector.From(selector); - using var nativeValue = NativeStringView.From(value, nameof(value)); - var selectorValue = nativeSelector.Value; - NativeStatus.Check( - NativeMethods.mln_map_set_feature_state(Handle, &selectorValue, nativeValue.Value) - ); - } - - /// Copies per-feature state from this map. - public byte[] GetFeatureState(FeatureStateSelector selector) - { - using var nativeSelector = NativeFeatureStateSelector.From(selector); - var selectorValue = nativeSelector.Value; - MlnBuffer buffer = default; - NativeStatus.Check( - NativeMethods.mln_map_get_feature_state(Handle, &selectorValue, &buffer) - ); - return ValueStructs.ReadBuffer(buffer); - } - - /// Removes per-feature state from this map. - public void RemoveFeatureState(FeatureStateSelector selector) - { - using var nativeSelector = NativeFeatureStateSelector.From(selector); - var selectorValue = nativeSelector.Value; - NativeStatus.Check(NativeMethods.mln_map_remove_feature_state(Handle, &selectorValue)); - } - - /// Gets the style document this map's style was last parsed from. - /// - /// This is the loaded document, not a serialization of the live style: runtime - /// mutations do not change it, and a failed parse leaves the previously parsed - /// document in place. The result is empty when no document has been parsed. - /// - public byte[] GetLoadedStyleJson() => - CopyMapBytes( - (map, text, capacity, size) => - NativeMethods.mln_map_copy_loaded_style_json(map, text, capacity, size) - ); - - /// Gets the URL this map's style was last requested from. - /// - /// records the URL when the request is made, before - /// the response arrives, and clears it, so this can - /// disagree with . The result is empty for - /// inline JSON, for a map with no style, and for an empty URL alike. - /// - public string GetStyleUrl() => - CopyMapText( - (map, text, capacity, size) => - NativeMethods.mln_map_copy_style_url(map, text, capacity, size) - ); - - /// Selects which map-originated event types this map queues. - /// - /// The mask reads the bits in and ignores the rest, - /// so selects every map-originated type. An event type this - /// mask clears is never queued, so it neither reaches a batch nor wakes a parked - /// . Select every event type the host reads, because - /// , the two still-image types, and - /// carry state a host reaches no - /// other way. Narrowing gates later events and keeps queued ones, so a host drains what it - /// already caused. - /// - public void SetEventMask(RuntimeEventMask mask) - { - NativeStatus.Check(NativeMethods.mln_map_set_event_mask(Handle, (ulong)mask)); - } - - /// Reports which map-originated event types this map queues. - /// - /// The value is the mask last set, including bits outside - /// that this map ignores. A map created with the - /// default reports every event type this library selects - /// by default, including any this binding does not declare. - /// - public RuntimeEventMask GetEventMask() - { - ulong mask = 0; - NativeStatus.Check(NativeMethods.mln_map_get_event_mask(Handle, &mask)); - return (RuntimeEventMask)mask; - } - - /// Adds a style source from UTF-8 JSON bytes. - public void AddStyleSourceJson(string sourceId, byte[] sourceJson) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeJson = NativeStringView.From(sourceJson, nameof(sourceJson)); - NativeStatus.Check( - NativeMethods.mln_map_add_style_source_json( - Handle, - nativeSourceId.Value, - nativeJson.Value - ) - ); - } - - /// Removes a style source and reports whether it existed. - public bool RemoveStyleSource(string sourceId) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - bool removed = false; - NativeStatus.Check( - NativeMethods.mln_map_remove_style_source(Handle, nativeSourceId.Value, &removed) - ); - return removed; - } - - /// Whether a style source exists. - public bool StyleSourceExists(string sourceId) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - bool exists = false; - NativeStatus.Check( - NativeMethods.mln_map_style_source_exists(Handle, nativeSourceId.Value, &exists) - ); - return exists; - } - - /// Gets a style source type when the source exists. - public SourceType? StyleSourceType(string sourceId) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - uint sourceType = 0; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_source_type( - Handle, - nativeSourceId.Value, - &sourceType, - &found - ) - ); - return found ? (SourceType)sourceType : null; - } - - /// Gets copied style source metadata when the source exists. - public SourceInfo? StyleSourceInfo(string sourceId) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - var info = new mln_style_source_info { size = (uint)sizeof(mln_style_source_info) }; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_source_info(Handle, nativeSourceId.Value, &info, &found) - ); - if (!found) - { - return null; - } - - string? attribution = null; - if (info.has_attribution != 0) - { - attribution = string.Empty; - if (info.attribution_size > 0) - { - var buffer = new byte[checked((int)info.attribution_size)]; - nuint attributionSize = 0; - bool attributionFound = false; - fixed (byte* bufferPointer = buffer) - { - NativeStatus.Check( - NativeMethods.mln_map_copy_style_source_attribution( - Handle, - nativeSourceId.Value, - (sbyte*)bufferPointer, - (nuint)buffer.Length, - &attributionSize, - &attributionFound - ) - ); - } - - if (!attributionFound) - { - return null; - } - - if (attributionSize > (nuint)buffer.Length) - { - throw new InvalidOperationException( - $"Native style source attribution size {attributionSize} exceeds buffer length {buffer.Length}." - ); - } - - fixed (byte* bufferPointer = buffer) - { - attribution = RuntimeStructs.CopyUtf8((sbyte*)bufferPointer, attributionSize); - } - } - } - - var fields = (mln_style_source_info_field)info.fields; - string? url = null; - if (fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_URL)) - { - url = string.Empty; - if (info.url_size > 0) - { - var buffer = new byte[checked((int)info.url_size)]; - nuint urlSize = 0; - bool urlFound = false; - fixed (byte* bufferPointer = buffer) - { - NativeStatus.Check( - NativeMethods.mln_map_copy_style_source_url( - Handle, - nativeSourceId.Value, - (sbyte*)bufferPointer, - (nuint)buffer.Length, - &urlSize, - &urlFound - ) - ); - } - - if (!urlFound) - { - return null; - } - - if (urlSize > (nuint)buffer.Length) - { - throw new InvalidOperationException( - $"Native style source URL size {urlSize} exceeds buffer length {buffer.Length}." - ); - } - - fixed (byte* bufferPointer = buffer) - { - url = RuntimeStructs.CopyUtf8((sbyte*)bufferPointer, urlSize); - } - } - } - - TileJson? tileJson = null; - if (fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_TILEJSON)) + ArgumentNullException.ThrowIfNull(data); + return NativeCompletion.SubmitCommandChecked(completion => { - MlnStyleStringList tileUrlList = default; - bool tileUrlsFound = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_source_tile_urls( - Handle, - nativeSourceId.Value, - &tileUrlList, - &tileUrlsFound - ) - ); - if (!tileUrlsFound) + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + data.WithLive(prepared => { - return null; - } - - var tileUrls = CopyStyleStringList(tileUrlList); - tileJson = new TileJson( - tileUrls, - info.min_zoom, - info.max_zoom, - (TileScheme)info.scheme, - info.scheme, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_BOUNDS) - ? MapStructs.FromNative(info.bounds) - : null - ); - } - - return new SourceInfo( - sourceId, - (SourceType)info.type, - info.type, - info.is_volatile != 0, - attribution, - url, - tileJson, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_TILE_SIZE) - ? info.tile_size - : null, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) - ? (VectorTileEncoding)info.vector_encoding - : null, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) - ? info.vector_encoding - : null, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) - ? (RasterDemEncoding)info.raster_encoding - : null, - fields.HasFlag(mln_style_source_info_field.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) - ? info.raster_encoding - : null - ); - } - - /// Lists style source IDs in style order. - public string[] StyleSourceIds() - { - MlnStyleIdList list = default; - NativeStatus.Check(NativeMethods.mln_map_list_style_source_ids(Handle, &list)); - return CopyStyleIdList(list); - } - - /// Adds a GeoJSON source that loads data from a URL. - /// - /// is fixed when the source is created; - /// and keep it. - /// - public void AddGeoJsonSourceUrl(string sourceId, string url, GeoJsonSourceOptions? options) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - using var nativeOptions = options is null ? null : NativeGeoJsonSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_geojson_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value, - nativeOptions is null ? null : &optionsValue - ) - ); - } - - /// Updates a GeoJSON source to load data from a URL. - public void SetGeoJsonSourceUrl(string sourceId, string url) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - NativeStatus.Check( - NativeMethods.mln_map_set_geojson_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value - ) - ); + NativeStatus.Check( + NativeMethods.mln_map_add_geojson_source_data( + Handle, + nativeSourceId.Value, + prepared, + completion + ) + ); + }); + }); } - /// Adds a GeoJSON source backed by prepared data. + /// Updates a GeoJSON source with prepared data. /// - /// The call borrows ; the source adopts the options baked into it - /// when the data was prepared and keeps its own reference, so the handle may be released - /// afterward. + /// The call borrows ; the command fails when the options baked into + /// it differ from the source's, except for cluster properties. /// - public void AddGeoJsonSourceData(string sourceId, GeoJsonSourceDataHandle data) + public Task SetGeoJsonSourceDataAsync( + string sourceId, + GeoJsonSourceDataHandle data + ) { ArgumentNullException.ThrowIfNull(data); - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - data.WithLive(prepared => - NativeStatus.Check( - NativeMethods.mln_map_add_geojson_source_data( - Handle, - nativeSourceId.Value, - prepared - ) - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + data.WithLive(prepared => + { + NativeStatus.Check( + NativeMethods.mln_map_set_geojson_source_data( + Handle, + nativeSourceId.Value, + prepared, + completion + ) + ); + }); + }); } - /// Updates a GeoJSON source with prepared data. + /// Overrides synchronous tiling for a GeoJSON source. /// - /// The call borrows and fails when the options baked into it - /// differ from the source's, except for cluster properties. + /// The effective behavior is the source's baked-in option OR this override. /// - public void SetGeoJsonSourceData(string sourceId, GeoJsonSourceDataHandle data) + public Task SetGeoJsonSourceSynchronousTilingAsync( + string sourceId, + bool enabled + ) { - ArgumentNullException.ThrowIfNull(data); - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - data.WithLive(prepared => + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); NativeStatus.Check( - NativeMethods.mln_map_set_geojson_source_data( + NativeMethods.mln_map_set_geojson_source_synchronous_tiling( Handle, nativeSourceId.Value, - prepared + enabled ? (byte)1 : (byte)0, + completion ) - ) - ); - } - - /// Overrides synchronous tiling for a GeoJSON source. - /// - /// The effective behavior is the source's baked-in option OR this override. - /// - public void SetGeoJsonSourceSynchronousTiling(string sourceId, bool enabled) - { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - NativeStatus.Check( - NativeMethods.mln_map_set_geojson_source_synchronous_tiling( - Handle, - nativeSourceId.Value, - enabled ? (byte)1 : (byte)0 - ) - ); + ); + }); } /// Adds a custom geometry source with tile callbacks. @@ -1043,76 +809,107 @@ public void SetGeoJsonSourceSynchronousTiling(string sourceId, bool enabled) /// The upcall stubs this installs live until MapLibre stops referencing them: until the source /// is removed, until a style load leaves a style without it, or until this map closes. The /// binding releases them from the callback the C API invokes then, so nothing here depends on - /// the events selects. + /// the events selects. /// - public void AddCustomGeometrySource(string sourceId, CustomGeometrySourceOptions options) + public Task AddCustomGeometrySourceAsync( + string sourceId, + CustomGeometrySourceOptions options + ) { ArgumentNullException.ThrowIfNull(options); - AddCustomGeometrySource(sourceId, new CustomGeometrySourceState(options)); + return AddCustomGeometrySourceAsync(sourceId, new CustomGeometrySourceState(options)); } - internal void AddCustomGeometrySource(string sourceId, CustomGeometrySourceState sourceState) + internal Task AddCustomGeometrySourceAsync( + string sourceId, + CustomGeometrySourceState sourceState + ) { using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); try { - var descriptor = sourceState.Descriptor; - NativeStatus.Check( - AddCustomGeometrySourceNative(Handle, nativeSourceId.Value, &descriptor) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + var descriptor = sourceState.Descriptor; + NativeStatus.Check( + AddCustomGeometrySourceNative( + Handle, + nativeSourceId.Value, + &descriptor, + completion + ) + ); + }); } catch { - // A rejected add never referenced the state, so no release callback is owed for it. sourceState.Dispose(); throw; } } /// Sets custom geometry source tile data. - public void SetCustomGeometrySourceTileData( + public Task SetCustomGeometrySourceTileDataAsync( string sourceId, CanonicalTileId tileId, byte[] data ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeData = NativeStringView.From(data, nameof(data)); - var nativeTileId = StyleStructs.ToNative(tileId); - NativeStatus.Check( - NativeMethods.mln_map_set_custom_geometry_source_tile_data( - Handle, - nativeSourceId.Value, - nativeTileId, - nativeData.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeData = NativeStringView.From(data, nameof(data)); + var nativeTileId = StyleStructs.ToNative(tileId); + NativeStatus.Check( + NativeMethods.mln_map_set_custom_geometry_source_tile_data( + Handle, + nativeSourceId.Value, + nativeTileId, + nativeData.Value, + completion + ) + ); + }); } /// Invalidates one custom geometry source tile. - public void InvalidateCustomGeometrySourceTile(string sourceId, CanonicalTileId tileId) + public Task InvalidateCustomGeometrySourceTileAsync( + string sourceId, + CanonicalTileId tileId + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - NativeStatus.Check( - NativeMethods.mln_map_invalidate_custom_geometry_source_tile( - Handle, - nativeSourceId.Value, - StyleStructs.ToNative(tileId) - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + NativeStatus.Check( + NativeMethods.mln_map_invalidate_custom_geometry_source_tile( + Handle, + nativeSourceId.Value, + StyleStructs.ToNative(tileId), + completion + ) + ); + }); } /// Invalidates custom geometry source tiles that intersect bounds. - public void InvalidateCustomGeometrySourceRegion(string sourceId, LatLngBounds bounds) + public Task InvalidateCustomGeometrySourceRegionAsync( + string sourceId, + LatLngBounds bounds + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - NativeStatus.Check( - NativeMethods.mln_map_invalidate_custom_geometry_source_region( - Handle, - nativeSourceId.Value, - MapStructs.ToNative(bounds) - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + NativeStatus.Check( + NativeMethods.mln_map_invalidate_custom_geometry_source_region( + Handle, + nativeSourceId.Value, + MapStructs.ToNative(bounds), + completion + ) + ); + }); } /// Adds a custom MVT vector source with tile callbacks. @@ -1120,23 +917,37 @@ public void InvalidateCustomGeometrySourceRegion(string sourceId, LatLngBounds b /// The upcall stubs this installs live until MapLibre stops referencing them: until the source /// is removed, until a style load leaves a style without it, or until this map closes. The /// binding releases them from the callback the C API invokes then, so nothing here depends on - /// the events selects. + /// the events selects. /// - public void AddCustomMvtVectorSource(string sourceId, CustomMvtVectorSourceOptions options) + public Task AddCustomMvtVectorSourceAsync( + string sourceId, + CustomMvtVectorSourceOptions options + ) { ArgumentNullException.ThrowIfNull(options); - AddCustomMvtVectorSource(sourceId, new CustomMvtVectorSourceState(options)); + return AddCustomMvtVectorSourceAsync(sourceId, new CustomMvtVectorSourceState(options)); } - internal void AddCustomMvtVectorSource(string sourceId, CustomMvtVectorSourceState sourceState) + internal Task AddCustomMvtVectorSourceAsync( + string sourceId, + CustomMvtVectorSourceState sourceState + ) { using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); try { - var descriptor = sourceState.Descriptor; - NativeStatus.Check( - AddCustomMvtVectorSourceNative(Handle, nativeSourceId.Value, &descriptor) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + var descriptor = sourceState.Descriptor; + NativeStatus.Check( + AddCustomMvtVectorSourceNative( + Handle, + nativeSourceId.Value, + &descriptor, + completion + ) + ); + }); } catch { @@ -1147,999 +958,927 @@ internal void AddCustomMvtVectorSource(string sourceId, CustomMvtVectorSourceSta } /// Sets custom MVT vector source tile data. - public void SetCustomMvtVectorSourceTileData( + public Task SetCustomMvtVectorSourceTileDataAsync( string sourceId, CanonicalTileId tileId, byte[] data ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeData = NativeStringView.From(data, nameof(data)); - var nativeTileId = StyleStructs.ToNative(tileId); - NativeStatus.Check( - NativeMethods.mln_map_set_custom_mvt_vector_source_tile_data( - Handle, - nativeSourceId.Value, - nativeTileId, - nativeData.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeData = NativeStringView.From(data, nameof(data)); + var nativeTileId = StyleStructs.ToNative(tileId); + NativeStatus.Check( + NativeMethods.mln_map_set_custom_mvt_vector_source_tile_data( + Handle, + nativeSourceId.Value, + nativeTileId, + nativeData.Value, + completion + ) + ); + }); } /// Reports a custom MVT vector source error for one tile. - public void SetCustomMvtVectorSourceTileError( + public Task SetCustomMvtVectorSourceTileErrorAsync( string sourceId, CanonicalTileId tileId, string message ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeMessage = NativeStringView.From(message, nameof(message)); - var nativeTileId = StyleStructs.ToNative(tileId); - NativeStatus.Check( - NativeMethods.mln_map_set_custom_mvt_vector_source_tile_error( - Handle, - nativeSourceId.Value, - nativeTileId, - nativeMessage.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeMessage = NativeStringView.From(message, nameof(message)); + var nativeTileId = StyleStructs.ToNative(tileId); + NativeStatus.Check( + NativeMethods.mln_map_set_custom_mvt_vector_source_tile_error( + Handle, + nativeSourceId.Value, + nativeTileId, + nativeMessage.Value, + completion + ) + ); + }); } /// Invalidates one custom MVT vector source tile. - public void InvalidateCustomMvtVectorSourceTile(string sourceId, CanonicalTileId tileId) + public Task InvalidateCustomMvtVectorSourceTileAsync( + string sourceId, + CanonicalTileId tileId + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - NativeStatus.Check( - NativeMethods.mln_map_invalidate_custom_mvt_vector_source_tile( - Handle, - nativeSourceId.Value, - StyleStructs.ToNative(tileId) - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + NativeStatus.Check( + NativeMethods.mln_map_invalidate_custom_mvt_vector_source_tile( + Handle, + nativeSourceId.Value, + StyleStructs.ToNative(tileId), + completion + ) + ); + }); } /// Adds a vector source that loads TileJSON from a URL. - public void AddVectorSourceUrl(string sourceId, string url, TileSourceOptions? options) + public Task AddVectorSourceUrlAsync( + string sourceId, + string url, + TileSourceOptions? options + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_vector_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_vector_source_url( + Handle, + nativeSourceId.Value, + nativeUrl.Value, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Adds a vector source from inline tile URL templates. - public void AddVectorSourceTiles( + public Task AddVectorSourceTilesAsync( string sourceId, IReadOnlyList tiles, TileSourceOptions? options ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_vector_source_tiles( - Handle, - nativeSourceId.Value, - nativeTiles.Count == 0 ? null : nativeTiles.Pointer, - nativeTiles.Count, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_vector_source_tiles( + Handle, + nativeSourceId.Value, + nativeTiles.Count == 0 ? null : nativeTiles.Pointer, + nativeTiles.Count, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Adds a raster source that loads TileJSON from a URL. - public void AddRasterSourceUrl(string sourceId, string url, TileSourceOptions? options) + public Task AddRasterSourceUrlAsync( + string sourceId, + string url, + TileSourceOptions? options + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_raster_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_raster_source_url( + Handle, + nativeSourceId.Value, + nativeUrl.Value, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Adds a raster source from inline tile URL templates. - public void AddRasterSourceTiles( + public Task AddRasterSourceTilesAsync( string sourceId, IReadOnlyList tiles, TileSourceOptions? options ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_raster_source_tiles( - Handle, - nativeSourceId.Value, - nativeTiles.Count == 0 ? null : nativeTiles.Pointer, - nativeTiles.Count, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_raster_source_tiles( + Handle, + nativeSourceId.Value, + nativeTiles.Count == 0 ? null : nativeTiles.Pointer, + nativeTiles.Count, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Adds a raster DEM source that loads TileJSON from a URL. - public void AddRasterDemSourceUrl(string sourceId, string url, TileSourceOptions? options) + public Task AddRasterDemSourceUrlAsync( + string sourceId, + string url, + TileSourceOptions? options + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_raster_dem_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_raster_dem_source_url( + Handle, + nativeSourceId.Value, + nativeUrl.Value, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Adds a raster DEM source from inline tile URL templates. - public void AddRasterDemSourceTiles( + public Task AddRasterDemSourceTilesAsync( string sourceId, IReadOnlyList tiles, TileSourceOptions? options ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); - using var nativeOptions = options is null ? null : NativeTileSourceOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_add_raster_dem_source_tiles( - Handle, - nativeSourceId.Value, - nativeTiles.Count == 0 ? null : nativeTiles.Pointer, - nativeTiles.Count, - nativeOptions is null ? null : &optionsValue - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeTiles = NativeStringViewArray.From(tiles, nameof(tiles)); + using var nativeOptions = options is null + ? null + : NativeTileSourceOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; + NativeStatus.Check( + NativeMethods.mln_map_add_raster_dem_source_tiles( + Handle, + nativeSourceId.Value, + nativeTiles.Count == 0 ? null : nativeTiles.Pointer, + nativeTiles.Count, + nativeOptions is null ? null : &optionsValue, + completion + ) + ); + }); } /// Sets or replaces a style image. - public void SetStyleImage( + public Task SetStyleImageAsync( string imageId, PremultipliedRgba8Image image, StyleImageOptions? options ) { - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - using var nativeImage = NativeStyleImage.From(image); - var imageValue = nativeImage.Value; - using var nativeOptions = options is null ? null : NativeStyleImageOptions.From(options); - var optionsValue = nativeOptions?.Value ?? default; - NativeStatus.Check( - NativeMethods.mln_map_set_style_image( - Handle, - nativeImageId.Value, - &imageValue, - nativeOptions is null ? null : &optionsValue - ) - ); - } - - /// Removes a style image and reports whether it existed. - public bool RemoveStyleImage(string imageId) - { - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - bool removed = false; - NativeStatus.Check( - NativeMethods.mln_map_remove_style_image(Handle, nativeImageId.Value, &removed) - ); - return removed; - } - - /// Whether a style image exists. - public bool StyleImageExists(string imageId) - { - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - bool exists = false; - NativeStatus.Check( - NativeMethods.mln_map_style_image_exists(Handle, nativeImageId.Value, &exists) - ); - return exists; - } - - /// Gets style image metadata when the image exists. - public StyleImageInfo? StyleImageInfo(string imageId) - { - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - var info = NativeMethods.mln_style_image_info_default(); - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_image_info(Handle, nativeImageId.Value, &info, &found) - ); - return found ? StyleStructs.FromNative(info) : null; - } - - /// Copies a style image's stretchable intervals when it exists. - public ( - IReadOnlyList StretchX, - IReadOnlyList StretchY - )? StyleImageStretches(string imageId) - { - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - nuint xCount = 0; - nuint yCount = 0; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_copy_style_image_stretches( - Handle, - nativeImageId.Value, - null, - 0, - &xCount, - null, - 0, - &yCount, - &found - ) - ); - if (!found) - { - return null; - } - - var rawX = new mln_image_stretch[checked((int)xCount)]; - var rawY = new mln_image_stretch[checked((int)yCount)]; - fixed (mln_image_stretch* pointerX = rawX) - fixed (mln_image_stretch* pointerY = rawY) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); + using var nativeImage = NativeStyleImage.From(image); + var imageValue = nativeImage.Value; + using var nativeOptions = options is null + ? null + : NativeStyleImageOptions.From(options); + var optionsValue = nativeOptions?.Value ?? default; NativeStatus.Check( - NativeMethods.mln_map_copy_style_image_stretches( + NativeMethods.mln_map_set_style_image( Handle, nativeImageId.Value, - rawX.Length == 0 ? null : pointerX, - (nuint)rawX.Length, - &xCount, - rawY.Length == 0 ? null : pointerY, - (nuint)rawY.Length, - &yCount, - &found + &imageValue, + nativeOptions is null ? null : &optionsValue, + completion ) ); - } - return (ToStretches(rawX), ToStretches(rawY)); + }); } - private static IReadOnlyList ToStretches(mln_image_stretch[] raw) => - Array.ConvertAll(raw, stretch => new ImageStretch(stretch.from, stretch.to)); - - private static IReadOnlyList? NullIfEmpty(IReadOnlyList? values) => - values is null || values.Count == 0 ? null : values; - - /// Copies a style image as premultiplied RGBA8 pixels when it exists. - public StyleImage? CopyStyleImagePremultipliedRgba8(string imageId) + /// Removes a runtime style image. + public Task RemoveStyleImageAsync(string imageId) { - var info = StyleImageInfo(imageId); - if (info is null) - { - return null; - } - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - var bytes = new byte[checked((int)info.ByteLength)]; - nuint byteLength = 0; - bool found = false; - fixed (byte* bytesPointer = bytes) - { + return NativeCompletion.SubmitCommandChecked(completion => NativeStatus.Check( - NativeMethods.mln_map_copy_style_image_premultiplied_rgba8( - Handle, - nativeImageId.Value, - bytes.Length == 0 ? null : bytesPointer, - (nuint)bytes.Length, - &byteLength, - &found - ) - ); - } + NativeMethods.mln_map_remove_style_image(Handle, nativeImageId.Value, completion) + ) + ); + } - if (!found) - { - return null; - } + /// Submits a command that removes one runtime style image. + /// + /// The command's command completion event reports + /// when no image has . + /// + private static IReadOnlyList ToStretches(mln_image_stretch[] raw) => + Array.ConvertAll(raw, stretch => new ImageStretch(stretch.from, stretch.to)); - if (byteLength > (nuint)bytes.Length) + /// Adds an image source that loads image data from a URL. + public Task AddImageSourceUrlAsync( + string sourceId, + IReadOnlyList coordinates, + string url + ) + { + return NativeCompletion.SubmitCommandChecked(completion => { - throw new InvalidOperationException( - $"Native style image byte length {byteLength} exceeds buffer length {bytes.Length}." - ); - } + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); + var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); + fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + { + NativeStatus.Check( + NativeMethods.mln_map_add_image_source_url( + Handle, + nativeSourceId.Value, + coordinatesPointer, + (nuint)nativeCoordinates.Length, + nativeUrl.Value, + completion + ) + ); + } + }); + } - if (byteLength != (nuint)bytes.Length) + /// Adds an image source with inline premultiplied RGBA8 image data. + public Task AddImageSourceImageAsync( + string sourceId, + IReadOnlyList coordinates, + PremultipliedRgba8Image image + ) + { + return NativeCompletion.SubmitCommandChecked(completion => { - Array.Resize(ref bytes, checked((int)byteLength)); - } - - // Native storage keeps no empty-versus-absent distinction for stretches. - var stretches = StyleImageStretches(imageId); - return new StyleImage( - new PremultipliedRgba8Image( - bytes, - new TextureImageInfo(info.Width, info.Height, info.Stride, (ulong)byteLength) - ), - new StyleImageOptions + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeImage = NativeStyleImage.From(image); + var imageValue = nativeImage.Value; + var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); + fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) { - PixelRatio = info.PixelRatio, - Sdf = info.Sdf, - StretchX = NullIfEmpty(stretches?.StretchX), - StretchY = NullIfEmpty(stretches?.StretchY), - Content = info.Content, - TextFitWidth = info.TextFitWidth, - TextFitHeight = info.TextFitHeight, + NativeStatus.Check( + NativeMethods.mln_map_add_image_source_image( + Handle, + nativeSourceId.Value, + coordinatesPointer, + (nuint)nativeCoordinates.Length, + &imageValue, + completion + ) + ); } - ); + }); } - /// Adds an image source that loads image data from a URL. - public void AddImageSourceUrl(string sourceId, IReadOnlyList coordinates, string url) + /// Updates an image source to load image data from a URL. + public Task SetImageSourceUrlAsync(string sourceId, string url) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); - fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeUrl = NativeStringView.From(url, nameof(url)); NativeStatus.Check( - NativeMethods.mln_map_add_image_source_url( + NativeMethods.mln_map_set_image_source_url( Handle, nativeSourceId.Value, - coordinatesPointer, - (nuint)nativeCoordinates.Length, - nativeUrl.Value + nativeUrl.Value, + completion ) ); - } + }); } - /// Adds an image source with inline premultiplied RGBA8 image data. - public void AddImageSourceImage( + /// Updates an image source with inline premultiplied RGBA8 image data. + public Task SetImageSourceImageAsync( string sourceId, - IReadOnlyList coordinates, PremultipliedRgba8Image image ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeImage = NativeStyleImage.From(image); - var imageValue = nativeImage.Value; - var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); - fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeImage = NativeStyleImage.From(image); + var imageValue = nativeImage.Value; NativeStatus.Check( - NativeMethods.mln_map_add_image_source_image( + NativeMethods.mln_map_set_image_source_image( Handle, nativeSourceId.Value, - coordinatesPointer, - (nuint)nativeCoordinates.Length, - &imageValue + &imageValue, + completion ) ); - } + }); } - /// Updates an image source to load image data from a URL. - public void SetImageSourceUrl(string sourceId, string url) + /// Updates image source coordinates. + public Task SetImageSourceCoordinatesAsync( + string sourceId, + IReadOnlyList coordinates + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeUrl = NativeStringView.From(url, nameof(url)); - NativeStatus.Check( - NativeMethods.mln_map_set_image_source_url( - Handle, - nativeSourceId.Value, - nativeUrl.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); + fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + { + NativeStatus.Check( + NativeMethods.mln_map_set_image_source_coordinates( + Handle, + nativeSourceId.Value, + coordinatesPointer, + (nuint)nativeCoordinates.Length, + completion + ) + ); + } + }); } - /// Updates an image source with inline premultiplied RGBA8 image data. - public void SetImageSourceImage(string sourceId, PremultipliedRgba8Image image) + /// Gets image source coordinates when the source exists. + public Task GetImageSourceCoordinatesAsync( + string sourceId, + CancellationToken cancellationToken = default + ) { using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeImage = NativeStyleImage.From(image); - var imageValue = nativeImage.Value; - NativeStatus.Check( - NativeMethods.mln_map_set_image_source_image(Handle, nativeSourceId.Value, &imageValue) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_get_image_source_coordinates( + Handle, + nativeSourceId.Value, + completion + ), + result => result->value_count == 0 ? null : ReadCoordinates(result), + cancellationToken ); } - /// Updates image source coordinates. - public void SetImageSourceCoordinates(string sourceId, IReadOnlyList coordinates) + /// Adds a hillshade layer for a raster DEM source. + public Task AddHillshadeLayerAsync( + string layerId, + string sourceId, + string beforeLayerId + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - var nativeCoordinates = ToNativeCoordinates(coordinates, nameof(coordinates)); - fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeBeforeLayerId = NativeStringView.From( + beforeLayerId, + nameof(beforeLayerId) + ); NativeStatus.Check( - NativeMethods.mln_map_set_image_source_coordinates( + NativeMethods.mln_map_add_hillshade_layer( Handle, + nativeLayerId.Value, nativeSourceId.Value, - coordinatesPointer, - (nuint)nativeCoordinates.Length + nativeBeforeLayerId.Value, + completion ) ); - } + }); } - /// Gets image source coordinates when the source exists. - public LatLng[]? GetImageSourceCoordinates(string sourceId) + /// Adds a color-relief layer for a raster DEM source. + public Task AddColorReliefLayerAsync( + string layerId, + string sourceId, + string beforeLayerId + ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - var coordinates = new mln_lat_lng[4]; - nuint coordinateCount = 0; - bool found = false; - fixed (mln_lat_lng* coordinatesPointer = coordinates) + return NativeCompletion.SubmitCommandChecked(completion => { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeBeforeLayerId = NativeStringView.From( + beforeLayerId, + nameof(beforeLayerId) + ); NativeStatus.Check( - NativeMethods.mln_map_get_image_source_coordinates( + NativeMethods.mln_map_add_color_relief_layer( Handle, + nativeLayerId.Value, nativeSourceId.Value, - coordinatesPointer, - (nuint)coordinates.Length, - &coordinateCount, - &found + nativeBeforeLayerId.Value, + completion ) ); - } - - if (!found) - { - return null; - } - - var result = new LatLng[checked((int)coordinateCount)]; - for (var index = 0; index < result.Length; index++) - { - result[index] = CoreStructs.FromNative(coordinates[index]); - } - return result; - } - - /// Adds a hillshade layer for a raster DEM source. - public void AddHillshadeLayer(string layerId, string sourceId, string beforeLayerId) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeBeforeLayerId = NativeStringView.From(beforeLayerId, nameof(beforeLayerId)); - NativeStatus.Check( - NativeMethods.mln_map_add_hillshade_layer( - Handle, - nativeLayerId.Value, - nativeSourceId.Value, - nativeBeforeLayerId.Value - ) - ); - } - - /// Adds a color-relief layer for a raster DEM source. - public void AddColorReliefLayer(string layerId, string sourceId, string beforeLayerId) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeBeforeLayerId = NativeStringView.From(beforeLayerId, nameof(beforeLayerId)); - NativeStatus.Check( - NativeMethods.mln_map_add_color_relief_layer( - Handle, - nativeLayerId.Value, - nativeSourceId.Value, - nativeBeforeLayerId.Value - ) - ); + }); } /// Adds a source-free location indicator layer. - public void AddLocationIndicatorLayer(string layerId, string beforeLayerId) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - using var nativeBeforeLayerId = NativeStringView.From(beforeLayerId, nameof(beforeLayerId)); - NativeStatus.Check( - NativeMethods.mln_map_add_location_indicator_layer( - Handle, - nativeLayerId.Value, - nativeBeforeLayerId.Value - ) - ); + public Task AddLocationIndicatorLayerAsync( + string layerId, + string beforeLayerId + ) + { + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + using var nativeBeforeLayerId = NativeStringView.From( + beforeLayerId, + nameof(beforeLayerId) + ); + NativeStatus.Check( + NativeMethods.mln_map_add_location_indicator_layer( + Handle, + nativeLayerId.Value, + nativeBeforeLayerId.Value, + completion + ) + ); + }); } /// Sets a location indicator layer location. - public void SetLocationIndicatorLocation(string layerId, LatLng coordinate, double altitude) + public Task SetLocationIndicatorLocationAsync( + string layerId, + LatLng coordinate, + double altitude + ) { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_location_indicator_location( - Handle, - nativeLayerId.Value, - CoreStructs.ToNative(coordinate), - altitude - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + NativeStatus.Check( + NativeMethods.mln_map_set_location_indicator_location( + Handle, + nativeLayerId.Value, + CoreStructs.ToNative(coordinate), + altitude, + completion + ) + ); + }); } /// Sets a location indicator layer bearing in degrees. - public void SetLocationIndicatorBearing(string layerId, double bearing) + public Task SetLocationIndicatorBearingAsync(string layerId, double bearing) { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_location_indicator_bearing( - Handle, - nativeLayerId.Value, - bearing - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + NativeStatus.Check( + NativeMethods.mln_map_set_location_indicator_bearing( + Handle, + nativeLayerId.Value, + bearing, + completion + ) + ); + }); } /// Sets a location indicator layer accuracy radius in meters. - public void SetLocationIndicatorAccuracyRadius(string layerId, double radius) + public Task SetLocationIndicatorAccuracyRadiusAsync( + string layerId, + double radius + ) { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_location_indicator_accuracy_radius( - Handle, - nativeLayerId.Value, - radius - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + NativeStatus.Check( + NativeMethods.mln_map_set_location_indicator_accuracy_radius( + Handle, + nativeLayerId.Value, + radius, + completion + ) + ); + }); } /// Sets a location indicator layer image-name property. - public void SetLocationIndicatorImageName( + public Task SetLocationIndicatorImageNameAsync( string layerId, LocationIndicatorImageKind imageKind, string imageId ) { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); - NativeStatus.Check( - NativeMethods.mln_map_set_location_indicator_image_name( - Handle, - nativeLayerId.Value, - (uint)imageKind, - nativeImageId.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); + using var nativeImageId = NativeStringView.From(imageId, nameof(imageId)); + NativeStatus.Check( + NativeMethods.mln_map_set_location_indicator_image_name( + Handle, + nativeLayerId.Value, + (uint)imageKind, + nativeImageId.Value, + completion + ) + ); + }); } - /// Adds a style layer from UTF-8 JSON bytes. - public void AddStyleLayerJson(byte[] layerJson, string beforeLayerId) + /// Adds a style layer. + public Task AddStyleLayerJsonAsync(byte[] layerJson, string beforeLayerId) { using var nativeJson = NativeStringView.From(layerJson, nameof(layerJson)); using var nativeBeforeLayerId = NativeStringView.From(beforeLayerId, nameof(beforeLayerId)); - NativeStatus.Check( - NativeMethods.mln_map_add_style_layer_json( - Handle, - nativeJson.Value, - nativeBeforeLayerId.Value - ) - ); - } - - /// Removes a style layer and reports whether it existed. - public bool RemoveStyleLayer(string layerId) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - bool removed = false; - NativeStatus.Check( - NativeMethods.mln_map_remove_style_layer(Handle, nativeLayerId.Value, &removed) - ); - return removed; - } - - /// Whether a style layer exists. - public bool StyleLayerExists(string layerId) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - bool exists = false; - NativeStatus.Check( - NativeMethods.mln_map_style_layer_exists(Handle, nativeLayerId.Value, &exists) - ); - return exists; + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_add_style_layer_json( + Handle, + nativeJson.Value, + nativeBeforeLayerId.Value, + completion + ) + ); + }); } - /// Gets a style layer type when the layer exists. - public string? StyleLayerType(string layerId) + /// Submits a command that removes one style layer. + /// + /// The command's command completion event reports + /// when no layer has . + /// + public Task RemoveStyleLayerAsync(string layerId) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - mln_buffer_view layerType = default; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_layer_type( - Handle, - nativeLayerId.Value, - &layerType, - &found - ) - ); - return found ? RuntimeStructs.CopyUtf8((sbyte*)layerType.data, layerType.size) : null; + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_remove_style_layer(Handle, nativeLayerId.Value, completion) + ); + }); } /// Lists style layer IDs in style order. - public string[] StyleLayerIds() - { - MlnStyleIdList list = default; - NativeStatus.Check(NativeMethods.mln_map_list_style_layer_ids(Handle, &list)); - return CopyStyleIdList(list); - } + public Task StyleLayerIdsAsync(CancellationToken cancellationToken = default) => + RunMapOperationAsync( + completion => NativeMethods.mln_map_list_style_layer_ids(Handle, completion), + ReadStrings, + cancellationToken + ); - /// Moves a style layer before another layer, or to the top when beforeLayerId is empty. - public void MoveStyleLayer(string layerId, string beforeLayerId) + /// Moves a style layer. + public Task MoveStyleLayerAsync(string layerId, string beforeLayerId) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativeBeforeLayerId = NativeStringView.From(beforeLayerId, nameof(beforeLayerId)); - NativeStatus.Check( - NativeMethods.mln_map_move_style_layer( - Handle, - nativeLayerId.Value, - nativeBeforeLayerId.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_move_style_layer( + Handle, + nativeLayerId.Value, + nativeBeforeLayerId.Value, + completion + ) + ); + }); } /// Gets a full style-spec layer JSON snapshot when the layer exists. - public byte[]? GetStyleLayerJson(string layerId) + public Task GetStyleLayerJsonAsync( + string layerId, + CancellationToken cancellationToken = default + ) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - MlnBuffer buffer = default; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_map_get_style_layer_json(Handle, nativeLayerId.Value, &buffer, &found) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_get_style_layer_json(Handle, nativeLayerId.Value, completion), + ReadOptionalBuffer, + cancellationToken ); - return found ? ValueStructs.ReadBuffer(buffer) : null; } - /// Sets the style light document from UTF-8 JSON bytes. - public void SetStyleLightJson(byte[] lightJson) + /// Sets the style light document. + public Task SetStyleLightJsonAsync(byte[] lightJson) { using var nativeJson = NativeStringView.From(lightJson, nameof(lightJson)); - NativeStatus.Check(NativeMethods.mln_map_set_style_light_json(Handle, nativeJson.Value)); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_style_light_json(Handle, nativeJson.Value, completion) + ); + }); } - /// Sets one style light property from UTF-8 JSON bytes. - public void SetStyleLightProperty(string propertyName, byte[] value) + /// Sets one style light property. + public Task SetStyleLightPropertyAsync(string propertyName, byte[] value) { using var nativePropertyName = NativeStringView.From(propertyName, nameof(propertyName)); using var nativeValue = NativeStringView.From(value, nameof(value)); - NativeStatus.Check( - NativeMethods.mln_map_set_style_light_property( - Handle, - nativePropertyName.Value, - nativeValue.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_style_light_property( + Handle, + nativePropertyName.Value, + nativeValue.Value, + completion + ) + ); + }); } /// Gets one style light property snapshot, or null when undefined. - public byte[]? GetStyleLightProperty(string propertyName) + public Task GetStyleLightPropertyAsync( + string propertyName, + CancellationToken cancellationToken = default + ) { using var nativePropertyName = NativeStringView.From(propertyName, nameof(propertyName)); - MlnBuffer buffer = default; - NativeStatus.Check( - NativeMethods.mln_map_get_style_light_property( - Handle, - nativePropertyName.Value, - &buffer - ) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_get_style_light_property( + Handle, + nativePropertyName.Value, + completion + ), + ReadOptionalBuffer, + cancellationToken ); - return ValueStructs.ReadOptionalBuffer(buffer); } - /// Sets the style's global transition options. - /// - /// This replaces the whole transition configuration rather than merging into it, and null - /// duration and delay clear the style-wide override. Loading a style replaces these options, - /// so apply an override after the style loads. - /// - public void SetStyleTransitionOptions(StyleTransitionOptions options) + /// Sets the style's transition options. + public Task SetStyleTransitionOptionsAsync(StyleTransitionOptions options) { - var native = StyleStructs.ToNative(options); - NativeStatus.Check(NativeMethods.mln_map_set_style_transition_options(Handle, &native)); + return NativeCompletion.SubmitCommandChecked(completion => + { + var native = StyleStructs.ToNative(options); + NativeStatus.Check( + NativeMethods.mln_map_set_style_transition_options(Handle, &native, completion) + ); + }); } /// Gets the style's global transition options. - public StyleTransitionOptions GetStyleTransitionOptions() - { - var native = NativeMethods.mln_style_transition_options_default(); - NativeStatus.Check(NativeMethods.mln_map_get_style_transition_options(Handle, &native)); - return StyleStructs.FromNative(native); - } - - /// Sets one layer property from UTF-8 JSON bytes. - public void SetLayerProperty(string layerId, string propertyName, byte[] value) + public Task GetStyleTransitionOptionsAsync( + CancellationToken cancellationToken = default + ) => + RunMapOperationAsync( + completion => NativeMethods.mln_map_get_style_transition_options(Handle, completion), + result => + StyleStructs.FromNative( + NativeCompletion.Value(result) + ), + cancellationToken + ); + + /// Sets one layer property. + public Task SetLayerPropertyAsync( + string layerId, + string propertyName, + byte[] value + ) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativePropertyName = NativeStringView.From(propertyName, nameof(propertyName)); using var nativeValue = NativeStringView.From(value, nameof(value)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_property( - Handle, - nativeLayerId.Value, - nativePropertyName.Value, - nativeValue.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_property( + Handle, + nativeLayerId.Value, + nativePropertyName.Value, + nativeValue.Value, + completion + ) + ); + }); } /// Gets one layer property snapshot, or null when undefined. - public byte[]? GetLayerProperty(string layerId, string propertyName) + public Task GetLayerPropertyAsync( + string layerId, + string propertyName, + CancellationToken cancellationToken = default + ) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativePropertyName = NativeStringView.From(propertyName, nameof(propertyName)); - MlnBuffer buffer = default; - NativeStatus.Check( - NativeMethods.mln_map_get_layer_property( - Handle, - nativeLayerId.Value, - nativePropertyName.Value, - &buffer - ) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_get_layer_property( + Handle, + nativeLayerId.Value, + nativePropertyName.Value, + completion + ), + ReadOptionalBuffer, + cancellationToken ); - return ValueStructs.ReadOptionalBuffer(buffer); } - /// Sets or clears one layer filter from UTF-8 JSON bytes. - public void SetLayerFilter(string layerId, byte[]? filter) + /// Sets or clears a layer filter. + public Task SetLayerFilterAsync(string layerId, byte[]? filter) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativeFilter = filter is null ? null : NativeStringView.From(filter, nameof(filter)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_filter( - Handle, - nativeLayerId.Value, - nativeFilter?.Pointer - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_filter( + Handle, + nativeLayerId.Value, + nativeFilter?.Pointer, + completion + ) + ); + }); } /// Gets one layer filter snapshot, or null when no filter exists. - public byte[]? GetLayerFilter(string layerId) + public Task GetLayerFilterAsync( + string layerId, + CancellationToken cancellationToken = default + ) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - MlnBuffer buffer = default; - NativeStatus.Check( - NativeMethods.mln_map_get_layer_filter(Handle, nativeLayerId.Value, &buffer) + return RunMapOperationAsync( + completion => + NativeMethods.mln_map_get_layer_filter(Handle, nativeLayerId.Value, completion), + ReadOptionalBuffer, + cancellationToken ); - return ValueStructs.ReadOptionalBuffer(buffer); } - /// Sets one layer's source-layer ID. - /// - /// Layer types that take no source, such as background, are rejected. - /// - public void SetLayerSourceLayer(string layerId, string sourceLayer) + /// Sets a layer's source-layer ID. + public Task SetLayerSourceLayerAsync(string layerId, string sourceLayer) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativeSourceLayer = NativeStringView.From(sourceLayer, nameof(sourceLayer)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_source_layer( - Handle, - nativeLayerId.Value, - nativeSourceLayer.Value - ) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_source_layer( + Handle, + nativeLayerId.Value, + nativeSourceLayer.Value, + completion + ) + ); + }); } - /// Gets one layer's source-layer ID, empty when it carries none. - public string GetLayerSourceLayer(string layerId) => CopyLayerText(layerId, sourceLayer: true); - - /// Sets one layer's source ID. - /// - /// Layer types that take no source, such as background, are rejected. The named source need - /// not exist yet. - /// - public void SetLayerSourceId(string layerId, string sourceId) + /// Sets a layer's source ID. + public Task SetLayerSourceIdAsync(string layerId, string sourceId) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_source_id( - Handle, - nativeLayerId.Value, - nativeSourceId.Value - ) - ); - } - - /// Gets one layer's source ID, empty when it carries none. - public string GetLayerSourceId(string layerId) => CopyLayerText(layerId, sourceLayer: false); - - private delegate mln_status CopyMapTextCall( - MlnMap map, - sbyte* text, - nuint capacity, - nuint* size - ); - - private delegate mln_status CopyMapBytesCall( - MlnMap map, - byte* bytes, - nuint capacity, - nuint* size - ); - - private byte[] CopyMapBytes(CopyMapBytesCall copy) - { - nuint required = 0; - NativeStatus.Check(copy(Handle, null, 0, &required)); - if (required == 0) - { - return []; - } - var buffer = new byte[checked((int)required)]; - nuint copied = 0; - fixed (byte* bufferPointer = buffer) - { - NativeStatus.Check(copy(Handle, bufferPointer, required, &copied)); - } - return copied == required ? buffer : buffer[..checked((int)copied)]; - } - - private string CopyMapText(CopyMapTextCall copy) - { - nuint required = 0; - NativeStatus.Check(copy(Handle, null, 0, &required)); - if (required == 0) - { - return string.Empty; - } - - var buffer = new byte[checked((int)required)]; - nuint copied = 0; - fixed (byte* bufferPointer = buffer) + return NativeCompletion.SubmitCommandChecked(completion => { - NativeStatus.Check(copy(Handle, (sbyte*)bufferPointer, required, &copied)); - return RuntimeStructs.CopyUtf8((sbyte*)bufferPointer, copied) ?? string.Empty; - } + NativeStatus.Check( + NativeMethods.mln_map_set_layer_source_id( + Handle, + nativeLayerId.Value, + nativeSourceId.Value, + completion + ) + ); + }); } - private string CopyLayerText(string layerId, bool sourceLayer) + /// Sets the lowest layer zoom. + public Task SetLayerMinZoomAsync(string layerId, double minZoom) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - nuint required = 0; - NativeStatus.Check( - sourceLayer - ? NativeMethods.mln_map_copy_layer_source_layer( - Handle, - nativeLayerId.Value, - null, - 0, - &required - ) - : NativeMethods.mln_map_copy_layer_source_id( + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_min_zoom( Handle, nativeLayerId.Value, - null, - 0, - &required + minZoom, + completion ) - ); - if (required == 0) - { - return string.Empty; - } - - var buffer = new byte[checked((int)required)]; - nuint copied = 0; - fixed (byte* bufferPointer = buffer) - { - NativeStatus.Check( - sourceLayer - ? NativeMethods.mln_map_copy_layer_source_layer( - Handle, - nativeLayerId.Value, - (sbyte*)bufferPointer, - required, - &copied - ) - : NativeMethods.mln_map_copy_layer_source_id( - Handle, - nativeLayerId.Value, - (sbyte*)bufferPointer, - required, - &copied - ) ); - return RuntimeStructs.CopyUtf8((sbyte*)bufferPointer, copied) ?? string.Empty; - } + }); } - /// Sets the lowest zoom at which one layer draws. - /// Pass double.NegativeInfinity for no lower bound. - public void SetLayerMinZoom(string layerId, double minZoom) + /// Sets the highest layer zoom. + public Task SetLayerMaxZoomAsync(string layerId, double maxZoom) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_min_zoom(Handle, nativeLayerId.Value, minZoom) - ); + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_max_zoom( + Handle, + nativeLayerId.Value, + maxZoom, + completion + ) + ); + }); } - /// Gets the lowest zoom at which one layer draws. - /// A layer with no lower bound reports double.NegativeInfinity. - public double GetLayerMinZoom(string layerId) + /// Sets layer visibility. + public Task SetLayerVisibilityAsync( + string layerId, + StyleLayerVisibility visibility + ) { using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - double minZoom = 0; - NativeStatus.Check( - NativeMethods.mln_map_get_layer_min_zoom(Handle, nativeLayerId.Value, &minZoom) - ); - return minZoom; + return NativeCompletion.SubmitCommandChecked(completion => + { + NativeStatus.Check( + NativeMethods.mln_map_set_layer_visibility( + Handle, + nativeLayerId.Value, + (uint)visibility, + completion + ) + ); + }); } - /// Sets the highest zoom at which one layer draws. - /// Pass double.PositiveInfinity for no upper bound. - public void SetLayerMaxZoom(string layerId, double maxZoom) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_max_zoom(Handle, nativeLayerId.Value, maxZoom) - ); - } + /// Releases the map and waits for native teardown. + public void Close() => CloseAsync().GetAwaiter().GetResult(); - /// Gets the highest zoom at which one layer draws. - /// A layer with no upper bound reports double.PositiveInfinity. - public double GetLayerMaxZoom(string layerId) + /// Releases the map without blocking and reports native teardown. + public Task CloseAsync() { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - double maxZoom = 0; - NativeStatus.Check( - NativeMethods.mln_map_get_layer_max_zoom(Handle, nativeLayerId.Value, &maxZoom) - ); - return maxZoom; - } + if (!IsClosed) + { + state.Close(); + runtime.UnregisterMap(this); + } - /// Sets whether one layer draws. - public void SetLayerVisibility(string layerId, StyleLayerVisibility visibility) - { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - NativeStatus.Check( - NativeMethods.mln_map_set_layer_visibility( - Handle, - nativeLayerId.Value, - (uint)visibility - ) - ); + return teardown; } - /// Gets whether one layer draws. - public StyleLayerVisibility GetLayerVisibility(string layerId) + private mln_status StartRelease(MlnMap handle) { - using var nativeLayerId = NativeStringView.From(layerId, nameof(layerId)); - uint visibility = 0; - NativeStatus.Check( - NativeMethods.mln_map_get_layer_visibility(Handle, nativeLayerId.Value, &visibility) + teardown = NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_map_release(handle, completion) ); - return (StyleLayerVisibility)visibility; - } - - /// Destroys the map on its owner thread. - /// - /// Closing discards this map's queued runtime events and its recorded loading - /// failure without a flush and without a terminal event. - /// - public void Close() - { - state.Close(); - runtime.UnregisterMap(this); + return mln_status.MLN_STATUS_OK; } internal static IDisposable UseCustomGeometrySourceInstallForTest( @@ -2198,69 +1937,54 @@ string parameterName return nativeCoordinates; } - private static string[] CopyStyleIdList(MlnStyleIdList list) - { - if (list.IsNull) - { - return []; - } + private static Task RunMapOperationAsync( + CompletionSubmit start, + CompletionConverter convert, + CancellationToken cancellationToken + ) => NativeCompletion.Submit(start, convert).WaitAsync(cancellationToken); - try - { - nuint count = 0; - NativeStatus.Check(NativeMethods.mln_style_id_list_count(list, &count)); - var ids = new string[checked((int)count)]; - for (var index = 0; index < ids.Length; index++) - { - mln_buffer_view id = default; - NativeStatus.Check(NativeMethods.mln_style_id_list_get(list, (nuint)index, &id)); - ids[index] = RuntimeStructs.CopyUtf8((sbyte*)id.data, id.size); - } + private static byte[] ReadBuffer(mln_completion_result* result) => + ValueStructs.CopyBufferView(NativeCompletion.Value(result)); - return ids; - } - finally - { - NativeMethods.mln_style_id_list_destroy(list); - } - } + private static byte[]? ReadOptionalBuffer(mln_completion_result* result) => + result->value_count == 0 ? null : ReadBuffer(result); - private static string[] CopyStyleStringList(MlnStyleStringList list) + private static string[] ReadStrings(mln_completion_result* result) { - if (list.IsNull) + var views = NativeCompletion.Values(result); + var strings = new string[views.Length]; + for (var index = 0; index < views.Length; index++) { - return []; + strings[index] = RuntimeStructs.CopyUtf8((sbyte*)views[index].data, views[index].size); } + return strings; + } - try - { - nuint count = 0; - NativeStatus.Check(NativeMethods.mln_style_string_list_count(list, &count)); - var values = new string[checked((int)count)]; - for (var index = 0; index < values.Length; index++) - { - mln_buffer_view value = default; - NativeStatus.Check( - NativeMethods.mln_style_string_list_get(list, (nuint)index, &value) - ); - values[index] = RuntimeStructs.CopyUtf8((sbyte*)value.data, value.size); - } + private static CameraOptions ReadCamera(mln_completion_result* result) => + MapStructs.CameraOptionsFromNative(NativeCompletion.Value(result)); - return values; - } - finally - { - NativeMethods.mln_style_string_list_destroy(list); - } + private static LatLngBounds ReadBounds(mln_completion_result* result) => + MapStructs.FromNative(NativeCompletion.Value(result)); + + private static ScreenPoint[] ReadPoints(mln_completion_result* result) + { + var values = NativeCompletion.Values(result); + return values.ToArray().Select(MapStructs.FromNative).ToArray(); + } + + private static LatLng[] ReadCoordinates(mln_completion_result* result) + { + var values = NativeCompletion.Values(result); + return values.ToArray().Select(CoreStructs.FromNative).ToArray(); } /// public void Dispose() { - if (state.TryClose()) - { - runtime.UnregisterMap(this); - } + Close(); GC.KeepAlive(runtime); } + + /// + public ValueTask DisposeAsync() => new(CloseAsync()); } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapOptions.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapOptions.cs index e16cbc183..b0b7139e1 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapOptions.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapOptions.cs @@ -45,7 +45,7 @@ public sealed record MapOptions /// /// Map-originated event types this map queues, every event type this library reports /// by default. The mask applies during construction. See - /// . + /// . /// public RuntimeEventMask EventMask { get; set; } = DefaultEventMask(); @@ -63,15 +63,15 @@ internal mln_map_options ToNative() var options = NativeMethods.mln_map_options_default(); if (Width is { } width) { - options.width = width; + options.initial_extent.width = width; } if (Height is { } height) { - options.height = height; + options.initial_extent.height = height; } if (ScaleFactor is { } scaleFactor) { - options.scale_factor = scaleFactor; + options.initial_extent.scale_factor = scaleFactor; } if (MapMode is { } mapMode) { diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapProjectionHandle.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapProjectionHandle.cs index 8d92bbecc..1f7d6fec5 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapProjectionHandle.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Map/MapProjectionHandle.cs @@ -1,13 +1,20 @@ using Maplibre.NativeFfi.Camera; using Maplibre.NativeFfi.Geo; using Maplibre.NativeFfi.Internal.C; +using Maplibre.NativeFfi.Internal.Memory; using Maplibre.NativeFfi.Internal.Pointer; using Maplibre.NativeFfi.Internal.Status; using Maplibre.NativeFfi.Internal.Struct; +using Maplibre.NativeFfi.Runtime; namespace Maplibre.NativeFfi.Map; -/// Any-thread projection snapshot handle with serialized native calls. +/// Any-thread standalone projection handle. +/// +/// Every call after creation is synchronous, runs on the calling thread, is internally serialized, +/// and is thread-safe. A projection copies the map's transform state at creation, never +/// observes later map changes, and remains usable after its source map and runtime close. +/// public sealed unsafe class MapProjectionHandle : IDisposable { private readonly NativeHandleState state; @@ -16,44 +23,57 @@ private MapProjectionHandle(MlnMapProjection handle) { state = new NativeHandleState( handle, - static handle => NativeMethods.mln_map_projection_destroy(handle), + static live => NativeMethods.mln_map_projection_close(live), nameof(MapProjectionHandle) ); } - internal static MapProjectionHandle Create(MapHandle map) + internal static Task CreateAsync( + MapHandle map, + CancellationToken cancellationToken + ) { ArgumentNullException.ThrowIfNull(map); - MlnMapProjection projection = default; - NativeStatus.Check(NativeMethods.mln_map_projection_create(map.Handle, &projection)); - return new MapProjectionHandle(projection); + cancellationToken.ThrowIfCancellationRequested(); + return NativeCompletion + .Submit( + completion => NativeMethods.mln_map_projection_create(map.Handle, completion), + static result => new MapProjectionHandle( + NativeCompletion.Value(result) + ) + ) + .WaitAsync(cancellationToken); } + internal MlnMapProjection Handle => state.Handle; + /// Whether this wrapper has successfully closed its native handle. public bool IsClosed => state.IsClosed; - /// Gets the projection camera descriptor. + /// Copies the projection camera, observing every earlier projection setter. public CameraOptions GetCamera() { - return state.WithLive(handle => + var camera = state.WithLive(live => { - var camera = NativeMethods.mln_camera_options_default(); - NativeStatus.Check(NativeMethods.mln_map_projection_get_camera(handle, &camera)); - return MapStructs.CameraOptionsFromNative(camera); + var native = NativeMethods.mln_camera_options_default(); + NativeStatus.Check(NativeMethods.mln_map_projection_get_camera(live, &native)); + return native; }); + return MapStructs.CameraOptionsFromNative(camera); } - /// Sets the projection camera descriptor, applying only non-null fields. + /// Applies a camera update; only fields present on apply. public void SetCamera(CameraOptions camera) { - state.WithLive(handle => + var nativeCamera = MapStructs.ToNative(camera); + state.WithLive(live => { - var nativeCamera = MapStructs.ToNative(camera); - NativeStatus.Check(NativeMethods.mln_map_projection_set_camera(handle, &nativeCamera)); + var native = nativeCamera; + NativeStatus.Check(NativeMethods.mln_map_projection_set_camera(live, &native)); }); } - /// Sets a camera that makes the supplied coordinates visible with padding. + /// Applies a camera fit for the coordinates. public void SetVisibleCoordinates(IReadOnlyList coordinates, EdgeInsets padding) { ArgumentNullException.ThrowIfNull(coordinates); @@ -62,15 +82,14 @@ public void SetVisibleCoordinates(IReadOnlyList coordinates, EdgeInsets { nativeCoordinates[index] = CoreStructs.ToNative(coordinates[index]); } - - state.WithLive(handle => + var nativePadding = MapStructs.ToNative(padding); + state.WithLive(live => { - var nativePadding = MapStructs.ToNative(padding); fixed (mln_lat_lng* coordinatesPointer = nativeCoordinates) { NativeStatus.Check( NativeMethods.mln_map_projection_set_visible_coordinates( - handle, + live, nativeCoordinates.Length == 0 ? null : coordinatesPointer, (nuint)nativeCoordinates.Length, nativePadding @@ -80,61 +99,57 @@ public void SetVisibleCoordinates(IReadOnlyList coordinates, EdgeInsets }); } - /// Sets a camera that makes the supplied geometry visible with padding. + /// Applies a camera fit for GeoJSON Geometry bytes. public void SetVisibleGeometry(byte[] geometry, EdgeInsets padding) { ArgumentNullException.ThrowIfNull(geometry); - state.WithLive(handle => - { - using var nativeGeometry = NativeStringView.From(geometry, nameof(geometry)); - var nativePadding = MapStructs.ToNative(padding); + using var nativeGeometry = NativeStringView.From(geometry, nameof(geometry)); + var nativePadding = MapStructs.ToNative(padding); + state.WithLive(live => NativeStatus.Check( NativeMethods.mln_map_projection_set_visible_geometry( - handle, + live, nativeGeometry.Value, nativePadding ) - ); - }); + ) + ); } - /// Converts a geographic coordinate to a screen pixel using this projection snapshot. + /// Converts a geographic coordinate to a logical-pixel screen point. public ScreenPoint PixelForLatLng(LatLng coordinate) { - return state.WithLive(handle => + var nativeCoordinate = CoreStructs.ToNative(coordinate); + var point = state.WithLive(live => { - var nativeCoordinate = CoreStructs.ToNative(coordinate); - mln_screen_point point = default; + mln_screen_point native = default; NativeStatus.Check( - NativeMethods.mln_map_projection_pixel_for_lat_lng(handle, nativeCoordinate, &point) + NativeMethods.mln_map_projection_pixel_for_lat_lng(live, nativeCoordinate, &native) ); - return MapStructs.FromNative(point); + return native; }); + return MapStructs.FromNative(point); } - /// Converts a screen pixel to a geographic coordinate using this projection snapshot. + /// Converts a logical-pixel screen point to a geographic coordinate. public LatLng LatLngForPixel(ScreenPoint point) { - return state.WithLive(handle => + var nativePoint = MapStructs.ToNative(point); + var coordinate = state.WithLive(live => { - var nativePoint = MapStructs.ToNative(point); - mln_lat_lng coordinate = default; + mln_lat_lng native = default; NativeStatus.Check( - NativeMethods.mln_map_projection_lat_lng_for_pixel(handle, nativePoint, &coordinate) + NativeMethods.mln_map_projection_lat_lng_for_pixel(live, nativePoint, &native) ); - return CoreStructs.FromNative(coordinate); + return native; }); + return CoreStructs.FromNative(coordinate); } - /// Destroys the projection after active calls complete. - public void Close() - { - state.Close(); - } + /// + /// Closes the projection after waiting for projection calls already running on other threads. + /// + public void Close() => state.Close(); - /// - public void Dispose() - { - state.TryClose(); - } + public void Dispose() => state.TryClose(); } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderHandles.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderHandles.cs index 01025dbae..57c83eb0b 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderHandles.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderHandles.cs @@ -1,1308 +1,1029 @@ -using System.Runtime.InteropServices; using Maplibre.NativeFfi.Error; -using Maplibre.NativeFfi.Geo; using Maplibre.NativeFfi.Internal.C; using Maplibre.NativeFfi.Internal.Pointer; using Maplibre.NativeFfi.Internal.Status; using Maplibre.NativeFfi.Internal.Struct; using Maplibre.NativeFfi.Map; using Maplibre.NativeFfi.Query; +using Maplibre.NativeFfi.Runtime; namespace Maplibre.NativeFfi.Render; -internal unsafe delegate mln_status OpenGLSurfaceAttach( +internal unsafe delegate mln_status AttachNative( MlnMap map, - mln_opengl_surface_descriptor* descriptor, - MlnRenderSession* outSession -); + T* descriptor, + mln_render_session_attach_options* options, + MlnRenderSession* outSession, + mln_completion* completion +) + where T : unmanaged; -internal unsafe delegate mln_status OpenGLOwnedTextureAttach( - MlnMap map, - mln_opengl_owned_texture_descriptor* descriptor, - MlnRenderSession* outSession -); +internal unsafe delegate mln_status StartRenderOperation(mln_completion* completion); -internal unsafe delegate mln_status OpenGLBorrowedTextureAttach( - MlnMap map, - mln_opengl_borrowed_texture_descriptor* descriptor, - MlnRenderSession* outSession -); - -internal unsafe delegate mln_status RenderSessionResize( - MlnRenderSession session, - uint width, - uint height, - double scaleFactor -); - -internal unsafe delegate mln_status RenderSessionRenderUpdate( - MlnRenderSession session, - mln_render_result* out_result, - bool* out_needs_repaint -); - -internal unsafe delegate mln_status MetalOwnedTextureAcquireFrame( - MlnRenderSession session, - mln_metal_owned_texture_frame* frame -); - -internal unsafe delegate mln_status MetalOwnedTextureReleaseFrame( - MlnRenderSession session, - mln_metal_owned_texture_frame* frame -); - -/// Owner-thread render session handle bound to a map. +/// A render session and its asynchronous attachment completion. public sealed unsafe class RenderSessionHandle : IDisposable { - private static readonly OpenGLSurfaceAttach DefaultOpenGLSurfaceAttach = static ( - map, - descriptor, - outSession - ) => NativeMethods.mln_opengl_surface_attach(map, descriptor, outSession); - private static readonly OpenGLOwnedTextureAttach DefaultOpenGLOwnedTextureAttach = static ( - map, - descriptor, - outSession - ) => NativeMethods.mln_opengl_owned_texture_attach(map, descriptor, outSession); - private static readonly OpenGLBorrowedTextureAttach DefaultOpenGLBorrowedTextureAttach = - static (map, descriptor, outSession) => - NativeMethods.mln_opengl_borrowed_texture_attach(map, descriptor, outSession); - private static readonly RenderSessionResize DefaultResize = static ( - session, - width, - height, - scaleFactor - ) => NativeMethods.mln_render_session_resize(session, width, height, scaleFactor); - private static readonly RenderSessionRenderUpdate DefaultRenderUpdate = static ( - session, - outResult, - outNeedsRepaint - ) => NativeMethods.mln_render_session_render_update(session, outResult, outNeedsRepaint); - private static readonly TextureRead DefaultTextureRead = static (session, data, length, info) => - NativeMethods.mln_texture_read_premultiplied_rgba8(session, data, length, info); - private static readonly StatusDestroy DefaultDestroy = static session => - NativeMethods.mln_render_session_destroy(session); - private static readonly MetalOwnedTextureAcquireFrame DefaultAcquireMetalFrame = static ( - session, - frame - ) => NativeMethods.mln_metal_owned_texture_acquire_frame(session, frame); - private static readonly MetalOwnedTextureReleaseFrame DefaultReleaseMetalFrame = static ( - session, - frame - ) => NativeMethods.mln_metal_owned_texture_release_frame(session, frame); - private static readonly VulkanOwnedTextureAcquireFrame DefaultAcquireVulkanFrame = static ( - session, - frame - ) => NativeMethods.mln_vulkan_owned_texture_acquire_frame(session, frame); - private static readonly OpenGLOwnedTextureAcquireFrame DefaultAcquireOpenGLFrame = static ( - session, - frame - ) => NativeMethods.mln_opengl_owned_texture_acquire_frame(session, frame); - - [ThreadStatic] - private static OpenGLSurfaceAttach? openGLSurfaceAttachForTest; - - [ThreadStatic] - private static OpenGLOwnedTextureAttach? openGLOwnedTextureAttachForTest; - - [ThreadStatic] - private static OpenGLBorrowedTextureAttach? openGLBorrowedTextureAttachForTest; - - [ThreadStatic] - private static RenderSessionResize? resizeForTest; - - [ThreadStatic] - private static RenderSessionRenderUpdate? renderUpdateForTest; - - [ThreadStatic] - private static TextureRead? textureReadForTest; - - [ThreadStatic] - private static StatusDestroy? destroyForTest; - - [ThreadStatic] - private static MetalOwnedTextureAcquireFrame? acquireMetalFrameForTest; - - [ThreadStatic] - private static MetalOwnedTextureReleaseFrame? releaseMetalFrameForTest; - - [ThreadStatic] - private static VulkanOwnedTextureAcquireFrame? acquireVulkanFrameForTest; - - [ThreadStatic] - private static OpenGLOwnedTextureAcquireFrame? acquireOpenGLFrameForTest; - - [ThreadStatic] - private static Func< - mln_metal_owned_texture_frame, - FrameScope, - MetalOwnedTextureFrame - >? readMetalFrameForTest; - - private readonly object frameGate = new(); - private readonly MapHandle? map; + private readonly MapHandle map; private readonly NativeHandleState state; - private bool hasActiveTextureFrame; - - private RenderSessionHandle(MapHandle? map, MlnRenderSession handle) - : this(map, handle, DestroyNative) { } + private readonly object frameGate = new(); + private readonly HashSet acquiredFrames = []; - private RenderSessionHandle( - MapHandle? map, - MlnRenderSession handle, - StatusDestroy destroy - ) + private RenderSessionHandle(MapHandle map, MlnRenderSession handle, Task attachment) { this.map = map; state = new NativeHandleState( handle, - destroy, + static value => NativeMethods.mln_render_session_destroy(value), nameof(RenderSessionHandle) ); + Attachment = RetainUntilCompletedAsync(attachment, this); } - internal static RenderSessionHandle CreateForTest(MlnRenderSession handle) => - new(null, handle, static _ => mln_status.MLN_STATUS_OK); - - internal static IDisposable UseOpenGLAttachMethodsForTest( - OpenGLSurfaceAttach surfaceAttach, - OpenGLOwnedTextureAttach ownedTextureAttach, - OpenGLBorrowedTextureAttach borrowedTextureAttach - ) - { - var previousSurface = openGLSurfaceAttachForTest; - var previousOwnedTexture = openGLOwnedTextureAttachForTest; - var previousBorrowedTexture = openGLBorrowedTextureAttachForTest; - openGLSurfaceAttachForTest = surfaceAttach; - openGLOwnedTextureAttachForTest = ownedTextureAttach; - openGLBorrowedTextureAttachForTest = borrowedTextureAttach; - return new RestoreOpenGLAttachMethods( - previousSurface, - previousOwnedTexture, - previousBorrowedTexture - ); - } + /// + /// Completes after the selected driver initializes the target. A caller-graphics-thread host + /// services driver work on the graphics thread while this task is pending. + /// + public Task Attachment { get; } + + private static Task RetainUntilCompletedAsync(Task attachment, RenderSessionHandle session) => + attachment + .ContinueWith( + static (completed, retained) => + { + GC.KeepAlive(retained); + return completed; + }, + session, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ) + .Unwrap(); - internal static IDisposable UseSessionMethodsForTest( - RenderSessionResize resize, - RenderSessionRenderUpdate renderUpdate, - TextureRead textureRead, - StatusDestroy destroy - ) + public RenderSessionCapabilityInfo Capabilities { - var previousResize = resizeForTest; - var previousRenderUpdate = renderUpdateForTest; - var previousTextureRead = textureReadForTest; - var previousDestroy = destroyForTest; - resizeForTest = resize; - renderUpdateForTest = renderUpdate; - textureReadForTest = textureRead; - destroyForTest = destroy; - return new RestoreSessionMethods( - previousResize, - previousRenderUpdate, - previousTextureRead, - previousDestroy - ); + get + { + var value = new mln_render_session_capabilities + { + size = (uint)sizeof(mln_render_session_capabilities), + }; + NativeStatus.Check(NativeMethods.mln_render_session_get_capabilities(Handle, &value)); + return new( + (RenderDriverKind)value.driver, + value.texture_ring_depth, + (RenderSessionCapabilities)value.flags + ); + } } - internal static IDisposable UseMetalFrameMethodsForTest( - MetalOwnedTextureAcquireFrame acquire, - MetalOwnedTextureReleaseFrame release, - Func readFrame - ) + public RenderSessionSnapshot Snapshot { - var previousAcquire = acquireMetalFrameForTest; - var previousRelease = releaseMetalFrameForTest; - var previousRead = readMetalFrameForTest; - acquireMetalFrameForTest = acquire; - releaseMetalFrameForTest = release; - readMetalFrameForTest = readFrame; - return new RestoreMetalFrameMethods(previousAcquire, previousRelease, previousRead); + get + { + var value = new mln_render_session_snapshot + { + size = (uint)sizeof(mln_render_session_snapshot), + }; + NativeStatus.Check(NativeMethods.mln_render_session_get_snapshot(Handle, &value)); + return new( + (RenderSessionState)value.state, + (RenderDriverKind)value.driver, + (RenderResult)value.latest_result, + new(value.extent.width, value.extent.height, value.extent.scale_factor), + value.generation, + value.map_update_generation, + value.rendered_update_generation, + value.extent_generation, + value.frame_generation, + value.latest_demand_token, + value.pending_demand_count, + value.acquired_frame_count, + value.target_ready != 0, + value.pending_changes != 0 + ); + } } - internal static IDisposable UseTextureFrameAcquireMethodsForTest( - VulkanOwnedTextureAcquireFrame acquireVulkan, - OpenGLOwnedTextureAcquireFrame acquireOpenGL - ) - { - var previousVulkan = acquireVulkanFrameForTest; - var previousOpenGL = acquireOpenGLFrameForTest; - acquireVulkanFrameForTest = acquireVulkan; - acquireOpenGLFrameForTest = acquireOpenGL; - return new RestoreTextureFrameAcquireMethods(previousVulkan, previousOpenGL); - } + internal MlnRenderSession Handle => state.Handle; public static RenderSessionHandle AttachMetalSurface( MapHandle map, - MetalSurfaceDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check(NativeMethods.mln_metal_surface_attach(map.Handle, &native, &session)); - return new RenderSessionHandle(map, session); - } + MetalSurfaceDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => NativeMethods.mln_metal_surface_attach(m, d, o, s, p) + ); public static RenderSessionHandle AttachVulkanSurface( MapHandle map, - VulkanSurfaceDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check(NativeMethods.mln_vulkan_surface_attach(map.Handle, &native, &session)); - return new RenderSessionHandle(map, session); - } + VulkanSurfaceDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => NativeMethods.mln_vulkan_surface_attach(m, d, o, s, p) + ); public static RenderSessionHandle AttachOpenGLSurface( MapHandle map, - OpenGLSurfaceDescriptor descriptor + OpenGLSurfaceDescriptor descriptor, + RenderSessionAttachOptions? options ) { - ArgumentNullException.ThrowIfNull(map); var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check(OpenGLSurfaceAttachNative(map.Handle, &native, &session)); - return new RenderSessionHandle(map, session); + using var selector = ApplyCanvasSelector(descriptor.Context, ref native.context); + return Attach( + map, + native, + options ?? DefaultOpenGLHostOptions(descriptor.Context), + static (m, d, o, s, p) => NativeMethods.mln_opengl_surface_attach(m, d, o, s, p) + ); } + public static RenderSessionHandle AttachWebGpuSurface( + MapHandle map, + WebGpuSurfaceDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options ?? CallerGraphicsOptions(), + static (m, d, o, s, p) => NativeMethods.mln_webgpu_surface_attach(m, d, o, s, p) + ); + public static RenderSessionHandle AttachMetalOwnedTexture( MapHandle map, - MetalOwnedTextureDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check( - NativeMethods.mln_metal_owned_texture_attach(map.Handle, &native, &session) + MetalOwnedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => NativeMethods.mln_metal_owned_texture_attach(m, d, o, s, p) ); - return new RenderSessionHandle(map, session); - } public static RenderSessionHandle AttachMetalBorrowedTexture( MapHandle map, - MetalBorrowedTextureDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check( - NativeMethods.mln_metal_borrowed_texture_attach(map.Handle, &native, &session) + MetalBorrowedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => NativeMethods.mln_metal_borrowed_texture_attach(m, d, o, s, p) ); - return new RenderSessionHandle(map, session); - } public static RenderSessionHandle AttachVulkanOwnedTexture( MapHandle map, - VulkanOwnedTextureDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check( - NativeMethods.mln_vulkan_owned_texture_attach(map.Handle, &native, &session) + VulkanOwnedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => NativeMethods.mln_vulkan_owned_texture_attach(m, d, o, s, p) ); - return new RenderSessionHandle(map, session); - } public static RenderSessionHandle AttachVulkanBorrowedTexture( MapHandle map, - VulkanBorrowedTextureDescriptor descriptor - ) - { - ArgumentNullException.ThrowIfNull(map); - var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check( - NativeMethods.mln_vulkan_borrowed_texture_attach(map.Handle, &native, &session) + VulkanBorrowedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options, + static (m, d, o, s, p) => + NativeMethods.mln_vulkan_borrowed_texture_attach(m, d, o, s, p) ); - return new RenderSessionHandle(map, session); - } public static RenderSessionHandle AttachOpenGLOwnedTexture( MapHandle map, - OpenGLOwnedTextureDescriptor descriptor + OpenGLOwnedTextureDescriptor descriptor, + RenderSessionAttachOptions? options ) { - ArgumentNullException.ThrowIfNull(map); var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check(OpenGLOwnedTextureAttachNative(map.Handle, &native, &session)); - return new RenderSessionHandle(map, session); + using var selector = ApplyCanvasSelector(descriptor.Context, ref native.context); + return Attach( + map, + native, + options ?? DefaultOpenGLOwnedTextureOptions(descriptor.Context), + static (m, d, o, s, p) => NativeMethods.mln_opengl_owned_texture_attach(m, d, o, s, p) + ); } public static RenderSessionHandle AttachOpenGLBorrowedTexture( MapHandle map, - OpenGLBorrowedTextureDescriptor descriptor + OpenGLBorrowedTextureDescriptor descriptor, + RenderSessionAttachOptions? options ) { - ArgumentNullException.ThrowIfNull(map); var native = RenderStructs.ToNative(descriptor); - MlnRenderSession session = default; - NativeStatus.Check(OpenGLBorrowedTextureAttachNative(map.Handle, &native, &session)); - return new RenderSessionHandle(map, session); + using var selector = ApplyCanvasSelector(descriptor.Context, ref native.context); + return Attach( + map, + native, + options ?? DefaultOpenGLHostOptions(descriptor.Context), + static (m, d, o, s, p) => + NativeMethods.mln_opengl_borrowed_texture_attach(m, d, o, s, p) + ); } - internal MlnRenderSession Handle => state.Handle; + public static RenderSessionHandle AttachWebGpuOwnedTexture( + MapHandle map, + WebGpuOwnedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options ?? CallerGraphicsOptions(), + static (m, d, o, s, p) => NativeMethods.mln_webgpu_owned_texture_attach(m, d, o, s, p) + ); - public bool IsClosed => state.IsClosed; + public static RenderSessionHandle AttachWebGpuBorrowedTexture( + MapHandle map, + WebGpuBorrowedTextureDescriptor descriptor, + RenderSessionAttachOptions? options + ) => + Attach( + map, + RenderStructs.ToNative(descriptor), + options ?? CallerGraphicsOptions(), + static (m, d, o, s, p) => + NativeMethods.mln_webgpu_borrowed_texture_attach(m, d, o, s, p) + ); - /// - /// Resizes this attached render session. Surface and owned-texture sessions resize in place; - /// a borrowed texture target throws an unsupported-feature error, since its owner sizes it — - /// hand over a new texture with the backend's set-target method instead. A resize keeps the - /// session's renderer along with the tile pyramid, glyph and image atlases, and symbol - /// placement. A scale factor change retires the renderer instead, because shaders are compiled - /// for one pixel ratio. Map-owned feature state survives either way. - /// - public void Resize(uint width, uint height, double scaleFactor) + public void RequestFrame(FrameDemand demand) { - ThrowIfTextureFrameActive(nameof(Resize)); - NativeStatus.Check(ResizeNative(Handle, width, height, scaleFactor)); + var native = NativeMethods.mln_frame_demand_default(); + native.flags = (uint)demand.Flags; + native.token = demand.Token; + native.coalescing_boundary = demand.CoalescingBoundary; + native.timeout_ns = demand.TimeoutNanoseconds; + NativeStatus.Check(NativeMethods.mln_render_session_request_frame(Handle, &native)); } - /// - /// Presents this attached surface session through a new surface, keeping the session's renderer - /// and its cached state. The descriptor must name the same graphics context this session - /// attached with; one whose context device is neither null nor this session's device throws an - /// invalid-argument error and leaves this session rendering into the surface it has. The - /// descriptor's extent applies as applies one. - /// - public void SetMetalSurfaceTarget(MetalSurfaceDescriptor descriptor) + public RenderFrameBatch DrainFrameResults() { - ThrowIfTextureFrameActive(nameof(SetMetalSurfaceTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_metal_surface_set_target(Handle, &native)); + MlnRenderFrameBatch batch = default; + NativeStatus.Check(NativeMethods.mln_render_session_drain_frame_results(Handle, &batch)); + try + { + return new RenderFrameBatch(batch); + } + catch + { + NativeMethods.mln_render_frame_batch_release(batch); + throw; + } } - /// - /// Presents this attached surface session through a new surface. See - /// for what replacing a surface preserves. The outgoing - /// VkSurfaceKHR must still be valid, since this session holds a swapchain built from it. The - /// replacement must report the color format and surface-transform support this session - /// compiled a render pass and shaders for, or it throws an unsupported-feature error. - /// - public void SetVulkanSurfaceTarget(VulkanSurfaceDescriptor descriptor) + public bool TryAcquireFrame( + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out AcquiredFrameHandle? frame + ) { - ThrowIfTextureFrameActive(nameof(SetVulkanSurfaceTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_vulkan_surface_set_target(Handle, &native)); + MlnAcquiredFrame value = default; + var status = NativeMethods.mln_render_session_acquire_frame(Handle, &value); + if (status == mln_status.MLN_STATUS_NOT_READY) + { + frame = null; + return false; + } + NativeStatus.Check(status); + frame = new AcquiredFrameHandle(this, value); + lock (frameGate) + acquiredFrames.Add(frame); + return true; } /// - /// Presents this attached surface session through a new surface. See - /// for what replacing a surface preserves. The new surface is - /// made current on the next render, so a host may hand over a replacement for one it has already - /// destroyed, and an unusable surface is reported by the next . + /// Services typed native work on the caller's current graphics thread. The first successful + /// call fixes that native thread as the session's graphics thread. /// - public void SetOpenGLSurfaceTarget(OpenGLSurfaceDescriptor descriptor) + public int ServiceDriverWork(int maxWork) { - ThrowIfTextureFrameActive(nameof(SetOpenGLSurfaceTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_opengl_surface_set_target(Handle, &native)); + ArgumentOutOfRangeException.ThrowIfNegative(maxWork); + nuint serviced = 0; + NativeStatus.Check( + NativeMethods.mln_render_session_service_driver_work( + Handle, + checked((nuint)maxWork), + &serviced + ) + ); + return checked((int)serviced); } - /// - /// Renders this attached texture session into a new caller-owned texture, keeping the session's - /// renderer unless the scale factor changes, which starts a new renderer. The replacement must - /// belong to the device this session attached with, which throws an invalid-argument error - /// otherwise, and carry the pixel format it attached with, which throws an unsupported-feature - /// error otherwise; both leave this session rendering into the texture it has. The caller owns - /// the replacement and keeps it valid until the next replacement, detach, or dispose. The - /// outgoing texture is neither read nor released here. - /// - public void SetMetalBorrowedTextureTarget(MetalBorrowedTextureDescriptor descriptor) + public Task SetMetalSurfaceAsync( + MetalSurfaceDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - ThrowIfTextureFrameActive(nameof(SetMetalBorrowedTextureTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_metal_borrowed_texture_set_target(Handle, &native)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_metal_surface_set_target(Handle, &native, completion); + }) + .WaitAsync(cancellationToken); } - /// - /// Renders this attached texture session into a new caller-owned image. See - /// for what replacing a target preserves. The - /// replacement must carry the format and both layouts this session attached with, since its - /// render pass was built around them. - /// - public void SetVulkanBorrowedTextureTarget(VulkanBorrowedTextureDescriptor descriptor) + public Task SetVulkanSurfaceAsync( + VulkanSurfaceDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - ThrowIfTextureFrameActive(nameof(SetVulkanBorrowedTextureTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_vulkan_borrowed_texture_set_target(Handle, &native)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_vulkan_surface_set_target(Handle, &native, completion); + }) + .WaitAsync(cancellationToken); } - /// - /// Renders this attached texture session into a new caller-owned texture. See - /// for what replacing a target preserves. The - /// replacement belongs to the context this session attached with, or one in its share group, and - /// the host context must be current on this thread. - /// - public void SetOpenGLBorrowedTextureTarget(OpenGLBorrowedTextureDescriptor descriptor) + public Task SetOpenGLSurfaceAsync( + OpenGLSurfaceDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - ThrowIfTextureFrameActive(nameof(SetOpenGLBorrowedTextureTarget)); - var native = RenderStructs.ToNative(descriptor); - NativeStatus.Check(NativeMethods.mln_opengl_borrowed_texture_set_target(Handle, &native)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + using var selector = ApplyCanvasSelector(descriptor.Context, ref native.context); + return NativeMethods.mln_opengl_surface_set_target(Handle, &native, completion); + }) + .WaitAsync(cancellationToken); } - /// - /// Renders the latest available map render update into this session's render target. The map - /// retains its latest update, so repeated calls re-render it and report - /// again. Every other result names the wake to wait for: - /// and resolve on a - /// render-update-available event, and resolves when - /// the host changes the render target. The returned - /// flag tells whether the map asked for another frame while it rendered this one. - /// - public RenderUpdate RenderUpdate() + public Task SetWebGpuSurfaceAsync( + WebGpuSurfaceDescriptor descriptor, + CancellationToken cancellationToken = default + ) + { + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_webgpu_surface_set_target(Handle, &native, completion); + }) + .WaitAsync(cancellationToken); + } + + public Task SetMetalBorrowedTextureAsync( + MetalBorrowedTextureDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - ThrowIfTextureFrameActive(nameof(RenderUpdate)); - var result = mln_render_result.MLN_RENDER_RESULT_NO_UPDATE; - var needsRepaint = false; - NativeStatus.Check(RenderUpdateNative(Handle, &result, &needsRepaint)); - return new RenderUpdate((RenderResult)result, needsRepaint); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_metal_borrowed_texture_set_target( + Handle, + &native, + completion + ); + }) + .WaitAsync(cancellationToken); } - public void Detach() + public Task SetVulkanBorrowedTextureAsync( + VulkanBorrowedTextureDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - ThrowIfTextureFrameActive(nameof(Detach)); - NativeStatus.Check(NativeMethods.mln_render_session_detach(Handle)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_vulkan_borrowed_texture_set_target( + Handle, + &native, + completion + ); + }) + .WaitAsync(cancellationToken); } - public void ReduceMemoryUse() + public Task SetOpenGLBorrowedTextureAsync( + OpenGLBorrowedTextureDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_render_session_reduce_memory_use(Handle)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + using var selector = ApplyCanvasSelector(descriptor.Context, ref native.context); + return NativeMethods.mln_opengl_borrowed_texture_set_target( + Handle, + &native, + completion + ); + }) + .WaitAsync(cancellationToken); } - public void ClearData() + public Task SetWebGpuBorrowedTextureAsync( + WebGpuBorrowedTextureDescriptor descriptor, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_render_session_clear_data(Handle)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(descriptor); + return NativeMethods.mln_webgpu_borrowed_texture_set_target( + Handle, + &native, + completion + ); + }) + .WaitAsync(cancellationToken); } - public void DumpDebugLogs() + /// + /// Resizes this attached render session. Surface and owned-texture sessions resize in place; + /// a borrowed texture target reports an unsupported-feature error, since its owner sizes it — + /// hand over a new texture with the backend's set-target method instead. A resize keeps the + /// session's renderer along with the tile pyramid, glyph and image atlases, and symbol + /// placement. A scale factor change retires the renderer instead, because shaders are compiled + /// for one pixel ratio. Map-owned feature state survives either way. + /// + public Task ResizeAsync( + RenderTargetExtent extent, + CancellationToken cancellationToken = default + ) { - NativeStatus.Check(NativeMethods.mln_render_session_dump_debug_logs(Handle)); + return NativeCompletion + .SubmitUnit(completion => + { + var native = RenderStructs.ToNative(extent); + return NativeMethods.mln_render_session_resize(Handle, &native, completion); + }) + .WaitAsync(cancellationToken); } - public QueriedFeature[] QueryRenderedFeatures( + public Task BarrierAsync(CancellationToken cancellationToken = default) => + StartOperationAsync( + operation => NativeMethods.mln_render_session_barrier(Handle, operation), + cancellationToken + ); + + public Task ReduceMemoryUseAsync(CancellationToken cancellationToken = default) => + StartOperationAsync( + operation => NativeMethods.mln_render_session_reduce_memory_use(Handle, operation), + cancellationToken + ); + + public Task ClearDataAsync(CancellationToken cancellationToken = default) => + StartOperationAsync( + operation => NativeMethods.mln_render_session_clear_data(Handle, operation), + cancellationToken + ); + + public Task DumpDebugLogsAsync(CancellationToken cancellationToken = default) => + StartOperationAsync( + operation => NativeMethods.mln_render_session_dump_debug_logs(Handle, operation), + cancellationToken + ); + + public Task ReadPremultipliedRgba8Async( + CancellationToken cancellationToken = default + ) => + NativeCompletion + .Submit( + completion => + NativeMethods.mln_texture_read_premultiplied_rgba8(Handle, completion), + static result => + { + var value = NativeCompletion.Value(result); + return ValueStructs.CopyBufferView(value.data); + } + ) + .WaitAsync(cancellationToken); + + public Task ReadImageAsync( + CancellationToken cancellationToken = default + ) => + NativeCompletion + .Submit( + completion => + NativeMethods.mln_texture_read_premultiplied_rgba8(Handle, completion), + static result => + { + var value = NativeCompletion.Value(result); + return new PremultipliedRgba8Image( + ValueStructs.CopyBufferView(value.data), + RenderStructs.FromNative(value.info) + ); + } + ) + .WaitAsync(cancellationToken); + + public Task> QueryRenderedFeaturesAsync( RenderedQueryGeometry geometry, - RenderedFeatureQueryOptions? options - ) => QueryRenderedFeaturesCore(geometry, options); + RenderedFeatureQueryOptions? options, + CancellationToken cancellationToken = default + ) + { + using var nativeGeometry = NativeRenderedQueryGeometry.From(geometry); + using var nativeOptions = options is null + ? null + : NativeRenderedFeatureQueryOptions.From(options); + return NativeCompletion + .Submit( + completion => + { + var geometryValue = nativeGeometry.Value; + var optionsValue = nativeOptions?.Value ?? default; + return NativeMethods.mln_render_session_query_rendered_features( + Handle, + &geometryValue, + nativeOptions is null ? null : &optionsValue, + completion + ); + }, + CopyQueriedFeatures + ) + .WaitAsync(cancellationToken); + } - public QueriedFeature[] QuerySourceFeatures( + public Task> QuerySourceFeaturesAsync( string sourceId, - SourceFeatureQueryOptions? options - ) => QuerySourceFeaturesCore(sourceId, options); + SourceFeatureQueryOptions? options, + CancellationToken cancellationToken = default + ) + { + using var source = NativeStringView.From(sourceId, nameof(sourceId)); + using var nativeOptions = options is null + ? null + : NativeSourceFeatureQueryOptions.From(options); + return NativeCompletion + .Submit( + completion => + { + var optionsValue = nativeOptions?.Value ?? default; + return NativeMethods.mln_render_session_query_source_features( + Handle, + source.Value, + nativeOptions is null ? null : &optionsValue, + completion + ); + }, + CopyQueriedFeatures + ) + .WaitAsync(cancellationToken); + } - /// - /// Queries a feature extension from the latest render session state. - /// - public byte[] QueryFeatureExtension( + public Task QueryFeatureExtensionAsync( string sourceId, byte[] feature, string extension, string extensionField, - byte[]? arguments + byte[]? arguments, + CancellationToken cancellationToken = default ) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); + using var source = NativeStringView.From(sourceId, nameof(sourceId)); using var nativeFeature = NativeStringView.From(feature, nameof(feature)); using var nativeExtension = NativeStringView.From(extension, nameof(extension)); - using var nativeExtensionField = NativeStringView.From( - extensionField, - nameof(extensionField) - ); + using var field = NativeStringView.From(extensionField, nameof(extensionField)); using var nativeArguments = arguments is null ? null : NativeStringView.From(arguments, nameof(arguments)); - MlnBuffer result = default; - NativeStatus.Check( - NativeMethods.mln_render_session_query_feature_extensions( - Handle, - nativeSourceId.Value, - nativeFeature.Value, - nativeExtension.Value, - nativeExtensionField.Value, - nativeArguments?.Pointer, - &result + return NativeCompletion + .Submit( + completion => + { + var argumentValue = nativeArguments?.Value ?? default; + return NativeMethods.mln_render_session_query_feature_extensions( + Handle, + source.Value, + nativeFeature.Value, + nativeExtension.Value, + field.Value, + nativeArguments is null ? null : &argumentValue, + completion + ); + }, + CopyBuffer ) - ); - return ValueStructs.ReadBuffer(result); + .WaitAsync(cancellationToken); } - public TextureImageInfo TextureImageInfo() - { - ThrowIfTextureFrameActive(nameof(TextureImageInfo)); - var info = new mln_texture_image_info { size = (uint)sizeof(mln_texture_image_info) }; - var status = TextureReadNative(Handle, null, 0, &info); - var copied = RenderStructs.FromNative(info); - if ( - status == mln_status.MLN_STATUS_OK - || (status == mln_status.MLN_STATUS_INVALID_ARGUMENT && copied.ByteLength > 0) - ) - { - return copied; - } - - NativeStatus.Check(status); - throw new InvalidOperationException("Unreachable native texture status."); - } + public Task DetachAsync(CancellationToken cancellationToken = default) => + StartOperationAsync( + operation => NativeMethods.mln_render_session_detach(Handle, operation), + cancellationToken + ); - public TextureImageInfo ReadPremultipliedRgba8(NativeBuffer buffer) + public RenderAbandonResult Abandon() { - ArgumentNullException.ThrowIfNull(buffer); - ThrowIfTextureFrameActive(nameof(ReadPremultipliedRgba8)); - var info = new mln_texture_image_info { size = (uint)sizeof(mln_texture_image_info) }; - fixed (byte* data = buffer.Span) + var result = new mln_render_abandon_result { - NativeStatus.Check( - TextureReadNative( - Handle, - buffer.ByteLength == 0 ? null : data, - (nuint)buffer.ByteLength, - &info - ) - ); - } - // An empty destination reaches native code as a size probe, which succeeds without - // copying, so report the buffer as too small unless the frame carries no bytes. - if (buffer.ByteLength == 0 && info.byte_length > 0) + size = (uint)sizeof(mln_render_abandon_result), + }; + NativeStatus.Check(NativeMethods.mln_render_session_abandon(Handle, &result)); + lock (frameGate) { - throw new InvalidArgumentException( - MaplibreStatus.InvalidArgument, - null, - $"Buffer length 0 is smaller than the required {info.byte_length} bytes.", - null - ); + foreach (var frame in acquiredFrames) + frame.InvalidateAccessors(); } - return RenderStructs.FromNative(info); + return new((RenderAbandonDisposition)result.disposition, result.quarantined_resource_count); } - public MetalOwnedTextureFrameHandle AcquireMetalOwnedTextureFrame() - { - ReserveActiveTextureFrame(); - mln_metal_owned_texture_frame* pointer = null; - var acquired = false; - var reservationHeld = true; - FrameScope? scope = null; - try - { - pointer = (mln_metal_owned_texture_frame*) - NativeMemory.AllocZeroed((nuint)sizeof(mln_metal_owned_texture_frame)); - pointer->size = (uint)sizeof(mln_metal_owned_texture_frame); - NativeStatus.Check(AcquireMetalFrameNative(Handle, pointer)); - acquired = true; - scope = new FrameScope(nameof(MetalOwnedTextureFrame)); - var frame = ReadMetalFrame(*pointer, scope); - var handle = new MetalOwnedTextureFrameHandle(this, pointer, scope, frame, true); - reservationHeld = false; - return handle; - } - catch - { - if (acquired) - { - ReleaseAcquiredFrameAfterConstructionFailure( - pointer, - static (session, frame) => session.ReleaseMetalFrame(frame), - nameof(MetalOwnedTextureFrameHandle) - ); - } - scope?.Dispose(); - if (pointer is not null) - { - NativeMemory.Free(pointer); - } - if (reservationHeld) - { - UnregisterActiveTextureFrame(); - } - throw; - } - } + public void Close() => state.Close(); - public VulkanOwnedTextureFrameHandle AcquireVulkanOwnedTextureFrame() + public void Dispose() { - ReserveActiveTextureFrame(); - mln_vulkan_owned_texture_frame* pointer = null; - var acquired = false; - var reservationHeld = true; - FrameScope? scope = null; - try - { - pointer = (mln_vulkan_owned_texture_frame*) - NativeMemory.AllocZeroed((nuint)sizeof(mln_vulkan_owned_texture_frame)); - pointer->size = (uint)sizeof(mln_vulkan_owned_texture_frame); - NativeStatus.Check(AcquireVulkanFrameNative(Handle, pointer)); - acquired = true; - scope = new FrameScope(nameof(VulkanOwnedTextureFrame)); - var frame = RenderStructs.FromNative(*pointer, scope); - var handle = new VulkanOwnedTextureFrameHandle(this, pointer, scope, frame, true); - reservationHeld = false; - return handle; - } - catch - { - if (acquired) - { - ReleaseAcquiredFrameAfterConstructionFailure( - pointer, - static (session, frame) => session.ReleaseVulkanFrame(frame), - nameof(VulkanOwnedTextureFrameHandle) - ); - } - scope?.Dispose(); - if (pointer is not null) - { - NativeMemory.Free(pointer); - } - if (reservationHeld) - { - UnregisterActiveTextureFrame(); - } - throw; - } + state.TryClose(); + GC.KeepAlive(map); } - public OpenGLOwnedTextureFrameHandle AcquireOpenGLOwnedTextureFrame() + internal void FrameReleased(AcquiredFrameHandle frame) { - ReserveActiveTextureFrame(); - mln_opengl_owned_texture_frame* pointer = null; - var acquired = false; - var reservationHeld = true; - FrameScope? scope = null; - try - { - pointer = (mln_opengl_owned_texture_frame*) - NativeMemory.AllocZeroed((nuint)sizeof(mln_opengl_owned_texture_frame)); - pointer->size = (uint)sizeof(mln_opengl_owned_texture_frame); - NativeStatus.Check(AcquireOpenGLFrameNative(Handle, pointer)); - acquired = true; - scope = new FrameScope(nameof(OpenGLOwnedTextureFrame)); - var frame = RenderStructs.FromNative(*pointer, scope); - var handle = new OpenGLOwnedTextureFrameHandle(this, pointer, scope, frame, true); - reservationHeld = false; - return handle; - } - catch - { - if (acquired) - { - ReleaseAcquiredFrameAfterConstructionFailure( - pointer, - static (session, frame) => session.ReleaseOpenGLFrame(frame), - nameof(OpenGLOwnedTextureFrameHandle) - ); - } - scope?.Dispose(); - if (pointer is not null) - { - NativeMemory.Free(pointer); - } - if (reservationHeld) - { - UnregisterActiveTextureFrame(); - } - throw; - } + lock (frameGate) + acquiredFrames.Remove(frame); } - internal mln_status ReleaseMetalFrame(mln_metal_owned_texture_frame* frame) => - ReleaseMetalFrameNative(Handle, frame); - - internal mln_status ReleaseVulkanFrame(mln_vulkan_owned_texture_frame* frame) => - NativeMethods.mln_vulkan_owned_texture_release_frame(Handle, frame); + private static RenderSessionAttachOptions CallerGraphicsOptions() => + new() { Driver = RenderDriverKind.CallerGraphicsThread }; - internal mln_status ReleaseOpenGLFrame(mln_opengl_owned_texture_frame* frame) => - NativeMethods.mln_opengl_owned_texture_release_frame(Handle, frame); - - private void ReleaseAcquiredFrameAfterConstructionFailure( - T* pointer, - FrameRelease release, - string typeName - ) - where T : unmanaged - { - try - { - var status = release(this, pointer); - if (status == mln_status.MLN_STATUS_OK) - { - return; - } - - NativeLeakReporter.Report( - new NativeLeakReport( - NativeLeakReportKind.DisposeFailed, - typeName, - 0, - status, - $"Construction failed after acquiring {typeName} frame 0x{(nint)pointer:x}; cleanup returned {status}." - ) - ); - } - catch (Exception error) + private static RenderSessionAttachOptions DefaultOpenGLHostOptions( + OpenGLContextDescriptor? context + ) => + new() { - NativeLeakReporter.Report( - new NativeLeakReport( - NativeLeakReportKind.DisposeFailed, - typeName, - 0, - null, - $"Construction failed after acquiring {typeName} frame 0x{(nint)pointer:x}; cleanup threw {error.GetType().Name}: {error.Message}" - ) - ); - } - } - - private static OpenGLSurfaceAttach OpenGLSurfaceAttachNative => - openGLSurfaceAttachForTest ?? DefaultOpenGLSurfaceAttach; - - private static OpenGLOwnedTextureAttach OpenGLOwnedTextureAttachNative => - openGLOwnedTextureAttachForTest ?? DefaultOpenGLOwnedTextureAttach; - - private static OpenGLBorrowedTextureAttach OpenGLBorrowedTextureAttachNative => - openGLBorrowedTextureAttachForTest ?? DefaultOpenGLBorrowedTextureAttach; - - private static RenderSessionResize ResizeNative => resizeForTest ?? DefaultResize; - - private static RenderSessionRenderUpdate RenderUpdateNative => - renderUpdateForTest ?? DefaultRenderUpdate; - - private static TextureRead TextureReadNative => textureReadForTest ?? DefaultTextureRead; - - private static mln_status DestroyNative(MlnRenderSession session) => - (destroyForTest ?? DefaultDestroy)(session); - - private static MetalOwnedTextureAcquireFrame AcquireMetalFrameNative => - acquireMetalFrameForTest ?? DefaultAcquireMetalFrame; - - private static MetalOwnedTextureReleaseFrame ReleaseMetalFrameNative => - releaseMetalFrameForTest ?? DefaultReleaseMetalFrame; + Driver = context is WebGLContextDescriptor { Kind: WebGLContextKind.TransferredCanvas } + ? RenderDriverKind.CoreWorker + : RenderDriverKind.CallerGraphicsThread, + }; - private static VulkanOwnedTextureAcquireFrame AcquireVulkanFrameNative => - acquireVulkanFrameForTest ?? DefaultAcquireVulkanFrame; - - private static OpenGLOwnedTextureAcquireFrame AcquireOpenGLFrameNative => - acquireOpenGLFrameForTest ?? DefaultAcquireOpenGLFrame; - - private static MetalOwnedTextureFrame ReadMetalFrame( - mln_metal_owned_texture_frame frame, - FrameScope scope + private static RenderSessionAttachOptions DefaultOpenGLOwnedTextureOptions( + OpenGLContextDescriptor? context ) => - readMetalFrameForTest is { } reader - ? reader(frame, scope) - : RenderStructs.FromNative(frame, scope); - - private sealed class RestoreOpenGLAttachMethods( - OpenGLSurfaceAttach? previousSurface, - OpenGLOwnedTextureAttach? previousOwnedTexture, - OpenGLBorrowedTextureAttach? previousBorrowedTexture - ) : IDisposable - { - public void Dispose() + new() { - openGLSurfaceAttachForTest = previousSurface; - openGLOwnedTextureAttachForTest = previousOwnedTexture; - openGLBorrowedTextureAttachForTest = previousBorrowedTexture; - } - } - - private sealed class RestoreSessionMethods( - RenderSessionResize? previousResize, - RenderSessionRenderUpdate? previousRenderUpdate, - TextureRead? previousTextureRead, - StatusDestroy? previousDestroy - ) : IDisposable + Driver = context switch + { + EglContextDescriptor { Ownership: OpenGLContextOwnership.Dedicated } => + RenderDriverKind.CoreWorker, + WebGLContextDescriptor { Kind: WebGLContextKind.TransferredCanvas } => + RenderDriverKind.CoreWorker, + _ => RenderDriverKind.CallerGraphicsThread, + }, + }; + + private static RenderSessionHandle Attach( + MapHandle map, + T descriptor, + RenderSessionAttachOptions? options, + AttachNative attach + ) + where T : unmanaged { - public void Dispose() + ArgumentNullException.ThrowIfNull(map); + var nativeOptions = RenderStructs.ToNative(options); + MlnRenderSession* session = stackalloc MlnRenderSession[1]; + var attachment = NativeCompletion.SubmitUnit(completion => { - resizeForTest = previousResize; - renderUpdateForTest = previousRenderUpdate; - textureReadForTest = previousTextureRead; - destroyForTest = previousDestroy; - } + var descriptorValue = descriptor; + var optionsValue = nativeOptions; + return attach(map.Handle, &descriptorValue, &optionsValue, session, completion); + }); + return new RenderSessionHandle(map, *session, attachment); } - private sealed class RestoreMetalFrameMethods( - MetalOwnedTextureAcquireFrame? previousAcquire, - MetalOwnedTextureReleaseFrame? previousRelease, - Func? previousRead - ) : IDisposable + private static NativeStringView? ApplyCanvasSelector( + OpenGLContextDescriptor? context, + ref mln_opengl_context_descriptor native + ) { - public void Dispose() - { - acquireMetalFrameForTest = previousAcquire; - releaseMetalFrameForTest = previousRelease; - readMetalFrameForTest = previousRead; - } + if ( + context is not WebGLContextDescriptor { Kind: WebGLContextKind.TransferredCanvas } webgl + ) + return null; + var selector = NativeStringView.From(webgl.CanvasSelector, nameof(webgl.CanvasSelector)); + native.data.webgl.canvas_selector = selector.Value; + return selector; } - private sealed class RestoreTextureFrameAcquireMethods( - VulkanOwnedTextureAcquireFrame? previousVulkan, - OpenGLOwnedTextureAcquireFrame? previousOpenGL - ) : IDisposable - { - public void Dispose() - { - acquireVulkanFrameForTest = previousVulkan; - acquireOpenGLFrameForTest = previousOpenGL; - } - } + private Task StartOperationAsync( + StartRenderOperation start, + CancellationToken cancellationToken + ) => NativeCompletion.SubmitUnit(completion => start(completion)).WaitAsync(cancellationToken); - private QueriedFeature[] QueryRenderedFeaturesCore( - RenderedQueryGeometry geometry, - RenderedFeatureQueryOptions? options - ) + private static byte[] CopyBuffer(mln_completion_result* result) => + ValueStructs.CopyBufferView(NativeCompletion.Value(result)); + + private static IReadOnlyList CopyQueriedFeatures(mln_completion_result* result) { - using var nativeGeometry = NativeRenderedQueryGeometry.From(geometry); - using var nativeOptions = options is null - ? null - : NativeRenderedFeatureQueryOptions.From(options); - var geometryValue = nativeGeometry.Value; - MlnQueriedFeatureList result = default; - if (nativeOptions is null) - { - NativeStatus.Check( - NativeMethods.mln_render_session_query_rendered_features( - Handle, - &geometryValue, - null, - &result - ) - ); - } - else + var values = NativeCompletion.Values(result); + var features = new QueriedFeature[values.Length]; + for (var index = 0; index < features.Length; index++) { - var optionsValue = nativeOptions.Value; - NativeStatus.Check( - NativeMethods.mln_render_session_query_rendered_features( - Handle, - &geometryValue, - &optionsValue, - &result - ) + var native = values[index]; + var fields = (mln_queried_feature_field)native.fields; + features[index] = new QueriedFeature( + ValueStructs.CopyBufferView(native.feature), + fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_ID) + ? RuntimeStructs.CopyUtf8(native.source_id.data, native.source_id.size) + : null, + fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_LAYER_ID) + ? RuntimeStructs.CopyUtf8( + native.source_layer_id.data, + native.source_layer_id.size + ) + : null, + fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE) + ? ValueStructs.CopyBufferView(native.state) + : null ); } - return CopyQueriedFeatureList(result); + return features; } - private QueriedFeature[] QuerySourceFeaturesCore( - string sourceId, - SourceFeatureQueryOptions? options - ) + internal static RenderFrameResult FromNative(mln_render_frame_result value) => + new( + (RenderResult)value.disposition, + value.token, + value.map_update_generation, + value.extent_generation, + value.frame_generation, + value.needs_repaint != 0 + ); +} + +/// Owned, stable frame-result records from one level-triggered drain. +public sealed unsafe class RenderFrameBatch : IReadOnlyList, IDisposable +{ + private readonly object gate = new(); + private MlnRenderFrameBatch handle; + + internal RenderFrameBatch(MlnRenderFrameBatch handle) { - using var nativeSourceId = NativeStringView.From(sourceId, nameof(sourceId)); - using var nativeOptions = options is null - ? null - : NativeSourceFeatureQueryOptions.From(options); - MlnQueriedFeatureList result = default; - if (nativeOptions is null) + this.handle = handle; + nuint count = 0; + NativeStatus.Check(NativeMethods.mln_render_frame_batch_count(handle, &count)); + if (count > int.MaxValue) { - NativeStatus.Check( - NativeMethods.mln_render_session_query_source_features( - Handle, - nativeSourceId.Value, - null, - &result - ) - ); - } - else - { - var optionsValue = nativeOptions.Value; - NativeStatus.Check( - NativeMethods.mln_render_session_query_source_features( - Handle, - nativeSourceId.Value, - &optionsValue, - &result - ) - ); + throw new OverflowException("The frame-result batch is too large."); } - return CopyQueriedFeatureList(result); + Count = (int)count; } - private static QueriedFeature[] CopyQueriedFeatureList(MlnQueriedFeatureList list) - { - if (list.IsNull) - { - return []; - } + public int Count { get; } - try + public RenderFrameResult this[int index] + { + get { - nuint count = 0; - NativeStatus.Check(NativeMethods.mln_queried_feature_list_count(list, &count)); - var features = new QueriedFeature[checked((int)count)]; - for (var index = 0; index < features.Length; index++) + ArgumentOutOfRangeException.ThrowIfNegative(index); + if (index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + lock (gate) { - var native = NativeMethods.mln_queried_feature_default(); + if (handle.IsNull) + throw new ObjectDisposedException(nameof(RenderFrameBatch)); + var value = new mln_render_frame_result + { + size = (uint)sizeof(mln_render_frame_result), + }; NativeStatus.Check( - NativeMethods.mln_queried_feature_list_get(list, (nuint)index, &native) - ); - var fields = (mln_queried_feature_field)native.fields; - features[index] = new QueriedFeature( - ValueStructs.CopyBufferView(native.feature), - fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_ID) - ? RuntimeStructs.CopyUtf8(native.source_id.data, native.source_id.size) - : null, - fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_LAYER_ID) - ? RuntimeStructs.CopyUtf8( - native.source_layer_id.data, - native.source_layer_id.size - ) - : null, - fields.HasFlag(mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE) - ? ValueStructs.CopyBufferView(native.state) - : null + NativeMethods.mln_render_frame_batch_get(handle, (nuint)index, &value) ); + return RenderSessionHandle.FromNative(value); } - - return features; - } - finally - { - NativeMethods.mln_queried_feature_list_destroy(list); } } - /// Destroys the render session on the map owner thread. - public void Close() - { - ThrowIfTextureFrameActive(nameof(Close)); - state.Close(); - } + public IEnumerator GetEnumerator() => new Enumerator(this); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); - /// public void Dispose() { - if (IsTextureFrameActive()) + lock (gate) { - ReportDisposeWithActiveTextureFrame(); - GC.KeepAlive(map); - return; + if (handle.IsNull) + return; + NativeMethods.mln_render_frame_batch_release(handle); + handle = default; } - - state.TryClose(); - GC.KeepAlive(map); } - private bool IsTextureFrameActive() + private sealed class Enumerator(RenderFrameBatch batch) : IEnumerator { - lock (frameGate) - { - return hasActiveTextureFrame; - } - } + private int index = -1; - private void ThrowIfTextureFrameActive(string operation) - { - if (!IsTextureFrameActive()) + public RenderFrameResult Current => batch[index]; + object System.Collections.IEnumerator.Current => Current; + + public bool MoveNext() { - return; + if (index + 1 >= batch.Count) + return false; + index++; + return true; } - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - $"{operation} cannot run while a texture frame is active.", - null - ); + public void Reset() => index = -1; + + public void Dispose() { } } +} + +/// An acquired texture-ring slot whose native accessors remain valid until release. +public sealed unsafe class AcquiredFrameHandle : IDisposable +{ + private readonly RenderSessionHandle session; + private readonly FrameScope scope = new(); + private readonly object gate = new(); + private MlnAcquiredFrame handle; - private void ReportDisposeWithActiveTextureFrame() + internal AcquiredFrameHandle(RenderSessionHandle session, MlnAcquiredFrame handle) { - NativeLeakReporter.Report( - new NativeLeakReport( - NativeLeakReportKind.DisposeFailed, - nameof(RenderSessionHandle), - Handle.Value, - null, - "Dispose could not close RenderSessionHandle while a texture frame is active. Release the frame on the owner thread, then call Close() to observe errors and retry." - ) - ); + this.session = session; + this.handle = handle; } - internal void ReserveActiveTextureFrame() + public RenderFrameResult Result { - lock (frameGate) + get { - if (hasActiveTextureFrame) + lock (gate) { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - "A texture frame is active already.", - null + var value = new mln_render_frame_result + { + size = (uint)sizeof(mln_render_frame_result), + }; + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_result(RequireHandleLocked(), &value) ); + return RenderSessionHandle.FromNative(value); } - - hasActiveTextureFrame = true; } } - internal void UnregisterActiveTextureFrame() + public GpuSync ProducerSync { - lock (frameGate) + get { - hasActiveTextureFrame = false; + lock (gate) + { + var value = NativeMethods.mln_gpu_sync_default(); + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_producer_sync( + RequireHandleLocked(), + &value + ) + ); + return new( + (GpuSyncKind)value.kind, + NativePointer.FromNativeAddress((nint)value.@object), + value.value + ); + } } } -} -internal unsafe delegate mln_status FrameRelease(RenderSessionHandle session, T* frame) - where T : unmanaged; -internal unsafe delegate mln_status TextureRead( - MlnRenderSession session, - byte* data, - nuint length, - mln_texture_image_info* info -); -internal unsafe delegate mln_status VulkanOwnedTextureAcquireFrame( - MlnRenderSession session, - mln_vulkan_owned_texture_frame* frame -); -internal unsafe delegate mln_status OpenGLOwnedTextureAcquireFrame( - MlnRenderSession session, - mln_opengl_owned_texture_frame* frame -); - -internal sealed unsafe class TextureFrameState - where T : unmanaged -{ - private readonly RenderSessionHandle session; - private readonly FrameScope scope; - private readonly FrameRelease release; - private readonly string typeName; - private T* pointer; - - internal TextureFrameState( - RenderSessionHandle session, - T* pointer, - FrameScope scope, - FrameRelease release, - string typeName, - bool activeFrameReserved = false - ) + public MetalOwnedTextureFrame GetMetalTexture() { - this.session = session; - this.pointer = pointer; - this.scope = scope; - this.release = release; - this.typeName = typeName; - if (!activeFrameReserved) + lock (gate) { - session.ReserveActiveTextureFrame(); + var value = new mln_metal_owned_texture_frame + { + size = (uint)sizeof(mln_metal_owned_texture_frame), + }; + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_metal_texture(RequireHandleLocked(), &value) + ); + return RenderStructs.FromNative(value, scope); } } - internal bool IsClosed => pointer is null; - - internal void Close() + public VulkanOwnedTextureFrame GetVulkanTexture() { - if (pointer is null) + lock (gate) { - return; + var value = new mln_vulkan_owned_texture_frame + { + size = (uint)sizeof(mln_vulkan_owned_texture_frame), + }; + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_vulkan_texture(RequireHandleLocked(), &value) + ); + return RenderStructs.FromNative(value, scope); } - - NativeStatus.Check(release(session, pointer)); - MarkClosed(); } - internal void TryClose() + public OpenGLOwnedTextureFrame GetOpenGLTexture() { - if (pointer is null) - { - return; - } - - if (session.IsClosed) - { - ReportParentClosed(); - MarkClosed(); - return; - } - - mln_status status; - try - { - status = release(session, pointer); - } - catch (MaplibreException) when (session.IsClosed) + lock (gate) { - ReportParentClosed(); - MarkClosed(); - return; - } - - if (status != mln_status.MLN_STATUS_OK) - { - NativeLeakReporter.Report( - new NativeLeakReport( - NativeLeakReportKind.DisposeFailed, - typeName, - 0, - status, - $"Dispose could not release {typeName} frame 0x{(nint)pointer:x}; native release returned {status}. Call Close() on the owner thread to observe the error and retry." - ) + var value = new mln_opengl_owned_texture_frame + { + size = (uint)sizeof(mln_opengl_owned_texture_frame), + }; + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_opengl_texture(RequireHandleLocked(), &value) ); - return; + return RenderStructs.FromNative(value, scope); } - - MarkClosed(); } - private void ReportParentClosed() + public WebGpuOwnedTextureFrame GetWebGpuTexture() { - NativeLeakReporter.Report( - new NativeLeakReport( - NativeLeakReportKind.DisposeFailed, - typeName, - 0, - null, - $"Dispose could not release {typeName} frame 0x{(nint)pointer:x}; the parent RenderSessionHandle is already closed." - ) - ); + lock (gate) + { + var value = new mln_webgpu_owned_texture_frame + { + size = (uint)sizeof(mln_webgpu_owned_texture_frame), + }; + NativeStatus.Check( + NativeMethods.mln_acquired_frame_get_webgpu_texture(RequireHandleLocked(), &value) + ); + return RenderStructs.FromNative(value, scope); + } } - private void MarkClosed() + public void Release(GpuSync? consumerCompletion) { - var current = pointer; - pointer = null; - scope.Dispose(); - NativeMemory.Free(current); - session.UnregisterActiveTextureFrame(); + lock (gate) + { + if (handle.IsNull) + throw new ObjectDisposedException(nameof(AcquiredFrameHandle)); + var sync = NativeMethods.mln_gpu_sync_default(); + if (consumerCompletion is { } completion) + { + sync.kind = (uint)completion.Kind; + sync.@object = (void*)completion.Object.Address; + sync.value = completion.Value; + } + var frame = handle; + NativeStatus.Check(NativeMethods.mln_acquired_frame_release(&frame, &sync)); + handle = frame; + scope.Dispose(); + } + session.FrameReleased(this); } -} -public sealed unsafe class MetalOwnedTextureFrameHandle : IDisposable -{ - private readonly TextureFrameState state; - - internal MetalOwnedTextureFrameHandle( - RenderSessionHandle session, - mln_metal_owned_texture_frame* pointer, - FrameScope scope, - MetalOwnedTextureFrame frame, - bool activeFrameReserved = false - ) + internal void InvalidateAccessors() { - state = new TextureFrameState( - session, - pointer, - scope, - static (session, frame) => session.ReleaseMetalFrame(frame), - nameof(MetalOwnedTextureFrameHandle), - activeFrameReserved - ); - Frame = frame; + lock (gate) + scope.Dispose(); } - public bool IsClosed => state.IsClosed; - - public MetalOwnedTextureFrame Frame { get; } - - public void Close() => state.Close(); - - public void Dispose() => state.TryClose(); -} - -public sealed unsafe class VulkanOwnedTextureFrameHandle : IDisposable -{ - private readonly TextureFrameState state; - - internal VulkanOwnedTextureFrameHandle( - RenderSessionHandle session, - mln_vulkan_owned_texture_frame* pointer, - FrameScope scope, - VulkanOwnedTextureFrame frame, - bool activeFrameReserved = false - ) + public void Dispose() { - state = new TextureFrameState( - session, - pointer, - scope, - static (session, frame) => session.ReleaseVulkanFrame(frame), - nameof(VulkanOwnedTextureFrameHandle), - activeFrameReserved - ); - Frame = frame; + lock (gate) + { + if (handle.IsNull) + return; + var sync = NativeMethods.mln_gpu_sync_default(); + var frame = handle; + NativeStatus.Check(NativeMethods.mln_acquired_frame_release(&frame, &sync)); + handle = frame; + scope.Dispose(); + } + session.FrameReleased(this); } - public bool IsClosed => state.IsClosed; - - public VulkanOwnedTextureFrame Frame { get; } - - public void Close() => state.Close(); - - public void Dispose() => state.TryClose(); -} - -public sealed unsafe class OpenGLOwnedTextureFrameHandle : IDisposable -{ - private readonly TextureFrameState state; - - internal OpenGLOwnedTextureFrameHandle( - RenderSessionHandle session, - mln_opengl_owned_texture_frame* pointer, - FrameScope scope, - OpenGLOwnedTextureFrame frame, - bool activeFrameReserved = false - ) + private MlnAcquiredFrame RequireHandleLocked() { - state = new TextureFrameState( - session, - pointer, - scope, - static (session, frame) => session.ReleaseOpenGLFrame(frame), - nameof(OpenGLOwnedTextureFrameHandle), - activeFrameReserved - ); - Frame = frame; + if (handle.IsNull) + throw new ObjectDisposedException(nameof(AcquiredFrameHandle)); + scope.EnsureActive(); + return handle; } - - public bool IsClosed => state.IsClosed; - - public OpenGLOwnedTextureFrame Frame { get; } - - public void Close() => state.Close(); - - public void Dispose() => state.TryClose(); } internal sealed class FrameScope : IDisposable { - private readonly string owner; - - internal FrameScope(string owner) - { - this.owner = owner; - } + private int active = 1; - public bool IsClosed { get; private set; } - - internal void EnsureActive() + public void EnsureActive() { - if (IsClosed) - { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - $"{owner} is closed.", - null - ); - } + if (Volatile.Read(ref active) == 0) + throw new ObjectDisposedException("acquired frame"); } - public void Dispose() - { - IsClosed = true; - } + public void Dispose() => Interlocked.Exchange(ref active, 0); } diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderTypes.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderTypes.cs index 39ee8b148..14642c1bf 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderTypes.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Render/RenderTypes.cs @@ -12,40 +12,137 @@ public enum RenderMode : uint } /// -/// Outcome of a successful render-update call. This is an open domain: a value may have no named -/// member here, so a switch over it needs a default case. Unknown values keep their raw value. +/// Terminal disposition of one accepted frame demand. Unknown values keep their raw value. /// public enum RenderResult : uint { - /// The call rendered a frame into the render target. + /// The session rendered a frame into the render target. Rendered = 0, - /// - /// The call produced no frame. Wait for a render-update-available event. - /// + /// No newer map update was available. NoUpdate = 1, - /// - /// The map has not applied the session's current size yet. Wait for the next - /// render-update-available event. - /// + /// An ordered extent change had not reached the driver. SizePending = 2, - /// - /// The render target had no frame to draw into. Wait for a host event that changes the render - /// target, or back off and retry. - /// + /// The target could not produce a frame. TargetNotReady = 3, + + /// A newer demand replaced this demand in its coalescing boundary. + Superseded = 4, + + /// The demand's timeout elapsed before work began. + DeadlineMissed = 5, +} + +public enum RenderSessionState : uint +{ + Attaching = 1, + Attached = 2, + Detaching = 3, + Detached = 4, + TargetLost = 5, + Abandoned = 6, } +public enum RenderDriverKind : uint +{ + CoreWorker = 1, + CallerGraphicsThread = 2, +} + +[Flags] +public enum RenderSessionCapabilities : uint +{ + None = 0, + FrameAcquisition = 1u << 0, + Readback = 1u << 1, + ConsumerSync = 1u << 2, + Presentation = 1u << 3, +} + +[Flags] +public enum FrameDemandFlags : uint +{ + None = 0, + IfNeeded = 1u << 0, + Present = 1u << 1, +} + +public readonly record struct FrameDemand( + FrameDemandFlags Flags, + ulong Token, + ulong CoalescingBoundary, + ulong TimeoutNanoseconds +); + /// -/// Outcome of a successful render-update call. is true only when -/// is and the map asked for another -/// frame while it rendered this one, as during an ongoing camera transition. This is the same -/// signal that a map-render-frame-finished event carries in its repaint flag, delivered without -/// the event round trip. +/// One frame demand's outcome. is meaningful only when +/// is ; it is true when the map +/// asked for another frame while it rendered this one, as during an ongoing camera transition. /// -public readonly record struct RenderUpdate(RenderResult Result, bool NeedsRepaint); +public readonly record struct RenderFrameResult( + RenderResult Disposition, + ulong Token, + ulong MapUpdateGeneration, + ulong ExtentGeneration, + ulong FrameGeneration, + bool NeedsRepaint +); + +public readonly record struct RenderSessionSnapshot( + RenderSessionState State, + RenderDriverKind Driver, + RenderResult LatestResult, + RenderTargetExtent Extent, + ulong Generation, + ulong MapUpdateGeneration, + ulong RenderedUpdateGeneration, + ulong ExtentGeneration, + ulong FrameGeneration, + ulong LatestDemandToken, + uint PendingDemandCount, + uint AcquiredFrameCount, + bool TargetReady, + bool PendingChanges +); + +public readonly record struct RenderSessionCapabilityInfo( + RenderDriverKind Driver, + uint TextureRingDepth, + RenderSessionCapabilities Flags +); + +public enum GpuSyncKind : uint +{ + CpuComplete = 0, + MetalSharedEvent = 1, + VulkanTimelineSemaphore = 2, + OpenGLFence = 3, + WebGpuToken = 4, +} + +public readonly record struct GpuSync(GpuSyncKind Kind, NativePointer Object, ulong Value) +{ + public static GpuSync CpuComplete => new(GpuSyncKind.CpuComplete, default, 0); +} + +public enum RenderAbandonDisposition : uint +{ + Clean = 0, + Quarantined = 1, +} + +public readonly record struct RenderAbandonResult( + RenderAbandonDisposition Disposition, + uint QuarantinedResourceCount +); + +public sealed record RenderSessionAttachOptions +{ + public RenderDriverKind Driver { get; init; } = RenderDriverKind.CoreWorker; + public uint RequestedTextureRingDepth { get; init; } +} /// Render backend support flags reported by the native library. [Flags] @@ -138,9 +235,9 @@ public sealed class VulkanContextDescriptor } /// -/// How a render session's OpenGL context relates to the thread that attached it. This is an open -/// domain: a value may have no named member here, so a switch over it needs a default case. -/// Unknown values keep their raw value. +/// How a render session's OpenGL context relates to its driver thread and host graphics state. This +/// is an open domain: a value may have no named member here, so a switch over it needs a default +/// case. Unknown values keep their raw value. /// public enum OpenGLContextOwnership : uint { @@ -152,8 +249,9 @@ public enum OpenGLContextOwnership : uint Shared = 0, /// - /// The session owns its thread's OpenGL context. It makes its context current once and keeps - /// it current between renders, and it joins no share group. + /// The session owns its driver thread's OpenGL context. It keeps the context current between + /// renders and joins no share group. The driver may be a native core worker or a dedicated host + /// thread. /// Dedicated = 1, } @@ -180,8 +278,8 @@ public abstract class OpenGLContextDescriptor private protected OpenGLContextDescriptor() { } /// - /// Whether the session shares its thread with host graphics work. WGL and EGL surface targets - /// support both. A texture target hands its texture to the host, so it is shared only. + /// Whether the session shares its driver thread and graphics objects with the host. A private + /// EGL owned texture target uses dedicated ownership and grants readback without acquisition. /// public OpenGLContextOwnership Ownership { get; set; } = OpenGLContextOwnership.Shared; } @@ -220,6 +318,26 @@ public sealed class EglContextDescriptor : OpenGLContextDescriptor public NativePointer GetProcAddress { get; set; } } +public enum WebGLContextKind : uint +{ + Existing = 0, + TransferredCanvas = 1, +} + +public sealed class WebGLContextDescriptor : OpenGLContextDescriptor +{ + public WebGLContextKind Kind { get; set; } + public int Context { get; set; } + public string CanvasSelector { get; set; } = string.Empty; +} + +public sealed class WebGpuContextDescriptor +{ + public NativePointer Instance { get; set; } + public NativePointer Device { get; set; } + public NativePointer Queue { get; set; } +} + public sealed class MetalSurfaceDescriptor { public RenderTargetExtent Extent { get; set; } @@ -241,6 +359,14 @@ public sealed class OpenGLSurfaceDescriptor public OpenGLContextDescriptor? Context { get; set; } } +public sealed class WebGpuSurfaceDescriptor +{ + public RenderTargetExtent Extent { get; set; } + public WebGpuContextDescriptor? Context { get; set; } + public NativePointer Surface { get; set; } + public uint Format { get; set; } +} + public sealed class MetalOwnedTextureDescriptor { public RenderTargetExtent Extent { get; set; } @@ -305,6 +431,135 @@ public sealed class OpenGLBorrowedTextureDescriptor public uint Target { get; set; } } +public sealed class WebGpuOwnedTextureDescriptor +{ + public RenderTargetExtent Extent { get; set; } + public WebGpuContextDescriptor? Context { get; set; } +} + +public sealed class WebGpuBorrowedTextureDescriptor +{ + public RenderTargetExtent Extent { get; set; } + public uint PhysicalWidth { get; set; } + public uint PhysicalHeight { get; set; } + public WebGpuContextDescriptor? Context { get; set; } + public NativePointer Texture { get; set; } + public NativePointer TextureView { get; set; } + public uint Format { get; set; } +} + +public sealed class WebGpuOwnedTextureFrame +{ + private readonly FrameScope scope; + private readonly ulong generation; + private readonly uint width; + private readonly uint height; + private readonly double scaleFactor; + private readonly ulong frameId; + private readonly NativePointer texture; + private readonly NativePointer textureView; + private readonly NativePointer device; + private readonly uint format; + + internal WebGpuOwnedTextureFrame( + FrameScope scope, + ulong generation, + uint width, + uint height, + double scaleFactor, + ulong frameId, + NativePointer texture, + NativePointer textureView, + NativePointer device, + uint format + ) + { + this.scope = scope; + this.generation = generation; + this.width = width; + this.height = height; + this.scaleFactor = scaleFactor; + this.frameId = frameId; + this.texture = texture; + this.textureView = textureView; + this.device = device; + this.format = format; + } + + public ulong Generation + { + get + { + scope.EnsureActive(); + return generation; + } + } + public uint Width + { + get + { + scope.EnsureActive(); + return width; + } + } + public uint Height + { + get + { + scope.EnsureActive(); + return height; + } + } + public double ScaleFactor + { + get + { + scope.EnsureActive(); + return scaleFactor; + } + } + public ulong FrameId + { + get + { + scope.EnsureActive(); + return frameId; + } + } + public NativePointer Texture + { + get + { + scope.EnsureActive(); + return texture; + } + } + public NativePointer TextureView + { + get + { + scope.EnsureActive(); + return textureView; + } + } + public NativePointer Device + { + get + { + scope.EnsureActive(); + return device; + } + } + public uint Format + { + get + { + scope.EnsureActive(); + return format; + } + } +} + public sealed class MetalOwnedTextureFrame { private readonly FrameScope scope; diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/NativeCompletion.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/NativeCompletion.cs new file mode 100644 index 000000000..1ba648176 --- /dev/null +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/NativeCompletion.cs @@ -0,0 +1,154 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Maplibre.NativeFfi.Internal.C; +using Maplibre.NativeFfi.Internal.Status; + +namespace Maplibre.NativeFfi.Runtime; + +/// The terminal outcome of an accepted ordered command. +public readonly record struct CommandCompletion( + CommandDisposition Disposition, + ulong Generation, + int RawStatus, + string Diagnostic +); + +internal unsafe delegate mln_status CompletionSubmit(mln_completion* completion); +internal unsafe delegate void CompletionAction(mln_completion* completion); +internal unsafe delegate T CompletionConverter(mln_completion_result* result); + +internal static unsafe class NativeCompletion +{ + internal static T Value(mln_completion_result* result) + where T : unmanaged + { + if (result->value is null || result->value_count != 1) + { + throw new InvalidOperationException("Native completion returned no value."); + } + return *(T*)result->value; + } + + internal static ReadOnlySpan Values(mln_completion_result* result) + where T : unmanaged + { + if (result->value_count == 0) + { + return []; + } + if (result->value is null) + { + throw new InvalidOperationException("Native completion returned a null array."); + } + return new ReadOnlySpan(result->value, checked((int)result->value_count)); + } + + internal static Task SubmitCommand(CompletionSubmit submit) => + Submit( + submit, + static result => new CommandCompletion( + (CommandDisposition)result->disposition, + result->generation, + (int)result->status, + StateBase.Diagnostic(result->diagnostic) + ), + true + ); + + internal static Task SubmitCommandChecked(CompletionAction submit) => + SubmitCommand(completion => + { + submit(completion); + return mln_status.MLN_STATUS_OK; + }); + + internal static Task SubmitUnit(CompletionSubmit submit) => Submit(submit, static _ => true); + + internal static Task Submit( + CompletionSubmit submit, + CompletionConverter convert, + bool acceptErrorStatus = false + ) + { + var state = new State(convert, acceptErrorStatus); + var root = GCHandle.Alloc(state); + var completion = new mln_completion + { + size = (uint)sizeof(mln_completion), + callback = &Complete, + user_data = (void*)GCHandle.ToIntPtr(root), + release_user_data = &Release, + }; + mln_status status; + try + { + status = submit(&completion); + } + catch + { + root.Free(); + throw; + } + if (status != mln_status.MLN_STATUS_OK) + { + root.Free(); + NativeStatus.Check(status); + } + return state.Task; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void Complete(void* userData, mln_completion_result* result) + { + try + { + ((StateBase)GCHandle.FromIntPtr((nint)userData).Target!).Complete(result); + } + catch + { + // Exceptions never cross the C boundary. StateBase records converter failures. + } + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void Release(void* userData) + { + GCHandle.FromIntPtr((nint)userData).Free(); + } + + private abstract class StateBase + { + internal abstract void Complete(mln_completion_result* result); + + internal static string Diagnostic(mln_buffer_view view) => + view.data is null || view.size == 0 + ? string.Empty + : Marshal.PtrToStringUTF8((nint)view.data, checked((int)view.size)) ?? string.Empty; + } + + private sealed class State(CompletionConverter convert, bool acceptErrorStatus) + : StateBase + { + private readonly TaskCompletionSource source = new( + TaskCreationOptions.RunContinuationsAsynchronously + ); + + internal Task Task => source.Task; + + internal override void Complete(mln_completion_result* result) + { + try + { + if (!acceptErrorStatus) + { + NativeStatus.Check((int)result->status, Diagnostic(result->diagnostic)); + } + source.TrySetResult(convert(result)); + } + catch (Exception error) + { + source.TrySetException(error); + } + } + } +} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OfflineOperationHandle.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OfflineOperationHandle.cs deleted file mode 100644 index b4e1d7865..000000000 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OfflineOperationHandle.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Maplibre.NativeFfi.Error; -using Maplibre.NativeFfi.Internal.Status; - -namespace Maplibre.NativeFfi.Runtime; - -/// Owner-thread offline database operation token. -public sealed class OfflineOperationHandle : IDisposable -{ - private readonly object gate = new(); - private readonly RuntimeHandle runtime; - private bool closed; - - internal OfflineOperationHandle( - RuntimeHandle runtime, - ulong id, - OfflineOperationKind kind, - OfflineOperationResultKind resultKind - ) - { - this.runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - if (id == 0) - { - throw new InvalidArgumentException( - MaplibreStatus.InvalidArgument, - null, - "Offline operation id must not be zero.", - null - ); - } - - Id = id; - Kind = kind; - ResultKind = resultKind; - } - - /// Native offline operation identifier. - public ulong Id { get; } - - /// Operation domain. - public OfflineOperationKind Kind { get; } - - /// Expected result shape. - public OfflineOperationResultKind ResultKind { get; } - - /// Whether this operation has been consumed or discarded. - public bool IsClosed - { - get - { - lock (gate) - { - return closed; - } - } - } - - internal ulong RequireLive(RuntimeHandle expectedRuntime) - { - lock (gate) - { - if (closed) - { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - "OfflineOperationHandle is already closed.", - null - ); - } - - if (!ReferenceEquals(runtime, expectedRuntime)) - { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - "OfflineOperationHandle belongs to a different RuntimeHandle.", - null - ); - } - - return Id; - } - } - - internal ulong RequireLive( - RuntimeHandle expectedRuntime, - OfflineOperationKind expectedKind, - OfflineOperationResultKind expectedResultKind - ) - { - lock (gate) - { - var id = RequireLive(expectedRuntime); - if (Kind != expectedKind || ResultKind != expectedResultKind) - { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - $"OfflineOperationHandle has kind {Kind} and result kind {ResultKind}, expected {expectedKind} and {expectedResultKind}.", - null - ); - } - - return id; - } - } - - internal void MarkConsumed() - { - lock (gate) - { - closed = true; - } - } - - /// Discards the native offline operation result on the runtime owner thread. - public void Close() => runtime.DiscardOfflineOperation(this); - - /// - public void Dispose() - { - if (IsClosed) - { - return; - } - - try - { - Close(); - } - catch - { - // Dispose is best-effort for owner-thread cleanup; Close reports errors. - } - } -} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OwnerThread.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OwnerThread.cs deleted file mode 100644 index 11257d980..000000000 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/OwnerThread.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.Collections.Concurrent; -using System.Runtime.ExceptionServices; -using Maplibre.NativeFfi.Error; - -namespace Maplibre.NativeFfi.Runtime; - -/// Dedicated owner-thread executor for thread-affine MapLibre Native handles. -public sealed class OwnerThread : IDisposable -{ - private readonly object gate = new(); - private readonly BlockingCollection queue = []; - private readonly ManualResetEventSlim started = new(); - private readonly Thread thread; - private bool closing; - private bool closed; - private int ownerManagedThreadId; - - public OwnerThread() - { - thread = new Thread(Run) { IsBackground = true, Name = "MapLibre Native owner thread" }; - thread.Start(); - started.Wait(); - } - - /// Whether the owner thread has stopped accepting and running work. - public bool IsClosed - { - get - { - lock (gate) - { - return closed; - } - } - } - - /// The managed thread ID for the bound owner thread. - public int OwnerManagedThreadId => ownerManagedThreadId; - - /// Runs an operation on the owner thread and returns after it completes. - public void Invoke(Action operation) - { - ArgumentNullException.ThrowIfNull(operation); - Invoke(() => - { - operation(); - return true; - }); - } - - /// Runs an operation on the owner thread and returns its result. - public T Invoke(Func operation) - { - ArgumentNullException.ThrowIfNull(operation); - lock (gate) - { - if (closing || closed) - { - ThrowClosed(); - } - } - - if (Environment.CurrentManagedThreadId == ownerManagedThreadId) - { - return operation(); - } - - var item = new OwnerThreadWorkItem(operation); - lock (gate) - { - if (closing || closed) - { - ThrowClosed(); - } - - queue.Add(item); - } - - return item.GetResult(); - } - - /// Stops accepting new work and waits for queued work to finish. - public void Close() - { - var shouldCompleteAdding = false; - lock (gate) - { - if (closed) - { - return; - } - - if (!closing) - { - closing = true; - shouldCompleteAdding = true; - } - } - - if (shouldCompleteAdding) - { - queue.CompleteAdding(); - } - - if (Environment.CurrentManagedThreadId != ownerManagedThreadId) - { - thread.Join(); - } - } - - public void Dispose() - { - Close(); - // Owner-thread disposal cannot join the running thread or dispose queue primitives - // that are still in use by the current work item. - if (Environment.CurrentManagedThreadId != ownerManagedThreadId) - { - started.Dispose(); - queue.Dispose(); - } - } - - private void Run() - { - ownerManagedThreadId = Environment.CurrentManagedThreadId; - started.Set(); - try - { - foreach (var item in queue.GetConsumingEnumerable()) - { - item.Execute(); - } - } - finally - { - lock (gate) - { - closing = true; - closed = true; - } - } - } - - private static void ThrowClosed() - { - throw new InvalidStateException( - MaplibreStatus.InvalidState, - null, - "OwnerThread is closed.", - null - ); - } -} - -internal abstract class OwnerThreadWorkItem -{ - internal abstract void Execute(); -} - -internal sealed class OwnerThreadWorkItem : OwnerThreadWorkItem -{ - private readonly Func operation; - private readonly ManualResetEventSlim completed = new(); - private ExceptionDispatchInfo? error; - private T? result; - - internal OwnerThreadWorkItem(Func operation) - { - this.operation = operation; - } - - internal override void Execute() - { - try - { - result = operation(); - } - catch (Exception exception) - { - error = ExceptionDispatchInfo.Capture(exception); - } - finally - { - completed.Set(); - } - } - - internal T GetResult() - { - completed.Wait(); - completed.Dispose(); - error?.Throw(); - return result!; - } -} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeHandle.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeHandle.cs index 5567968cd..8c39f4700 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeHandle.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeHandle.cs @@ -1,3 +1,4 @@ +using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Internal.C; using Maplibre.NativeFfi.Internal.Callback; using Maplibre.NativeFfi.Internal.Loader; @@ -12,38 +13,29 @@ namespace Maplibre.NativeFfi.Runtime; internal unsafe delegate mln_status RuntimeSetResourceProvider( MlnRuntime runtime, - mln_resource_provider* provider + mln_resource_provider* provider, + mln_completion* completion ); internal unsafe delegate mln_status RuntimeSetResourceTransform( MlnRuntime runtime, - mln_resource_transform* transform + mln_resource_transform* transform, + mln_completion* completion ); -internal unsafe delegate mln_status RuntimeTakeOfflineRegionStatusResult( - MlnRuntime runtime, - ulong operationId, - mln_offline_region_status* outStatus -); - -/// Owner-thread runtime handle for MapLibre Native work and event draining. -public sealed unsafe class RuntimeHandle : IDisposable +/// Any-thread runtime handle with autonomous native execution. +public sealed unsafe class RuntimeHandle : IDisposable, IAsyncDisposable { private static readonly RuntimeSetResourceProvider DefaultSetResourceProvider = static ( runtime, - provider - ) => NativeMethods.mln_runtime_set_resource_provider(runtime, provider); + provider, + completion + ) => NativeMethods.mln_runtime_set_resource_provider(runtime, provider, completion); private static readonly RuntimeSetResourceTransform DefaultSetResourceTransform = static ( runtime, - transform - ) => NativeMethods.mln_runtime_set_resource_transform(runtime, transform); - private static readonly RuntimeTakeOfflineRegionStatusResult DefaultTakeOfflineRegionStatus = - static (runtime, operationId, outStatus) => - NativeMethods.mln_runtime_offline_region_get_status_take_result( - runtime, - operationId, - outStatus - ); + transform, + completion + ) => NativeMethods.mln_runtime_set_resource_transform(runtime, transform, completion); [ThreadStatic] private static RuntimeSetResourceProvider? setResourceProviderForTest; @@ -51,27 +43,17 @@ public sealed unsafe class RuntimeHandle : IDisposable [ThreadStatic] private static RuntimeSetResourceTransform? setResourceTransformForTest; - [ThreadStatic] - private static RuntimeTakeOfflineRegionStatusResult? takeOfflineRegionStatusForTest; - - private readonly Lock callbackGate = new(); private readonly Lock mapGate = new(); private readonly Dictionary> liveMaps = []; private readonly NativeHandleState state; - private ResourceProviderState? resourceProviderState; - private ResourceTransformState? resourceTransformState; - private HttpHeaderTransformState? httpHeaderTransformState; + private volatile Task teardown = Task.CompletedTask; private RuntimeHandle(MlnRuntime handle) { - state = new NativeHandleState( - handle, - static handle => NativeMethods.mln_runtime_destroy(handle), - nameof(RuntimeHandle) - ); + state = new NativeHandleState(handle, StartRelease, nameof(RuntimeHandle)); } - /// Creates a runtime on the current thread. + /// Creates a runtime. public static RuntimeHandle Create(RuntimeOptions options) { ArgumentNullException.ThrowIfNull(options); @@ -79,7 +61,6 @@ public static RuntimeHandle Create(RuntimeOptions options) using var nativeOptions = options.ToNative(); var value = nativeOptions.Value; MlnRuntime runtime = default; - NativeStatus.Check(NativeMethods.mln_runtime_create(&value, &runtime)); return new RuntimeHandle(runtime); } @@ -90,77 +71,66 @@ public static RuntimeHandle Create(RuntimeOptions options) public bool IsClosed => state.IsClosed; /// Installs or replaces the runtime-scoped resource provider callback. - public void SetResourceProvider(ResourceProviderCallback callback) + public Task SetResourceProviderAsync(ResourceProviderCallback callback) { var replacement = new ResourceProviderState(callback); - lock (callbackGate) + try { - try + return NativeCompletion.SubmitCommand(completion => { var descriptor = replacement.Descriptor; - NativeStatus.Check(SetResourceProviderNative(Handle, &descriptor)); - var previous = resourceProviderState; - resourceProviderState = replacement; - previous?.Dispose(); - } - catch (Exception error) - { - DisposeAndSuppress(error, replacement); - throw; - } + return SetResourceProviderNative(Handle, &descriptor, completion); + }); + } + catch (Exception error) + { + DisposeAndSuppress(error, replacement); + throw; } } /// Installs or replaces the runtime-scoped resource transform callback. - public void SetResourceTransform(ResourceTransformCallback callback) + public Task SetResourceTransformAsync(ResourceTransformCallback callback) { var replacement = new ResourceTransformState(callback); - lock (callbackGate) + try { - try + return NativeCompletion.SubmitCommand(completion => { var descriptor = replacement.Descriptor; - NativeStatus.Check(SetResourceTransformNative(Handle, &descriptor)); - var previous = resourceTransformState; - resourceTransformState = replacement; - previous?.Dispose(); - } - catch (Exception error) - { - DisposeAndSuppress(error, replacement); - throw; - } + return SetResourceTransformNative(Handle, &descriptor, completion); + }); + } + catch (Exception error) + { + DisposeAndSuppress(error, replacement); + throw; } } /// Installs or replaces headers added to built-in HTTP requests. - public void SetHttpHeaderTransform(HttpHeaderTransformCallback callback) + public Task SetHttpHeaderTransformAsync(HttpHeaderTransformCallback callback) { var replacement = new HttpHeaderTransformState(callback); - lock (callbackGate) + try { - try + return NativeCompletion.SubmitCommand(completion => { var descriptor = replacement.Descriptor; - NativeStatus.Check( - NativeMethods.mln_runtime_set_http_header_transform(Handle, &descriptor) + return NativeMethods.mln_runtime_set_http_header_transform( + Handle, + &descriptor, + completion ); - var previous = httpHeaderTransformState; - httpHeaderTransformState = replacement; - previous?.Dispose(); - } - catch (Exception error) - { - DisposeAndSuppress(error, replacement); - throw; - } + }); + } + catch (Exception error) + { + DisposeAndSuppress(error, replacement); + throw; } } - internal ResourceProviderState? ResourceProviderStateForTest => resourceProviderState; - - internal ResourceTransformState? ResourceTransformStateForTest => resourceTransformState; - internal static IDisposable UseResourceCallbackInstallMethodsForTest( RuntimeSetResourceProvider setProvider, RuntimeSetResourceTransform setTransform @@ -179,28 +149,6 @@ RuntimeSetResourceTransform setTransform private static RuntimeSetResourceTransform SetResourceTransformNative => setResourceTransformForTest ?? DefaultSetResourceTransform; - internal static IDisposable UseOfflineTakeResultMethodsForTest( - RuntimeTakeOfflineRegionStatusResult takeOfflineRegionStatus - ) - { - var previous = takeOfflineRegionStatusForTest; - takeOfflineRegionStatusForTest = takeOfflineRegionStatus; - return new RestoreOfflineTakeResultMethods(previous); - } - - private static RuntimeTakeOfflineRegionStatusResult TakeOfflineRegionStatusNative => - takeOfflineRegionStatusForTest ?? DefaultTakeOfflineRegionStatus; - - private sealed class RestoreOfflineTakeResultMethods( - RuntimeTakeOfflineRegionStatusResult? previous - ) : IDisposable - { - public void Dispose() - { - takeOfflineRegionStatusForTest = previous; - } - } - private sealed class RestoreResourceCallbackInstallMethods( RuntimeSetResourceProvider? previousProvider, RuntimeSetResourceTransform? previousTransform @@ -214,422 +162,168 @@ public void Dispose() } /// Clears the runtime-scoped resource provider callback. - public void ClearResourceProvider() - { - lock (callbackGate) - { - NativeStatus.Check(NativeMethods.mln_runtime_clear_resource_provider(Handle)); - var previous = resourceProviderState; - resourceProviderState = null; - previous?.Dispose(); - } - } + public Task ClearResourceProviderAsync() => + NativeCompletion.SubmitCommand(completion => + NativeMethods.mln_runtime_clear_resource_provider(Handle, completion) + ); /// Clears the runtime-scoped resource transform callback. - public void ClearResourceTransform() - { - lock (callbackGate) - { - NativeStatus.Check(NativeMethods.mln_runtime_clear_resource_transform(Handle)); - var previous = resourceTransformState; - resourceTransformState = null; - previous?.Dispose(); - } - } + public Task ClearResourceTransformAsync() => + NativeCompletion.SubmitCommand(completion => + NativeMethods.mln_runtime_clear_resource_transform(Handle, completion) + ); /// Clears headers added to built-in HTTP requests. - public void ClearHttpHeaderTransform() - { - lock (callbackGate) - { - NativeStatus.Check(NativeMethods.mln_runtime_clear_http_header_transform(Handle)); - var previous = httpHeaderTransformState; - httpHeaderTransformState = null; - previous?.Dispose(); - } - } + public Task ClearHttpHeaderTransformAsync() => + NativeCompletion.SubmitCommand(completion => + NativeMethods.mln_runtime_clear_http_header_transform(Handle, completion) + ); /// Starts an ambient cache maintenance operation. - public OfflineOperationHandle StartAmbientCacheOperation(AmbientCacheOperation operation) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_run_ambient_cache_operation_start( + public Task RunAmbientCacheOperationAsync(AmbientCacheOperation operation) => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_run_ambient_cache_operation( Handle, (uint)operation, - &operationId + completion ) ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.AmbientCache, - OfflineOperationResultKind.None - ); - } /// Starts a change to this runtime's maximum ambient cache size. /// /// MapLibre evicts ambient resources to fit the new budget, so lowering it discards cached /// resources. Offline regions are unaffected. /// - public OfflineOperationHandle StartSetMaximumAmbientCacheSize(ulong size) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_set_maximum_ambient_cache_size_start( - Handle, - size, - &operationId - ) - ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.SetMaximumAmbientCacheSize, - OfflineOperationResultKind.None + public Task SetMaximumAmbientCacheSizeAsync(ulong size) => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_set_maximum_ambient_cache_size(Handle, size, completion) ); - } /// Starts an offline region creation operation. - public OfflineOperationHandle StartCreateOfflineRegion( + public Task CreateOfflineRegionAsync( OfflineRegionDefinition definition, byte[] metadata ) { ArgumentNullException.ThrowIfNull(metadata); using var nativeDefinition = NativeOfflineRegionDefinition.From(definition); - var definitionValue = nativeDefinition.Value; - ulong operationId = 0; - fixed (byte* metadataPointer = metadata) - { - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_create_start( - Handle, - &definitionValue, - metadata.Length == 0 ? null : metadataPointer, - (nuint)metadata.Length, - &operationId - ) - ); - } - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionCreate, - OfflineOperationResultKind.Region + return NativeCompletion.Submit( + completion => + { + var definitionValue = nativeDefinition.Value; + fixed (byte* metadataPointer = metadata) + { + return NativeMethods.mln_runtime_offline_region_create( + Handle, + &definitionValue, + metadata.Length == 0 ? null : metadataPointer, + (nuint)metadata.Length, + completion + ); + } + }, + ReadOfflineRegion ); } /// Starts an offline region lookup operation. - public OfflineOperationHandle StartOfflineRegion(long id) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_get_start(Handle, id, &operationId) + public Task GetOfflineRegionAsync(long id) => + NativeCompletion.Submit( + completion => NativeMethods.mln_runtime_offline_region_get(Handle, id, completion), + ReadOptionalOfflineRegion ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionGet, - OfflineOperationResultKind.OptionalRegion - ); - } /// Starts an offline region list operation. - public OfflineOperationHandle StartOfflineRegions() - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_regions_list_start(Handle, &operationId) + public Task> ListOfflineRegionsAsync() => + NativeCompletion.Submit( + completion => NativeMethods.mln_runtime_offline_regions_list(Handle, completion), + ReadOfflineRegions ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionsList, - OfflineOperationResultKind.RegionList - ); - } /// Starts an offline region database merge operation. - public OfflineOperationHandle StartMergeOfflineRegionsDatabase(string path) + public Task> MergeOfflineRegionsDatabaseAsync(string path) { ArgumentNullException.ThrowIfNull(path); using var nativePath = NativeUtf8String.FromNullableString(path, nameof(path)); - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_regions_merge_database_start( - Handle, - nativePath.Pointer, - &operationId - ) - ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionsMergeDatabase, - OfflineOperationResultKind.RegionList + return NativeCompletion.Submit( + completion => + NativeMethods.mln_runtime_offline_regions_merge_database( + Handle, + nativePath.Pointer, + completion + ), + ReadOfflineRegions ); } /// Starts an offline region metadata update operation. - public OfflineOperationHandle StartUpdateOfflineRegionMetadata(long id, byte[] metadata) + public Task UpdateOfflineRegionMetadataAsync(long id, byte[] metadata) { ArgumentNullException.ThrowIfNull(metadata); - ulong operationId = 0; - fixed (byte* metadataPointer = metadata) - { - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_update_metadata_start( - Handle, - id, - metadata.Length == 0 ? null : metadataPointer, - (nuint)metadata.Length, - &operationId - ) - ); - } - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionUpdateMetadata, - OfflineOperationResultKind.Region + return NativeCompletion.Submit( + completion => + { + fixed (byte* metadataPointer = metadata) + { + return NativeMethods.mln_runtime_offline_region_update_metadata( + Handle, + id, + metadata.Length == 0 ? null : metadataPointer, + (nuint)metadata.Length, + completion + ); + } + }, + ReadOfflineRegion ); } /// Starts an offline region status lookup operation. - public OfflineOperationHandle StartOfflineRegionStatus(long id) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_get_status_start(Handle, id, &operationId) - ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionGetStatus, - OfflineOperationResultKind.RegionStatus + public Task GetOfflineRegionStatusAsync(long id) => + NativeCompletion.Submit( + completion => + NativeMethods.mln_runtime_offline_region_get_status(Handle, id, completion), + ReadOfflineRegionStatus ); - } /// Starts an offline region observed-state update operation. - public OfflineOperationHandle StartSetOfflineRegionObserved(long id, bool observed) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_set_observed_start( + public Task SetOfflineRegionObservedAsync(long id, bool observed) => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_offline_region_set_observed( Handle, id, observed ? (byte)1 : (byte)0, - &operationId + completion ) ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionSetObserved, - OfflineOperationResultKind.None - ); - } /// Starts an offline region download-state update operation. - public OfflineOperationHandle StartSetOfflineRegionDownloadState( + public Task SetOfflineRegionDownloadStateAsync( long id, OfflineRegionDownloadState downloadState ) { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_set_download_state_start( + return NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_offline_region_set_download_state( Handle, id, (uint)downloadState, - &operationId + completion ) ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionSetDownloadState, - OfflineOperationResultKind.None - ); } /// Starts an offline region invalidation operation. - public OfflineOperationHandle StartInvalidateOfflineRegion(long id) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_invalidate_start(Handle, id, &operationId) + public Task InvalidateOfflineRegionAsync(long id) => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_offline_region_invalidate(Handle, id, completion) ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionInvalidate, - OfflineOperationResultKind.None - ); - } /// Starts an offline region delete operation. - public OfflineOperationHandle StartDeleteOfflineRegion(long id) - { - ulong operationId = 0; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_delete_start(Handle, id, &operationId) + public Task DeleteOfflineRegionAsync(long id) => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_offline_region_delete(Handle, id, completion) ); - return new OfflineOperationHandle( - this, - operationId, - OfflineOperationKind.RegionDelete, - OfflineOperationResultKind.None - ); - } - - public OfflineRegionInfo TakeCreateOfflineRegionResult(OfflineOperationHandle operation) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionCreate, - OfflineOperationResultKind.Region - ); - MlnOfflineRegionSnapshot snapshot = default; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_create_take_result( - Handle, - operationId, - &snapshot - ) - ); - operation.MarkConsumed(); - return OfflineStructs.ReadSnapshot(snapshot); - } - - public OfflineRegionInfo? TakeOfflineRegionResult(OfflineOperationHandle operation) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionGet, - OfflineOperationResultKind.OptionalRegion - ); - MlnOfflineRegionSnapshot snapshot = default; - bool found = false; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_get_take_result( - Handle, - operationId, - &snapshot, - &found - ) - ); - operation.MarkConsumed(); - return found ? OfflineStructs.ReadSnapshot(snapshot) : null; - } - - public IReadOnlyList TakeOfflineRegionsResult( - OfflineOperationHandle operation - ) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionsList, - OfflineOperationResultKind.RegionList - ); - MlnOfflineRegionList list = default; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_regions_list_take_result(Handle, operationId, &list) - ); - operation.MarkConsumed(); - return OfflineStructs.ReadList(list); - } - - public IReadOnlyList TakeMergeOfflineRegionsDatabaseResult( - OfflineOperationHandle operation - ) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionsMergeDatabase, - OfflineOperationResultKind.RegionList - ); - MlnOfflineRegionList list = default; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_regions_merge_database_take_result( - Handle, - operationId, - &list - ) - ); - operation.MarkConsumed(); - return OfflineStructs.ReadList(list); - } - - public OfflineRegionInfo TakeUpdateOfflineRegionMetadataResult(OfflineOperationHandle operation) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionUpdateMetadata, - OfflineOperationResultKind.Region - ); - MlnOfflineRegionSnapshot snapshot = default; - NativeStatus.Check( - NativeMethods.mln_runtime_offline_region_update_metadata_take_result( - Handle, - operationId, - &snapshot - ) - ); - operation.MarkConsumed(); - return OfflineStructs.ReadSnapshot(snapshot); - } - - public OfflineRegionStatus TakeOfflineRegionStatusResult(OfflineOperationHandle operation) - { - ArgumentNullException.ThrowIfNull(operation); - var operationId = operation.RequireLive( - this, - OfflineOperationKind.RegionGetStatus, - OfflineOperationResultKind.RegionStatus - ); - var status = new mln_offline_region_status - { - size = (uint)sizeof(mln_offline_region_status), - }; - NativeStatus.Check(TakeOfflineRegionStatusNative(Handle, operationId, &status)); - operation.MarkConsumed(); - return OfflineStructs.ReadStatus(status); - } - - internal void DiscardOfflineOperation(OfflineOperationHandle operation) - { - ArgumentNullException.ThrowIfNull(operation); - if (operation.IsClosed) - { - return; - } - - var operationId = operation.RequireLive(this); - MlnRuntime runtime; - try - { - runtime = Handle; - } - catch (Error.InvalidStateException) when (IsClosed) - { - // The runtime already owns cleanup for its pending operations. - operation.MarkConsumed(); - return; - } - - NativeStatus.Check( - NativeMethods.mln_runtime_offline_operation_discard(runtime, operationId) - ); - operation.MarkConsumed(); - } internal void RegisterMap(Map.MapHandle map) { @@ -672,88 +366,37 @@ internal void UnregisterMap(Map.MapHandle map) return null; } - /// Advances this runtime. - /// - /// The call parks the owner thread when allows it, - /// then drains the owner-thread task queues. Drain the queued runtime events - /// with afterwards. - /// - /// drains and returns, a positive value parks for - /// up to that long, and a negative value parks until a wake arrives. Timers and - /// ready file descriptors wake the runtime only when they queue owner-thread - /// work, so pass a bounded timeout to cap how long a call waits. - /// - /// - /// A non-zero timeout blocks the calling thread. Call it outside any lock that a - /// thread signalling a takes. A queued event releases a - /// park, so a host that pumps and drains in a loop keeps making progress. - /// - /// - /// bounds the drain. Null or a negative value drains - /// without a bound. Zero or a positive value stops the drain at the first task - /// boundary after that long, measured from the start of the drain. The first - /// queued task always runs, so a bounded pump always makes progress, and tasks - /// left behind set the wake flag, so the next pump returns without parking and - /// continues them. The budget bounds the task queues alone: expired timers and - /// ready file descriptors are serviced regardless, and a single task runs to - /// completion once started, so one long task can overrun the budget. - /// - /// - public void Pump(TimeSpan timeout, TimeSpan? budget) - { - var timeoutMilliseconds = timeout < TimeSpan.Zero ? -1L : (long)timeout.TotalMilliseconds; - var budgetMilliseconds = - budget is not { } budgetValue || budgetValue < TimeSpan.Zero - ? -1L - : (long)budgetValue.TotalMilliseconds; - NativeStatus.Check( - NativeMethods.mln_runtime_pump(Handle, timeoutMilliseconds, budgetMilliseconds) - ); - } - - /// - public void Pump(TimeSpan timeout) - { - Pump(timeout, null); - } - - /// - /// Acquires a wake source that releases this runtime's parked owner thread. The - /// returned source is usable from any thread, and the caller disposes it. - /// - public WakeSource AcquireWakeSource() - { - MlnWakeSource source = default; - NativeStatus.Check(NativeMethods.mln_runtime_wake_source_acquire(Handle, &source)); - return new WakeSource(source); - } - /// Drains every queued runtime event into one batch of copies. /// - /// The batch reports the events this runtime and its maps queued under the masks they select, - /// in queue order, as managed copies of the borrowed native records. + /// The batch reports the events that this runtime and its maps queued under their masks. + /// Managed copies preserve queue order after the owned native batch is released. /// - public RuntimeEventBatch DrainEvents() => Drain(0); - - /// Drains at most queued runtime events. - /// - /// Zero drains every queued event. A positive value drains at most that many and reports how - /// many stayed queued in , so a host that takes - /// a bounded slice per iteration learns to come back. - /// - public RuntimeEventBatch DrainEvents(int maxEvents) + public RuntimeEventBatch DrainEvents() { - ArgumentOutOfRangeException.ThrowIfNegative(maxEvents); - return Drain((nuint)maxEvents); + MlnEventBatch batch = default; + NativeStatus.Check(NativeMethods.mln_runtime_drain_events(Handle, &batch)); + try + { + var view = new mln_runtime_event_batch_view + { + size = (uint)sizeof(mln_runtime_event_batch_view), + }; + NativeStatus.Check(NativeMethods.mln_event_batch_get(batch, &view)); + List events; + lock (mapGate) + { + events = RuntimeStructs.ReadBatch(view, this, MapForLocked); + } + + return new RuntimeEventBatch(events); + } + finally + { + NativeMethods.mln_event_batch_release(batch); + } } /// Selects which runtime-originated event types this runtime queues. - /// - /// The mask reads the bits in and ignores the - /// rest, so selects every runtime-originated type. An event - /// type this mask clears is never queued, so it neither reaches a batch nor wakes a parked - /// . - /// public void SetEventMask(RuntimeEventMask mask) { NativeStatus.Check(NativeMethods.mln_runtime_set_event_mask(Handle, (ulong)mask)); @@ -773,52 +416,90 @@ public RuntimeEventMask GetEventMask() return (RuntimeEventMask)mask; } - private RuntimeEventBatch Drain(nuint maxEvents) - { - var batch = NativeMethods.mln_runtime_event_batch_default(); - NativeStatus.Check(NativeMethods.mln_runtime_drain_events(Handle, maxEvents, &batch)); + /// Waits until all previously accepted runtime work reaches a terminal state. + public Task BarrierAsync() => + NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_barrier(Handle, completion) + ); - // The batch borrows native storage that the next drain reuses, so every event is read - // into managed objects before this returns. - List events; - lock (mapGate) + /// Releases the runtime's public native handle and waits for native teardown. + /// + /// The wait covers this runtime's threads and its released maps' teardown, so a host that + /// returns from this call may exit the process. Call it from a host thread: a MapLibre callback + /// that waits here blocks the teardown it waits for. Use to release + /// the handle without blocking. + /// + public void Close() => CloseAsync().GetAwaiter().GetResult(); + + /// Releases the runtime's public native handle without blocking. + /// + /// The returned task completes after every earlier accepted submission, including released + /// maps' teardown, has finished and the runtime's threads and resources are gone. A host that + /// awaits it may exit the process. The handle is consumed once this method returns, so a + /// rejected release throws before there is a task to await. + /// + public Task CloseAsync() + { + if (!IsClosed) { - events = RuntimeStructs.ReadBatch(batch, this, MapForLocked); + state.Close(); } - return new RuntimeEventBatch(events, (ulong)batch.remaining_count); + return teardown; } - /// Destroys the runtime on its owner thread. - public void Close() + /// + public void Dispose() => Close(); + + /// + public ValueTask DisposeAsync() => new(CloseAsync()); + + private mln_status StartRelease(MlnRuntime handle) { - state.Close(); - DisposeCallbackState(); + teardown = NativeCompletion.SubmitUnit(completion => + NativeMethods.mln_runtime_release(handle, completion) + ); + return mln_status.MLN_STATUS_OK; } - /// - public void Dispose() + private static OfflineRegionInfo ReadOfflineRegion(mln_completion_result* result) + { + if (result->value is null || result->value_count != 1) + { + throw new InvalidOperationException("Native offline region completion had no value."); + } + return OfflineStructs.ReadInfo(*(mln_offline_region_info*)result->value); + } + + private static OfflineRegionInfo? ReadOptionalOfflineRegion(mln_completion_result* result) => + result->value_count == 0 ? null : ReadOfflineRegion(result); + + private static IReadOnlyList ReadOfflineRegions( + mln_completion_result* result + ) { - if (state.TryClose()) + if (result->value_count != 0 && result->value is null) + { + throw new InvalidOperationException( + "Native offline region completion had a null array." + ); + } + var regions = new OfflineRegionInfo[checked((int)result->value_count)]; + var values = (mln_offline_region_info*)result->value; + for (nuint index = 0; index < result->value_count; index++) { - DisposeCallbackState(); + regions[(int)index] = OfflineStructs.ReadInfo(values[index]); } + return regions; } - private void DisposeCallbackState() + private static OfflineRegionStatus ReadOfflineRegionStatus(mln_completion_result* result) { - lock (callbackGate) + if (result->value is null || result->value_count != 1) { - var provider = resourceProviderState; - var transform = resourceTransformState; - var headerTransform = httpHeaderTransformState; - resourceProviderState = null; - resourceTransformState = null; - httpHeaderTransformState = null; - provider?.Dispose(); - transform?.Dispose(); - headerTransform?.Dispose(); + throw new InvalidOperationException("Native offline status completion had no value."); } + return OfflineStructs.ReadStatus(*(mln_offline_region_status*)result->value); } private static void DisposeAndSuppress(Exception error, IDisposable? disposable) diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeTypes.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeTypes.cs index 9e28d6f72..f73be8615 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeTypes.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/RuntimeTypes.cs @@ -1,3 +1,5 @@ +using Maplibre.NativeFfi.Camera; +using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Geo; using Maplibre.NativeFfi.Internal; using Maplibre.NativeFfi.Map; @@ -15,29 +17,12 @@ public enum AmbientCacheOperation : uint Clear = 4, } -public enum OfflineOperationKind : uint +public enum CommandDisposition : uint { - AmbientCache = 1, - RegionCreate = 2, - RegionGet = 3, - RegionsList = 4, - RegionsMergeDatabase = 5, - RegionUpdateMetadata = 6, - RegionGetStatus = 7, - RegionSetObserved = 8, - RegionSetDownloadState = 9, - RegionInvalidate = 10, - RegionDelete = 11, - SetMaximumAmbientCacheSize = 12, -} - -public enum OfflineOperationResultKind : uint -{ - None = 0, - Region = 1, - OptionalRegion = 2, - RegionList = 3, - RegionStatus = 4, + Committed = 0, + Superseded = 1, + Failed = 2, + Cancelled = 3, } public enum RuntimeEventSourceType : uint @@ -69,8 +54,7 @@ public enum RuntimeEventType : uint OfflineRegionStatusChanged = 19, OfflineRegionResponseError = 20, OfflineRegionTileCountLimitExceeded = 21, - OfflineOperationCompleted = 22, - MapCameraTransitionFinished = 23, + MapCameraTransitionFinished = 22, } /// Event types a map or a runtime queues. @@ -78,7 +62,7 @@ public enum RuntimeEventType : uint /// Each bit is 1 shifted left by the value it selects, so a /// mask a host computes from a decoded event type matches these constants. /// -/// reads the bits in and +/// reads the bits in and /// reads the bits in , so /// both accept and a host reads a mask, changes one bit, and writes it back. /// @@ -111,7 +95,6 @@ public enum RuntimeEventMask : ulong OfflineRegionResponseError = 1UL << (int)RuntimeEventType.OfflineRegionResponseError, OfflineRegionTileCountLimitExceeded = 1UL << (int)RuntimeEventType.OfflineRegionTileCountLimitExceeded, - OfflineOperationCompleted = 1UL << (int)RuntimeEventType.OfflineOperationCompleted, /// Selects every map-originated event type this binding declares. AllMapEvents = @@ -139,8 +122,7 @@ public enum RuntimeEventMask : ulong AllRuntimeEvents = OfflineRegionStatusChanged | OfflineRegionResponseError - | OfflineRegionTileCountLimitExceeded - | OfflineOperationCompleted, + | OfflineRegionTileCountLimitExceeded, /// Selects every event type this binding declares. All = AllMapEvents | AllRuntimeEvents, @@ -152,11 +134,7 @@ public enum RuntimeEventMask : ulong /// that produced it is closed. /// /// The copied events in queue order. -/// -/// Events still queued for the runtime after this batch. A nonzero value means another drain -/// reports more events. -/// -public sealed record RuntimeEventBatch(IReadOnlyList Events, ulong RemainingCount) +public sealed record RuntimeEventBatch(IReadOnlyList Events) { private readonly IReadOnlyList events = ValueEquality.Snapshot(Events); @@ -167,12 +145,9 @@ public IReadOnlyList Events } public bool Equals(RuntimeEventBatch? other) => - other is not null - && ValueEquality.SequenceEquals(events, other.events) - && RemainingCount == other.RemainingCount; + other is not null && ValueEquality.SequenceEquals(events, other.events); - public override int GetHashCode() => - HashCode.Combine(ValueEquality.SequenceHashCode(events), RemainingCount); + public override int GetHashCode() => ValueEquality.SequenceHashCode(events); } /// One copied runtime event from . @@ -201,7 +176,7 @@ public override int GetHashCode() => /// /// and /// : a -/// value. +/// value. /// /// /// @@ -211,12 +186,6 @@ public override int GetHashCode() => /// for the failure text. /// /// -/// -/// -/// : the operation result as a -/// MaplibreStatus value. -/// -/// /// Every other event kind: 0. /// /// @@ -282,16 +251,6 @@ uint RawReason public sealed record OfflineRegionTileCountLimit(long RegionId, ulong Limit) : RuntimeEventPayload; - public sealed record OfflineOperationCompleted( - ulong OperationId, - OfflineOperationKind OperationKind, - uint RawOperationKind, - OfflineOperationResultKind ResultKind, - uint RawResultKind, - int ResultStatus, - bool Found - ) : RuntimeEventPayload; - /// Payload for a map camera transition-finished event. /// /// The transition ID the caller set on the AnimationOptions that started this @@ -300,6 +259,7 @@ bool Found /// public sealed record CameraTransitionFinished(ulong TransitionId) : RuntimeEventPayload; + /// Terminal outcome of an accepted ordered command. /// Payload for a payload kind this binding does not declare. /// /// holds the event record's fixed payload window, copied unchanged, diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/WakeSource.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/WakeSource.cs deleted file mode 100644 index c94713742..000000000 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Runtime/WakeSource.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Maplibre.NativeFfi.Internal.C; -using Maplibre.NativeFfi.Internal.Pointer; -using Maplibre.NativeFfi.Internal.Status; - -namespace Maplibre.NativeFfi.Runtime; - -/// -/// Releases a runtime owner thread parked in . -/// -/// -/// A wake source is usable from any thread. It stays usable after its runtime -/// closes, and signalling it then does nothing. -/// -public sealed unsafe class WakeSource : IDisposable -{ - private readonly NativeHandleState state; - - internal WakeSource(MlnWakeSource source) - { - state = new NativeHandleState( - source, - static handle => - { - NativeMethods.mln_wake_source_destroy(handle); - return mln_status.MLN_STATUS_OK; - }, - nameof(WakeSource) - ); - } - - /// Whether this wrapper has released its native handle. - public bool IsClosed => state.IsClosed; - - /// Sets the runtime's wake flag and releases the parked owner thread. - /// - /// A signal raised while the owner thread is running sets the wake flag, so the - /// next returns without parking. Signalling - /// after the runtime closes succeeds and does nothing. - /// - public void Signal() - { - state.WithLive(handle => NativeStatus.Check(NativeMethods.mln_wake_source_signal(handle))); - } - - /// Releases the wake source. - public void Close() - { - state.Close(); - } - - /// - public void Dispose() - { - state.TryClose(); - } -} diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Style/GeoJsonSourceDataHandle.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Style/GeoJsonSourceDataHandle.cs index f4454fb66..b0297bf63 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Style/GeoJsonSourceDataHandle.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Style/GeoJsonSourceDataHandle.cs @@ -10,8 +10,8 @@ namespace Maplibre.NativeFfi.Style; /// parses and tiles one complete GeoJSON document with its source /// options baked in, without a runtime or a map. The prepared value is immutable, so one /// handle may be installed on any number of sources with -/// and -/// and released at any time afterward; +/// and +/// and released at any time afterward; /// release never invalidates a source the data was installed on. /// public sealed unsafe class GeoJsonSourceDataHandle : IDisposable @@ -52,7 +52,7 @@ public static GeoJsonSourceDataHandle Create(byte[] data, GeoJsonSourceOptions? /// Whether this wrapper has successfully closed its native handle. public bool IsClosed => state.IsClosed; - /// Runs with release held off until it returns. + /// Runs after checking that this wrapper still owns the handle. internal void WithLive(Action use) => state.WithLive(use); /// Destroys the prepared data; sources it was installed on keep their reference. diff --git a/bindings/dotnet/src/Maplibre.NativeFfi/Style/StyleTypes.cs b/bindings/dotnet/src/Maplibre.NativeFfi/Style/StyleTypes.cs index 340a91698..b8e004da6 100644 --- a/bindings/dotnet/src/Maplibre.NativeFfi/Style/StyleTypes.cs +++ b/bindings/dotnet/src/Maplibre.NativeFfi/Style/StyleTypes.cs @@ -123,6 +123,27 @@ public sealed record TileSourceOptions public LatLngBounds? Bounds { get; set; } } +/// Copied metadata that MapLibre currently retains for a style layer. +/// +/// and are present when the layer carries them. +/// preserves a visibility value that has no named +/// member. The value remains valid after its layer or map +/// closes. +/// +/// Style-spec layer type string, for example "fill". +/// Lowest zoom at which the layer draws; negative infinity with no lower bound. +/// Highest zoom at which the layer draws; positive infinity with no upper bound. +public sealed record LayerInfo( + string Id, + string Type, + double MinZoom, + double MaxZoom, + StyleLayerVisibility Visibility, + uint RawVisibility, + string? SourceId, + string? SourceLayer +); + /// Whether a style layer draws. /// /// This is an open domain: MapLibre Native may report a value with no named member here, so a diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomGeometrySourceTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomGeometrySourceTests.cs index 8f7353d4d..20e8e6ee2 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomGeometrySourceTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomGeometrySourceTests.cs @@ -119,21 +119,26 @@ public unsafe void CustomGeometrySourceInstallFailureReleasesOnlyTheRejectedStat { var failInstall = false; using var install = MapHandle.UseCustomGeometrySourceInstallForTest( - (_, _, _) => + (_, _, _, _) => failInstall ? mln_status.MLN_STATUS_INVALID_STATE : mln_status.MLN_STATUS_OK ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var installed = new CustomGeometrySourceState( new CustomGeometrySourceOptions { FetchTile = _ => { } } ); - map.AddCustomGeometrySource("custom", installed); + map.AddCustomGeometrySourceAsync("custom", installed); failInstall = true; var rejected = new CustomGeometrySourceState( new CustomGeometrySourceOptions { FetchTile = _ => { } } ); - Assert.Throws(() => map.AddCustomGeometrySource("custom", rejected)); + Assert.Throws(() => + map.AddCustomGeometrySourceAsync("custom", rejected).GetAwaiter().GetResult() + ); // A rejected add owes no release callback, so the binding frees that state itself and // leaves the state the accepted add handed over untouched. @@ -146,14 +151,17 @@ public unsafe void CustomGeometrySourceInstallFailureReleasesOnlyTheRejectedStat [BindingSpecTest("BND-105", "BND-124")] [Fact] - public void CustomGeometrySourceApisAdaptThroughNativeMap() + public async Task CustomGeometrySourceApisAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyleJson); var tile = new CanonicalTileId(0, 0, 0); - map.AddCustomGeometrySource( + map.AddCustomGeometrySourceAsync( "custom", new CustomGeometrySourceOptions { @@ -168,35 +176,40 @@ public void CustomGeometrySourceApisAdaptThroughNativeMap() Wrap = false, } ); - map.SetCustomGeometrySourceTileData( + map.SetCustomGeometrySourceTileDataAsync( "custom", tile, """{"type":"FeatureCollection","features":[]}"""u8.ToArray() ); - map.InvalidateCustomGeometrySourceTile("custom", tile); - map.InvalidateCustomGeometrySourceRegion( + map.InvalidateCustomGeometrySourceTileAsync("custom", tile); + map.InvalidateCustomGeometrySourceRegionAsync( "custom", new LatLngBounds(new LatLng(-1, -1), new LatLng(1, 1)) ); - Assert.Equal(SourceType.CustomVector, map.StyleSourceType("custom")); - Assert.True(map.RemoveStyleSource("custom")); + Assert.Equal(SourceType.CustomVector, (await map.StyleSourceInfoAsync("custom"))?.Type); + RuntimeEventTestHelpers.WaitForCommand(runtime, map.RemoveStyleSourceAsync("custom")); + Assert.Null(await map.StyleSourceInfoAsync("custom")); } [BindingSpecTest("BND-124")] [Fact] - public void RemovingACustomGeometrySourceReleasesItsCallbackState() + public async Task RemovingACustomGeometrySourceReleasesItsCallbackState() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomGeometrySourceState( new CustomGeometrySourceOptions { FetchTile = _ => { } } ); - map.AddCustomGeometrySource("custom", state); + map.AddCustomGeometrySourceAsync("custom", state); Assert.True(state.IsHandleAllocatedForTest); - Assert.True(map.RemoveStyleSource("custom")); + RuntimeEventTestHelpers.WaitForCommand(runtime, map.RemoveStyleSourceAsync("custom")); + Assert.Null(await map.StyleSourceInfoAsync("custom")); Assert.False(state.IsHandleAllocatedForTest); } @@ -205,15 +218,16 @@ public void RemovingACustomGeometrySourceReleasesItsCallbackState() [Fact] public void ClosingAMapReleasesItsCustomGeometrySourceCallbackState() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 512, Height = 512 }); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomGeometrySourceState( new CustomGeometrySourceOptions { FetchTile = _ => { } } ); - map.AddCustomGeometrySource("custom", state); + map.AddCustomGeometrySourceAsync("custom", state); - map.Close(); + TestHandles.Close(map); + runtime.BarrierAsync().GetAwaiter().GetResult(); Assert.False(state.IsHandleAllocatedForTest); } @@ -222,8 +236,8 @@ public void ClosingAMapReleasesItsCustomGeometrySourceCallbackState() [Fact] public void AStyleReplacementReleasesADroppedSourceWithoutStyleLoadedEvents() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( runtime, new MapOptions { @@ -232,19 +246,19 @@ public void AStyleReplacementReleasesADroppedSourceWithoutStyleLoadedEvents() EventMask = RuntimeEventMask.All & ~RuntimeEventMask.MapStyleLoaded, } ); - map.SetStyleJson(EmptyStyleJson); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomGeometrySourceState( new CustomGeometrySourceOptions { FetchTile = _ => { } } ); - map.AddCustomGeometrySource("custom", state); + map.AddCustomGeometrySourceAsync("custom", state); // The replacement style drops the source, and the C API reports that through the release // callback rather than through an event, so the host's cleared mask stays cleared. - map.SetStyleJson(EmptyStyleJson); + map.SetStyleJsonAsync(EmptyStyleJson); var drained = new List(); for (var attempt = 0; attempt < 1000 && state.IsHandleAllocatedForTest; attempt++) { - runtime.Pump(TimeSpan.FromMilliseconds(1)); + Thread.Sleep(1); drained.AddRange(runtime.DrainEvents().Events.Select(polled => polled.Type)); } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomMvtVectorSourceTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomMvtVectorSourceTests.cs index 19bbb08ab..f1a69c9f8 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomMvtVectorSourceTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/CustomMvtVectorSourceTests.cs @@ -94,40 +94,49 @@ public unsafe void CustomMvtVectorSourceInstallFailureReleasesOnlyTheRejectedSta { var failInstall = false; using var install = MapHandle.UseCustomMvtVectorSourceInstallForTest( - (_, _, _) => + (_, _, _, _) => failInstall ? mln_status.MLN_STATUS_INVALID_STATE : mln_status.MLN_STATUS_OK ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var installed = new CustomMvtVectorSourceState( new CustomMvtVectorSourceOptions { FetchTile = _ => { } } ); - map.AddCustomMvtVectorSource("custom-mvt", installed); + map.AddCustomMvtVectorSourceAsync("custom-mvt", installed); failInstall = true; var rejected = new CustomMvtVectorSourceState( new CustomMvtVectorSourceOptions { FetchTile = _ => { } } ); Assert.Throws(() => - map.AddCustomMvtVectorSource("custom-mvt", rejected) + map.AddCustomMvtVectorSourceAsync("custom-mvt", rejected).GetAwaiter().GetResult() ); + // A rejected add owes no release callback, so the binding frees that state itself and + // leaves the state the accepted add handed over untouched. Assert.False(rejected.IsHandleAllocatedForTest); Assert.True(installed.IsHandleAllocatedForTest); + // The faked install never handed the state to MapLibre, so no release is owed for it. installed.Dispose(); } [BindingSpecTest("BND-105", "BND-124")] [Fact] - public void CustomMvtVectorSourceApisAdaptThroughNativeMap() + public async Task CustomMvtVectorSourceApisAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyleJson); var tile = new CanonicalTileId(0, 0, 0); - map.AddCustomMvtVectorSource( + map.AddCustomMvtVectorSourceAsync( "custom-mvt", new CustomMvtVectorSourceOptions { @@ -137,28 +146,36 @@ public void CustomMvtVectorSourceApisAdaptThroughNativeMap() MaximumZoom = 10, } ); - map.SetCustomMvtVectorSourceTileData("custom-mvt", tile, []); - map.SetCustomMvtVectorSourceTileError("custom-mvt", tile, "tile missing"); - map.InvalidateCustomMvtVectorSourceTile("custom-mvt", tile); + map.SetCustomMvtVectorSourceTileDataAsync("custom-mvt", tile, []); + map.SetCustomMvtVectorSourceTileErrorAsync("custom-mvt", tile, "tile missing"); + map.InvalidateCustomMvtVectorSourceTileAsync("custom-mvt", tile); - Assert.Equal(SourceType.CustomMvtVector, map.StyleSourceType("custom-mvt")); - Assert.True(map.RemoveStyleSource("custom-mvt")); + Assert.Equal( + SourceType.CustomMvtVector, + (await map.StyleSourceInfoAsync("custom-mvt"))?.Type + ); + RuntimeEventTestHelpers.WaitForCommand(runtime, map.RemoveStyleSourceAsync("custom-mvt")); + Assert.Null(await map.StyleSourceInfoAsync("custom-mvt")); } [BindingSpecTest("BND-124")] [Fact] - public void RemovingACustomMvtVectorSourceReleasesItsCallbackState() + public async Task RemovingACustomMvtVectorSourceReleasesItsCallbackState() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomMvtVectorSourceState( new CustomMvtVectorSourceOptions { FetchTile = _ => { } } ); - map.AddCustomMvtVectorSource("custom-mvt", state); + map.AddCustomMvtVectorSourceAsync("custom-mvt", state); Assert.True(state.IsHandleAllocatedForTest); - Assert.True(map.RemoveStyleSource("custom-mvt")); + RuntimeEventTestHelpers.WaitForCommand(runtime, map.RemoveStyleSourceAsync("custom-mvt")); + Assert.Null(await map.StyleSourceInfoAsync("custom-mvt")); Assert.False(state.IsHandleAllocatedForTest); } @@ -167,15 +184,16 @@ public void RemovingACustomMvtVectorSourceReleasesItsCallbackState() [Fact] public void ClosingAMapReleasesItsCustomMvtVectorSourceCallbackState() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyleJson); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 512, Height = 512 }); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomMvtVectorSourceState( new CustomMvtVectorSourceOptions { FetchTile = _ => { } } ); - map.AddCustomMvtVectorSource("custom-mvt", state); + map.AddCustomMvtVectorSourceAsync("custom-mvt", state); - map.Close(); + TestHandles.Close(map); + runtime.BarrierAsync().GetAwaiter().GetResult(); Assert.False(state.IsHandleAllocatedForTest); } @@ -184,8 +202,8 @@ public void ClosingAMapReleasesItsCustomMvtVectorSourceCallbackState() [Fact] public void AStyleReplacementReleasesADroppedSourceWithoutStyleLoadedEvents() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( runtime, new MapOptions { @@ -194,17 +212,19 @@ public void AStyleReplacementReleasesADroppedSourceWithoutStyleLoadedEvents() EventMask = RuntimeEventMask.All & ~RuntimeEventMask.MapStyleLoaded, } ); - map.SetStyleJson(EmptyStyleJson); + map.SetStyleJsonAsync(EmptyStyleJson); var state = new CustomMvtVectorSourceState( new CustomMvtVectorSourceOptions { FetchTile = _ => { } } ); - map.AddCustomMvtVectorSource("custom-mvt", state); + map.AddCustomMvtVectorSourceAsync("custom-mvt", state); - map.SetStyleJson(EmptyStyleJson); + // The replacement style drops the source, and the C API reports that through the release + // callback rather than through an event, so the host's cleared mask stays cleared. + map.SetStyleJsonAsync(EmptyStyleJson); var drained = new List(); for (var attempt = 0; attempt < 1000 && state.IsHandleAllocatedForTest; attempt++) { - runtime.Pump(TimeSpan.FromMilliseconds(1)); + Thread.Sleep(1); drained.AddRange(runtime.DrainEvents().Events.Select(polled => polled.Type)); } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/GeoJsonSourceTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/GeoJsonSourceTests.cs index 37b0d8f75..ada6ffe63 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/GeoJsonSourceTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/GeoJsonSourceTests.cs @@ -16,11 +16,14 @@ public sealed class GeoJsonSourceTests [BindingSpecTest("BND-065", "BND-105")] [Fact] - public void PreparedGeoJsonSourceDataAddsAndUpdatesThroughNativeMap() + public async Task PreparedGeoJsonSourceDataAddsAndUpdatesThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); using var initial = GeoJsonSourceDataHandle.Create(EmptyFeatureCollection, null); using var updated = GeoJsonSourceDataHandle.Create( @@ -28,27 +31,36 @@ public void PreparedGeoJsonSourceDataAddsAndUpdatesThroughNativeMap() null ); - map.AddGeoJsonSourceData("geo-data", initial); - map.SetGeoJsonSourceData("geo-data", updated); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.AddGeoJsonSourceDataAsync("geo-data", initial), + MaplibreStatus.Ok + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceDataAsync("geo-data", updated), + MaplibreStatus.Ok + ); - Assert.True(map.StyleSourceExists("geo-data")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo-data")); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("geo-data"))?.Type); } [BindingSpecTest("BND-060", "BND-105")] [Fact] - public void ClusteredGeoJsonSourceOptionsValidateDuringPreparation() + public async Task ClusteredGeoJsonSourceOptionsValidateDuringPreparation() { using var prepared = GeoJsonSourceDataHandle.Create(NearbyPoints, ClusterOptions()); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); - map.AddGeoJsonSourceData("clustered", prepared); + map.AddGeoJsonSourceDataAsync("clustered", prepared); - Assert.True(map.StyleSourceExists("clustered")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("clustered")); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("clustered"))?.Type); // Cluster-expression validation happens at preparation, before any map exists. var options = ClusterOptions(); @@ -63,63 +75,82 @@ public void ClusteredGeoJsonSourceOptionsValidateDuringPreparation() [BindingSpecTest("BND-105")] [Fact] - public void PreparedDataInstallsOnManySourcesAndOutlivesRelease() + public async Task PreparedDataInstallsOnManySourcesAndOutlivesRelease() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); var prepared = GeoJsonSourceDataHandle.Create(NearbyPoints, null); - map.AddGeoJsonSourceData("geo-a", prepared); - map.AddGeoJsonSourceData("geo-b", prepared); - map.SetGeoJsonSourceData("geo-a", prepared); + map.AddGeoJsonSourceDataAsync("geo-a", prepared); + map.AddGeoJsonSourceDataAsync("geo-b", prepared); + var setCommand = map.SetGeoJsonSourceDataAsync("geo-a", prepared); - // Install calls borrow the handle; releasing it never invalidates the sources. + // Install calls borrow the handle; releasing it right after submitting the + // commands never invalidates the sources. prepared.Close(); Assert.True(prepared.IsClosed); - Assert.True(map.StyleSourceExists("geo-a")); - Assert.True(map.StyleSourceExists("geo-b")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo-a")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo-b")); + RuntimeEventTestHelpers.AssertCommandFinishes(runtime, setCommand, MaplibreStatus.Ok); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("geo-a"))?.Type); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("geo-b"))?.Type); } [BindingSpecTest("BND-105")] [Fact] - public void SetRejectsDataPreparedWithDifferentOptions() + public async Task SetRejectsDataPreparedWithDifferentOptions() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); using var clustered = GeoJsonSourceDataHandle.Create(NearbyPoints, ClusterOptions()); - map.AddGeoJsonSourceData("clustered", clustered); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.AddGeoJsonSourceDataAsync("clustered", clustered), + MaplibreStatus.Ok + ); + // The options match happens on the map thread, so a mismatch surfaces as an + // asynchronous command failure rather than a synchronous throw. using var unclustered = GeoJsonSourceDataHandle.Create(NearbyPoints, null); - var error = Assert.Throws(() => - map.SetGeoJsonSourceData("clustered", unclustered) + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceDataAsync("clustered", unclustered), + MaplibreStatus.InvalidArgument ); - Assert.Equal(MaplibreStatus.InvalidArgument, error.Status); // Cluster aggregations are part of the options match, so data // prepared with different cluster properties is rejected too. var reaggregated = ClusterOptions(); reaggregated.ClusterProperties = """{"weight_max":["max",["get","weight"]]}"""u8.ToArray(); using var mismatched = GeoJsonSourceDataHandle.Create(NearbyPoints, reaggregated); - var propertiesError = Assert.Throws(() => - map.SetGeoJsonSourceData("clustered", mismatched) + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceDataAsync("clustered", mismatched), + MaplibreStatus.InvalidArgument ); - Assert.Equal(MaplibreStatus.InvalidArgument, propertiesError.Status); + + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("clustered"))?.Type); } [BindingSpecTest("BND-023", "BND-040")] [Fact] - public void ClosedPreparedDataRejectsUseAndCloseAgainNoOps() + public async Task ClosedPreparedDataRejectsUseAndCloseAgainNoOps() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); var prepared = GeoJsonSourceDataHandle.Create(EmptyFeatureCollection, null); prepared.Close(); @@ -128,21 +159,24 @@ public void ClosedPreparedDataRejectsUseAndCloseAgainNoOps() Assert.True(prepared.IsClosed); var error = Assert.Throws(() => - map.AddGeoJsonSourceData("geo-closed", prepared) + map.AddGeoJsonSourceDataAsync("geo-closed", prepared).GetAwaiter().GetResult() ); Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.False(map.StyleSourceExists("geo-closed")); + Assert.Null(await map.StyleSourceInfoAsync("geo-closed")); } [BindingSpecTest("BND-065", "BND-105")] [Fact] - public void PreparationRunsOffThreadAndInstallsOnMapThread() + public async Task PreparationRunsOffThreadAndInstallsOnMapThread() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); - // The map stays on its owner thread, so it joins a dedicated worker instead of awaiting. + // Preparation is legal from any thread, so it runs on a dedicated worker. GeoJsonSourceDataHandle? worked = null; Exception? failure = null; var worker = new Thread(() => @@ -161,30 +195,47 @@ public void PreparationRunsOffThreadAndInstallsOnMapThread() Assert.Null(failure); using var prepared = Assert.IsType(worked); - map.AddGeoJsonSourceData("geo-worker", prepared); - Assert.True(map.StyleSourceExists("geo-worker")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo-worker")); + map.AddGeoJsonSourceDataAsync("geo-worker", prepared); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("geo-worker"))?.Type); } [BindingSpecTest("BND-105")] [Fact] public void SynchronousTilingOverrideAppliesToExistingSourceOnly() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); using var prepared = GeoJsonSourceDataHandle.Create(NearbyPoints, null); - map.AddGeoJsonSourceData("geo-sync", prepared); + map.AddGeoJsonSourceDataAsync("geo-sync", prepared); - map.SetGeoJsonSourceSynchronousTiling("geo-sync", true); - map.SetGeoJsonSourceData("geo-sync", prepared); - map.SetGeoJsonSourceSynchronousTiling("geo-sync", false); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceSynchronousTilingAsync("geo-sync", true), + MaplibreStatus.Ok + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceDataAsync("geo-sync", prepared), + MaplibreStatus.Ok + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceSynchronousTilingAsync("geo-sync", false), + MaplibreStatus.Ok + ); - var error = Assert.Throws(() => - map.SetGeoJsonSourceSynchronousTiling("geo-missing", true) + // The missing-source check runs on the map thread, so it surfaces as an + // asynchronous command failure. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetGeoJsonSourceSynchronousTilingAsync("geo-missing", true), + MaplibreStatus.InvalidArgument ); - Assert.Equal(MaplibreStatus.InvalidArgument, error.Status); } private static GeoJsonSourceOptions ClusterOptions() => diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/LoggingTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/LoggingTests.cs index f6d52d4e0..445d7e4dd 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/LoggingTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/LoggingTests.cs @@ -1,8 +1,5 @@ -using System.Reflection; using Maplibre.NativeFfi.Error; -using Maplibre.NativeFfi.Internal.C; using Maplibre.NativeFfi.Internal.Callback; -using Maplibre.NativeFfi.Internal.Status; using Maplibre.NativeFfi.Log; using Xunit; @@ -33,24 +30,27 @@ public void InvalidAsyncSeverityMaskMapsNativeStatus() [BindingSpecTest("BND-120", "BND-121")] [Fact] - public unsafe void LogCallbackInstallReplaceClearAndHostFailureUseDocumentedBehavior() + public void LogCallbackInstallReplaceClearAndHostFailureUseDocumentedBehavior() { - using var methods = LogCallbackState.UseCallbackMethodsForTest( - (_, _) => mln_status.MLN_STATUS_OK, - () => mln_status.MLN_STATUS_OK - ); var records = new List(); + LogCallback first = record => + { + records.Add(record); + return true; + }; + LogCallback second = record => + { + records.Add(record with { Message = "replacement:" + record.Message }); + return false; + }; try { - Maplibre.SetLogCallback(record => - { - records.Add(record); - return true; - }); + Maplibre.SetLogCallback(first); Assert.Equal( 1u, LogCallbackState.EmitForTest( + first, (uint)LogSeverity.Warning, (uint)LogEvent.Render, 42, @@ -58,14 +58,11 @@ public unsafe void LogCallbackInstallReplaceClearAndHostFailureUseDocumentedBeha ) ); - Maplibre.SetLogCallback(record => - { - records.Add(record with { Message = "replacement:" + record.Message }); - return false; - }); + Maplibre.SetLogCallback(second); Assert.Equal( 0u, LogCallbackState.EmitForTest( + second, (uint)LogSeverity.Error, (uint)LogEvent.Style, 7, @@ -77,6 +74,7 @@ public unsafe void LogCallbackInstallReplaceClearAndHostFailureUseDocumentedBeha Assert.Equal( 0u, LogCallbackState.EmitForTest( + _ => throw new InvalidOperationException("boom"), (uint)LogSeverity.Info, (uint)LogEvent.General, 0, @@ -85,15 +83,6 @@ public unsafe void LogCallbackInstallReplaceClearAndHostFailureUseDocumentedBeha ); Maplibre.ClearLogCallback(); - Assert.Equal( - 0u, - LogCallbackState.EmitForTest( - (uint)LogSeverity.Info, - (uint)LogEvent.General, - 0, - "after" - ) - ); } finally { @@ -135,7 +124,20 @@ public void UnknownLogEnumValuesPreserveRawValues() return true; }); - Assert.Equal(1u, LogCallbackState.EmitForTest(999, 998, 0, "unknown")); + Assert.Equal( + 1u, + LogCallbackState.EmitForTest( + record => + { + copiedRecord = record; + return true; + }, + 999, + 998, + 0, + "unknown" + ) + ); } finally { @@ -148,66 +150,4 @@ public void UnknownLogEnumValuesPreserveRawValues() Assert.Equal((LogEvent)998, copiedRecord.Event); Assert.Equal(998u, copiedRecord.RawEvent); } - - [BindingSpecTest("BND-026", "BND-122")] - [Fact] - public unsafe void LogCallbackInstallFailurePreservesPreviousCallbackAndReleasesReplacement() - { - var failInstall = false; - LogCallbackState? failedReplacement = null; - var diagnostic = "install failed"; - using var methods = LogCallbackState.UseCallbackMethodsForTest( - (_, userData) => - { - if (!failInstall) - { - return mln_status.MLN_STATUS_OK; - } - - failedReplacement = LogCallbackState.StateForTokenForTest((nint)userData); - return mln_status.MLN_STATUS_INVALID_STATE; - }, - () => mln_status.MLN_STATUS_OK - ); - using var diagnostics = NativeStatus.UseDiagnosticProviderForTest(() => diagnostic); - - Maplibre.SetLogCallback(_ => true); - var previous = Assert.IsType(LogCallbackState.CurrentForTest); - - try - { - failInstall = true; - var error = Assert.Throws(() => - Maplibre.SetLogCallback(_ => false) - ); - - Assert.Same(previous, LogCallbackState.CurrentForTest); - Assert.False(previous.IsRetiredForTest); - Assert.NotNull(failedReplacement); - Assert.True(failedReplacement.IsRetiredForTest); - Assert.Equal("install failed", error.Diagnostic); - } - finally - { - Maplibre.ClearLogCallback(); - } - } - - [BindingSpecTest("BND-123")] - [Fact] - public void LogCallbackStateDisposeIsIdempotent() - { - var state = Assert.IsAssignableFrom( - Activator.CreateInstance( - typeof(LogCallbackState), - BindingFlags.Instance | BindingFlags.NonPublic, - binder: null, - args: [new LogCallback(_ => true)], - culture: null - ) - ); - - state.Dispose(); - state.Dispose(); - } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapCameraOptionsTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapCameraOptionsTests.cs index bdf900bf4..eb8ba96e7 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapCameraOptionsTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapCameraOptionsTests.cs @@ -21,10 +21,13 @@ private static void AssertClose(LatLng expected, LatLng actual) [Fact] public void ViewportAndTileOptionsRoundTripThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetViewportOptions( + map.SetViewportOptionsAsync( new ViewportOptions { NorthOrientation = NorthOrientation.Right, @@ -33,7 +36,7 @@ public void ViewportAndTileOptionsRoundTripThroughNativeMap() FrustumOffset = new EdgeInsets(1, 2, 3, 4), } ); - map.SetTileOptions( + var completion = map.SetTileOptionsAsync( new TileOptions { PrefetchZoomDelta = 3, @@ -44,28 +47,48 @@ public void ViewportAndTileOptionsRoundTripThroughNativeMap() LodMode = TileLodMode.Distance, } ); + RuntimeEventTestHelpers.WaitForCommand(runtime, completion); - var viewport = map.GetViewportOptions(); + var snapshot = map.GetSnapshot(); + var viewport = snapshot.Viewport; Assert.Equal(NorthOrientation.Right, viewport.NorthOrientation); Assert.Equal(ConstrainMode.WidthAndHeight, viewport.ConstrainMode); Assert.Equal(ViewportMode.FlippedY, viewport.ViewportMode); Assert.Equal(new EdgeInsets(1, 2, 3, 4), viewport.FrustumOffset); - var tile = map.GetTileOptions(); + var tile = snapshot.Tile; Assert.Equal(3u, tile.PrefetchZoomDelta); Assert.Equal(1.5, tile.LodMinimumRadius); Assert.Equal(2.5, tile.LodScale); Assert.Equal(45, tile.LodPitchThreshold); Assert.Equal(1.25, tile.LodZoomShift); Assert.Equal(TileLodMode.Distance, tile.LodMode); + + var freeCameraCompletion = map.SetFreeCameraOptionsAsync( + new FreeCameraOptions + { + Position = new Vec3(0.5, 0.5, 0.125), + Orientation = new Quaternion(0, 0, 0, 1), + } + ); + RuntimeEventTestHelpers.WaitForCommand(runtime, freeCameraCompletion); + var freeCamera = map.GetSnapshot().FreeCamera; + Assert.NotNull(freeCamera.Position); + Assert.Equal(0.5, freeCamera.Position.Value.X, 12); + Assert.Equal(0.5, freeCamera.Position.Value.Y, 12); + Assert.Equal(0.125, freeCamera.Position.Value.Z, 12); + Assert.NotNull(freeCamera.Orientation); } [BindingSpecTest("BND-102")] [Fact] - public void CameraFitHelpersCopyDescriptorsThroughNativeMap() + public async Task CameraFitHelpersCopyDescriptorsThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var bounds = new LatLngBounds(new LatLng(-10, -20), new LatLng(10, 20)); var fit = new CameraFitOptions { @@ -74,11 +97,20 @@ public void CameraFitHelpersCopyDescriptorsThroughNativeMap() Pitch = 10, }; - var boundsCamera = map.CameraForLatLngBounds(bounds, fit); - var coordinatesCamera = map.CameraForLatLngs([bounds.Southwest, bounds.Northeast], fit); - var geometryCamera = map.CameraForGeometry( + var boundsCamera = await map.CameraForLatLngBoundsAsync( + bounds, + fit, + TestContext.Current.CancellationToken + ); + var coordinatesCamera = await map.CameraForLatLngsAsync( + [bounds.Southwest, bounds.Northeast], + fit, + TestContext.Current.CancellationToken + ); + var geometryCamera = await map.CameraForGeometryAsync( """{"type":"LineString","coordinates":[[-20,-10],[20,10]]}"""u8.ToArray(), - fit + fit, + TestContext.Current.CancellationToken ); Assert.NotNull(boundsCamera.Center); @@ -91,13 +123,16 @@ public void CameraFitHelpersCopyDescriptorsThroughNativeMap() [BindingSpecTest("BND-102", "BND-103")] [Fact] - public void BoundsAndProjectionOptionsRoundTripThroughNativeMap() + public async Task BoundsAndProjectionOptionsRoundTripThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var bounds = new LatLngBounds(new LatLng(-10, -20), new LatLng(10, 20)); - map.SetBounds( + map.SetBoundsAsync( new BoundOptions { Bounds = new BoundsConstraint.Bounded(bounds), @@ -107,7 +142,7 @@ public void BoundsAndProjectionOptionsRoundTripThroughNativeMap() MaximumPitch = 60, } ); - map.SetProjectionMode( + var completion = map.SetProjectionModeAsync( new ProjectionModeOptions { Axonometric = true, @@ -115,8 +150,9 @@ public void BoundsAndProjectionOptionsRoundTripThroughNativeMap() YSkew = 0.2, } ); + RuntimeEventTestHelpers.WaitForCommand(runtime, completion); - var copiedBounds = map.GetBounds(); + var copiedBounds = map.GetSnapshot().Bounds; Assert.Equal(new BoundsConstraint.Bounded(bounds), copiedBounds.Bounds); Assert.NotNull(copiedBounds.MinimumZoom); Assert.Equal(1, copiedBounds.MinimumZoom.Value, 12); @@ -127,386 +163,215 @@ public void BoundsAndProjectionOptionsRoundTripThroughNativeMap() Assert.NotNull(copiedBounds.MaximumPitch); Assert.Equal(60, copiedBounds.MaximumPitch.Value, 12); - var projectionMode = map.GetProjectionMode(); + var projectionMode = map.GetSnapshot().ProjectionMode; Assert.True(projectionMode.Axonometric); Assert.NotNull(projectionMode.XSkew); Assert.Equal(0.1, projectionMode.XSkew.Value, 12); Assert.NotNull(projectionMode.YSkew); Assert.Equal(0.2, projectionMode.YSkew.Value, 12); - var visibleBounds = map.LatLngBoundsForCamera( - new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 } + var camera = new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 }; + var visibleBounds = await map.LatLngBoundsForCameraAsync( + camera, + TestContext.Current.CancellationToken ); - var unwrappedBounds = map.LatLngBoundsForCameraUnwrapped( - new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 } + var unwrappedBounds = await map.LatLngBoundsForCameraUnwrappedAsync( + camera, + TestContext.Current.CancellationToken ); Assert.True(visibleBounds.Southwest.Latitude <= visibleBounds.Northeast.Latitude); Assert.True(unwrappedBounds.Southwest.Latitude <= unwrappedBounds.Northeast.Latitude); } - [BindingSpecTest("BND-102")] + [BindingSpecTest("BND-102", "BND-104")] [Fact] - public void CameraBoundsDistinguishUnboundedFromWorldBounds() + public async Task CameraCommandsReturnIdsAndSnapshotsAdvance() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - double JumpedLongitude(double longitude) - { - map.JumpTo(new CameraOptions { Center = new LatLng(0, longitude), Zoom = 2 }); - var camera = map.GetCamera(); - Assert.NotNull(camera.Center); - return camera.Center.Value.Longitude; - } - - Assert.Equal(BoundsConstraint.Unbounded.Instance, map.GetBounds().Bounds); - // An unbounded map wraps across the antimeridian. - Assert.Equal(-160, JumpedLongitude(200), 6); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + var before = map.GetCameraSnapshot(); - map.SetBounds( - new BoundOptions + var command = map.UpdateCameraAsync( + new CameraUpdate { - Bounds = new BoundsConstraint.Bounded( - new LatLngBounds(new LatLng(-90, -180), new LatLng(90, 180)) - ), + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions + { + Center = new LatLng(12.5, 34.25), + Zoom = 5.5, + Bearing = 45, + Pitch = 30, + }, } ); + var ordered = await map.QueryCameraAsync(TestContext.Current.CancellationToken); - var world = Assert.IsType(map.GetBounds().Bounds); - Assert.Equal(180, world.Bounds.Northeast.Longitude, 6); - // World bounds clamp at the antimeridian instead of wrapping. - Assert.Equal(180, JumpedLongitude(200), 6); - - map.SetBounds(new BoundOptions { Bounds = BoundsConstraint.Unbounded.Instance }); - Assert.Equal(BoundsConstraint.Unbounded.Instance, map.GetBounds().Bounds); - // Releasing the constraint restores antimeridian wrapping. - Assert.Equal(-160, JumpedLongitude(200), 6); + RuntimeEventTestHelpers.AssertCommitted(command); + Assert.True(ordered.Generation > before.Generation); + Assert.Equal(12.5, ordered.Camera.Center!.Value.Latitude, 12); + Assert.Equal(5.5, ordered.Camera.Zoom!.Value, 12); + Assert.Equal(ordered, map.GetCameraSnapshot()); } [BindingSpecTest("BND-104")] [Fact] - public void InvalidMapAndProjectionInputsPropagateNativeDiagnostics() + public async Task InvalidCameraUpdatePropagatesNativeDiagnostic() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - var mapError = Assert.Throws(() => - map.JumpTo(new CameraOptions { Zoom = double.NaN }) + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } ); - var projectionError = Assert.Throws(() => - map.SetProjectionMode(new ProjectionModeOptions { XSkew = double.NaN }) + + var error = await Assert.ThrowsAsync(() => + map.UpdateCameraAsync( + new CameraUpdate + { + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions { Zoom = double.NaN }, + } + ) ); - Assert.Equal(MaplibreStatus.InvalidArgument, mapError.Status); - Assert.NotNull(mapError.RawStatus); - Assert.NotEmpty(mapError.Diagnostic); - Assert.Equal(MaplibreStatus.InvalidArgument, projectionError.Status); - Assert.NotNull(projectionError.RawStatus); - Assert.NotEmpty(projectionError.Diagnostic); + Assert.Equal(MaplibreStatus.InvalidArgument, error.Status); + Assert.NotEmpty(error.Diagnostic); } - [BindingSpecTest("BND-103")] + [BindingSpecTest("BND-043", "BND-103")] [Fact] - public void CoordinateProjectionRoundTripsThroughNativeMap() + public async Task CoordinateProjectionRoundTripsThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var coordinate = new LatLng(12.5, 34.25); - var point = map.PixelForLatLng(coordinate); - AssertClose(coordinate, map.LatLngForPixel(point)); - - var points = map.PixelsForLatLngs([coordinate, new LatLng(0, 0)]); - Assert.Equal(2, points.Length); - var coordinates = map.LatLngsForPixels(points); - Assert.Equal(2, coordinates.Length); + var point = await map.PixelForLatLngAsync( + coordinate, + TestContext.Current.CancellationToken + ); + AssertClose( + coordinate, + await map.LatLngForPixelAsync(point, TestContext.Current.CancellationToken) + ); + var points = await map.PixelsForLatLngsAsync( + [coordinate, new LatLng(0, 0)], + TestContext.Current.CancellationToken + ); + var coordinates = await map.LatLngsForPixelsAsync( + points, + TestContext.Current.CancellationToken + ); AssertClose(coordinate, coordinates[0]); AssertClose(new LatLng(0, 0), coordinates[1]); - - Assert.Empty(map.PixelsForLatLngs([])); - Assert.Empty(map.LatLngsForPixels([])); } [BindingSpecTest("BND-103")] [Fact] - public void ProjectionSnapshotSupportsCameraAndCoordinateConversions() + public async Task ProjectionConvertsSynchronouslyAndObservesItsOwnSetters() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - using var projection = map.CreateProjection(); - var coordinate = new LatLng(12.5, 34.25); - - projection.SetCamera(new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 }); - projection.SetVisibleCoordinates( - [new LatLng(-10, -20), new LatLng(10, 20)], - new EdgeInsets(1, 2, 3, 4) + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } ); - projection.SetVisibleGeometry( - """{"type":"LineString","coordinates":[[-20,-10],[20,10]]}"""u8.ToArray(), - new EdgeInsets(1, 2, 3, 4) + using var projection = await map.CreateProjectionAsync( + TestContext.Current.CancellationToken ); + var coordinate = new LatLng(12.5, 34.25); - var camera = projection.GetCamera(); - Assert.NotNull(camera.Center); + // Synchronous round trip: pixel -> latlng -> pixel. var point = projection.PixelForLatLng(coordinate); AssertClose(coordinate, projection.LatLngForPixel(point)); - projection.Close(); - Assert.True(projection.IsClosed); - } + // A setter changes later conversions on the same handle. + projection.SetCamera(new CameraOptions { Center = new LatLng(40, -75), Zoom = 6 }); + var camera = projection.GetCamera(); + Assert.NotNull(camera.Center); + AssertClose(new LatLng(40, -75), camera.Center.Value); + var movedPoint = projection.PixelForLatLng(coordinate); + Assert.NotEqual(point, movedPoint); + AssertClose(coordinate, projection.LatLngForPixel(movedPoint)); - [BindingSpecTest("BND-043")] - [Fact] - public void ProjectionSnapshotRemainsUsableAfterSourceMapCloses() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - using var projection = map.CreateProjection(); - var coordinate = new LatLng(12.5, 34.25); + projection.SetVisibleCoordinates( + [new LatLng(-10, -20), new LatLng(10, 20)], + new EdgeInsets(1, 1, 1, 1) + ); + Assert.NotNull(projection.GetCamera().Center); + + // A projection is usable from a second thread. + var threadRoundTrip = await Task.Run( + () => projection.LatLngForPixel(projection.PixelForLatLng(coordinate)), + TestContext.Current.CancellationToken + ); + AssertClose(coordinate, threadRoundTrip); map.Close(); + runtime.Close(); + // The projection stays usable after its map and runtime close, including from a + // thread that first touches it after those closes. Exception? failure = null; var worker = new Thread(() => { failure = Record.Exception(() => { - var camera = projection.GetCamera(); - Assert.NotNull(camera); - var point = projection.PixelForLatLng(coordinate); - AssertClose(coordinate, projection.LatLngForPixel(point)); - projection.Close(); + var closedCamera = projection.GetCamera(); + Assert.NotNull(closedCamera); + AssertClose( + coordinate, + projection.LatLngForPixel(projection.PixelForLatLng(coordinate)) + ); }); }); worker.Start(); worker.Join(); Assert.Null(failure); - runtime.Close(); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void FreeCameraOptionsCanBeCopiedThroughNativeMap() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - var freeCamera = map.GetFreeCameraOptions(); - map.SetFreeCameraOptions(freeCamera); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void CameraTransitionCommandsAcceptOptionalAnimationDescriptors() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - var camera = new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 }; - var animation = new AnimationOptions - { - Duration = 0, - MinimumZoom = 0, - Easing = new UnitBezier(0, 0, 1, 1), - }; - - map.EaseTo(camera, null); - map.EaseTo(camera, animation); - map.FlyTo(camera, null); - map.FlyTo(camera, animation); - map.MoveBy(0, 0); - map.MoveByAnimated(0, 0, null); - map.MoveByAnimated(0, 0, animation); - map.ScaleBy(1, null); - map.ScaleBy(1, new ScreenPoint(256, 256)); - map.ScaleByAnimated(1, null, null); - map.ScaleByAnimated(1, new ScreenPoint(256, 256), null); - map.ScaleByAnimated(1, null, animation); - map.ScaleByAnimated(1, new ScreenPoint(256, 256), animation); - map.RotateBy(new ScreenPoint(0, 0), new ScreenPoint(1, 1)); - map.RotateByAnimated(new ScreenPoint(0, 0), new ScreenPoint(1, 1), null); - map.RotateByAnimated(new ScreenPoint(0, 0), new ScreenPoint(1, 1), animation); - map.PitchBy(0); - map.PitchByAnimated(0, null); - map.PitchByAnimated(0, animation); - map.CancelTransitions(); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void GestureInProgressBracketsHostDrivenCameraCommands() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - Assert.False(map.IsGestureInProgress()); - - map.SetGestureInProgress(true); - map.MoveBy(8, -4); - Assert.True(map.IsGestureInProgress()); - - map.SetGestureInProgress(false); - Assert.False(map.IsGestureInProgress()); - } - - private sealed record CameraEventTally( - List FinishedTransitionIds, - List DidChangeModes - ); - - private static CameraEventTally DrainCameraEvents(RuntimeHandle runtime) - { - var finished = new List(); - var didChange = new List(); - foreach (var runtimeEvent in runtime.DrainEvents().Events) - { - switch (runtimeEvent.Type) - { - case RuntimeEventType.MapCameraTransitionFinished: - var payload = Assert.IsType( - runtimeEvent.Payload - ); - finished.Add(payload.TransitionId); - break; - case RuntimeEventType.MapCameraDidChange: - didChange.Add((CameraChangeMode)runtimeEvent.Code); - break; - } - } - - return new CameraEventTally(finished, didChange); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void ZeroDurationEaseReportsItsTransitionIdOnceAsAnImmediateChange() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - var camera = new CameraOptions { Center = new LatLng(0, 0), Zoom = 11 }; - - map.EaseTo(camera, new AnimationOptions { Duration = 0, TransitionId = 7 }); - - var tally = DrainCameraEvents(runtime); - Assert.Equal([7ul], tally.FinishedTransitionIds); - Assert.Contains(CameraChangeMode.Immediate, tally.DidChangeModes); - Assert.DoesNotContain(CameraChangeMode.Animated, tally.DidChangeModes); - - // The transition ended, so no later pump repeats the event. - runtime.Pump(TimeSpan.Zero); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void SupersededCameraTransitionReportsOnlyTheReplacedTransitionId() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - var camera = new CameraOptions { Center = new LatLng(0, 0), Zoom = 11 }; - - map.EaseTo(camera, new AnimationOptions { Duration = 5000, TransitionId = 11 }); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); - - map.EaseTo( - camera with - { - Zoom = 13, - }, - new AnimationOptions { Duration = 5000, TransitionId = 12 } - ); - - var tally = DrainCameraEvents(runtime); - Assert.Equal([11ul], tally.FinishedTransitionIds); - Assert.Contains(CameraChangeMode.Animated, tally.DidChangeModes); - - // The superseding transition is still running, so it reports nothing yet. - map.CancelTransitions(); - Assert.Equal([12ul], DrainCameraEvents(runtime).FinishedTransitionIds); - } - [BindingSpecTest("BND-102")] - [Fact] - public void CancelledCameraTransitionReportsItsTransitionIdOnce() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - map.FlyTo( - new CameraOptions { Center = new LatLng(40, -70), Zoom = 14 }, - new AnimationOptions { Duration = 5000, TransitionId = 21 } - ); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); - - map.CancelTransitions(); - Assert.Equal([21ul], DrainCameraEvents(runtime).FinishedTransitionIds); - - map.CancelTransitions(); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); + // Close is synchronous and idempotent through Dispose. + projection.Close(); + Assert.True(projection.IsClosed); } - [BindingSpecTest("BND-102")] + [BindingSpecTest("BND-103")] [Fact] - public void CameraTransitionThatRunsToCompletionReportsItsTransitionIdOnce() + public async Task ProjectionCreatedAfterCameraCommandObservesThatCommand() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( runtime, - new MapOptions + new MapOptions { Width = 512, Height = 512 } + ); + var center = new LatLng(40, -75); + map.UpdateCameraAsync( + new CameraUpdate { - Width = 512, - Height = 512, - MapMode = MapMode.Static, + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions { Center = center, Zoom = 6 }, } ); - map.EaseTo( - new CameraOptions { Center = new LatLng(0, 0), Zoom = 11 }, - new AnimationOptions { Duration = 5000, TransitionId = 31 } + using var projection = await map.CreateProjectionAsync( + TestContext.Current.CancellationToken ); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); - - // A static map advances its transitions to their end state the next time it - // updates, so requesting a still image runs this ease to completion. - map.RequestStillImage(); - runtime.Pump(TimeSpan.Zero); - - var tally = DrainCameraEvents(runtime); - Assert.Equal([31ul], tally.FinishedTransitionIds); - Assert.Equal(11, map.GetCamera().Zoom); - - runtime.Pump(TimeSpan.Zero); - Assert.Empty(DrainCameraEvents(runtime).FinishedTransitionIds); - } - - [BindingSpecTest("BND-102")] - [Fact] - public void JumpToAppliesCameraFieldsThroughNativeMap() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + var camera = projection.GetCamera(); + Assert.NotNull(camera.Center); + AssertClose(center, camera.Center.Value); - map.JumpTo( - new CameraOptions + // The projection never observes map changes made after creation. + map.UpdateCameraAsync( + new CameraUpdate { - Center = new LatLng(12.5, 34.25), - Zoom = 5.5, - Bearing = 45, - Pitch = 30, + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions { Center = new LatLng(0, 0), Zoom = 1 }, } ); - - var camera = map.GetCamera(); - Assert.NotNull(camera.Center); - Assert.Equal(12.5, camera.Center.Value.Latitude, 12); - Assert.Equal(34.25, camera.Center.Value.Longitude, 12); - Assert.NotNull(camera.Zoom); - Assert.Equal(5.5, camera.Zoom.Value, 12); - Assert.NotNull(camera.Bearing); - Assert.Equal(45, camera.Bearing.Value, 12); - Assert.NotNull(camera.Pitch); - Assert.Equal(30, camera.Pitch.Value, 12); - - // BND-070: successive snapshots of an unchanged camera compare equal. - Assert.Equal(camera, map.GetCamera()); + await map.QueryCameraAsync(TestContext.Current.CancellationToken); + AssertClose(center, projection.GetCamera().Center!.Value); } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapFeatureStateTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapFeatureStateTests.cs new file mode 100644 index 000000000..986143af8 --- /dev/null +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MapFeatureStateTests.cs @@ -0,0 +1,108 @@ +using System.Text; +using System.Text.Json; +using Maplibre.NativeFfi.Error; +using Maplibre.NativeFfi.Map; +using Maplibre.NativeFfi.Query; +using Maplibre.NativeFfi.Runtime; +using Xunit; + +namespace Maplibre.NativeFfi.Tests; + +public sealed class MapFeatureStateTests +{ + private static FeatureStateSelector Selector(string? stateKey = null) => + new() + { + SourceId = "geo", + FeatureId = "42", + StateKey = stateKey, + }; + + private static JsonElement ParseState(byte[] state) => + JsonDocument.Parse(Encoding.UTF8.GetString(state)).RootElement; + + [BindingSpecTest("BND-105")] + [Fact] + public async Task FeatureStateRoundTripsThroughTheMapStore() + { + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + + // Missing feature state reads back as an empty JSON object. + var missing = ParseState( + await map.GetFeatureStateAsync(Selector(), TestContext.Current.CancellationToken) + ); + Assert.Equal(JsonValueKind.Object, missing.ValueKind); + Assert.Empty(missing.EnumerateObject()); + + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetFeatureStateAsync(Selector(), """{"hover":true,"rank":2}"""u8.ToArray()), + MaplibreStatus.Ok + ); + + var state = ParseState( + await map.GetFeatureStateAsync(Selector(), TestContext.Current.CancellationToken) + ); + Assert.True(state.GetProperty("hover").GetBoolean()); + Assert.Equal(2, state.GetProperty("rank").GetInt32()); + + // Removing one key leaves the others in place. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveFeatureStateAsync(Selector("hover")), + MaplibreStatus.Ok + ); + var remaining = ParseState( + await map.GetFeatureStateAsync(Selector(), TestContext.Current.CancellationToken) + ); + Assert.False(remaining.TryGetProperty("hover", out _)); + Assert.Equal(2, remaining.GetProperty("rank").GetInt32()); + + // Removing without a key clears the feature's state. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveFeatureStateAsync(Selector()), + MaplibreStatus.Ok + ); + var cleared = ParseState( + await map.GetFeatureStateAsync(Selector(), TestContext.Current.CancellationToken) + ); + Assert.Empty(cleared.EnumerateObject()); + } + + [BindingSpecTest("BND-025")] + [Fact] + public async Task FeatureStateCommandsValidateSelectorShape() + { + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + var withoutFeature = new FeatureStateSelector { SourceId = "geo" }; + + // Set and get require a feature ID. + await Assert.ThrowsAsync(() => + map.SetFeatureStateAsync(withoutFeature, """{"hover":true}"""u8.ToArray()) + ); + await Assert.ThrowsAsync(() => + map.GetFeatureStateAsync(withoutFeature, TestContext.Current.CancellationToken) + ); + + // Remove accepts a bare state key only alongside a feature ID. + await Assert.ThrowsAsync(() => + map.RemoveFeatureStateAsync( + new FeatureStateSelector { SourceId = "geo", StateKey = "hover" } + ) + ); + + // Set requires one JSON object. + await Assert.ThrowsAsync(() => + map.SetFeatureStateAsync(Selector(), """["not-an-object"]"""u8.ToArray()) + ); + } +} diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MetalRenderSessionLifecycleTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MetalRenderSessionLifecycleTests.cs new file mode 100644 index 000000000..56289c1ea --- /dev/null +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/MetalRenderSessionLifecycleTests.cs @@ -0,0 +1,90 @@ +using System.Runtime.InteropServices; +using Maplibre.NativeFfi.Map; +using Maplibre.NativeFfi.Render; +using Maplibre.NativeFfi.Runtime; +using Xunit; + +namespace Maplibre.NativeFfi.Tests; + +public sealed partial class MetalRenderSessionLifecycleTests +{ + [LibraryImport( + "/System/Library/Frameworks/Metal.framework/Metal", + EntryPoint = "MTLCreateSystemDefaultDevice" + )] + private static partial nint MetalCreateSystemDefaultDevice(); + + [LibraryImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_release")] + private static partial void ObjectiveCRelease(nint value); + + [BindingSpecTest("BND-162", "BND-193")] + [Fact] + public async Task CallerDriverAttachmentAndDetachRequireExplicitService() + { + Assert.SkipUnless( + Maplibre.SupportedRenderBackends().HasFlag(RenderBackend.Metal), + "The selected native preset does not provide Metal." + ); + + Assert.True(OperatingSystem.IsMacOS()); + var device = MetalCreateSystemDefaultDevice(); + Assert.NotEqual(0, device); + try + { + var runtime = RuntimeHandle.Create(new RuntimeOptions()); + var map = await MapHandle.CreateAsync( + runtime, + new MapOptions + { + Width = 32, + Height = 16, + ScaleFactor = 1.0, + }, + TestContext.Current.CancellationToken + ); + var session = RenderSessionHandle.AttachMetalOwnedTexture( + map, + new MetalOwnedTextureDescriptor + { + Extent = new RenderTargetExtent(32, 16, 1.0), + Context = new MetalContextDescriptor + { + Device = NativePointer.FromBorrowedAddress(device), + }, + }, + new RenderSessionAttachOptions { Driver = RenderDriverKind.CallerGraphicsThread } + ); + + Assert.False(session.Attachment.IsCompleted); + ServiceUntilCompleted(session, session.Attachment); + Assert.Equal(RenderSessionState.Attached, session.Snapshot.State); + Assert.Equal(RenderDriverKind.CallerGraphicsThread, session.Capabilities.Driver); + + var detach = session.DetachAsync(TestContext.Current.CancellationToken); + Assert.False(detach.IsCompleted); + ServiceUntilCompleted(session, detach); + Assert.Equal(RenderSessionState.Detached, session.Snapshot.State); + + session.Close(); + map.Close(); + await runtime.CloseAsync(); + } + finally + { + ObjectiveCRelease(device); + } + } + + private static void ServiceUntilCompleted(RenderSessionHandle session, Task operation) + { + var cancellationToken = TestContext.Current.CancellationToken; + while (!operation.IsCompleted) + { + cancellationToken.ThrowIfCancellationRequested(); + session.ServiceDriverWork(0); + Thread.Yield(); + } + + operation.GetAwaiter().GetResult(); + } +} diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleIdentityTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleIdentityTests.cs index 634b2d1b3..1779f012b 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleIdentityTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleIdentityTests.cs @@ -17,40 +17,47 @@ public sealed unsafe class NativeHandleIdentityTests [Fact] public void ReleasedMapIdReplayedAfterANewMapReportsInvalidArgument() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); - var first = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + var first = TestHandles.CreateMap(runtime, new MapOptions { Width = 512, Height = 512 }); var releasedId = first.Handle; - first.Close(); + TestHandles.Close(first); // The released slot is the one the next map takes, so the replayed id // names a retired generation of a slot that is live again. - using var second = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var second = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - var error = Assert.Throws(() => GetSize(releasedId)); + var error = Assert.Throws(() => GetSnapshot(releasedId)); Assert.Equal(MaplibreStatus.InvalidArgument, error.Status); Assert.Contains("stale", error.Diagnostic, StringComparison.OrdinalIgnoreCase); // The live map is unaffected by the replay. - var (width, _, _) = GetSize(second.Handle); - Assert.Equal(512u, width); + Assert.Equal(512u, GetSnapshot(second.Handle).logical_extent.width); } [BindingSpecTest("BND-047")] [Fact] public void MapIdPassedToARuntimeOperationReportsInvalidArgument() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); // MlnMap and MlnRuntime are distinct CLR types, so this call has no // expression in the safe public API and needs the raw id. var wrongKind = new MlnRuntime(map.Handle.Value); var error = Assert.Throws(() => - NativeStatus.Check(NativeMethods.mln_runtime_pump(wrongKind, 0, -1)) - ); + { + ulong mask = 0; + NativeStatus.Check(NativeMethods.mln_runtime_get_event_mask(wrongKind, &mask)); + }); Assert.Equal(MaplibreStatus.InvalidArgument, error.Status); Assert.Contains("map", error.Diagnostic, StringComparison.OrdinalIgnoreCase); @@ -59,33 +66,26 @@ public void MapIdPassedToARuntimeOperationReportsInvalidArgument() [BindingSpecTest("BND-049")] [Fact] - public void MapIdCalledFromAnotherThreadReportsWrongThread() + public void MapIdCanBeReadFromAnotherThread() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - var live = map.Handle; - Exception? thrown = null; + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + mln_map_snapshot snapshot = default; - var thread = new Thread(() => - { - thrown = Record.Exception(() => GetSize(live)); - }); + var thread = new Thread(() => snapshot = GetSnapshot(map.Handle)); thread.Start(); thread.Join(); - // The id is live, so the owner-thread rule decides rather than identity. - var error = Assert.IsType(thrown); - Assert.Equal(MaplibreStatus.WrongThread, error.Status); - Assert.DoesNotContain("stale", error.Diagnostic, StringComparison.OrdinalIgnoreCase); + Assert.Equal(512u, snapshot.logical_extent.width); } - private static (uint Width, uint Height, double ScaleFactor) GetSize(MlnMap map) + private static mln_map_snapshot GetSnapshot(MlnMap map) { - uint width = 0; - uint height = 0; - double scaleFactor = 0; - NativeStatus.Check(NativeMethods.mln_map_get_size(map, &width, &height, &scaleFactor)); - return (width, height, scaleFactor); + var snapshot = new mln_map_snapshot { size = (uint)sizeof(mln_map_snapshot) }; + NativeStatus.Check(NativeMethods.mln_map_snapshot_get(map, &snapshot)); + return snapshot; } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleStateTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleStateTests.cs index 68ce8b02f..359279000 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleStateTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeHandleStateTests.cs @@ -196,55 +196,6 @@ private static void CreateLeakedState() ); } - [BindingSpecTest("BND-197")] - [Fact] - public void CloseWaitsForAUseInFlightOnAnotherThread() - { - using var _ = Gate.EnterScope(); - destroyStatus = mln_status.MLN_STATUS_OK; - destroyCount = 0; - var state = new NativeHandleState( - SyntheticHandles.Runtime(1234), - Destroy, - "RuntimeHandle" - ); - - using var entered = new ManualResetEventSlim(false); - using var releaseUse = new ManualResetEventSlim(false); - using var closeReturned = new ManualResetEventSlim(false); - var destroysSeenByUse = -1; - - var useThread = new Thread(() => - state.WithLive(_ => - { - entered.Set(); - Assert.True(releaseUse.Wait(TimeSpan.FromSeconds(5))); - destroysSeenByUse = Volatile.Read(ref destroyCount); - }) - ); - useThread.Start(); - Assert.True(entered.Wait(TimeSpan.FromSeconds(5))); - - var closeThread = new Thread(() => - { - state.Close(); - closeReturned.Set(); - }); - closeThread.Start(); - - Assert.False(closeReturned.Wait(TimeSpan.FromMilliseconds(200))); - Assert.Equal(0, Volatile.Read(ref destroyCount)); - - releaseUse.Set(); - Assert.True(closeReturned.Wait(TimeSpan.FromSeconds(5))); - Assert.True(useThread.Join(TimeSpan.FromSeconds(5))); - - Assert.Equal(1, destroyCount); - Assert.Equal(0, destroysSeenByUse); - Assert.True(state.IsClosed); - } - - [BindingSpecTest("BND-197")] [Fact] public void AUseStartingAfterCloseBeginsIsRefused() { diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeStatusTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeStatusTests.cs index ba7f5f33b..fc8fa0481 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeStatusTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/NativeStatusTests.cs @@ -81,6 +81,14 @@ public void UnknownNativeStatusPreservesRawStatus() Assert.Equal("future status", error.Diagnostic); } + [Fact] + public void RenderDriverStatusesRemainDistinct() + { + Assert.Equal(MaplibreStatus.Busy, NativeStatus.StatusFromRaw(-7)); + Assert.Equal(MaplibreStatus.TargetLost, NativeStatus.StatusFromRaw(-8)); + Assert.Equal(MaplibreStatus.NotReady, NativeStatus.StatusFromRaw(-9)); + } + [BindingSpecTest("BND-022")] [Fact] public void DiagnosticIsCopiedBeforeLaterFailureChangesThreadLocalMessage() diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/PublicApiSurfaceTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/PublicApiSurfaceTests.cs index dfa12ae08..c5c897c71 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/PublicApiSurfaceTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/PublicApiSurfaceTests.cs @@ -17,10 +17,16 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Camera.BoundsConstraint+Bounded", "Maplibre.NativeFfi.Camera.BoundsConstraint+Unbounded", "Maplibre.NativeFfi.Camera.CameraChangeMode", + "Maplibre.NativeFfi.Camera.CameraDelta", + "Maplibre.NativeFfi.Camera.CameraDeltaKind", + "Maplibre.NativeFfi.Camera.CameraSnapshot", + "Maplibre.NativeFfi.Camera.CameraUpdate", + "Maplibre.NativeFfi.Camera.CameraUpdateMode", "Maplibre.NativeFfi.Camera.CameraFitOptions", "Maplibre.NativeFfi.Camera.CameraOptions", "Maplibre.NativeFfi.Camera.EdgeInsets", "Maplibre.NativeFfi.Camera.FreeCameraOptions", + "Maplibre.NativeFfi.Camera.GesturePhase", "Maplibre.NativeFfi.Camera.UnitBezier", "Maplibre.NativeFfi.Error.InvalidArgumentException", "Maplibre.NativeFfi.Error.InvalidStateException", @@ -45,9 +51,11 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Log.LogSeverityMask", "Maplibre.NativeFfi.Map.ConstrainMode", "Maplibre.NativeFfi.Map.DebugOptions", + "Maplibre.NativeFfi.Map.LogicalExtent", "Maplibre.NativeFfi.Map.MapHandle", "Maplibre.NativeFfi.Map.MapMode", "Maplibre.NativeFfi.Map.MapOptions", + "Maplibre.NativeFfi.Map.MapSnapshot", "Maplibre.NativeFfi.Map.MapProjectionHandle", "Maplibre.NativeFfi.Map.NorthOrientation", "Maplibre.NativeFfi.Map.ProjectionModeOptions", @@ -74,12 +82,16 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Query.RenderedQueryGeometry+LineString", "Maplibre.NativeFfi.Query.RenderedQueryGeometry+Point", "Maplibre.NativeFfi.Query.SourceFeatureQueryOptions", + "Maplibre.NativeFfi.Render.AcquiredFrameHandle", "Maplibre.NativeFfi.Render.EglContextDescriptor", + "Maplibre.NativeFfi.Render.FrameDemand", + "Maplibre.NativeFfi.Render.FrameDemandFlags", + "Maplibre.NativeFfi.Render.GpuSync", + "Maplibre.NativeFfi.Render.GpuSyncKind", "Maplibre.NativeFfi.Render.MetalBorrowedTextureDescriptor", "Maplibre.NativeFfi.Render.MetalContextDescriptor", "Maplibre.NativeFfi.Render.MetalOwnedTextureDescriptor", "Maplibre.NativeFfi.Render.MetalOwnedTextureFrame", - "Maplibre.NativeFfi.Render.MetalOwnedTextureFrameHandle", "Maplibre.NativeFfi.Render.MetalSurfaceDescriptor", "Maplibre.NativeFfi.Render.NativeBuffer", "Maplibre.NativeFfi.Render.OpenGLBorrowedTextureDescriptor", @@ -89,22 +101,36 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Render.OpenGLContextProvider", "Maplibre.NativeFfi.Render.OpenGLOwnedTextureDescriptor", "Maplibre.NativeFfi.Render.OpenGLOwnedTextureFrame", - "Maplibre.NativeFfi.Render.OpenGLOwnedTextureFrameHandle", "Maplibre.NativeFfi.Render.OpenGLSurfaceDescriptor", "Maplibre.NativeFfi.Render.PremultipliedRgba8Image", + "Maplibre.NativeFfi.Render.RenderAbandonDisposition", + "Maplibre.NativeFfi.Render.RenderAbandonResult", "Maplibre.NativeFfi.Render.RenderBackend", + "Maplibre.NativeFfi.Render.RenderDriverKind", + "Maplibre.NativeFfi.Render.RenderFrameBatch", + "Maplibre.NativeFfi.Render.RenderFrameResult", "Maplibre.NativeFfi.Render.RenderMode", "Maplibre.NativeFfi.Render.RenderResult", + "Maplibre.NativeFfi.Render.RenderSessionAttachOptions", + "Maplibre.NativeFfi.Render.RenderSessionCapabilities", + "Maplibre.NativeFfi.Render.RenderSessionCapabilityInfo", "Maplibre.NativeFfi.Render.RenderSessionHandle", + "Maplibre.NativeFfi.Render.RenderSessionSnapshot", + "Maplibre.NativeFfi.Render.RenderSessionState", "Maplibre.NativeFfi.Render.RenderTargetExtent", - "Maplibre.NativeFfi.Render.RenderUpdate", "Maplibre.NativeFfi.Render.TextureImageInfo", "Maplibre.NativeFfi.Render.VulkanBorrowedTextureDescriptor", "Maplibre.NativeFfi.Render.VulkanContextDescriptor", "Maplibre.NativeFfi.Render.VulkanOwnedTextureDescriptor", "Maplibre.NativeFfi.Render.VulkanOwnedTextureFrame", - "Maplibre.NativeFfi.Render.VulkanOwnedTextureFrameHandle", "Maplibre.NativeFfi.Render.VulkanSurfaceDescriptor", + "Maplibre.NativeFfi.Render.WebGLContextDescriptor", + "Maplibre.NativeFfi.Render.WebGLContextKind", + "Maplibre.NativeFfi.Render.WebGpuBorrowedTextureDescriptor", + "Maplibre.NativeFfi.Render.WebGpuContextDescriptor", + "Maplibre.NativeFfi.Render.WebGpuOwnedTextureDescriptor", + "Maplibre.NativeFfi.Render.WebGpuOwnedTextureFrame", + "Maplibre.NativeFfi.Render.WebGpuSurfaceDescriptor", "Maplibre.NativeFfi.Render.WglContextDescriptor", "Maplibre.NativeFfi.Resource.ByteRange", "Maplibre.NativeFfi.Resource.HttpHeader", @@ -125,17 +151,14 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Resource.ResourceTransformRequest", "Maplibre.NativeFfi.Resource.ResourceUsage", "Maplibre.NativeFfi.Runtime.AmbientCacheOperation", - "Maplibre.NativeFfi.Runtime.OfflineOperationHandle", - "Maplibre.NativeFfi.Runtime.OfflineOperationKind", - "Maplibre.NativeFfi.Runtime.OfflineOperationResultKind", - "Maplibre.NativeFfi.Runtime.OwnerThread", + "Maplibre.NativeFfi.Runtime.CommandCompletion", + "Maplibre.NativeFfi.Runtime.CommandDisposition", "Maplibre.NativeFfi.Runtime.RuntimeEvent", "Maplibre.NativeFfi.Runtime.RuntimeEventBatch", "Maplibre.NativeFfi.Runtime.RuntimeEventMask", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+CameraTransitionFinished", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+None", - "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+OfflineOperationCompleted", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+OfflineRegionResponseError", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+OfflineRegionStatusChanged", "Maplibre.NativeFfi.Runtime.RuntimeEventPayload+OfflineRegionTileCountLimit", @@ -147,13 +170,13 @@ public void ExpectedPublicTypesExist() "Maplibre.NativeFfi.Runtime.RuntimeEventType", "Maplibre.NativeFfi.Runtime.RuntimeHandle", "Maplibre.NativeFfi.Runtime.RuntimeOptions", - "Maplibre.NativeFfi.Runtime.WakeSource", "Maplibre.NativeFfi.Style.CustomGeometrySourceCallback", "Maplibre.NativeFfi.Style.CustomGeometrySourceOptions", "Maplibre.NativeFfi.Style.CustomMvtVectorSourceCallback", "Maplibre.NativeFfi.Style.CustomMvtVectorSourceOptions", "Maplibre.NativeFfi.Style.GeoJsonSourceDataHandle", "Maplibre.NativeFfi.Style.GeoJsonSourceOptions", + "Maplibre.NativeFfi.Style.LayerInfo", "Maplibre.NativeFfi.Style.LocationIndicatorImageKind", "Maplibre.NativeFfi.Style.RasterDemEncoding", "Maplibre.NativeFfi.Style.SourceInfo", @@ -270,9 +293,10 @@ public void NativePointerUsesBorrowedAddressFactory() ); } - // Default parameter values would create shortcut workflows outside the C API shape. + // Optional cancellation tokens follow the .NET asynchronous API convention. Other default + // parameter values would create shortcut workflows outside the C API shape. [Fact] - public void PublicSurfaceDoesNotUseDefaultParameterValues() + public void PublicSurfaceUsesDefaultsOnlyForCancellationTokens() { var violations = new List(); foreach (var type in typeof(Maplibre).Assembly.GetExportedTypes()) @@ -303,12 +327,10 @@ public void OwnedNativeHandlesDoNotExposePublicConstructors() { "Maplibre.NativeFfi.Map.MapHandle", "Maplibre.NativeFfi.Map.MapProjectionHandle", - "Maplibre.NativeFfi.Render.MetalOwnedTextureFrameHandle", - "Maplibre.NativeFfi.Render.OpenGLOwnedTextureFrameHandle", + "Maplibre.NativeFfi.Render.AcquiredFrameHandle", + "Maplibre.NativeFfi.Render.RenderFrameBatch", "Maplibre.NativeFfi.Render.RenderSessionHandle", - "Maplibre.NativeFfi.Render.VulkanOwnedTextureFrameHandle", "Maplibre.NativeFfi.Resource.ResourceRequestHandle", - "Maplibre.NativeFfi.Runtime.OfflineOperationHandle", "Maplibre.NativeFfi.Runtime.RuntimeHandle", }; @@ -350,7 +372,10 @@ List violations { foreach (var parameter in parameters) { - if (parameter.HasDefaultValue || parameter.IsOptional) + if ( + (parameter.HasDefaultValue || parameter.IsOptional) + && parameter.ParameterType != typeof(CancellationToken) + ) { violations.Add( $"{member.DeclaringType?.FullName}.{member.Name} has default parameter {parameter.Name}." diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/QueryStructTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/QueryStructTests.cs index 2047e4f3d..3856f82ae 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/QueryStructTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/QueryStructTests.cs @@ -99,4 +99,39 @@ public void QueryOptionsMaterializeOptionalFieldsAndFilters() RuntimeStructs.CopyUtf8(source.Value.filter->data, source.Value.filter->size) ); } + + [BindingSpecTest("BND-061")] + [Fact] + public void FeatureStateSelectorMaterializesOptionalFields() + { + using var selector = NativeFeatureStateSelector.From( + new FeatureStateSelector + { + SourceId = "source", + SourceLayerId = "layer", + FeatureId = "feature", + StateKey = "hover", + } + ); + + var value = selector.Value; + Assert.Equal( + (uint)( + mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID + | mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_FEATURE_ID + | mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_STATE_KEY + ), + value.fields + ); + Assert.Equal("source", RuntimeStructs.CopyUtf8(value.source_id.data, value.source_id.size)); + Assert.Equal( + "layer", + RuntimeStructs.CopyUtf8(value.source_layer_id.data, value.source_layer_id.size) + ); + Assert.Equal( + "feature", + RuntimeStructs.CopyUtf8(value.feature_id.data, value.feature_id.size) + ); + Assert.Equal("hover", RuntimeStructs.CopyUtf8(value.state_key.data, value.state_key.size)); + } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RenderSessionTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RenderSessionTests.cs index e61d0a1b0..1ea03fcf9 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RenderSessionTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RenderSessionTests.cs @@ -1,898 +1,133 @@ +using System.Reflection; using System.Runtime.InteropServices; -using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Internal.C; -using Maplibre.NativeFfi.Internal.Pointer; using Maplibre.NativeFfi.Internal.Struct; -using Maplibre.NativeFfi.Map; -using Maplibre.NativeFfi.Query; using Maplibre.NativeFfi.Render; -using Maplibre.NativeFfi.Runtime; using Xunit; namespace Maplibre.NativeFfi.Tests; public sealed unsafe class RenderSessionTests { - [BindingSpecTest("BND-161")] [Fact] - public void SurfaceDescriptorsMaterializeOpaquePointersAndExtent() + public void PhaseThreeEnumsPreserveFrozenValues() { - var metal = RenderStructs.ToNative( - new MetalSurfaceDescriptor - { - Extent = new RenderTargetExtent(320, 240, 2), - Layer = NativePointer.FromBorrowedAddress(123), - Context = new MetalContextDescriptor - { - Device = NativePointer.FromBorrowedAddress(456), - }, - } - ); - Assert.Equal(320u, metal.extent.width); - Assert.Equal(240u, metal.extent.height); - Assert.Equal(2, metal.extent.scale_factor); - Assert.Equal(123, (nint)metal.layer); - Assert.Equal(456, (nint)metal.context.device); - - var vulkan = RenderStructs.ToNative( - new VulkanSurfaceDescriptor - { - Extent = new RenderTargetExtent(640, 480, 1), - Surface = NativePointer.FromBorrowedAddress(111), - Context = new VulkanContextDescriptor - { - Instance = NativePointer.FromBorrowedAddress(222), - PhysicalDevice = NativePointer.FromBorrowedAddress(333), - Device = NativePointer.FromBorrowedAddress(444), - Queue = NativePointer.FromBorrowedAddress(555), - GraphicsQueueFamilyIndex = 7, - GetInstanceProcAddr = NativePointer.FromBorrowedAddress(666), - GetDeviceProcAddr = NativePointer.FromBorrowedAddress(777), - }, - } - ); - Assert.Equal(640u, vulkan.extent.width); - Assert.Equal(480u, vulkan.extent.height); - Assert.Equal(111, (nint)vulkan.surface); - Assert.Equal(222, (nint)vulkan.context.instance); - Assert.Equal(333, (nint)vulkan.context.physical_device); - Assert.Equal(444, (nint)vulkan.context.device); - Assert.Equal(555, (nint)vulkan.context.graphics_queue); - Assert.Equal(7u, vulkan.context.graphics_queue_family_index); - Assert.Equal(666, (nint)vulkan.context.get_instance_proc_addr); - Assert.Equal(777, (nint)vulkan.context.get_device_proc_addr); - - var opengl = RenderStructs.ToNative( - new OpenGLSurfaceDescriptor - { - Extent = new RenderTargetExtent(800, 600, 2), - Surface = NativePointer.FromBorrowedAddress(888), - Context = new WglContextDescriptor - { - DeviceContext = NativePointer.FromBorrowedAddress(999), - ShareContext = NativePointer.FromBorrowedAddress(1000), - GetProcAddress = NativePointer.FromBorrowedAddress(1001), - }, - } - ); - Assert.Equal(800u, opengl.extent.width); - Assert.Equal(600u, opengl.extent.height); - Assert.Equal(888, (nint)opengl.surface); - Assert.Equal( - mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WGL, - opengl.context.platform - ); - Assert.Equal(999, (nint)opengl.context.data.wgl.device_context); - Assert.Equal(1000, (nint)opengl.context.data.wgl.share_context); - Assert.Equal(1001, (nint)opengl.context.data.wgl.get_proc_address); - Assert.Equal( - mln_opengl_context_ownership.MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED, - opengl.context.ownership - ); - - var dedicated = RenderStructs.ToNative( - new OpenGLSurfaceDescriptor - { - Extent = new RenderTargetExtent(800, 600, 2), - Surface = NativePointer.FromBorrowedAddress(1100), - Context = new EglContextDescriptor - { - Display = NativePointer.FromBorrowedAddress(1101), - Config = NativePointer.FromBorrowedAddress(1102), - Ownership = OpenGLContextOwnership.Dedicated, - ClientApi = OpenGLClientApi.Gles, - }, - } - ); - Assert.Equal( - mln_opengl_context_ownership.MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED, - dedicated.context.ownership - ); - Assert.Equal( - mln_opengl_client_api.MLN_OPENGL_CLIENT_API_GLES, - dedicated.context.data.egl.client_api - ); - Assert.Equal(0, (nint)dedicated.context.data.egl.share_context); - } - - [BindingSpecTest("BND-161")] - [Fact] - public void TextureDescriptorsMaterializeOpaquePointersAndExtent() - { - var metalOwned = RenderStructs.ToNative( - new MetalOwnedTextureDescriptor - { - Extent = new RenderTargetExtent(128, 64, 2), - Context = new MetalContextDescriptor - { - Device = NativePointer.FromBorrowedAddress(10), - }, - } - ); - Assert.Equal(128u, metalOwned.extent.width); - Assert.Equal(64u, metalOwned.extent.height); - Assert.Equal(10, (nint)metalOwned.context.device); - - var metalBorrowed = RenderStructs.ToNative( - new MetalBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(128, 64, 2), - PhysicalWidth = 65, - PhysicalHeight = 33, - Texture = NativePointer.FromBorrowedAddress(20), - } - ); - Assert.Equal(20, (nint)metalBorrowed.texture); - // Odd sizes no logical extent maps onto, so these must survive as stated. - Assert.Equal(65u, metalBorrowed.physical_width); - Assert.Equal(33u, metalBorrowed.physical_height); - - var vulkanOwned = RenderStructs.ToNative( - new VulkanOwnedTextureDescriptor - { - Extent = new RenderTargetExtent(256, 128, 1), - Context = new VulkanContextDescriptor - { - Device = NativePointer.FromBorrowedAddress(30), - GetInstanceProcAddr = NativePointer.FromBorrowedAddress(31), - GetDeviceProcAddr = NativePointer.FromBorrowedAddress(32), - }, - } - ); - Assert.Equal(256u, vulkanOwned.extent.width); - Assert.Equal(30, (nint)vulkanOwned.context.device); - Assert.Equal(31, (nint)vulkanOwned.context.get_instance_proc_addr); - Assert.Equal(32, (nint)vulkanOwned.context.get_device_proc_addr); - - var vulkanBorrowed = RenderStructs.ToNative( - new VulkanBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(256, 128, 1), - PhysicalWidth = 65, - PhysicalHeight = 33, - Image = NativePointer.FromBorrowedAddress(40), - ImageView = NativePointer.FromBorrowedAddress(45), - Format = 50, - InitialLayout = 55, - FinalLayout = 60, - } - ); - Assert.Equal(40, (nint)vulkanBorrowed.image); - Assert.Equal(45, (nint)vulkanBorrowed.image_view); - Assert.Equal(50u, vulkanBorrowed.format); - Assert.Equal(55u, vulkanBorrowed.initial_layout); - Assert.Equal(60u, vulkanBorrowed.final_layout); - - var openglOwned = RenderStructs.ToNative( - new OpenGLOwnedTextureDescriptor - { - Extent = new RenderTargetExtent(512, 256, 1), - Context = new EglContextDescriptor - { - Display = NativePointer.FromBorrowedAddress(60), - Config = NativePointer.FromBorrowedAddress(61), - ShareContext = NativePointer.FromBorrowedAddress(62), - GetProcAddress = NativePointer.FromBorrowedAddress(63), - }, - } - ); - Assert.Equal(512u, openglOwned.extent.width); - Assert.Equal( - mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_EGL, - openglOwned.context.platform - ); - Assert.Equal(60, (nint)openglOwned.context.data.egl.display); - Assert.Equal(61, (nint)openglOwned.context.data.egl.config); - Assert.Equal(62, (nint)openglOwned.context.data.egl.share_context); - Assert.Equal(63, (nint)openglOwned.context.data.egl.get_proc_address); - Assert.Equal( - mln_opengl_client_api.MLN_OPENGL_CLIENT_API_UNSPECIFIED, - openglOwned.context.data.egl.client_api - ); - - var openglBorrowed = RenderStructs.ToNative( - new OpenGLBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(512, 256, 1), - PhysicalWidth = 65, - PhysicalHeight = 33, - Context = new WglContextDescriptor - { - DeviceContext = NativePointer.FromBorrowedAddress(70), - ShareContext = NativePointer.FromBorrowedAddress(71), - }, - Texture = 72, - Target = 0x0de1, - } - ); - Assert.Equal(72u, openglBorrowed.texture); - Assert.Equal(0x0de1u, openglBorrowed.target); - Assert.Equal(70, (nint)openglBorrowed.context.data.wgl.device_context); - } - - [BindingSpecTest("BND-060")] - [Fact] - public void RenderDescriptorsInitializeSizeAndPreserveCallerExtent() - { - var metal = RenderStructs.ToNative( - new MetalSurfaceDescriptor - { - Extent = new RenderTargetExtent(256, 256, 1.0), - Layer = NativePointer.FromBorrowedAddress(1), - } - ); - Assert.Equal((uint)sizeof(mln_render_target_extent), metal.extent.size); - Assert.Equal(256u, metal.extent.width); - Assert.Equal(256u, metal.extent.height); - Assert.Equal(1, metal.extent.scale_factor); - - var vulkanBorrowed = RenderStructs.ToNative( - new VulkanBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(256, 256, 1.0), - PhysicalWidth = 65, - PhysicalHeight = 33, - Image = NativePointer.FromBorrowedAddress(2), - ImageView = NativePointer.FromBorrowedAddress(3), - } - ); - Assert.Equal((uint)sizeof(mln_render_target_extent), vulkanBorrowed.extent.size); - Assert.Equal(256u, vulkanBorrowed.extent.width); - Assert.Equal(256u, vulkanBorrowed.extent.height); - Assert.Equal(1, vulkanBorrowed.extent.scale_factor); - Assert.Equal(5u, vulkanBorrowed.final_layout); - - var openglOwned = RenderStructs.ToNative( - new OpenGLOwnedTextureDescriptor { Extent = new RenderTargetExtent(256, 256, 1.0) } - ); - var openglOwnedDefault = NativeMethods.mln_opengl_owned_texture_descriptor_default(); - Assert.Equal((uint)sizeof(mln_opengl_owned_texture_descriptor), openglOwned.size); - Assert.Equal(openglOwnedDefault.context.platform, openglOwned.context.platform); - } - - [BindingSpecTest("BND-166", "BND-167")] - [Fact] - public void TextureImageInfoCopiesNativeFields() - { - var info = RenderStructs.FromNative( - new mln_texture_image_info - { - width = 1, - height = 2, - stride = 4, - byte_length = 8, - } - ); - - Assert.Equal(new TextureImageInfo(1, 2, 4, 8), info); + Assert.Equal(5u, (uint)RenderResult.DeadlineMissed); + Assert.Equal(2u, (uint)RenderDriverKind.CallerGraphicsThread); + Assert.Equal(5u, (uint)RenderSessionState.TargetLost); + Assert.Equal(1u << 2, (uint)RenderSessionCapabilities.ConsumerSync); + Assert.Equal(1u, (uint)WebGLContextKind.TransferredCanvas); + Assert.Equal(-9, (int)global::Maplibre.NativeFfi.Error.MaplibreStatus.NotReady); } - [BindingSpecTest("BND-023")] [Fact] - public void NativeBufferRejectsUseAfterDispose() + public void AttachOptionsUseNativeDefaultsAndLeaveSourcesInherited() { - using var buffer = new NativeBuffer(4); - Assert.Equal(4, buffer.Span.Length); - - buffer.Dispose(); - - Assert.Throws(() => + var options = new RenderSessionAttachOptions { - var span = buffer.Span; - _ = span.Length; - }); - } - - [BindingSpecTest("BND-168", "BND-173")] - [Fact] - public void TextureFramePropertiesRejectUseAfterScopeClose() - { - var metalScope = new FrameScope(nameof(MetalOwnedTextureFrame)); - var metal = new MetalOwnedTextureFrame( - metalScope, - 1, - 2, - 3, - 4, - 5, - NativePointer.FromBorrowedAddress(6), - NativePointer.FromBorrowedAddress(7), - 8 - ); - Assert.Equal(6, metal.Texture.Address); - metalScope.Dispose(); - Assert.Throws(() => metal.Texture); - - var vulkanScope = new FrameScope(nameof(VulkanOwnedTextureFrame)); - var vulkan = new VulkanOwnedTextureFrame( - vulkanScope, - 1, - 2, - 3, - 4, - 5, - NativePointer.FromBorrowedAddress(6), - NativePointer.FromBorrowedAddress(7), - NativePointer.FromBorrowedAddress(8), - 9, - 10 - ); - Assert.Equal(7, vulkan.ImageView.Address); - vulkanScope.Dispose(); - Assert.Throws(() => vulkan.ImageView); - - var openglScope = new FrameScope(nameof(OpenGLOwnedTextureFrame)); - var opengl = new OpenGLOwnedTextureFrame( - openglScope, - 1, - 2, - 3, - 4, - 5, - 6, - 0x0de1, - 0x8058, - 0x1908, - 0x1401 - ); - Assert.Equal(6u, opengl.Texture); - openglScope.Dispose(); - Assert.Throws(() => opengl.Texture); - } - - [BindingSpecTest("BND-169")] - [Fact] - public void FailedTextureFrameReleaseLeavesFrameLiveForRetry() - { - var pointer = (mln_metal_owned_texture_frame*) - NativeMemory.AllocZeroed((nuint)sizeof(mln_metal_owned_texture_frame)); - pointer->size = (uint)sizeof(mln_metal_owned_texture_frame); - var session = RenderSessionHandle.CreateForTest(SyntheticHandles.RenderSession(1234)); - var scope = new FrameScope(nameof(MetalOwnedTextureFrameHandle)); - var releaseCalls = 0; - var state = new TextureFrameState( - session, - pointer, - scope, - (_, _) => - releaseCalls++ == 0 - ? mln_status.MLN_STATUS_INVALID_STATE - : mln_status.MLN_STATUS_OK, - nameof(MetalOwnedTextureFrameHandle) - ); - - try + Driver = RenderDriverKind.CallerGraphicsThread, + RequestedTextureRingDepth = 3, + }; + var native = new mln_render_session_attach_options { - var error = Assert.Throws(state.Close); - - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.False(state.IsClosed); - scope.EnsureActive(); - Assert.Equal(1, releaseCalls); - - state.Close(); + size = (uint)sizeof(mln_render_session_attach_options), + driver = (uint)options.Driver, + requested_texture_ring_depth = options.RequestedTextureRingDepth, + }; - Assert.True(state.IsClosed); - Assert.Throws(scope.EnsureActive); - Assert.Equal(2, releaseCalls); - session.Close(); - } - finally - { - if (!state.IsClosed) - { - scope.Dispose(); - NativeMemory.Free(pointer); - } - } + Assert.Equal((uint)RenderDriverKind.CallerGraphicsThread, native.driver); + Assert.Equal(3u, native.requested_texture_ring_depth); } - [BindingSpecTest("BND-170")] [Fact] - public void SessionOperationsForbiddenDuringActiveTextureFrameFailBeforeNativeCall() + public void WebGLDescriptorsRepresentExistingAndTransferredContexts() { - var reports = new List(); - using var capture = NativeLeakReporter.CaptureForTest(reports.Add); - var resizeCalls = 0; - var renderUpdateCalls = 0; - var textureReadCalls = 0; - var destroyCalls = 0; - var metalAcquireCalls = 0; - var vulkanAcquireCalls = 0; - var openGLAcquireCalls = 0; - using var sessionMethods = RenderSessionHandle.UseSessionMethodsForTest( - (_, _, _, _) => - { - resizeCalls++; - return mln_status.MLN_STATUS_OK; - }, - (_, _, _) => - { - renderUpdateCalls++; - return mln_status.MLN_STATUS_OK; - }, - (_, _, _, _) => + var existing = RenderStructs.ToNative( + new WebGLContextDescriptor { - textureReadCalls++; - return mln_status.MLN_STATUS_OK; - }, - _ => - { - destroyCalls++; - return mln_status.MLN_STATUS_OK; + Kind = WebGLContextKind.Existing, + Context = 27, + Ownership = OpenGLContextOwnership.Shared, } ); - using var metalMethods = RenderSessionHandle.UseMetalFrameMethodsForTest( - (_, _) => - { - metalAcquireCalls++; - return mln_status.MLN_STATUS_OK; - }, - (_, _) => mln_status.MLN_STATUS_OK, - (_, _) => throw new InvalidOperationException("unexpected frame read") - ); - using var textureFrameMethods = RenderSessionHandle.UseTextureFrameAcquireMethodsForTest( - (_, _) => + var transferred = RenderStructs.ToNative( + new WebGLContextDescriptor { - vulkanAcquireCalls++; - return mln_status.MLN_STATUS_OK; - }, - (_, _) => - { - openGLAcquireCalls++; - return mln_status.MLN_STATUS_OK; + Kind = WebGLContextKind.TransferredCanvas, + CanvasSelector = "#map", + Ownership = OpenGLContextOwnership.Dedicated, } ); - var session = RenderSessionHandle.CreateForTest(SyntheticHandles.RenderSession(1234)); - var pointer = (mln_metal_owned_texture_frame*) - NativeMemory.AllocZeroed((nuint)sizeof(mln_metal_owned_texture_frame)); - pointer->size = (uint)sizeof(mln_metal_owned_texture_frame); - var scope = new FrameScope(nameof(MetalOwnedTextureFrameHandle)); - var state = new TextureFrameState( - session, - pointer, - scope, - (_, _) => mln_status.MLN_STATUS_OK, - nameof(MetalOwnedTextureFrameHandle) - ); - - try - { - AssertActiveFrameError(() => session.Resize(64, 64, 1)); - var extent = new RenderTargetExtent(64, 64, 1); - var handle = NativePointer.FromBorrowedAddress(1); - AssertActiveFrameError(() => - session.SetMetalSurfaceTarget( - new MetalSurfaceDescriptor { Extent = extent, Layer = handle } - ) - ); - AssertActiveFrameError(() => - session.SetVulkanSurfaceTarget( - new VulkanSurfaceDescriptor { Extent = extent, Surface = handle } - ) - ); - AssertActiveFrameError(() => - session.SetOpenGLSurfaceTarget( - new OpenGLSurfaceDescriptor { Extent = extent, Surface = handle } - ) - ); - AssertActiveFrameError(() => - session.SetMetalBorrowedTextureTarget( - new MetalBorrowedTextureDescriptor - { - Extent = extent, - PhysicalWidth = 64, - PhysicalHeight = 64, - Texture = handle, - } - ) - ); - AssertActiveFrameError(() => - session.SetVulkanBorrowedTextureTarget( - new VulkanBorrowedTextureDescriptor - { - Extent = extent, - PhysicalWidth = 64, - PhysicalHeight = 64, - Image = handle, - ImageView = handle, - } - ) - ); - AssertActiveFrameError(() => - session.SetOpenGLBorrowedTextureTarget( - new OpenGLBorrowedTextureDescriptor - { - Extent = extent, - PhysicalWidth = 64, - PhysicalHeight = 64, - Texture = 1, - Target = 0x0de1, - } - ) - ); - AssertActiveFrameError(() => _ = session.RenderUpdate()); - AssertActiveFrameError(session.Detach); - AssertActiveFrameError(() => _ = session.TextureImageInfo()); - using var buffer = new NativeBuffer(4); - AssertActiveFrameError(() => session.ReadPremultipliedRgba8(buffer)); - AssertActiveFrameError(() => _ = session.AcquireMetalOwnedTextureFrame()); - AssertActiveFrameError(() => _ = session.AcquireVulkanOwnedTextureFrame()); - AssertActiveFrameError(() => _ = session.AcquireOpenGLOwnedTextureFrame()); - AssertActiveFrameError(session.Close); - session.Dispose(); - Assert.Equal(0, resizeCalls); - Assert.Equal(0, renderUpdateCalls); - Assert.Equal(0, textureReadCalls); - Assert.Equal(0, destroyCalls); - Assert.Equal(0, metalAcquireCalls); - Assert.Equal(0, vulkanAcquireCalls); - Assert.Equal(0, openGLAcquireCalls); - var report = Assert.Single(reports); - Assert.Equal(NativeLeakReportKind.DisposeFailed, report.Kind); - Assert.Equal(nameof(RenderSessionHandle), report.TypeName); - Assert.Equal(SyntheticHandles.RenderSession(1234).Value, report.Handle); - Assert.Null(report.Status); - Assert.Contains("texture frame is active", report.Message, StringComparison.Ordinal); - } - finally - { - if (!state.IsClosed) - { - state.Close(); - } - session.Close(); - } - - static void AssertActiveFrameError(Action action) - { - var error = Assert.Throws(action); - - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.Null(error.RawStatus); - Assert.Contains("texture frame is active", error.Diagnostic, StringComparison.Ordinal); - } + Assert.Equal( + mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WEBGL, + existing.platform + ); + Assert.Equal(27, existing.data.webgl.context); + Assert.Equal((uint)WebGLContextKind.TransferredCanvas, transferred.data.webgl.kind); } - [BindingSpecTest("BND-162", "BND-171")] [Fact] - public void OpenGLAttachFamiliesReturnPublicSessionHandlesAndKeepBorrowedHandlesOwnedByCaller() + public void RawSurfaceAttachReturnsSessionAndAcceptsCompletion() { - // Deterministic attach hooks record which native family each public path reaches. - var attachCalls = new List(); - var destroyed = new List(); - using var methods = RenderSessionHandle.UseOpenGLAttachMethodsForTest( - (_, _, outSession) => - { - attachCalls.Add("surface"); - *outSession = SyntheticHandles.RenderSession(101); - return mln_status.MLN_STATUS_OK; - }, - (_, _, outSession) => - { - attachCalls.Add("owned"); - *outSession = SyntheticHandles.RenderSession(102); - return mln_status.MLN_STATUS_OK; - }, - (_, _, outSession) => - { - attachCalls.Add("borrowed"); - *outSession = SyntheticHandles.RenderSession(103); - return mln_status.MLN_STATUS_OK; - } - ); - using var sessionMethods = RenderSessionHandle.UseSessionMethodsForTest( - (_, _, _, _) => mln_status.MLN_STATUS_OK, - (_, _, _) => mln_status.MLN_STATUS_OK, - (_, _, _, _) => mln_status.MLN_STATUS_OK, - session => - { - destroyed.Add(session.Value); - return mln_status.MLN_STATUS_OK; - } + var method = typeof(NativeMethods).GetMethod( + "mln_opengl_surface_attach", + BindingFlags.Public | BindingFlags.Static ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); - var context = new WglContextDescriptor - { - DeviceContext = NativePointer.FromBorrowedAddress(1), - ShareContext = NativePointer.FromBorrowedAddress(2), - }; - var borrowed = new OpenGLBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - PhysicalWidth = 65, - PhysicalHeight = 33, - Context = context, - Texture = 77, - Target = 0x0de1, - }; - using var surface = RenderSessionHandle.AttachOpenGLSurface( - map, - new OpenGLSurfaceDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - Context = context, - Surface = NativePointer.FromBorrowedAddress(3), - } - ); - using var owned = RenderSessionHandle.AttachOpenGLOwnedTexture( - map, - new OpenGLOwnedTextureDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - Context = context, - } - ); - using var callerOwned = RenderSessionHandle.AttachOpenGLBorrowedTexture(map, borrowed); - - Assert.False(surface.IsClosed); - Assert.False(owned.IsClosed); - Assert.False(callerOwned.IsClosed); - Assert.Equal(["surface", "owned", "borrowed"], attachCalls); - - surface.Close(); - owned.Close(); - callerOwned.Close(); - - Assert.Equal( - [ - SyntheticHandles.RenderSession(101).Value, - SyntheticHandles.RenderSession(102).Value, - SyntheticHandles.RenderSession(103).Value, - ], - destroyed - ); - Assert.Equal(77u, borrowed.Texture); - Assert.Equal(0x0de1u, borrowed.Target); - var borrowedContext = Assert.IsType(borrowed.Context); - Assert.Equal(1, borrowedContext.DeviceContext.Address); - Assert.Equal(2, borrowedContext.ShareContext.Address); + Assert.NotNull(method); + var parameters = method!.GetParameters(); + Assert.Equal(5, parameters.Length); + Assert.Equal(typeof(MlnRenderSession*), parameters[3].ParameterType); + Assert.Equal(typeof(mln_completion*), parameters[4].ParameterType); } - [BindingSpecTest("BND-163")] [Fact] - public void SecondRenderSessionAttachInvalidStateLeavesFirstSessionOpen() + public void PublicSessionSurfaceUsesTasksAndExplicitDriverService() { - // The invalid-state branch depends on native state that is not deterministic here. - var attachCalls = 0; - using var methods = RenderSessionHandle.UseOpenGLAttachMethodsForTest( - (_, _, outSession) => - { - attachCalls++; - if (attachCalls == 1) - { - *outSession = SyntheticHandles.RenderSession(101); - return mln_status.MLN_STATUS_OK; - } + var type = typeof(RenderSessionHandle); - *outSession = default; - return mln_status.MLN_STATUS_INVALID_STATE; - }, - (_, _, _) => mln_status.MLN_STATUS_UNSUPPORTED, - (_, _, _) => mln_status.MLN_STATUS_UNSUPPORTED - ); - using var sessionMethods = RenderSessionHandle.UseSessionMethodsForTest( - (_, _, _, _) => mln_status.MLN_STATUS_OK, - (_, _, _) => mln_status.MLN_STATUS_OK, - (_, _, _, _) => mln_status.MLN_STATUS_OK, - _ => mln_status.MLN_STATUS_OK + Assert.Equal( + typeof(Task), + type.GetProperty(nameof(RenderSessionHandle.Attachment))!.PropertyType ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); - var descriptor = new OpenGLSurfaceDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - Context = new WglContextDescriptor - { - DeviceContext = NativePointer.FromBorrowedAddress(1), - ShareContext = NativePointer.FromBorrowedAddress(2), - }, - Surface = NativePointer.FromBorrowedAddress(3), - }; - - using var first = RenderSessionHandle.AttachOpenGLSurface(map, descriptor); - var error = Assert.Throws(() => - RenderSessionHandle.AttachOpenGLSurface(map, descriptor) + Assert.Equal( + typeof(Task), + type.GetMethod(nameof(RenderSessionHandle.DetachAsync))!.ReturnType ); - - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.False(first.IsClosed); - Assert.Equal(2, attachCalls); - } - - [BindingSpecTest("BND-164", "BND-165")] - [Fact] - public void RenderUpdateAfterResizeReportsSizePendingAndResizePassesExtent() - { - uint resizedWidth = 0; - uint resizedHeight = 0; - double resizedScale = 0; - var renderUpdateCalls = 0; - using var methods = RenderSessionHandle.UseSessionMethodsForTest( - (_, width, height, scale) => - { - resizedWidth = width; - resizedHeight = height; - resizedScale = scale; - return mln_status.MLN_STATUS_OK; - }, - (_, outResult, outNeedsRepaint) => - { - renderUpdateCalls++; - if (renderUpdateCalls == 1) - { - *outResult = mln_render_result.MLN_RENDER_RESULT_SIZE_PENDING; - *outNeedsRepaint = false; - } - else - { - *outResult = mln_render_result.MLN_RENDER_RESULT_RENDERED; - *outNeedsRepaint = true; - } - return mln_status.MLN_STATUS_OK; - }, - (_, _, _, _) => mln_status.MLN_STATUS_OK, - _ => mln_status.MLN_STATUS_OK + Assert.Equal( + typeof(Task), + type.GetMethod(nameof(RenderSessionHandle.BarrierAsync))!.ReturnType ); - var session = RenderSessionHandle.CreateForTest(SyntheticHandles.RenderSession(1234)); - - session.Resize(320, 240, 2); - var sizePending = session.RenderUpdate(); - var rendered = session.RenderUpdate(); - - Assert.Equal(RenderResult.SizePending, sizePending.Result); - Assert.False(sizePending.NeedsRepaint); - Assert.Equal(RenderResult.Rendered, rendered.Result); - Assert.True(rendered.NeedsRepaint); - Assert.Equal(320u, resizedWidth); - Assert.Equal(240u, resizedHeight); - Assert.Equal(2, resizedScale); - Assert.False(session.IsClosed); - session.Close(); - } - - [BindingSpecTest("BND-172")] - [Fact] - public void MetalFrameConstructionFailureAfterNativeAcquireReleasesFrame() - { - var acquireCalls = 0; - var releaseCalls = 0; - using var methods = RenderSessionHandle.UseMetalFrameMethodsForTest( - (_, frame) => - { - acquireCalls++; - frame->generation = 1; - return mln_status.MLN_STATUS_OK; - }, - (_, _) => - { - releaseCalls++; - return mln_status.MLN_STATUS_OK; - }, - (_, _) => throw new InvalidOperationException("copy failed") + Assert.Equal( + typeof(int), + type.GetMethod(nameof(RenderSessionHandle.ServiceDriverWork))!.ReturnType ); - var session = RenderSessionHandle.CreateForTest(SyntheticHandles.RenderSession(1234)); - - var error = Assert.Throws(() => - session.AcquireMetalOwnedTextureFrame() + Assert.Equal( + typeof(RenderFrameBatch), + type.GetMethod(nameof(RenderSessionHandle.DrainFrameResults))!.ReturnType ); - - Assert.Equal("copy failed", error.Message); - Assert.Equal(1, acquireCalls); - Assert.Equal(1, releaseCalls); - Assert.False(session.IsClosed); - session.Close(); + Assert.Null(type.GetMethod("RenderUpdate")); + Assert.Null(type.GetMethod("Resize")); + Assert.Null(type.GetMethod("ReadPremultipliedRgba8")); } - [BindingSpecTest("BND-160")] [Fact] - public void OpenGLAttachMethodsReportUnsupportedWhenBackendUnavailable() + public void AcquiredFrameAccessIsScopedToItsLease() { - if ((Maplibre.SupportedRenderBackends() & RenderBackend.OpenGL) != 0) - { - Assert.Skip("OpenGL native build exercises positive attach paths"); - } - - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); - var context = new WglContextDescriptor - { - DeviceContext = NativePointer.FromBorrowedAddress(1), - ShareContext = NativePointer.FromBorrowedAddress(1), - }; - - Assert.Throws(() => - RenderSessionHandle.AttachOpenGLOwnedTexture( - map, - new OpenGLOwnedTextureDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - Context = context, - } - ) - ); - Assert.Throws(() => - RenderSessionHandle.AttachOpenGLBorrowedTexture( - map, - new OpenGLBorrowedTextureDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - PhysicalWidth = 65, - PhysicalHeight = 33, - Context = context, - Texture = 1, - Target = 0x0de1, - } - ) - ); - Assert.Throws(() => - RenderSessionHandle.AttachOpenGLSurface( - map, - new OpenGLSurfaceDescriptor - { - Extent = new RenderTargetExtent(32, 16, 1), - Context = context, - Surface = NativePointer.FromBorrowedAddress(1), - } - ) - ); + var scope = new FrameScope(); + scope.EnsureActive(); + scope.Dispose(); + Assert.Throws(scope.EnsureActive); } - [BindingSpecTest("BND-061")] [Fact] - public void FeatureStateSelectorMaterializesOptionalFields() + public void PhaseThreeNativeStructsUseExpectedHandleSizedLayouts() { - using var selector = NativeFeatureStateSelector.From( - new FeatureStateSelector - { - SourceId = "source", - SourceLayerId = "layer", - FeatureId = "feature", - StateKey = "hover", - } - ); - - var value = selector.Value; - Assert.Equal( - (uint)( - mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID - | mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_FEATURE_ID - | mln_feature_state_selector_field.MLN_FEATURE_STATE_SELECTOR_STATE_KEY - ), - value.fields - ); - Assert.Equal("source", RuntimeStructs.CopyUtf8(value.source_id.data, value.source_id.size)); - Assert.Equal( - "layer", - RuntimeStructs.CopyUtf8(value.source_layer_id.data, value.source_layer_id.size) - ); - Assert.Equal( - "feature", - RuntimeStructs.CopyUtf8(value.feature_id.data, value.feature_id.size) - ); - Assert.Equal("hover", RuntimeStructs.CopyUtf8(value.state_key.data, value.state_key.size)); + Assert.Equal(32, sizeof(mln_frame_demand)); + Assert.Equal(48, sizeof(mln_render_frame_result)); + Assert.Equal(24, sizeof(mln_gpu_sync)); + Assert.Equal(8, Marshal.SizeOf()); + Assert.Equal(8, Marshal.SizeOf()); } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceProviderTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceProviderTests.cs index ffca48084..1ce4a214e 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceProviderTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceProviderTests.cs @@ -303,12 +303,12 @@ public void ResourceProviderStateDisposeIsIdempotent() [BindingSpecTest("BND-122")] [Fact] - public void ResourceProviderInstallFailurePreservesPreviousCallbackAndReleasesReplacement() + public void ResourceProviderInstallFailureReleasesReplacement() { var failInstall = false; ResourceProviderState? failedReplacement = null; using var install = RuntimeHandle.UseResourceCallbackInstallMethodsForTest( - (_, provider) => + (_, provider, _) => { if (!failInstall) { @@ -319,64 +319,60 @@ public void ResourceProviderInstallFailurePreservesPreviousCallbackAndReleasesRe GCHandle.FromIntPtr((nint)provider->user_data).Target; return mln_status.MLN_STATUS_INVALID_STATE; }, - (_, _) => mln_status.MLN_STATUS_OK + (_, _, _) => mln_status.MLN_STATUS_OK ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider((_, _) => ResourceProviderDecision.PassThrough); - var previous = Assert.IsType(runtime.ResourceProviderStateForTest); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync((_, _) => ResourceProviderDecision.PassThrough); failInstall = true; Assert.Throws(() => - runtime.SetResourceProvider((_, _) => ResourceProviderDecision.Handle) + runtime + .SetResourceProviderAsync((_, _) => ResourceProviderDecision.Handle) + .GetAwaiter() + .GetResult() ); - Assert.Same(previous, runtime.ResourceProviderStateForTest); - Assert.True(previous.IsHandleAllocatedForTest); Assert.NotNull(failedReplacement); Assert.False(failedReplacement.IsHandleAllocatedForTest); } - // Each transition also releases the rooted state of the provider it retires, - // which the C call guarantees is unreachable once it returns. + // Native callback ownership keeps each registration alive through its final invocation. [BindingSpecTest("BND-142")] [Fact] public void ResourceProviderIsConsultedUntilClearedWhileMapIsLive() { var firstCalls = 0; var secondCalls = 0; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - runtime.SetResourceProvider( + runtime.SetResourceProviderAsync( (_, _) => { Interlocked.Increment(ref firstCalls); return ResourceProviderDecision.PassThrough; } ); - var first = Assert.IsType(runtime.ResourceProviderStateForTest); LoadProbeStyle(runtime, map, "jar:file:/packaged/first.json"); Assert.True(Volatile.Read(ref firstCalls) > 0); - runtime.SetResourceProvider( + runtime.SetResourceProviderAsync( (_, _) => { Interlocked.Increment(ref secondCalls); return ResourceProviderDecision.PassThrough; } ); - var second = Assert.IsType(runtime.ResourceProviderStateForTest); - Assert.NotSame(first, second); - Assert.False(first.IsHandleAllocatedForTest); var firstCallsAfterReplace = Volatile.Read(ref firstCalls); LoadProbeStyle(runtime, map, "jar:file:/packaged/second.json"); Assert.True(Volatile.Read(ref secondCalls) > 0); Assert.Equal(firstCallsAfterReplace, Volatile.Read(ref firstCalls)); - runtime.ClearResourceProvider(); + runtime.ClearResourceProviderAsync(); - Assert.Null(runtime.ResourceProviderStateForTest); - Assert.False(second.IsHandleAllocatedForTest); var secondCallsAfterClear = Volatile.Read(ref secondCalls); LoadProbeStyle(runtime, map, "jar:file:/packaged/third.json"); Assert.Equal(firstCallsAfterReplace, Volatile.Read(ref firstCalls)); @@ -389,8 +385,8 @@ public void ResourceProviderSeesSchemeAliasAndItsResolvedUrl() { const string AliasUrl = "maplibre://maps/style"; string? resolvedUrl = null; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync( (request, handle) => { if (request.RequestedUrl != AliasUrl) @@ -403,9 +399,12 @@ public void ResourceProviderSeesSchemeAliasAndItsResolvedUrl() return ResourceProviderDecision.Handle; } ); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl(AliasUrl); + map.SetStyleUrlAsync(AliasUrl); RuntimeEventTestHelpers.WaitForMapEvent(runtime, map, RuntimeEventType.MapStyleLoaded); Assert.Equal("https://demotiles.maplibre.org/style.json", resolvedUrl); @@ -416,8 +415,8 @@ public void ResourceProviderSeesSchemeAliasAndItsResolvedUrl() public void InlineHandledResourceProviderCompletionLoadsStyleAndClosesRequest() { ResourceRequestHandle? handled = null; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync( (request, handle) => { Assert.Equal(StyleUrl, request.RequestedUrl); @@ -426,9 +425,12 @@ public void InlineHandledResourceProviderCompletionLoadsStyleAndClosesRequest() return ResourceProviderDecision.Handle; } ); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl(StyleUrl); + map.SetStyleUrlAsync(StyleUrl); var runtimeEvent = RuntimeEventTestHelpers.WaitForMapEvent( runtime, map, @@ -446,8 +448,8 @@ public void LaterHandledResourceProviderCompletionLoadsStyle() { using var providerCalled = new ManualResetEventSlim(false); ResourceRequestHandle? handled = null; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync( (request, handle) => { Assert.Equal(StyleUrl, request.RequestedUrl); @@ -456,9 +458,12 @@ public void LaterHandledResourceProviderCompletionLoadsStyle() return ResourceProviderDecision.Handle; } ); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl(StyleUrl); + map.SetStyleUrlAsync(StyleUrl); DriveRuntimeUntil(runtime, providerCalled); Assert.NotNull(handled); handled.Complete(StyleResponse()); @@ -479,8 +484,8 @@ public void CancelledResourceRequestReportsCancellationBeforeLateCompletionStatu { using var providerCalled = new ManualResetEventSlim(false); ResourceRequestHandle? handled = null; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync( (_, handle) => { handled = handle; @@ -488,13 +493,16 @@ public void CancelledResourceRequestReportsCancellationBeforeLateCompletionStatu return ResourceProviderDecision.Handle; } ); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl(StyleUrl); + map.SetStyleUrlAsync(StyleUrl); DriveRuntimeUntil(runtime, providerCalled); Assert.NotNull(handled); - map.SetStyleJson(System.Text.Encoding.UTF8.GetBytes(StyleJson)); + map.SetStyleJsonAsync(System.Text.Encoding.UTF8.GetBytes(StyleJson)); DriveRuntimeUntilCancelled(runtime, handled); var error = Assert.Throws(() => handled.Complete(StyleResponse())); @@ -508,8 +516,8 @@ public void CancelledResourceRequestReportsCancellationBeforeLateCompletionStatu [Fact] public void ResourceProviderErrorResponseProducesCopiedLoadingFailureEvent() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceProvider( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceProviderAsync( (_, handle) => { handle.Complete( @@ -522,9 +530,12 @@ public void ResourceProviderErrorResponseProducesCopiedLoadingFailureEvent() return ResourceProviderDecision.Handle; } ); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl(StyleUrl); + map.SetStyleUrlAsync(StyleUrl); var runtimeEvent = RuntimeEventTestHelpers.WaitForMapEvent( runtime, map, @@ -542,7 +553,7 @@ private static void LoadProbeStyle(RuntimeHandle runtime, MapHandle map, string // Drain earlier events so this probe observes its own failure. RuntimeEventTestHelpers.DrainUntilIdle(runtime); - map.SetStyleUrl(styleUrl); + map.SetStyleUrlAsync(styleUrl); var runtimeEvent = RuntimeEventTestHelpers.WaitForMapEvent( runtime, map, @@ -559,7 +570,7 @@ private static void DriveRuntimeUntil(RuntimeHandle runtime, ManualResetEventSli { for (var attempt = 0; attempt < 1000; attempt++) { - runtime.Pump(TimeSpan.Zero); + Thread.Sleep(1); if (signal.IsSet) { return; @@ -578,7 +589,7 @@ ResourceRequestHandle handle { for (var attempt = 0; attempt < 1000; attempt++) { - runtime.Pump(TimeSpan.Zero); + Thread.Sleep(1); if (handle.IsCancelled()) { return; diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceTransformTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceTransformTests.cs index b296ff6d0..2c27a0cc6 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceTransformTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/ResourceTransformTests.cs @@ -82,13 +82,13 @@ public void ResourceTransformStateDisposeIsIdempotent() [BindingSpecTest("BND-122")] [Fact] - public unsafe void ResourceTransformInstallFailurePreservesPreviousCallbackAndReleasesReplacement() + public unsafe void ResourceTransformInstallFailureReleasesReplacement() { var failInstall = false; ResourceTransformState? failedReplacement = null; using var install = RuntimeHandle.UseResourceCallbackInstallMethodsForTest( - (_, _) => mln_status.MLN_STATUS_OK, - (_, transform) => + (_, _, _) => mln_status.MLN_STATUS_OK, + (_, transform, _) => { if (!failInstall) { @@ -100,17 +100,17 @@ public unsafe void ResourceTransformInstallFailurePreservesPreviousCallbackAndRe return mln_status.MLN_STATUS_INVALID_STATE; } ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - runtime.SetResourceTransform(request => request.Url + "?first"); - var previous = Assert.IsType(runtime.ResourceTransformStateForTest); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + runtime.SetResourceTransformAsync(request => request.Url + "?first"); failInstall = true; Assert.Throws(() => - runtime.SetResourceTransform(request => request.Url + "?second") + runtime + .SetResourceTransformAsync(request => request.Url + "?second") + .GetAwaiter() + .GetResult() ); - Assert.Same(previous, runtime.ResourceTransformStateForTest); - Assert.True(previous.IsHandleAllocatedForTest); Assert.NotNull(failedReplacement); Assert.False(failedReplacement.IsHandleAllocatedForTest); } @@ -119,10 +119,10 @@ public unsafe void ResourceTransformInstallFailurePreservesPreviousCallbackAndRe [Fact] public void CanInstallReplaceAndClearResourceTransform() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); - runtime.SetResourceTransform(request => request.Url + "?first"); - runtime.SetResourceTransform(request => request.Url + "?second"); - runtime.ClearResourceTransform(); + runtime.SetResourceTransformAsync(request => request.Url + "?first"); + runtime.SetResourceTransformAsync(request => request.Url + "?second"); + runtime.ClearResourceTransformAsync(); } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventDrainTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventDrainTests.cs index c0e48d68b..683186e06 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventDrainTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventDrainTests.cs @@ -6,7 +6,7 @@ namespace Maplibre.NativeFfi.Tests; -public sealed unsafe class RuntimeEventDrainTests +public sealed class RuntimeEventDrainTests { private static readonly byte[] StyleJson = """{"version":8,"sources":{},"layers":[]}"""u8.ToArray(); @@ -15,10 +15,13 @@ public sealed unsafe class RuntimeEventDrainTests [Fact] public void OneDrainReportsEveryEventAStyleLoadProducedInQueueOrder() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleJson(StyleJson); + map.SetStyleJsonAsync(StyleJson); var batch = DriveUntil(runtime, RuntimeEventType.MapStyleLoaded).Batch; var types = batch.Select(runtimeEvent => runtimeEvent.Type).ToArray(); @@ -32,33 +35,17 @@ public void OneDrainReportsEveryEventAStyleLoadProducedInQueueOrder() Assert.All(batch, runtimeEvent => Assert.Same(map, runtimeEvent.MapSource)); } - [BindingSpecTest("BND-090")] - [Fact] - public void ABoundedDrainReportsRemainingEventsAndASecondDrainReachesZero() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(StyleJson); - - var bounded = DriveUntilABoundedDrainLeavesEvents(runtime); - Assert.Single(bounded.Events); - Assert.True(bounded.RemainingCount > 0, "a bounded drain took the whole queue"); - - // No pump runs between the two drains, so the second one takes exactly what the first - // one left. - var rest = runtime.DrainEvents(); - Assert.Equal((int)bounded.RemainingCount, rest.Events.Count); - Assert.Equal(0ul, rest.RemainingCount); - } - [BindingSpecTest("BND-092")] [Fact] public void ADrainedBatchKeepsItsMessagesAfterTheNextDrainReusesTheArena() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleUrl("first-unsupported-scheme://style.json"); + map.SetStyleUrlAsync("first-unsupported-scheme://style.json"); var first = DriveUntil(runtime, RuntimeEventType.MapLoadingFailed).Batch; var failure = first.Single(runtimeEvent => runtimeEvent.Type == RuntimeEventType.MapLoadingFailed @@ -66,7 +53,7 @@ public void ADrainedBatchKeepsItsMessagesAfterTheNextDrainReusesTheArena() Assert.Contains("first-unsupported-scheme", failure.Message, StringComparison.Ordinal); // The second drain refills the arena the first batch was copied from. - map.SetStyleUrl("second-unsupported-scheme://style.json"); + map.SetStyleUrlAsync("second-unsupported-scheme://style.json"); var second = DriveUntil(runtime, RuntimeEventType.MapLoadingFailed).Batch; Assert.Contains( "second-unsupported-scheme", @@ -83,14 +70,23 @@ public void ADrainedBatchKeepsItsMessagesAfterTheNextDrainReusesTheArena() [Fact] public void TheDefaultMaskReportsAllAndDeliversEveryDrivenType() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); Assert.Equal(RuntimeEventMask.All, runtime.GetEventMask()); Assert.Equal(RuntimeEventMask.All, map.GetEventMask()); - map.SetStyleJson(StyleJson); - map.JumpTo(new CameraOptions { Zoom = 4 }); + map.SetStyleJsonAsync(StyleJson); + map.UpdateCameraAsync( + new CameraUpdate + { + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions { Zoom = 4 }, + } + ); var types = DriveUntil(runtime, RuntimeEventType.MapStyleLoaded) .Everything.Select(runtimeEvent => runtimeEvent.Type) .ToArray(); @@ -104,11 +100,14 @@ public void TheDefaultMaskReportsAllAndDeliversEveryDrivenType() [Fact] public void AClearedEventTypeNeverReachesABatch() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetEventMask(RuntimeEventMask.All & ~RuntimeEventMask.MapLoadingStarted); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetEventMaskAsync(RuntimeEventMask.All & ~RuntimeEventMask.MapLoadingStarted); - map.SetStyleJson(StyleJson); + map.SetStyleJsonAsync(StyleJson); var types = DriveUntil(runtime, RuntimeEventType.MapStyleLoaded) .Everything.Select(runtimeEvent => runtimeEvent.Type) .ToArray(); @@ -119,22 +118,26 @@ public void AClearedEventTypeNeverReachesABatch() [BindingSpecTest("BND-091")] [Fact] - public void AMaskRoundTripsAndAReadModifyWriteKeepsTheOtherBits() + public async Task AMaskRoundTripsAndAReadModifyWriteKeepsTheOtherBits() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); runtime.SetEventMask(RuntimeEventMask.All); - map.SetEventMask(RuntimeEventMask.All); + map.SetEventMaskAsync(RuntimeEventMask.All); Assert.Equal(RuntimeEventMask.All, runtime.GetEventMask()); Assert.Equal(RuntimeEventMask.All, map.GetEventMask()); - map.SetEventMask(map.GetEventMask() & ~RuntimeEventMask.MapIdle); + map.SetEventMaskAsync(map.GetEventMask() & ~RuntimeEventMask.MapIdle); + await runtime.BarrierAsync(); Assert.Equal(RuntimeEventMask.All & ~RuntimeEventMask.MapIdle, map.GetEventMask()); - runtime.SetEventMask(runtime.GetEventMask() & ~RuntimeEventMask.OfflineOperationCompleted); + runtime.SetEventMask(runtime.GetEventMask() & ~RuntimeEventMask.OfflineRegionStatusChanged); Assert.Equal( - RuntimeEventMask.All & ~RuntimeEventMask.OfflineOperationCompleted, + RuntimeEventMask.All & ~RuntimeEventMask.OfflineRegionStatusChanged, runtime.GetEventMask() ); } @@ -143,14 +146,19 @@ public void AMaskRoundTripsAndAReadModifyWriteKeepsTheOtherBits() [Fact] public void AMaskBitOutsideAllIsRejected() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var undeclared = (RuntimeEventMask)(1UL << 63); Assert.Throws(() => runtime.SetEventMask(undeclared)); - Assert.Throws(() => map.SetEventMask(undeclared)); Assert.Throws(() => - MapHandle.Create( + map.SetEventMaskAsync(undeclared).GetAwaiter().GetResult() + ); + Assert.Throws(() => + TestHandles.CreateMap( runtime, new MapOptions { @@ -160,38 +168,39 @@ public void AMaskBitOutsideAllIsRejected() } ) ); - // The options mask is validated before the owner thread's live runtime is, so this - // rejection proves RuntimeOptions.EventMask reaches the native options struct. + // The options mask is validated during autonomous creation, so this rejection proves + // RuntimeOptions.EventMask reaches the native options struct. Assert.Throws(() => - RuntimeHandle.Create(new RuntimeOptions { EventMask = undeclared }) + TestHandles.CreateRuntime(new RuntimeOptions { EventMask = undeclared }) ); } [BindingSpecTest("BND-092")] [Fact] - public void TheDrainAndBothMaskSettersRejectAThreadThatDoesNotOwnTheRuntime() + public void EventOperationsAreAnyThread() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); var thrown = new List(); var thread = new Thread(() => { thrown.Add(Record.Exception(() => runtime.DrainEvents())); thrown.Add(Record.Exception(() => runtime.SetEventMask(RuntimeEventMask.All))); - thrown.Add(Record.Exception(() => map.SetEventMask(RuntimeEventMask.All))); + thrown.Add( + Record.Exception(() => + { + _ = map.SetEventMaskAsync(RuntimeEventMask.All); + }) + ); }); thread.Start(); thread.Join(); - Assert.All( - thrown, - error => - Assert.Equal( - MaplibreStatus.WrongThread, - Assert.IsType(error).Status - ) - ); + Assert.All(thrown, Assert.Null); } /// The one batch that carried the awaited type. @@ -201,31 +210,13 @@ private sealed record DrivenEvents( IReadOnlyList Everything ); - // Pumps until one drain of a single event leaves the queue non-empty. - private static RuntimeEventBatch DriveUntilABoundedDrainLeavesEvents(RuntimeHandle runtime) - { - for (var attempt = 0; attempt < 1000; attempt++) - { - runtime.Pump(TimeSpan.Zero); - var batch = runtime.DrainEvents(1); - if (batch.RemainingCount > 0) - { - return batch; - } - - Thread.Sleep(1); - } - - throw new TimeoutException("The style load never queued more than one event."); - } - - // Pumps and drains until one batch carries the awaited type. + // Drains until one batch carries the awaited type. private static DrivenEvents DriveUntil(RuntimeHandle runtime, RuntimeEventType eventType) { var everything = new List(); for (var attempt = 0; attempt < 1000; attempt++) { - runtime.Pump(TimeSpan.Zero); + Thread.Sleep(1); var batch = runtime.DrainEvents(); everything.AddRange(batch.Events); if (batch.Events.Any(runtimeEvent => runtimeEvent.Type == eventType)) diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventTestHelpers.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventTestHelpers.cs index dec79a318..9850ec5a5 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventTestHelpers.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeEventTestHelpers.cs @@ -1,13 +1,28 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Internal.C; using Maplibre.NativeFfi.Internal.Struct; using Maplibre.NativeFfi.Map; using Maplibre.NativeFfi.Runtime; +using Xunit; namespace Maplibre.NativeFfi.Tests; +internal static class TestHandles +{ + internal static RuntimeHandle CreateRuntime(RuntimeOptions options) => + RuntimeHandle.Create(options); + + internal static MapHandle CreateMap(RuntimeHandle runtime, MapOptions options) => + MapHandle.CreateAsync(runtime, options).GetAwaiter().GetResult(); + + internal static void Close(RuntimeHandle runtime) => runtime.Close(); + + internal static void Close(MapHandle map) => map.Close(); +} + internal static unsafe class RuntimeEventTestHelpers { /// The event stride this binding compiled against. @@ -21,7 +36,6 @@ RuntimeEventType eventType { for (var attempt = 0; attempt < 1000; attempt++) { - runtime.Pump(TimeSpan.Zero); foreach (var runtimeEvent in runtime.DrainEvents().Events) { if ( @@ -40,13 +54,50 @@ RuntimeEventType eventType throw new TimeoutException($"Timed out waiting for map event {eventType}."); } - /// Pumps and drains until one batch reports no events. + internal static CommandCompletion WaitForCommand( + RuntimeHandle runtime, + Task command + ) => command.GetAwaiter().GetResult(); + + /// + /// Waits for the command to finish, asserts its disposition matches the expected status + /// (committed for Ok with a nonzero generation, failed with the status code and a message + /// otherwise), and returns the payload. + /// + internal static CommandCompletion AssertCommandFinishes( + RuntimeHandle runtime, + Task command, + MaplibreStatus expectedStatus + ) + { + if (expectedStatus == MaplibreStatus.Ok) + { + return AssertCommitted(command); + } + + var completion = command.GetAwaiter().GetResult(); + Assert.Equal(CommandDisposition.Failed, completion.Disposition); + Assert.Equal((int)expectedStatus, completion.RawStatus); + Assert.NotEmpty(completion.Diagnostic); + return completion; + } + + internal static CommandCompletion AssertCommitted(Task command) + { + var completion = command.GetAwaiter().GetResult(); + Assert.Equal(CommandDisposition.Committed, completion.Disposition); + Assert.Equal((int)MaplibreStatus.Ok, completion.RawStatus); + Assert.NotEqual(0ul, completion.Generation); + return completion; + } + + /// Drains until one batch reports no events. internal static List DrainUntilIdle(RuntimeHandle runtime) { var events = new List(); for (var attempt = 0; attempt < 100; attempt++) { - runtime.Pump(TimeSpan.Zero); + Thread.Sleep(1); var batch = runtime.DrainEvents(); if (batch.Events.Count == 0) { @@ -88,9 +139,9 @@ uint eventSize fixed (byte* recordBytes = records) fixed (byte* messageBytes = messages) { - var batch = new mln_runtime_event_batch + var batch = new mln_runtime_event_batch_view { - size = (uint)Unsafe.SizeOf(), + size = (uint)Unsafe.SizeOf(), event_size = eventSize, events = (mln_runtime_event*)recordBytes, event_count = eventCount, diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeExecutorTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeExecutorTests.cs new file mode 100644 index 000000000..5cd0b3f0f --- /dev/null +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeExecutorTests.cs @@ -0,0 +1,53 @@ +using Maplibre.NativeFfi.Map; +using Maplibre.NativeFfi.Runtime; +using Xunit; + +namespace Maplibre.NativeFfi.Tests; + +public sealed class RuntimeExecutorTests +{ + [BindingSpecTest("BND-088")] + [Fact] + public async Task NativeWorkProgressesAutonomously() + { + using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + using var map = await MapHandle.CreateAsync( + runtime, + new MapOptions { Width = 512, Height = 512 }, + TestContext.Current.CancellationToken + ); + + map.SetStyleUrlAsync("unsupported://style.json"); + await runtime.BarrierAsync(); + + RuntimeEventTestHelpers.WaitForMapEvent(runtime, map, RuntimeEventType.MapLoadingFailed); + } + + [BindingSpecTest("BND-089")] + [Fact] + public async Task CrossThreadCommandsProgressAutonomously() + { + using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + using var map = await MapHandle.CreateAsync( + runtime, + new MapOptions { Width = 512, Height = 512 }, + TestContext.Current.CancellationToken + ); + + var completion = await Task.Run(map.RequestRepaintAsync); + await runtime.BarrierAsync(); + + Assert.Equal(CommandDisposition.Committed, completion.Disposition); + } + + [Fact] + public async Task RepeatedCompletionWaitsNeedNoEventDrain() + { + using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + + for (var index = 0; index < 256; index++) + { + await runtime.BarrierAsync(); + } + } +} diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeMapLifecycleTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeMapLifecycleTests.cs index 1f7bb4d16..54d365bc7 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeMapLifecycleTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeMapLifecycleTests.cs @@ -11,8 +11,8 @@ public sealed class RuntimeMapLifecycleTests [Fact] public void DefaultMapOptionsPreserveNativeCreationDefaults() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap(runtime, new MapOptions()); Assert.False(map.IsClosed); } @@ -29,8 +29,8 @@ public void MapOptionsMaterializeFastPforDecoding() .fast_pfor_enabled ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { FastPforEnabled = true }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap(runtime, new MapOptions { FastPforEnabled = true }); Assert.False(map.IsClosed); } @@ -39,197 +39,180 @@ public void MapOptionsMaterializeFastPforDecoding() [Fact] public void RuntimeAndMapCloseDeterministically() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); Assert.False(runtime.IsClosed); Assert.False(map.IsClosed); - map.Close(); - runtime.Close(); + TestHandles.Close(map); + TestHandles.Close(runtime); Assert.True(map.IsClosed); Assert.True(runtime.IsClosed); } - [BindingSpecTest("BND-042")] + [BindingSpecTest("BND-040")] [Fact] - public void RuntimeCloseFailsWhileMapIsLiveAndCanRetryAfterMapClose() + public async Task RuntimeCloseAsyncCompletesAfterNativeTeardown() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - var error = Assert.Throws(runtime.Close); - - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.Equal((int)MaplibreStatus.InvalidState, error.RawStatus); - Assert.False(runtime.IsClosed); - Assert.Contains("live maps", error.Diagnostic, StringComparison.OrdinalIgnoreCase); + var runtime = RuntimeHandle.Create(new RuntimeOptions()); + var map = await MapHandle.CreateAsync( + runtime, + new MapOptions { Width = 512, Height = 512 }, + TestContext.Current.CancellationToken + ); + map.SetStyleUrlAsync("unsupported://style.json"); map.Close(); - runtime.Close(); + + var teardown = runtime.CloseAsync(); Assert.True(runtime.IsClosed); + await teardown; + + var second = runtime.CloseAsync(); + Assert.True(second.IsCompletedSuccessfully); + await second; } - [BindingSpecTest("BND-100")] + [BindingSpecTest("BND-040")] [Fact] - public void MapDebugSettingsRoundTripThroughNativeMap() + public async Task RuntimeDisposeAsyncWaitsForNativeTeardown() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - - var options = DebugOptions.TileBorders | DebugOptions.ParseStatus; - map.SetDebugOptions(options); - map.SetRenderingStatsViewEnabled(true); + var runtime = RuntimeHandle.Create(new RuntimeOptions()); + await using (runtime) + { + await runtime.BarrierAsync(); + } - Assert.Equal(options, map.GetDebugOptions()); - Assert.True(map.GetRenderingStatsViewEnabled()); - _ = map.IsFullyLoaded(); - map.DumpDebugLogs(); + Assert.True(runtime.IsClosed); } + [BindingSpecTest("BND-042")] [Fact] - public void MapSizeReportsCreationExtentAndPixelRatio() + public void RuntimeCloseFailsWhileMapIsLiveAndCanRetryAfterMapClose() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( runtime, - new MapOptions - { - Width = 512, - Height = 256, - ScaleFactor = 2, - } + new MapOptions { Width = 512, Height = 512 } ); - Assert.Equal((512u, 256u, 2d), map.GetSize()); - } + var error = Assert.Throws(() => TestHandles.Close(runtime)); - [BindingSpecTest("BND-190", "BND-191")] - [Fact] - public void OwnerThreadViolationMapsToWrongThreadException() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - Exception? thrown = null; + Assert.Equal(MaplibreStatus.InvalidState, error.Status); + Assert.Equal((int)MaplibreStatus.InvalidState, error.RawStatus); + Assert.False(runtime.IsClosed); + Assert.Contains( + "live or pending children", + error.Diagnostic, + StringComparison.OrdinalIgnoreCase + ); - var thread = new Thread(() => - { - try - { - map.RequestRepaint(); - } - catch (Exception error) - { - thrown = error; - } - }); - thread.Start(); - thread.Join(); + TestHandles.Close(map); + TestHandles.Close(runtime); - var wrongThread = Assert.IsType(thrown); - Assert.Equal(MaplibreStatus.WrongThread, wrongThread.Status); - Assert.Equal((int)MaplibreStatus.WrongThread, wrongThread.RawStatus); + Assert.True(runtime.IsClosed); } - [BindingSpecTest("BND-192")] + [BindingSpecTest("BND-100")] [Fact] - public async Task OwnerThreadHelperConfinesRuntimeMapWorkToOneThread() + public void CommittedCommandIsVisibleInSnapshotsAtOrPastItsGeneration() { - using var ownerThread = new OwnerThread(); - RuntimeHandle? runtime = null; - MapHandle? map = null; - - var createThreadId = ownerThread.Invoke(() => - { - runtime = RuntimeHandle.Create(new RuntimeOptions()); - map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - return Environment.CurrentManagedThreadId; - }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - var task = Task.Run(() => - ownerThread.Invoke(() => - { - Assert.Equal(createThreadId, Environment.CurrentManagedThreadId); - map!.RequestRepaint(); - runtime!.Pump(TimeSpan.Zero); - _ = runtime.DrainEvents(); - return Environment.CurrentManagedThreadId; - }) + var options = DebugOptions.TileBorders | DebugOptions.ParseStatus; + var completion = RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetDebugOptionsAsync(options), + MaplibreStatus.Ok ); - Assert.Equal(createThreadId, await task); - ownerThread.Invoke(() => - { - map!.Close(); - runtime!.Close(); - }); + // The published snapshot fence: a snapshot at or past the commit's generation + // observes the committed value. + var snapshot = map.GetSnapshot(); + Assert.True(snapshot.Generation >= completion.Generation); + Assert.Equal(options, snapshot.DebugOptions); + Assert.False(snapshot.FullyLoaded); + map.DumpDebugLogsAsync(); } - [BindingSpecTest("BND-192")] + [BindingSpecTest("BND-100")] [Fact] - public void OwnerThreadHelperPropagatesOrdinaryBindingErrors() + public void RenderingStatsViewEnabledRoundTripsThroughSnapshot() { - using var ownerThread = new OwnerThread(); - RuntimeHandle? runtime = null; - MapHandle? map = null; - ownerThread.Invoke(() => - { - runtime = RuntimeHandle.Create(new RuntimeOptions()); - map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.Close(); - runtime.Close(); - }); - - var error = Assert.Throws(() => - ownerThread.Invoke(() => map!.RequestRepaint()) + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } ); - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.Null(error.RawStatus); + Assert.False(map.GetSnapshot().RenderingStatsViewEnabled); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetRenderingStatsViewEnabledAsync(true), + MaplibreStatus.Ok + ); + Assert.True(map.GetSnapshot().RenderingStatsViewEnabled); } - [BindingSpecTest("BND-192")] [Fact] - public void OwnerThreadHelperRejectsSubmissionsAfterClose() + public void MapSizeReportsCreationExtentAndPixelRatio() { - var ownerThread = new OwnerThread(); - ownerThread.Close(); - - var error = Assert.Throws(() => ownerThread.Invoke(() => { })); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions + { + Width = 512, + Height = 256, + ScaleFactor = 2, + } + ); - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.Null(error.RawStatus); + Assert.Equal(new LogicalExtent(512, 256, 2), map.GetSnapshot().LogicalExtent); } - [BindingSpecTest("BND-192")] + [BindingSpecTest("BND-190", "BND-191", "BND-192")] [Fact] - public void OwnerThreadHelperRejectsNestedSubmissionsAfterOwnerThreadClose() + public async Task RuntimeAndMapWorkAcrossManagedThreads() { - using var ownerThread = new OwnerThread(); - - ownerThread.Invoke(() => - { - ownerThread.Close(); + using var runtime = RuntimeHandle.Create(new RuntimeOptions()); + using var map = await MapHandle.CreateAsync( + runtime, + new MapOptions { Width = 512, Height = 512 }, + TestContext.Current.CancellationToken + ); - var error = Assert.Throws(() => ownerThread.Invoke(() => { })); + var completion = await Task.Run(map.RequestRepaintAsync); + var snapshot = await Task.Run(map.GetSnapshot); + await runtime.BarrierAsync(); - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.Null(error.RawStatus); - }); + Assert.Equal(CommandDisposition.Committed, completion.Disposition); + Assert.Equal(new LogicalExtent(512, 512, 1), snapshot.LogicalExtent); } [BindingSpecTest("BND-023")] [Fact] public void MethodsRejectClosedMapBeforeNativeCall() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.Close(); - runtime.Close(); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 512, Height = 512 }); + TestHandles.Close(map); + TestHandles.Close(runtime); - var error = Assert.Throws(map.RequestRepaint); + var error = Assert.Throws(() => + { + _ = map.RequestRepaintAsync(); + }); Assert.Equal(MaplibreStatus.InvalidState, error.Status); Assert.Null(error.RawStatus); diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeOfflineOperationTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeOfflineOperationTests.cs index 4dcdf9bbd..d425a333b 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeOfflineOperationTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeOfflineOperationTests.cs @@ -1,7 +1,4 @@ -using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Geo; -using Maplibre.NativeFfi.Internal.C; -using Maplibre.NativeFfi.Map; using Maplibre.NativeFfi.Offline; using Maplibre.NativeFfi.Runtime; using Xunit; @@ -11,112 +8,23 @@ namespace Maplibre.NativeFfi.Tests; public sealed class RuntimeOfflineOperationTests { [Fact] - public void AmbientCacheOperationCanBeStartedAndDiscarded() + public async Task AmbientCacheOperationsAreAwaitable() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - - using var operation = runtime.StartAmbientCacheOperation(AmbientCacheOperation.Invalidate); - - Assert.NotEqual(0u, operation.Id); - Assert.Equal(OfflineOperationKind.AmbientCache, operation.Kind); - Assert.Equal(OfflineOperationResultKind.None, operation.ResultKind); - Assert.False(operation.IsClosed); - - operation.Close(); - operation.Close(); - - Assert.True(operation.IsClosed); - } - - [Fact] - public void SetMaximumAmbientCacheSizeCanBeStartedAndDiscarded() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions { CachePath = ":memory:" }); - - using var operation = runtime.StartSetMaximumAmbientCacheSize(8UL << 20); - - Assert.NotEqual(0u, operation.Id); - Assert.Equal(OfflineOperationKind.SetMaximumAmbientCacheSize, operation.Kind); - Assert.Equal(OfflineOperationResultKind.None, operation.ResultKind); - - operation.Close(); - Assert.True(operation.IsClosed); - } - - [Fact] - public void OperationCloseAfterRuntimeCloseMarksOperationClosed() - { - var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var operation = runtime.StartAmbientCacheOperation(AmbientCacheOperation.Invalidate); - - runtime.Close(); - - operation.Close(); - Assert.True(operation.IsClosed); - operation.Close(); - } - - [BindingSpecTest("BND-084")] - [Fact] - public unsafe void FailedOfflineStatusTakeResultLeavesOperationLiveForRetry() - { - var calls = 0; - using var take = RuntimeHandle.UseOfflineTakeResultMethodsForTest( - (_, operationId, status) => - { - Assert.Equal(77u, operationId); - calls++; - if (calls == 1) - { - return mln_status.MLN_STATUS_INVALID_STATE; - } - - *status = new mln_offline_region_status - { - download_state = (uint) - mln_offline_region_download_state.MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, - completed_resource_count = 1, - completed_resource_size = 2, - completed_tile_count = 3, - required_tile_count = 4, - completed_tile_size = 5, - required_resource_count = 6, - required_resource_count_is_precise = 1, - complete = 1, - }; - return mln_status.MLN_STATUS_OK; - } + using var runtime = TestHandles.CreateRuntime( + new RuntimeOptions { CachePath = ":memory:" } ); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var operation = new OfflineOperationHandle( - runtime, - 77, - OfflineOperationKind.RegionGetStatus, - OfflineOperationResultKind.RegionStatus - ); - - var error = Assert.Throws(() => - runtime.TakeOfflineRegionStatusResult(operation) - ); - - Assert.Equal(MaplibreStatus.InvalidState, error.Status); - Assert.False(operation.IsClosed); - var status = runtime.TakeOfflineRegionStatusResult(operation); - - Assert.True(operation.IsClosed); - Assert.Equal(2, calls); - Assert.Equal(OfflineRegionDownloadState.Active, status.DownloadState); - Assert.Equal(6u, status.RequiredResourceCount); - Assert.True(status.RequiredResourceCountIsPrecise); - Assert.True(status.Complete); + await runtime.RunAmbientCacheOperationAsync(AmbientCacheOperation.Invalidate); + await runtime.SetMaximumAmbientCacheSizeAsync(8UL << 20); } [BindingSpecTest("BND-084", "BND-085")] [Fact] - public void OfflineRegionsAreCreatedListedAndDeletedThroughDrainedCompletions() + public async Task OfflineRegionsAreCreatedListedAndDeletedThroughTasks() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions { CachePath = ":memory:" }); + using var runtime = TestHandles.CreateRuntime( + new RuntimeOptions { CachePath = ":memory:" } + ); var definition = new OfflineRegionDefinition.TilePyramid( "custom://offline-style.json", new LatLngBounds(new LatLng(0, 0), new LatLng(1, 1)), @@ -125,47 +33,16 @@ public void OfflineRegionsAreCreatedListedAndDeletedThroughDrainedCompletions() 1, true ); - using var create = runtime.StartCreateOfflineRegion(definition, [1, 2, 3]); - CompleteOperation(runtime, create); - var region = runtime.TakeCreateOfflineRegionResult(create); - Assert.Equal([1, 2, 3], region.Metadata); - using var list = runtime.StartOfflineRegions(); - CompleteOperation(runtime, list); - Assert.Contains(runtime.TakeOfflineRegionsResult(list), listed => listed.Id == region.Id); + var region = await runtime.CreateOfflineRegionAsync(definition, [1, 2, 3]); + Assert.Equal([1, 2, 3], region.Metadata); + Assert.Contains(await runtime.ListOfflineRegionsAsync(), listed => listed.Id == region.Id); - using var delete = runtime.StartDeleteOfflineRegion(region.Id); - CompleteOperation(runtime, delete); - delete.Close(); + await runtime.DeleteOfflineRegionAsync(region.Id); - using var listAgain = runtime.StartOfflineRegions(); - CompleteOperation(runtime, listAgain); Assert.DoesNotContain( - runtime.TakeOfflineRegionsResult(listAgain), + await runtime.ListOfflineRegionsAsync(), listed => listed.Id == region.Id ); } - - // Pumps and drains until the operation reports the completion event that makes its result - // available, which is the only signal a host has for taking one. - private static void CompleteOperation(RuntimeHandle runtime, OfflineOperationHandle operation) - { - for (var attempt = 0; attempt < 1000; attempt++) - { - runtime.Pump(TimeSpan.FromMilliseconds(1)); - foreach (var drained in runtime.DrainEvents().Events) - { - if ( - drained.Payload is RuntimeEventPayload.OfflineOperationCompleted completed - && completed.OperationId == operation.Id - ) - { - Assert.Equal((int)MaplibreStatus.Ok, drained.Code); - return; - } - } - } - - throw new TimeoutException($"Offline operation {operation.Id} never completed."); - } } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeWakeTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeWakeTests.cs deleted file mode 100644 index ae0e78cbe..000000000 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/RuntimeWakeTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Diagnostics; -using Maplibre.NativeFfi.Map; -using Maplibre.NativeFfi.Runtime; -using Xunit; - -namespace Maplibre.NativeFfi.Tests; - -public sealed class RuntimeWakeTests -{ - private static readonly TimeSpan ParkTimeout = TimeSpan.FromSeconds(10); - - // Well below ParkTimeout, and far above the scheduling noise a loaded CI machine - // adds to a condition-variable wake. - private static readonly TimeSpan PromptReturn = TimeSpan.FromSeconds(5); - - // Pumps until the runtime is idle, so a park that follows is released by the - // signal the test raises. - private static void Quiesce(RuntimeHandle runtime) - { - for (var attempt = 0; attempt < 100; attempt++) - { - runtime.Pump(TimeSpan.Zero); - if (runtime.DrainEvents().Events.Count == 0) - { - return; - } - } - - Assert.Fail("The runtime kept producing events while idle."); - } - - [BindingSpecTest("BND-088")] - [Fact] - public void ParkedOwnerThreadWakesForNativeWorkAndForAWakeSource() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - Quiesce(runtime); - - // The style is malformed, so native reports the failure from its own - // threads and the failure reaches the parked owner thread. - map.SetStyleUrl("unsupported://style.json"); - var loadingFailed = false; - var loadStarted = Stopwatch.StartNew(); - for (var attempt = 0; attempt < 20 && !loadingFailed; attempt++) - { - runtime.Pump(ParkTimeout); - Assert.True( - loadStarted.Elapsed < PromptReturn, - "Parks sat out their timeouts while loading was pending." - ); - foreach (var drained in runtime.DrainEvents().Events) - { - if (drained.Type == RuntimeEventType.MapLoadingFailed) - { - loadingFailed = true; - } - } - } - - Assert.True(loadingFailed); - - // A source signalled from another thread matches a host's submission - // path, and the park it releases has no other work to end it. - var source = runtime.AcquireWakeSource(); - Quiesce(runtime); - var signaller = new Thread(() => - { - Thread.Sleep(TimeSpan.FromMilliseconds(20)); - source.Signal(); - }); - signaller.Start(); - - var parkStarted = Stopwatch.StartNew(); - runtime.Pump(ParkTimeout); - Assert.True( - parkStarted.Elapsed < PromptReturn, - "The parked owner thread timed out instead of taking the signal." - ); - signaller.Join(); - - // A wake source stays usable after its runtime closes, so hosts tear - // the two down in either order. - map.Close(); - runtime.Close(); - source.Signal(); - source.Close(); - Assert.True(source.IsClosed); - } - - [BindingSpecTest("BND-089")] - [Fact] - public void PumpClearsTheWakeFlagItReturnsOn() - { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var source = runtime.AcquireWakeSource(); - Quiesce(runtime); - - source.Signal(); - var signalledStarted = Stopwatch.StartNew(); - runtime.Pump(ParkTimeout); - Assert.True( - signalledStarted.Elapsed < PromptReturn, - "A pump waited even though the wake flag was set." - ); - - // The pump above cleared the wake flag, so this one waits its full timeout. - var idleStarted = Stopwatch.StartNew(); - runtime.Pump(TimeSpan.FromMilliseconds(200)); - Assert.True( - idleStarted.Elapsed >= TimeSpan.FromMilliseconds(100), - "The first pump left the wake flag set." - ); - } -} diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleImageTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleImageTests.cs index 0f5366a8b..9efbb6c99 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleImageTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleImageTests.cs @@ -26,11 +26,14 @@ public void PremultipliedRgba8ImageSnapshotsPixelsAndReturnsCopies() [BindingSpecTest("BND-105")] [Fact] - public void ImageSourceApisAdaptCoordinatesAndImagesThroughNativeMap() + public async Task ImageSourceApisAdaptCoordinatesAndImagesThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); var coordinates = new[] { new LatLng(10, 10), @@ -47,60 +50,54 @@ public void ImageSourceApisAdaptCoordinatesAndImagesThroughNativeMap() }; var image = new PremultipliedRgba8Image([0, 255, 0, 255], new TextureImageInfo(1, 1, 4, 4)); - map.AddImageSourceUrl("image-url", coordinates, "https://example.test/image.png"); - map.SetImageSourceUrl("image-url", "https://example.test/other.png"); - map.SetImageSourceCoordinates("image-url", updatedCoordinates); - map.AddImageSourceImage("image-inline", coordinates, image); - map.SetImageSourceImage("image-inline", image); + map.AddImageSourceUrlAsync("image-url", coordinates, "https://example.test/image.png"); + map.SetImageSourceUrlAsync("image-url", "https://example.test/other.png"); + map.SetImageSourceCoordinatesAsync("image-url", updatedCoordinates); + map.AddImageSourceImageAsync("image-inline", coordinates, image); + map.SetImageSourceImageAsync("image-inline", image); - Assert.Equal(SourceType.Image, map.StyleSourceType("image-url")); - Assert.Equal(SourceType.Image, map.StyleSourceType("image-inline")); - Assert.Equal(updatedCoordinates, map.GetImageSourceCoordinates("image-url")); - Assert.Equal(coordinates, map.GetImageSourceCoordinates("image-inline")); + Assert.Equal(SourceType.Image, (await map.StyleSourceInfoAsync("image-url"))?.Type); + Assert.Equal(SourceType.Image, (await map.StyleSourceInfoAsync("image-inline"))?.Type); + Assert.Equal(updatedCoordinates, await map.GetImageSourceCoordinatesAsync("image-url")); + Assert.Equal(coordinates, await map.GetImageSourceCoordinatesAsync("image-inline")); } [BindingSpecTest("BND-105")] [Fact] - public void StyleImageRoundTripsMetadataAndPixelsThroughNativeMap() + public async Task StyleImageRoundTripsMetadataAndPixelsThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); var image = new PremultipliedRgba8Image([255, 0, 0, 255], new TextureImageInfo(1, 1, 4, 4)); var options = new StyleImageOptions { PixelRatio = 2, Sdf = true }; - map.SetStyleImage("dot", image, options); - - Assert.True(map.StyleImageExists("dot")); - var info = map.StyleImageInfo("dot"); - Assert.NotNull(info); - Assert.Equal(1u, info.Width); - Assert.Equal(1u, info.Height); - Assert.Equal(4u, info.Stride); - Assert.Equal(4u, info.ByteLength); - Assert.Equal(2, info.PixelRatio); - Assert.True(info.Sdf); + RuntimeEventTestHelpers.AssertCommitted(map.SetStyleImageAsync("dot", image, options)); - var copied = map.CopyStyleImagePremultipliedRgba8("dot"); - Assert.NotNull(copied); + var copied = Assert.IsType(await map.StyleImageAsync("dot")); Assert.Equal([255, 0, 0, 255], copied.Image.Bytes); Assert.Equal(new TextureImageInfo(1, 1, 4, 4), copied.Image.Info); Assert.Equal(2, copied.Options.PixelRatio); Assert.True(copied.Options.Sdf); - Assert.True(map.RemoveStyleImage("dot")); - Assert.False(map.StyleImageExists("dot")); - Assert.Null(map.StyleImageInfo("dot")); - Assert.Null(map.CopyStyleImagePremultipliedRgba8("dot")); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleImageAsync("dot"), + MaplibreStatus.Ok + ); + Assert.Null(await map.StyleImageAsync("dot")); } [BindingSpecTest("BND-105")] [Fact] - public void NinePatchStyleImageRoundTripsStretchContentAndTextFit() + public async Task NinePatchStyleImageRoundTripsStretchContentAndTextFit() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 64, Height = 64 }); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); var image = new PremultipliedRgba8Image(new byte[16], new TextureImageInfo(2, 2, 8, 16)); var options = new StyleImageOptions @@ -110,34 +107,19 @@ public void NinePatchStyleImageRoundTripsStretchContentAndTextFit() Content = new ImageContent(0.5f, 0.5f, 1.5f, 1.5f), TextFitHeight = StyleImageTextFit.Proportional, }; - map.SetStyleImage("patch", image, options); - - var info = map.StyleImageInfo("patch"); - Assert.NotNull(info); - Assert.Equal(1UL, info.StretchXCount); - Assert.Equal(2UL, info.StretchYCount); - Assert.Equal(new ImageContent(0.5f, 0.5f, 1.5f, 1.5f), info.Content); - // An absent text fit stays distinguishable from a present default. - Assert.Null(info.TextFitWidth); - Assert.Equal(StyleImageTextFit.Proportional, info.TextFitHeight); - - var stretches = map.StyleImageStretches("patch"); - Assert.NotNull(stretches); - Assert.Equal([new ImageStretch(0, 1)], stretches.Value.StretchX); - Assert.Equal([new ImageStretch(0, 1), new ImageStretch(1, 2)], stretches.Value.StretchY); - Assert.Null(map.StyleImageStretches("missing")); + RuntimeEventTestHelpers.AssertCommitted(map.SetStyleImageAsync("patch", image, options)); - var copied = map.CopyStyleImagePremultipliedRgba8("patch"); - Assert.NotNull(copied); + var copied = Assert.IsType(await map.StyleImageAsync("patch")); Assert.Equal([new ImageStretch(0, 1)], copied.Options.StretchX); Assert.Equal([new ImageStretch(0, 1), new ImageStretch(1, 2)], copied.Options.StretchY); Assert.Equal(new ImageContent(0.5f, 0.5f, 1.5f, 1.5f), copied.Options.Content); Assert.Null(copied.Options.TextFitWidth); Assert.Equal(StyleImageTextFit.Proportional, copied.Options.TextFitHeight); + Assert.Null(await map.StyleImageAsync("missing")); // A backwards interval is rejected by C. - Assert.Throws(() => - map.SetStyleImage( + await Assert.ThrowsAsync(() => + map.SetStyleImageAsync( "bad", image, new StyleImageOptions { StretchX = [new ImageStretch(2, 1)] } diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleJsonTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleJsonTests.cs index 908b984a3..6be820ad3 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleJsonTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleJsonTests.cs @@ -1,3 +1,4 @@ +using Maplibre.NativeFfi.Error; using Maplibre.NativeFfi.Geo; using Maplibre.NativeFfi.Map; using Maplibre.NativeFfi.Runtime; @@ -10,15 +11,18 @@ public sealed class StyleJsonTests { [BindingSpecTest("BND-105")] [Fact] - public void UrlAndTileSourceApisAdaptThroughNativeMap() + public async Task UrlAndTileSourceApisAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyle()); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyle()); - map.AddGeoJsonSourceUrl("geo-url", "https://example.test/data.geojson", null); - map.SetGeoJsonSourceUrl("geo-url", "https://example.test/other.geojson"); - map.AddVectorSourceTiles( + map.AddGeoJsonSourceUrlAsync("geo-url", "https://example.test/data.geojson", null); + map.SetGeoJsonSourceUrlAsync("geo-url", "https://example.test/other.geojson"); + map.AddVectorSourceTilesAsync( "vector-tiles", ["https://example.test/vector/{z}/{x}/{y}.pbf"], new TileSourceOptions @@ -30,34 +34,38 @@ public void UrlAndTileSourceApisAdaptThroughNativeMap() VectorEncoding = VectorTileEncoding.Mvt, } ); - map.AddRasterSourceTiles( + map.AddRasterSourceTilesAsync( "raster-tiles", ["https://example.test/raster/{z}/{x}/{y}.png"], new TileSourceOptions { TileSize = 256 } ); - map.AddRasterDemSourceTiles( + map.AddRasterDemSourceTilesAsync( "dem-tiles", ["https://example.test/dem/{z}/{x}/{y}.png"], new TileSourceOptions { RasterEncoding = RasterDemEncoding.Mapbox } ); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo-url")); - Assert.Equal(SourceType.Vector, map.StyleSourceType("vector-tiles")); - Assert.Equal(SourceType.Raster, map.StyleSourceType("raster-tiles")); - Assert.Equal(SourceType.RasterDem, map.StyleSourceType("dem-tiles")); - Assert.Equal("https://example.test/other.geojson", map.StyleSourceInfo("geo-url")?.Url); - Assert.Equal("Vector attribution", map.StyleSourceInfo("vector-tiles")?.Attribution); - Assert.Equal(256u, map.StyleSourceInfo("raster-tiles")?.TileSize); - Assert.Equal(RasterDemEncoding.Mapbox, map.StyleSourceInfo("dem-tiles")?.RasterDemEncoding); + Assert.Equal(SourceType.GeoJson, (await map.StyleSourceInfoAsync("geo-url"))?.Type); + Assert.Equal(SourceType.Vector, (await map.StyleSourceInfoAsync("vector-tiles"))?.Type); + Assert.Equal(SourceType.Raster, (await map.StyleSourceInfoAsync("raster-tiles"))?.Type); + Assert.Equal(SourceType.RasterDem, (await map.StyleSourceInfoAsync("dem-tiles"))?.Type); Assert.Equal( - (uint)RasterDemEncoding.Mapbox, - map.StyleSourceInfo("dem-tiles")?.RawRasterDemEncoding + "https://example.test/other.geojson", + (await map.StyleSourceInfoAsync("geo-url"))?.Url ); + Assert.Equal( + "Vector attribution", + (await map.StyleSourceInfoAsync("vector-tiles"))?.Attribution + ); + Assert.Equal(256u, (await map.StyleSourceInfoAsync("raster-tiles"))?.TileSize); + var demInfo = await map.StyleSourceInfoAsync("dem-tiles"); + Assert.Equal(RasterDemEncoding.Mapbox, demInfo?.RasterDemEncoding); + Assert.Equal((uint)RasterDemEncoding.Mapbox, demInfo?.RawRasterDemEncoding); } [BindingSpecTest("BND-109")] [Fact] - public void SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() + public async Task SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() { SourceInfo urlInfo; SourceInfo inlineInfo; @@ -68,12 +76,14 @@ public void SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() "https://example.test/vector-b/{z}/{x}/{y}.pbf", }; - using (var runtime = RuntimeHandle.Create(new RuntimeOptions())) - using (var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 })) + using (var runtime = TestHandles.CreateRuntime(new RuntimeOptions())) + using ( + var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 512, Height = 512 }) + ) { - map.SetStyleJson(EmptyStyle()); - map.AddVectorSourceUrl("url-vector", "https://example.test/vector.json", null); - map.AddVectorSourceTiles( + map.SetStyleJsonAsync(EmptyStyle()); + map.AddVectorSourceUrlAsync("url-vector", "https://example.test/vector.json", null); + map.AddVectorSourceTilesAsync( "inline-vector", tileUrls, new TileSourceOptions @@ -87,8 +97,8 @@ public void SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() } ); - urlInfo = Assert.IsType(map.StyleSourceInfo("url-vector")); - inlineInfo = Assert.IsType(map.StyleSourceInfo("inline-vector")); + urlInfo = Assert.IsType(await map.StyleSourceInfoAsync("url-vector")); + inlineInfo = Assert.IsType(await map.StyleSourceInfoAsync("inline-vector")); Assert.Equal("https://example.test/vector.json", urlInfo.Url); Assert.Null(urlInfo.TileJson); @@ -109,21 +119,26 @@ public void SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() Assert.Null(inlineInfo.RasterDemEncoding); Assert.Null(inlineInfo.RawRasterDemEncoding); - Assert.True(map.RemoveStyleSource("url-vector")); - Assert.True(map.RemoveStyleSource("inline-vector")); + RuntimeEventTestHelpers.AssertCommitted(map.RemoveStyleSourceAsync("url-vector")); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleSourceAsync("inline-vector"), + MaplibreStatus.Ok + ); + Assert.Null(await map.StyleSourceInfoAsync("inline-vector")); } Assert.Equal("https://example.test/vector.json", urlInfo.Url); Assert.Equal(tileUrls, inlineInfo.TileJson?.TileUrls); Assert.Equal(bounds, inlineInfo.TileJson?.Bounds); - using var rebuiltRuntime = RuntimeHandle.Create(new RuntimeOptions()); - using var rebuiltMap = MapHandle.Create( + using var rebuiltRuntime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var rebuiltMap = TestHandles.CreateMap( rebuiltRuntime, new MapOptions { Width = 512, Height = 512 } ); - rebuiltMap.SetStyleJson(EmptyStyle()); - rebuiltMap.AddVectorSourceTiles( + rebuiltMap.SetStyleJsonAsync(EmptyStyle()); + rebuiltMap.AddVectorSourceTilesAsync( "rebuilt", inlineInfo.TileJson!.TileUrls, new TileSourceOptions @@ -137,42 +152,48 @@ public void SourceInspectionCopiesUrlAndInlineTileJsonAfterSourceRelease() VectorEncoding = inlineInfo.VectorEncoding, } ); - Assert.True(rebuiltMap.StyleSourceExists("rebuilt")); + Assert.NotNull(await rebuiltMap.StyleSourceInfoAsync("rebuilt")); } [BindingSpecTest("BND-101")] [Fact] - public void LoadedStyleDocumentAndUrlReadBackWhatWasLoaded() + public async Task LoadedStyleDocumentAndUrlReadBackWhatWasLoaded() { var styleJson = EmptyStyle(); - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); // Nothing parsed and nothing requested yet. - Assert.Empty(map.GetLoadedStyleJson()); - Assert.Equal(string.Empty, map.GetStyleUrl()); + Assert.Empty(await map.GetLoadedStyleJsonAsync()); + Assert.Equal(string.Empty, await map.GetStyleUrlAsync()); // The document reads back byte-for-byte, so it can be reloaded unchanged. - map.SetStyleJson(styleJson); - Assert.Equal(styleJson, map.GetLoadedStyleJson()); + map.SetStyleJsonAsync(styleJson); + Assert.Equal(styleJson, await map.GetLoadedStyleJsonAsync()); // Inline JSON clears the URL. - Assert.Equal(string.Empty, map.GetStyleUrl()); + Assert.Equal(string.Empty, await map.GetStyleUrlAsync()); // The URL is request state, recorded before the load can succeed, while the // document still reports the style that last parsed. - map.SetStyleUrl("https://example.test/style.json"); - Assert.Equal("https://example.test/style.json", map.GetStyleUrl()); - Assert.Equal(styleJson, map.GetLoadedStyleJson()); + map.SetStyleUrlAsync("https://example.test/style.json"); + Assert.Equal("https://example.test/style.json", await map.GetStyleUrlAsync()); + Assert.Equal(styleJson, await map.GetLoadedStyleJsonAsync()); } [BindingSpecTest("BND-081", "BND-101")] [Fact] public void SetStyleJsonReturnsCopiedStyleLoadedEventWithMapIdentity() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); - map.SetStyleJson(EmptyStyle()); + map.SetStyleJsonAsync(EmptyStyle()); var runtimeEvent = RuntimeEventTestHelpers.WaitForMapEvent( runtime, map, @@ -190,50 +211,140 @@ public void SetStyleJsonReturnsCopiedStyleLoadedEventWithMapIdentity() [BindingSpecTest("BND-105")] [Fact] - public void LayerJsonPropertiesAndFiltersAdaptThroughNativeMap() + public async Task LayerJsonPropertiesAndFiltersAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyle()); - map.AddStyleSourceJson("geo", GeoJsonSource()); - map.AddStyleLayerJson("""{"id":"fill","type":"fill","source":"geo"}"""u8.ToArray(), ""); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyle()); + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleSourceJsonAsync("geo", GeoJsonSource()) + ); + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleLayerJsonAsync( + """{"id":"fill","type":"fill","source":"geo"}"""u8.ToArray(), + "" + ) + ); - map.SetLayerProperty("fill", "fill-opacity", "0.5"u8.ToArray()); - map.SetLayerFilter("fill", """["==","kind","park"]"""u8.ToArray()); + RuntimeEventTestHelpers.AssertCommitted( + map.SetLayerPropertyAsync("fill", "fill-opacity", "0.5"u8.ToArray()) + ); + RuntimeEventTestHelpers.AssertCommitted( + map.SetLayerFilterAsync("fill", """["==","kind","park"]"""u8.ToArray()) + ); - Assert.Equal("0.5"u8.ToArray(), map.GetLayerProperty("fill", "fill-opacity")); - Assert.NotEmpty(Assert.IsType(map.GetStyleLayerJson("fill"))); - Assert.Equal("""["==","kind","park"]"""u8.ToArray(), map.GetLayerFilter("fill")); + Assert.Equal("0.5"u8.ToArray(), await map.GetLayerPropertyAsync("fill", "fill-opacity")); + Assert.NotEmpty(Assert.IsType(await map.GetStyleLayerJsonAsync("fill"))); + Assert.Equal("""["==","kind","park"]"""u8.ToArray(), await map.GetLayerFilterAsync("fill")); - map.SetLayerFilter("fill", null); - Assert.Null(map.GetLayerFilter("fill")); + RuntimeEventTestHelpers.AssertCommitted(map.SetLayerFilterAsync("fill", null)); + Assert.Null(await map.GetLayerFilterAsync("fill")); } [BindingSpecTest("BND-105")] [Fact] - public void StyleSourceAndLayerJsonAdaptThroughNativeMap() + public async Task StyleSourceAndLayerJsonAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson(EmptyStyle()); - - map.AddStyleSourceJson("geo", GeoJsonSource()); - Assert.True(map.StyleSourceExists("geo")); - Assert.Equal(SourceType.GeoJson, map.StyleSourceType("geo")); - Assert.Contains("geo", map.StyleSourceIds()); - var sourceInfo = map.StyleSourceInfo("geo"); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyle()); + + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleSourceJsonAsync("geo", GeoJsonSource()) + ); + Assert.Contains("geo", await map.StyleSourceIdsAsync()); + var sourceInfo = await map.StyleSourceInfoAsync("geo"); Assert.NotNull(sourceInfo); Assert.Equal("geo", sourceInfo.Id); Assert.Equal(SourceType.GeoJson, sourceInfo.Type); Assert.Null(sourceInfo.Attribution); - map.AddStyleLayerJson("""{"id":"background","type":"background"}"""u8.ToArray(), ""); - Assert.True(map.StyleLayerExists("background")); - Assert.Equal("background", map.StyleLayerType("background")); - Assert.Contains("background", map.StyleLayerIds()); + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleLayerJsonAsync( + """{"id":"background","type":"background"}"""u8.ToArray(), + "" + ) + ); + Assert.Equal("background", (await map.StyleLayerInfoAsync("background"))?.Type); + Assert.Contains("background", await map.StyleLayerIdsAsync()); + + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleLayerAsync("background"), + MaplibreStatus.Ok + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleSourceAsync("geo"), + MaplibreStatus.Ok + ); + Assert.Null(await map.StyleLayerInfoAsync("background")); + Assert.Null(await map.StyleSourceInfoAsync("geo")); + } + + [BindingSpecTest("BND-105")] + [Fact] + public async Task StyleRemovalCommandsReportNotFoundAndInUseFailures() + { + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync(EmptyStyle()); - Assert.True(map.RemoveStyleLayer("background")); - Assert.True(map.RemoveStyleSource("geo")); + // Removing a missing layer, source, or image finishes FAILED with NOT_FOUND. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleLayerAsync("missing"), + MaplibreStatus.NotFound + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleSourceAsync("missing"), + MaplibreStatus.NotFound + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleImageAsync("missing"), + MaplibreStatus.NotFound + ); + + // Removing a source a layer still uses finishes FAILED with INVALID_STATE. + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleSourceJsonAsync("geo", GeoJsonSource()) + ); + RuntimeEventTestHelpers.AssertCommitted( + map.AddStyleLayerJsonAsync( + """{"id":"fill","type":"fill","source":"geo"}"""u8.ToArray(), + "" + ) + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleSourceAsync("geo"), + MaplibreStatus.InvalidState + ); + Assert.NotNull(await map.StyleSourceInfoAsync("geo")); + + // After the layer goes away the removal commits and the found flag clears. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleLayerAsync("fill"), + MaplibreStatus.Ok + ); + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.RemoveStyleSourceAsync("geo"), + MaplibreStatus.Ok + ); + Assert.Null(await map.StyleSourceInfoAsync("geo")); } private static byte[] EmptyStyle() => """{"version":8,"sources":{},"layers":[]}"""u8.ToArray(); diff --git a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleLayerTests.cs b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleLayerTests.cs index 6e0705f1b..05cae4eba 100644 --- a/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleLayerTests.cs +++ b/bindings/dotnet/tests/Maplibre.NativeFfi.Tests/StyleLayerTests.cs @@ -11,40 +11,40 @@ public sealed class StyleLayerTests { [BindingSpecTest("BND-105")] [Fact] - public void DemAndLocationLayerHelpersAdaptThroughNativeMap() + public async Task DemAndLocationLayerHelpersAdaptThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 512, Height = 512 }); - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); - map.AddRasterDemSourceTiles("dem", ["https://example.test/dem/{z}/{x}/{y}.png"], null); - - map.AddHillshadeLayer("hillshade", "dem", ""); - map.AddColorReliefLayer("relief", "dem", ""); - map.AddLocationIndicatorLayer("location", ""); - map.SetLocationIndicatorLocation("location", new LatLng(12.5, 34.25), 100); - map.SetLocationIndicatorBearing("location", 45); - map.SetLocationIndicatorAccuracyRadius("location", 12); - map.SetLocationIndicatorImageName( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap( + runtime, + new MapOptions { Width = 512, Height = 512 } + ); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + map.AddRasterDemSourceTilesAsync("dem", ["https://example.test/dem/{z}/{x}/{y}.png"], null); + + map.AddHillshadeLayerAsync("hillshade", "dem", ""); + map.AddColorReliefLayerAsync("relief", "dem", ""); + map.AddLocationIndicatorLayerAsync("location", ""); + map.SetLocationIndicatorLocationAsync("location", new LatLng(12.5, 34.25), 100); + map.SetLocationIndicatorBearingAsync("location", 45); + map.SetLocationIndicatorAccuracyRadiusAsync("location", 12); + map.SetLocationIndicatorImageNameAsync( "location", LocationIndicatorImageKind.Top, "missing-image-name" ); - Assert.True(map.StyleLayerExists("hillshade")); - Assert.Equal("hillshade", map.StyleLayerType("hillshade")); - Assert.True(map.StyleLayerExists("relief")); - Assert.Equal("color-relief", map.StyleLayerType("relief")); - Assert.True(map.StyleLayerExists("location")); - Assert.Equal("location-indicator", map.StyleLayerType("location")); + Assert.Equal("hillshade", (await map.StyleLayerInfoAsync("hillshade"))?.Type); + Assert.Equal("color-relief", (await map.StyleLayerInfoAsync("relief"))?.Type); + Assert.Equal("location-indicator", (await map.StyleLayerInfoAsync("location"))?.Type); } [BindingSpecTest("BND-105")] [Fact] - public void LayerBaseAccessorsRoundTripThroughNativeMap() + public async Task LayerBaseAccessorsRoundTripThroughNativeMap() { - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); - map.SetStyleJson( + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 64, Height = 64 }); + map.SetStyleJsonAsync( System.Text.Encoding.UTF8.GetBytes( "{\"version\":8,\"sources\":{\"geo\":{\"type\":\"geojson\",\"data\":" + "{\"type\":\"FeatureCollection\",\"features\":[]}}},\"layers\":[" @@ -53,60 +53,83 @@ public void LayerBaseAccessorsRoundTripThroughNativeMap() ) ); - Assert.Equal(string.Empty, map.GetLayerSourceLayer("fill")); - map.SetLayerSourceLayer("fill", "roads"); - Assert.Equal("roads", map.GetLayerSourceLayer("fill")); - Assert.Equal("geo", map.GetLayerSourceId("fill")); + Assert.Null((await map.StyleLayerInfoAsync("fill"))?.SourceLayer); + RuntimeEventTestHelpers.AssertCommitted(map.SetLayerSourceLayerAsync("fill", "roads")); + Assert.Equal("roads", (await map.StyleLayerInfoAsync("fill"))?.SourceLayer); + Assert.Equal("geo", (await map.StyleLayerInfoAsync("fill"))?.SourceId); - // A layer type that takes no source is rejected rather than silently ignored. - Assert.Throws(() => map.SetLayerSourceLayer("bg", "roads")); - Assert.Equal(string.Empty, map.GetLayerSourceId("bg")); + // A layer type that takes no source rejects a source-layer mutation. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetLayerSourceLayerAsync("bg", "roads"), + MaplibreStatus.InvalidArgument + ); + Assert.Null((await map.StyleLayerInfoAsync("bg"))?.SourceId); // An unset zoom range crosses the boundary as infinities. - Assert.Equal(double.NegativeInfinity, map.GetLayerMinZoom("fill")); - Assert.Equal(double.PositiveInfinity, map.GetLayerMaxZoom("fill")); - map.SetLayerMinZoom("fill", 4); - map.SetLayerMaxZoom("fill", 12.5); - Assert.Equal(4, map.GetLayerMinZoom("fill")); - Assert.Equal(12.5, map.GetLayerMaxZoom("fill")); - - Assert.Equal(StyleLayerVisibility.Visible, map.GetLayerVisibility("fill")); - map.SetLayerVisibility("fill", StyleLayerVisibility.None); - Assert.Equal(StyleLayerVisibility.None, map.GetLayerVisibility("fill")); - - // An unknown raw visibility passes through to C, which rejects it. - Assert.Throws(() => - map.SetLayerVisibility("fill", (StyleLayerVisibility)900) + var unset = Assert.IsType(await map.StyleLayerInfoAsync("fill")); + Assert.Equal(double.NegativeInfinity, unset.MinZoom); + Assert.Equal(double.PositiveInfinity, unset.MaxZoom); + RuntimeEventTestHelpers.AssertCommitted(map.SetLayerMinZoomAsync("fill", 4)); + RuntimeEventTestHelpers.AssertCommitted(map.SetLayerMaxZoomAsync("fill", 12.5)); + + Assert.Equal(StyleLayerVisibility.Visible, unset.Visibility); + RuntimeEventTestHelpers.AssertCommitted( + map.SetLayerVisibilityAsync("fill", StyleLayerVisibility.None) + ); + + // The layer-info aggregate reports everything at once. + var info = Assert.IsType(await map.StyleLayerInfoAsync("fill")); + Assert.Equal("fill", info.Id); + Assert.Equal("fill", info.Type); + Assert.Equal(4, info.MinZoom); + Assert.Equal(12.5, info.MaxZoom); + Assert.Equal(StyleLayerVisibility.None, info.Visibility); + Assert.Equal((uint)StyleLayerVisibility.None, info.RawVisibility); + Assert.Equal("geo", info.SourceId); + Assert.Equal("roads", info.SourceLayer); + + // An unknown raw visibility is rejected by the completion. + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetLayerVisibilityAsync("fill", (StyleLayerVisibility)900), + MaplibreStatus.InvalidArgument + ); + Assert.Equal( + StyleLayerVisibility.None, + (await map.StyleLayerInfoAsync("fill"))?.Visibility ); - Assert.Throws(() => map.GetLayerMinZoom("missing")); + + // A missing layer reports not found rather than throwing. + Assert.Null(await map.StyleLayerInfoAsync("missing")); } [BindingSpecTest("BND-061")] [Fact] - public void StyleTransitionOptionsRoundTripThroughNativeMap() + public async Task StyleTransitionOptionsRoundTripThroughNativeMap() { const string transitionStyleJson = "{\"version\":8,\"transition\":{\"duration\":750,\"delay\":100}," + "\"sources\":{},\"layers\":[]}"; - using var runtime = RuntimeHandle.Create(new RuntimeOptions()); - using var map = MapHandle.Create(runtime, new MapOptions { Width = 64, Height = 64 }); + using var runtime = TestHandles.CreateRuntime(new RuntimeOptions()); + using var map = TestHandles.CreateMap(runtime, new MapOptions { Width = 64, Height = 64 }); // A map with no style yet reports no duration or delay. The placement flag always // reports, because MapLibre Native always holds a value for it. - var empty = map.GetStyleTransitionOptions(); + var empty = await map.GetStyleTransitionOptionsAsync(); Assert.Null(empty.Duration); Assert.Null(empty.Delay); Assert.True(empty.EnablePlacementTransitions); // The style parser fills in its own 300ms duration for a style that declares no // transition. - map.SetStyleJson("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); - var parsed = map.GetStyleTransitionOptions(); + map.SetStyleJsonAsync("""{"version":8,"sources":{},"layers":[]}"""u8.ToArray()); + var parsed = await map.GetStyleTransitionOptionsAsync(); Assert.Equal(300, parsed.Duration); Assert.Null(parsed.Delay); - map.SetStyleJson(System.Text.Encoding.UTF8.GetBytes(transitionStyleJson)); - var declared = map.GetStyleTransitionOptions(); + map.SetStyleJsonAsync(System.Text.Encoding.UTF8.GetBytes(transitionStyleJson)); + var declared = await map.GetStyleTransitionOptionsAsync(); Assert.Equal(750, declared.Duration); Assert.Equal(100, declared.Delay); Assert.True(declared.EnablePlacementTransitions); @@ -118,15 +141,17 @@ public void StyleTransitionOptionsRoundTripThroughNativeMap() Duration = 0, EnablePlacementTransitions = false, }; - map.SetStyleTransitionOptions(options); - Assert.Equal(options, map.GetStyleTransitionOptions()); + RuntimeEventTestHelpers.AssertCommitted(map.SetStyleTransitionOptionsAsync(options)); + Assert.Equal(options, await map.GetStyleTransitionOptionsAsync()); // Loading a style replaces the override with what that style declares. - map.SetStyleJson(System.Text.Encoding.UTF8.GetBytes(transitionStyleJson)); - Assert.Equal(declared, map.GetStyleTransitionOptions()); + map.SetStyleJsonAsync(System.Text.Encoding.UTF8.GetBytes(transitionStyleJson)); + Assert.Equal(declared, await map.GetStyleTransitionOptionsAsync()); - Assert.Throws(() => - map.SetStyleTransitionOptions(new StyleTransitionOptions { Delay = -1 }) + RuntimeEventTestHelpers.AssertCommandFinishes( + runtime, + map.SetStyleTransitionOptionsAsync(new StyleTransitionOptions { Delay = -1 }), + MaplibreStatus.InvalidArgument ); } } diff --git a/bindings/go/README.md b/bindings/go/README.md index 20395f0ca..3a605e91d 100644 --- a/bindings/go/README.md +++ b/bindings/go/README.md @@ -1,18 +1,12 @@ -# MapLibre Native Go Binding Status - -These bindings are draft low-level Go wrappers over the MapLibre Native C API. - -## Known draft deviations - -The owner-thread helper described by the binding specification is deferred. -Until that helper lands, Go callers are responsible for pinning runtime/map -lifecycles to one OS thread. - -Call `runtime.LockOSThread()` before creating a `RuntimeHandle`, keep the -runtime and its child handles on that locked goroutine, and close those handles -before unlocking the thread. Owner-thread-affine methods called from another OS -thread return `ErrWrongThread` with the native diagnostic when the C API reports -that status. - -TODO: add a binding-owned owner-thread helper that serializes create, pump, -event draining, operations, and close on one native owner thread. +# MapLibre Native Go binding + +These draft low-level bindings expose the MapLibre Native C API through +goroutine-safe runtime, map, and camera handles. Native workers make progress +autonomously. Commands return futures with terminal disposition and snapshot +generation. + +Runtime creation and public handle retirement are synchronous. Map creation, +ordered queries, and lifecycle transitions return typed futures. Render-session +graphics calls remain thread-affine. Poll event and frame-result drains from an +existing host cadence, and keep caller-driver service active while presentation +is paused. diff --git a/bindings/go/camera.go b/bindings/go/camera.go index b267601d8..db2ddfa20 100644 --- a/bindings/go/camera.go +++ b/bindings/go/camera.go @@ -247,12 +247,77 @@ func cAnimationOptions(options AnimationOptions) C.mln_animation_options { return raw } -func cAnimationOptionsPointer(options *AnimationOptions) (C.mln_animation_options, *C.mln_animation_options) { - if options == nil { - return C.mln_animation_options{}, nil +// CameraUpdateMode selects how an atomic camera update reaches its target. +type CameraUpdateMode uint32 + +const ( + CameraUpdateModeJump CameraUpdateMode = CameraUpdateMode(C.MLN_CAMERA_UPDATE_MODE_JUMP) + CameraUpdateModeEase CameraUpdateMode = CameraUpdateMode(C.MLN_CAMERA_UPDATE_MODE_EASE) + CameraUpdateModeFly CameraUpdateMode = CameraUpdateMode(C.MLN_CAMERA_UPDATE_MODE_FLY) +) + +// GesturePhase marks a gesture boundary carried with an atomic camera update. +type GesturePhase uint32 + +const ( + GesturePhaseNone GesturePhase = GesturePhase(C.MLN_GESTURE_PHASE_NONE) + GesturePhaseBegin GesturePhase = GesturePhase(C.MLN_GESTURE_PHASE_BEGIN) + GesturePhaseUpdate GesturePhase = GesturePhase(C.MLN_GESTURE_PHASE_UPDATE) + GesturePhaseEnd GesturePhase = GesturePhase(C.MLN_GESTURE_PHASE_END) + GesturePhaseCancel GesturePhase = GesturePhase(C.MLN_GESTURE_PHASE_CANCEL) +) + +// CameraUpdate is one atomic absolute camera update. +type CameraUpdate struct { + Mode CameraUpdateMode + Camera CameraOptions + Animation *AnimationOptions + GesturePhase GesturePhase +} + +func cCameraUpdate(update CameraUpdate) C.mln_camera_update { + raw := C.mln_camera_update_default() + raw.mode = C.uint32_t(update.Mode) + raw.camera = cCameraOptions(update.Camera) + if update.Animation != nil { + raw.animation = cAnimationOptions(*update.Animation) } - raw := cAnimationOptions(*options) - return raw, &raw + raw.gesture_phase = C.uint32_t(update.GesturePhase) + return raw +} + +// CameraDeltaKind selects one relative camera operation. +type CameraDeltaKind uint32 + +const ( + CameraDeltaKindMove CameraDeltaKind = CameraDeltaKind(C.MLN_CAMERA_DELTA_MOVE) + CameraDeltaKindScale CameraDeltaKind = CameraDeltaKind(C.MLN_CAMERA_DELTA_SCALE) + CameraDeltaKindBearing CameraDeltaKind = CameraDeltaKind(C.MLN_CAMERA_DELTA_BEARING) + CameraDeltaKindPitch CameraDeltaKind = CameraDeltaKind(C.MLN_CAMERA_DELTA_PITCH) +) + +// CameraDelta describes one relative camera operation. +type CameraDelta struct { + Kind CameraDeltaKind + Offset ScreenPoint + Amount float64 + Anchor *ScreenPoint + Animation *AnimationOptions +} + +func cCameraDelta(delta CameraDelta) C.mln_camera_delta { + raw := C.mln_camera_delta_default() + raw.kind = C.uint32_t(delta.Kind) + raw.offset = C.mln_screen_point{x: C.double(delta.Offset.X), y: C.double(delta.Offset.Y)} + raw.amount = C.double(delta.Amount) + if delta.Anchor != nil { + raw.has_anchor = true + raw.anchor = C.mln_screen_point{x: C.double(delta.Anchor.X), y: C.double(delta.Anchor.Y)} + } + if delta.Animation != nil { + raw.animation = cAnimationOptions(*delta.Animation) + } + return raw } // CameraFitOptions configures camera fitting queries. diff --git a/bindings/go/camera_test.go b/bindings/go/camera_test.go index 077e1c0d1..221a9c2cb 100644 --- a/bindings/go/camera_test.go +++ b/bindings/go/camera_test.go @@ -1,641 +1,137 @@ package maplibre import ( - "errors" + "context" "math" + "sync" "testing" "time" ) -func TestMapCameraCommandsUseNativeABI(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - camera := CameraOptions{}. - WithCenter(LatLng{Latitude: 10, Longitude: 20}). - WithZoom(2). - WithBearing(15). - WithPitch(20) - if err := m.JumpTo(camera); err != nil { - t.Fatalf("JumpTo(): %v", err) - } - gotCamera, err := m.Camera() - if err != nil { - t.Fatalf("Camera(): %v", err) - } - if gotCamera.Center == nil || gotCamera.Zoom == nil { - t.Fatalf("Camera() missing expected fields: %#v", gotCamera) - } - // BND-070: successive snapshots of an unchanged camera compare equal. - secondCamera, err := m.Camera() - if err != nil { - t.Fatalf("Camera(): %v", err) - } - if !gotCamera.Equal(secondCamera) { - t.Errorf("successive camera snapshots differ: %#v vs %#v", gotCamera, secondCamera) - } - if err := m.MoveBy(ScreenPoint{X: 1, Y: 2}); err != nil { - t.Fatalf("MoveBy(): %v", err) - } - anchor := ScreenPoint{X: 256, Y: 256} - if err := m.ScaleBy(1.1, &anchor); err != nil { - t.Fatalf("ScaleBy(): %v", err) - } - if err := m.RotateBy(ScreenPoint{X: 100, Y: 100}, ScreenPoint{X: 120, Y: 110}); err != nil { - t.Fatalf("RotateBy(): %v", err) - } - if err := m.PitchBy(1); err != nil { - t.Fatalf("PitchBy(): %v", err) - } - if err := m.CancelTransitions(); err != nil { - t.Fatalf("CancelTransitions(): %v", err) - } - - if got, err := m.IsGestureInProgress(); err != nil || got { - t.Fatalf("IsGestureInProgress() = %v, %v; want false, nil", got, err) - } - if err := m.SetGestureInProgress(true); err != nil { - t.Fatalf("SetGestureInProgress(true): %v", err) - } - if err := m.MoveBy(ScreenPoint{X: 8, Y: -4}); err != nil { - t.Fatalf("MoveBy(): %v", err) - } - if got, err := m.IsGestureInProgress(); err != nil || !got { - t.Fatalf("IsGestureInProgress() = %v, %v; want true, nil", got, err) - } - if err := m.SetGestureInProgress(false); err != nil { - t.Fatalf("SetGestureInProgress(false): %v", err) - } - if got, err := m.IsGestureInProgress(); err != nil || got { - t.Fatalf("IsGestureInProgress() = %v, %v; want false, nil", got, err) - } -} - -func TestMapAnimatedCameraCommandsUseOptionalAnimationOptions(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.CancelTransitions(); err != nil { - t.Errorf("CancelTransitions(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - camera := CameraOptions{}. - WithCenter(LatLng{Latitude: 1, Longitude: 2}). - WithZoom(1) - animation := AnimationOptions{}. - WithDurationMS(0). - WithVelocity(1.2). - WithMinZoom(0). - WithEasing(UnitBezier{X1: 0.25, Y1: 0.1, X2: 0.25, Y2: 1}) - if err := m.EaseTo(camera, &animation); err != nil { - t.Fatalf("EaseTo(): %v", err) - } - if err := m.FlyTo(camera, nil); err != nil { - t.Fatalf("FlyTo(nil animation): %v", err) - } - if err := m.MoveByAnimated(ScreenPoint{X: 1, Y: 1}, &animation); err != nil { - t.Fatalf("MoveByAnimated(): %v", err) - } - anchor := ScreenPoint{X: 256, Y: 256} - if err := m.ScaleByAnimated(1.01, &anchor, nil); err != nil { - t.Fatalf("ScaleByAnimated(nil animation): %v", err) - } - if err := m.RotateByAnimated(ScreenPoint{X: 100, Y: 100}, ScreenPoint{X: 110, Y: 100}, &animation); err != nil { - t.Fatalf("RotateByAnimated(): %v", err) - } - if err := m.PitchByAnimated(0.5, &animation); err != nil { - t.Fatalf("PitchByAnimated(): %v", err) - } -} - -type cameraTransitionTally struct { - finishedIDs []uint64 - sawDidChange bool - lastDidChangeCode int32 -} - -func drainCameraTransitionEvents(t *testing.T, runtime *RuntimeHandle) cameraTransitionTally { - t.Helper() - var tally cameraTransitionTally - for { - batch, err := runtime.DrainEvents(0) - if err != nil { - t.Fatalf("DrainEvents(): %v", err) - } - if len(batch.Events) == 0 { - return tally - } - for _, event := range batch.Events { - switch event.Type { - case RuntimeEventMapCameraTransitionFinished: - if event.PayloadType != RuntimeEventPayloadCameraTransitionFinished { - t.Fatalf("transition-finished payload type = %v, want %v", event.PayloadType, RuntimeEventPayloadCameraTransitionFinished) - } - payload, ok := event.Payload.(RuntimeEventCameraTransitionFinishedPayload) - if !ok { - t.Fatalf("transition-finished payload type = %T, want RuntimeEventCameraTransitionFinishedPayload", event.Payload) - } - tally.finishedIDs = append(tally.finishedIDs, payload.TransitionID) - case RuntimeEventMapCameraDidChange: - tally.sawDidChange = true - tally.lastDidChangeCode = event.Code - } - } - } -} - -func TestMapCameraTransitionIDReportsEachTerminalOutcomeOnce(t *testing.T) { - lockOSThreadForTest(t) - +func TestCameraSnapshotAndOrderedQueryCopyValues(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.CancelTransitions(); err != nil { - t.Errorf("CancelTransitions(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - camera := CameraOptions{}.WithCenter(LatLng{Latitude: 1, Longitude: 2}).WithZoom(11) - - // A zero-duration ease applies instantly, so it reports its end before the - // immediate did-change event it produced. - instant := AnimationOptions{}.WithDurationMS(0).WithTransitionID(7) - if err := m.EaseTo(camera, &instant); err != nil { - t.Fatalf("EaseTo(zero duration): %v", err) - } - tally := drainCameraTransitionEvents(t, runtime) - if len(tally.finishedIDs) != 1 || tally.finishedIDs[0] != 7 { - t.Fatalf("zero-duration ease finished IDs = %v, want [7]", tally.finishedIDs) - } - if !tally.sawDidChange || CameraChangeMode(tally.lastDidChangeCode) != CameraChangeModeImmediate { - t.Fatalf("zero-duration did-change code = %d, want %d", tally.lastDidChangeCode, CameraChangeModeImmediate) - } - - running := AnimationOptions{}.WithDurationMS(5000).WithTransitionID(11) - if err := m.EaseTo(camera.WithZoom(12), &running); err != nil { - t.Fatalf("EaseTo(running transition): %v", err) - } - tally = drainCameraTransitionEvents(t, runtime) - if len(tally.finishedIDs) != 0 { - t.Fatalf("running transition finished IDs = %v, want none", tally.finishedIDs) - } - - // A later camera command supersedes the running transition, which reports - // its end alongside an animated did-change event. - superseding := AnimationOptions{}.WithDurationMS(5000).WithTransitionID(12) - if err := m.EaseTo(camera.WithZoom(13), &superseding); err != nil { - t.Fatalf("EaseTo(superseding transition): %v", err) - } - tally = drainCameraTransitionEvents(t, runtime) - if len(tally.finishedIDs) != 1 || tally.finishedIDs[0] != 11 { - t.Fatalf("superseded transition finished IDs = %v, want [11]", tally.finishedIDs) - } - if !tally.sawDidChange || CameraChangeMode(tally.lastDidChangeCode) != CameraChangeModeAnimated { - t.Fatalf("superseded did-change code = %d, want %d", tally.lastDidChangeCode, CameraChangeModeAnimated) - } - - if err := m.CancelTransitions(); err != nil { - t.Fatalf("CancelTransitions(): %v", err) - } - tally = drainCameraTransitionEvents(t, runtime) - if len(tally.finishedIDs) != 1 || tally.finishedIDs[0] != 12 { - t.Fatalf("cancelled transition finished IDs = %v, want [12]", tally.finishedIDs) - } - - silent := AnimationOptions{}.WithDurationMS(0) - if err := m.EaseTo(camera.WithZoom(14), &silent); err != nil { - t.Fatalf("EaseTo(absent transition ID): %v", err) - } - tally = drainCameraTransitionEvents(t, runtime) - if len(tally.finishedIDs) != 0 { - t.Fatalf("absent transition ID finished IDs = %v, want none", tally.finishedIDs) - } -} - -func TestMapCameraTransitionIDReportsCompletionOnceWhenPumped(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.CancelTransitions(); err != nil { - t.Errorf("CancelTransitions(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - camera := CameraOptions{}.WithCenter(LatLng{Latitude: 3, Longitude: 4}).WithZoom(6) - animation := AnimationOptions{}.WithDurationMS(50).WithTransitionID(21) - if err := m.EaseTo(camera, &animation); err != nil { - t.Fatalf("EaseTo(): %v", err) - } - - // A transition only advances when the map updates; a repaint request drives - // that without touching the camera. - var finishedIDs []uint64 - deadline := time.Now().Add(10 * time.Second) - for len(finishedIDs) == 0 && time.Now().Before(deadline) { - if err := m.RequestRepaint(); err != nil { - t.Fatalf("RequestRepaint(): %v", err) - } - tally := drainCameraTransitionEvents(t, runtime) - finishedIDs = append(finishedIDs, tally.finishedIDs...) - time.Sleep(time.Millisecond) - } - if len(finishedIDs) != 1 || finishedIDs[0] != 21 { - t.Fatalf("completed transition finished IDs = %v, want [21]", finishedIDs) + t.Fatalf("NewMap(): %v", err) } + defer closeRuntimeForTest(runtime) + defer m.Close() - for range make([]struct{}, 50) { - if err := m.RequestRepaint(); err != nil { - t.Fatalf("RequestRepaint(): %v", err) - } - if tally := drainCameraTransitionEvents(t, runtime); len(tally.finishedIDs) != 0 { - t.Fatalf("repeated transition-finished IDs = %v, want none", tally.finishedIDs) - } - time.Sleep(time.Millisecond) - } -} - -func TestMapCameraFitAndBoundsHelpers(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() + command, err := m.JumpTo(CameraOptions{}.WithCenter(LatLng{Latitude: 12, Longitude: 34}).WithZoom(4)) if err != nil { - t.Fatalf("NewRuntime(): %v", err) + t.Fatalf("JumpTo(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) + if _, err := awaitForTest(command, err); err != nil { + t.Fatalf("JumpTo completion: %v", err) } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - bounds := LatLngBounds{ - Southwest: LatLng{Latitude: -10, Longitude: -20}, - Northeast: LatLng{Latitude: 10, Longitude: 20}, - } - fitOptions := CameraFitOptions{}. - WithPadding(EdgeInsets{Top: 4, Left: 3, Bottom: 2, Right: 1}). - WithBearing(0). - WithPitch(0) - camera, err := m.CameraForLatLngBounds(bounds, &fitOptions) - if err != nil { - t.Fatalf("CameraForLatLngBounds(): %v", err) - } - if camera.Center == nil || camera.Zoom == nil { - t.Fatalf("CameraForLatLngBounds() missing expected fields: %#v", camera) - } - camera, err = m.CameraForLatLngs([]LatLng{bounds.Southwest, bounds.Northeast}, nil) + operation, err := m.QueryCamera() if err != nil { - t.Fatalf("CameraForLatLngs(): %v", err) + t.Fatalf("QueryCamera(): %v", err) } - wrapped, err := m.LatLngBoundsForCamera(camera) + ordered, err := awaitForTest(operation, nil) if err != nil { - t.Fatalf("LatLngBoundsForCamera(): %v", err) + t.Fatalf("Take(): %v", err) } - if wrapped.Southwest.Latitude > wrapped.Northeast.Latitude { - t.Fatalf("LatLngBoundsForCamera() inverted latitude bounds: %#v", wrapped) - } - if _, err := m.LatLngBoundsForCameraUnwrapped(camera); err != nil { - t.Fatalf("LatLngBoundsForCameraUnwrapped(): %v", err) + if ordered.Camera.Center == nil || + math.Abs(ordered.Camera.Center.Latitude-12) > 1e-9 || + math.Abs(ordered.Camera.Center.Longitude-34) > 1e-9 { + t.Fatalf("ordered camera center = %#v", ordered.Camera.Center) } - constraints := BoundOptions{}. - WithBounds(bounds). - WithMinZoom(0). - WithMaxZoom(20). - WithMinPitch(0). - WithMaxPitch(60) - if err := m.SetBounds(constraints); err != nil { - t.Fatalf("SetBounds(): %v", err) - } - gotConstraints, err := m.Bounds() + published, err := m.CameraSnapshot() if err != nil { - t.Fatalf("Bounds(): %v", err) + t.Fatalf("CameraSnapshot(): %v", err) } - if gotConstraints.Bounds == nil || - gotConstraints.Bounds.Kind != BoundsConstraintBounded || - gotConstraints.Bounds.Bounds != bounds { - t.Fatalf("Bounds().Bounds = %#v, want bounded %#v", gotConstraints.Bounds, bounds) + if published.Camera.Center == nil { + t.Fatal("CameraSnapshot() omitted the center") } -} - -// The unbounded constraint is distinct from world bounds: an unbounded camera center pans across -// the antimeridian, while world bounds clamp longitude to the -180..180 range. -func TestMapBoundsSeparateUnboundedFromWorldBounds(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() + published.Camera.Center.Latitude = -80 + again, err := m.CameraSnapshot() if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - constraintKind := func(want BoundsConstraintKind) { - t.Helper() - got, err := m.Bounds() - if err != nil { - t.Fatalf("Bounds(): %v", err) - } - if got.Bounds == nil || got.Bounds.Kind != want { - t.Fatalf("Bounds().Bounds = %#v, want kind %v", got.Bounds, want) - } - } - jumpedLongitude := func(longitude float64) float64 { - t.Helper() - camera := CameraOptions{}. - WithCenter(LatLng{Latitude: 0, Longitude: longitude}). - WithZoom(2) - if err := m.JumpTo(camera); err != nil { - t.Fatalf("JumpTo(%v): %v", longitude, err) - } - snapshot, err := m.Camera() - if err != nil { - t.Fatalf("Camera(): %v", err) - } - if snapshot.Center == nil { - t.Fatalf("Camera() missing center: %#v", snapshot) - } - return snapshot.Center.Longitude - } - assertLongitude := func(got, want float64) { - t.Helper() - if math.Abs(got-want) > 1e-6 { - t.Fatalf("camera longitude = %v, want %v", got, want) - } - } - - constraintKind(BoundsConstraintUnbounded) - assertLongitude(jumpedLongitude(200), -160) - - world := LatLngBounds{ - Southwest: LatLng{Latitude: -90, Longitude: -180}, - Northeast: LatLng{Latitude: 90, Longitude: 180}, + t.Fatalf("CameraSnapshot(): %v", err) } - if err := m.SetBounds(BoundOptions{}.WithBounds(world)); err != nil { - t.Fatalf("SetBounds(world): %v", err) + if again.Camera.Center == nil || math.Abs(again.Camera.Center.Latitude-12) > 1e-9 { + t.Fatalf("snapshot mutation affected native state: %#v", again.Camera.Center) } - constraintKind(BoundsConstraintBounded) - assertLongitude(jumpedLongitude(200), 180) - - if err := m.SetBounds(BoundOptions{}.WithUnbounded()); err != nil { - t.Fatalf("SetBounds(unbounded): %v", err) - } - constraintKind(BoundsConstraintUnbounded) - assertLongitude(jumpedLongitude(200), -160) } -func TestMapFreeCameraOptionsRoundTripCurrentValues(t *testing.T) { - lockOSThreadForTest(t) - +func TestCameraCommandsAcceptConcurrentGoroutines(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - freeCamera, err := m.FreeCameraOptions() + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(256, 256, 1))) if err != nil { - t.Fatalf("FreeCameraOptions(): %v", err) + t.Fatalf("NewMap(): %v", err) } - if freeCamera.Position == nil || freeCamera.Orientation == nil { - t.Fatalf("FreeCameraOptions() missing expected fields: %#v", freeCamera) + defer closeRuntimeForTest(runtime) + defer m.Close() + + const count = 16 + generations := make(chan uint64, count) + errs := make(chan error, count) + var wg sync.WaitGroup + for i := 0; i < count; i++ { + wg.Add(1) + go func(zoom int) { + defer wg.Done() + future, err := m.JumpTo(CameraOptions{}.WithZoom(float64(zoom))) + if err != nil { + errs <- err + return + } + completion, err := future.Await(context.Background()) + if err != nil { + errs <- err + return + } + generations <- completion.Generation + }(i) } - if err := m.SetFreeCameraOptions(FreeCameraOptions{}. - WithPosition(*freeCamera.Position). - WithOrientation(*freeCamera.Orientation)); err != nil { - t.Fatalf("SetFreeCameraOptions(current values): %v", err) + wg.Wait() + close(generations) + close(errs) + for err := range errs { + t.Errorf("JumpTo() from goroutine: %v", err) } -} - -func TestMapCameraCommandsReportNativeValidation(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMap() - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMap(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) + seen := make(map[uint64]struct{}, count) + for generation := range generations { + if generation == 0 { + t.Error("JumpTo() returned a zero generation") } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) + if _, exists := seen[generation]; exists { + t.Errorf("duplicate generation %d", generation) } - }() - - if err := m.ScaleBy(0, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("ScaleBy(0) error = %v, want ErrInvalidArgument", err) - } - invalidAnimation := AnimationOptions{}.WithDurationMS(-1) - if err := m.MoveByAnimated(ScreenPoint{}, &invalidAnimation); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("MoveByAnimated(invalid animation) error = %v, want ErrInvalidArgument", err) - } - if _, err := m.CameraForLatLngs(nil, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("CameraForLatLngs(nil) error = %v, want ErrInvalidArgument", err) - } - invalidBounds := BoundOptions{}.WithMinZoom(3).WithMaxZoom(2) - if err := m.SetBounds(invalidBounds); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetBounds(invalid min/max) error = %v, want ErrInvalidArgument", err) + seen[generation] = struct{}{} } - // An unknown kind sets neither constraint bit, so without validation the - // call would report success and leave the existing constraint in place. - unknownKind := BoundOptions{Bounds: &BoundsConstraint{Kind: BoundsConstraintKind(99)}} - if err := m.SetBounds(unknownKind); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetBounds(unknown kind) error = %v, want ErrInvalidArgument", err) - } - invalidFreeCamera := FreeCameraOptions{}.WithOrientation(Quaternion{}) - if err := m.SetFreeCameraOptions(invalidFreeCamera); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetFreeCameraOptions(invalid orientation) error = %v, want ErrInvalidArgument", err) + if len(seen) != count { + t.Fatalf("committed generations = %d, want %d", len(seen), count) } } -func TestMapViewportTileAndProjectionOptionsRoundTrip(t *testing.T) { - lockOSThreadForTest(t) - +func TestCameraOperationProgressesAutonomously(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(64, 64, 1))) if err != nil { - _ = runtime.Close() t.Fatalf("NewMap(): %v", err) } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() + defer closeRuntimeForTest(runtime) + defer m.Close() - viewport := ViewportOptions{}. - WithNorthOrientation(NorthOrientationUp). - WithConstrainMode(ConstrainModeWidthAndHeight). - WithViewportMode(ViewportModeDefault). - WithFrustumOffset(EdgeInsets{Top: 1, Left: 2, Bottom: 3, Right: 4}) - if err := m.SetViewportOptions(viewport); err != nil { - t.Fatalf("SetViewportOptions(): %v", err) - } - gotViewport, err := m.ViewportOptions() + operation, err := m.QueryCamera() if err != nil { - t.Fatalf("ViewportOptions(): %v", err) - } - if gotViewport.NorthOrientation == nil || *gotViewport.NorthOrientation != NorthOrientationUp { - t.Fatalf("ViewportOptions().NorthOrientation = %v", gotViewport.NorthOrientation) + t.Fatalf("QueryCamera(): %v", err) } - if gotViewport.FrustumOffset == nil || *gotViewport.FrustumOffset != (EdgeInsets{Top: 1, Left: 2, Bottom: 3, Right: 4}) { - t.Fatalf("ViewportOptions().FrustumOffset = %#v", gotViewport.FrustumOffset) - } - - tileOptions := TileOptions{}. - WithPrefetchZoomDelta(2). - WithLODMode(TileLODModeDefault) - if err := m.SetTileOptions(tileOptions); err != nil { - t.Fatalf("SetTileOptions(): %v", err) - } - gotTileOptions, err := m.TileOptions() - if err != nil { - t.Fatalf("TileOptions(): %v", err) - } - if gotTileOptions.PrefetchZoomDelta == nil || *gotTileOptions.PrefetchZoomDelta != 2 { - t.Fatalf("TileOptions().PrefetchZoomDelta = %v", gotTileOptions.PrefetchZoomDelta) - } - - projectionMode := ProjectionModeOptions{}. - WithAxonometric(true). - WithSkew(0.5, 0.25) - if err := m.SetProjectionMode(projectionMode); err != nil { - t.Fatalf("SetProjectionMode(): %v", err) - } - gotProjectionMode, err := m.ProjectionMode() - if err != nil { - t.Fatalf("ProjectionMode(): %v", err) - } - if gotProjectionMode.Axonometric == nil || !*gotProjectionMode.Axonometric { - t.Fatalf("ProjectionMode().Axonometric = %v", gotProjectionMode.Axonometric) - } -} - -func TestTileOptionsRejectInvalidPrefetchZoomDelta(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMap() - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMap(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - if err := m.SetTileOptions(TileOptions{}.WithPrefetchZoomDelta(256)); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetTileOptions(invalid prefetch) error = %v, want ErrInvalidArgument", err) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if _, err := operation.Await(ctx); err != nil { + t.Fatalf("Await(): %v", err) } } diff --git a/bindings/go/completion.go b/bindings/go/completion.go new file mode 100644 index 000000000..9ea5bbb1e --- /dev/null +++ b/bindings/go/completion.go @@ -0,0 +1,226 @@ +package maplibre + +/* +#include "internal/cgo_completion_shim.h" +*/ +import "C" + +import ( + "context" + "runtime/cgo" + "sync" + "unsafe" + + internalstatus "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/status" +) + +// CommandCompletion describes the terminal outcome of an ordered map command. +type CommandCompletion struct { + Disposition CommandDisposition + Generation uint64 + RawStatus int32 + Diagnostic string +} + +type futureResult[T any] struct { + value T + err error +} + +type futureState[T any] struct { + ready chan struct{} + mu sync.Mutex + result futureResult[T] + retained any + completed bool +} + +// Future is a one-shot native result. Await may be called from any goroutine. +type Future[T any] struct { + state *futureState[T] +} + +// completedFuture returns a future that already carries value, for work the +// binding satisfied without a native submission. +func completedFuture[T any](value T) *Future[T] { + state := &futureState[T]{ready: make(chan struct{}), completed: true} + state.result = futureResult[T]{value: value} + close(state.ready) + return &Future[T]{state: state} +} + +func (future *Future[T]) retain(value any) { + future.state.mu.Lock() + defer future.state.mu.Unlock() + if !future.state.completed { + future.state.retained = value + } +} + +// Done closes when native has delivered the terminal result. It lets a host +// service another loop without blocking in Await. +func (future *Future[T]) Done() <-chan struct{} { + if future == nil || future.state == nil { + closed := make(chan struct{}) + close(closed) + return closed + } + return future.state.ready +} + +// Await blocks until native completes the work or the context is cancelled. +func (future *Future[T]) Await(ctx context.Context) (T, error) { + var zero T + if future == nil || future.state == nil { + return zero, newBindingError(ErrInvalidArgument, "Future is nil") + } + select { + case <-future.state.ready: + future.state.mu.Lock() + defer future.state.mu.Unlock() + return future.state.result.value, future.state.result.err + case <-ctx.Done(): + return zero, ctx.Err() + } +} + +type completionReceiver interface { + complete(*C.mln_completion_result) + release() +} + +type completionBridge[T any] struct { + state *futureState[T] + convert func(*C.mln_completion_result) (T, error) +} + +func (bridge *completionBridge[T]) complete(raw *C.mln_completion_result) { + var result futureResult[T] + _, isCommand := any(*new(T)).(CommandCompletion) + if raw == nil { + result.err = newBindingError(ErrInvalidState, "native completion returned nil") + } else if raw.status != C.MLN_STATUS_OK && !isCommand { + diagnostic := "" + if raw.diagnostic.data != nil && raw.diagnostic.size != 0 { + diagnostic = string(unsafe.Slice((*byte)(raw.diagnostic.data), int(raw.diagnostic.size))) + } + result.err = newStatusError(&internalstatus.NativeError{ + Status: int32(raw.status), Diagnostic: diagnostic, + }) + } else { + result.value, result.err = bridge.convert(raw) + } + bridge.state.mu.Lock() + bridge.state.result = result + bridge.state.completed = true + bridge.state.mu.Unlock() + close(bridge.state.ready) +} + +func (bridge *completionBridge[T]) release() { + bridge.state.mu.Lock() + bridge.state.retained = nil + bridge.state.mu.Unlock() +} + +func startCompletion[T any]( + start func(*C.mln_completion) int32, + convert func(*C.mln_completion_result) (T, error), +) (*Future[T], error) { + state := &futureState[T]{ready: make(chan struct{})} + bridge := &completionBridge[T]{state: state, convert: convert} + handle := cgo.NewHandle(completionReceiver(bridge)) + completion := C.mln_go_make_completion_from_handle(C.uintptr_t(handle)) + if err := checkNative(func() int32 { return start(&completion) }); err != nil { + handle.Delete() + return nil, err + } + return &Future[T]{state: state}, nil +} + +func completionUnit(result *C.mln_completion_result) (struct{}, error) { + if result.value != nil || result.value_count != 0 { + return struct{}{}, newBindingError(ErrInvalidState, "unit completion returned a value") + } + return struct{}{}, nil +} + +func completionCommand(result *C.mln_completion_result) (CommandCompletion, error) { + if _, err := completionUnit(result); err != nil { + return CommandCompletion{}, err + } + return CommandCompletion{ + Disposition: CommandDisposition(result.disposition), + Generation: uint64(result.generation), + RawStatus: int32(result.status), + Diagnostic: completionDiagnostic(result), + }, nil +} + +func completionDiagnostic(result *C.mln_completion_result) string { + if result.diagnostic.data == nil || result.diagnostic.size == 0 { + return "" + } + return string(unsafe.Slice((*byte)(result.diagnostic.data), int(result.diagnostic.size))) +} + +func completionValue[T any](result *C.mln_completion_result) (T, error) { + var zero T + if result.value == nil || result.value_count != 1 { + return zero, newBindingError(ErrInvalidState, "native completion returned no value") + } + return *(*T)(result.value), nil +} + +func completionSlice[T any](result *C.mln_completion_result) ([]T, error) { + if result.value_count == 0 { + return []T{}, nil + } + if result.value == nil { + return nil, newBindingError(ErrInvalidState, "native completion returned a null slice") + } + return append([]T(nil), unsafe.Slice((*T)(result.value), int(result.value_count))...), nil +} + +func completionBuffer(result *C.mln_completion_result) ([]byte, error) { + view, err := completionValue[C.mln_buffer_view](result) + if err != nil { + return nil, err + } + if view.size == 0 { + return nil, nil + } + if view.data == nil { + return nil, newBindingError(ErrInvalidState, "native completion returned a null buffer") + } + return append([]byte(nil), unsafe.Slice((*byte)(view.data), int(view.size))...), nil +} + +func completionNullableBuffer(result *C.mln_completion_result) ([]byte, error) { + if result.value_count == 0 { + return nil, nil + } + return completionBuffer(result) +} + +//export mln_go_completion_callback +func mln_go_completion_callback(userData unsafe.Pointer, result *C.mln_completion_result) { + if userData == nil { + return + } + receiver, ok := cgo.Handle(uintptr(userData)).Value().(completionReceiver) + if ok { + receiver.complete(result) + } +} + +//export mln_go_completion_release +func mln_go_completion_release(userData unsafe.Pointer) { + if userData != nil { + handle := cgo.Handle(uintptr(userData)) + if receiver, ok := handle.Value().(completionReceiver); ok { + receiver.release() + } + handle.Delete() + } +} diff --git a/bindings/go/completion_test.go b/bindings/go/completion_test.go new file mode 100644 index 000000000..bb2834d96 --- /dev/null +++ b/bindings/go/completion_test.go @@ -0,0 +1,56 @@ +package maplibre + +import ( + "runtime" + "runtime/cgo" + "testing" + "time" +) + +type completionRetentionProbe struct { + finalized chan struct{} +} + +func TestCompletionBridgeRetainsValueAfterFutureIsDropped(t *testing.T) { + finalized := make(chan struct{}) + probe := &completionRetentionProbe{finalized: finalized} + runtime.SetFinalizer(probe, func(probe *completionRetentionProbe) { + close(probe.finalized) + }) + + state := &futureState[struct{}]{ready: make(chan struct{})} + bridge := &completionBridge[struct{}]{state: state} + handle := cgo.NewHandle(completionReceiver(bridge)) + future := &Future[struct{}]{state: state} + future.retain(probe) + + probe = nil + future = nil + state = nil + bridge = nil + runtime.GC() + select { + case <-finalized: + t.Fatal("retained value was finalized while native still owned the completion bridge") + default: + } + + receiver := handle.Value().(completionReceiver) + receiver.release() + handle.Delete() + receiver = nil + + deadline := time.Now().Add(5 * time.Second) + for { + runtime.GC() + select { + case <-finalized: + return + default: + if time.Now().After(deadline) { + t.Fatal("retained value was not released with the native completion bridge") + } + runtime.Gosched() + } + } +} diff --git a/bindings/go/custom_geometry_test.go b/bindings/go/custom_geometry_test.go index d3c533259..2b21b79f0 100644 --- a/bindings/go/custom_geometry_test.go +++ b/bindings/go/custom_geometry_test.go @@ -23,19 +23,17 @@ func liveCustomGeometrySources(t *testing.T, baseline int64) int64 { return callback.CustomGeometrySourceLiveCountForTest() - baseline } -// loadStyleAndCollect loads an inline style and pumps until the map settles, +// loadStyleAndCollect loads an inline style and waits until the map settles, // returning every event it drained. func loadStyleAndCollect(t *testing.T, runtime *RuntimeHandle, m *MapHandle, style string) []RuntimeEvent { t.Helper() - if err := m.SetStyleJSON([]byte(style)); err != nil { + if _, err := m.SetStyleJSON([]byte(style)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } var events []RuntimeEvent for range make([]struct{}, 200) { - if err := runtime.Pump(2*time.Millisecond, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } @@ -58,7 +56,7 @@ func TestCustomGeometrySourceDescriptors(t *testing.T) { wrap := false fetches := 0 cancels := 0 - if err := m.AddCustomGeometrySource("custom", CustomGeometrySourceOptions{ + if _, err := m.AddCustomGeometrySource("custom", CustomGeometrySourceOptions{ FetchTile: func(CanonicalTileID) { fetches++ }, CancelTile: func(CanonicalTileID) { cancels++ }, MinZoom: &minZoom, @@ -75,28 +73,23 @@ func TestCustomGeometrySourceDescriptors(t *testing.T) { t.Fatalf("callbacks invoked during registration: fetches=%d cancels=%d", fetches, cancels) } tileID := CanonicalTileID{Z: 0, X: 0, Y: 0} - if err := m.SetCustomGeometrySourceTileData("custom", tileID, []byte(`{"type":"FeatureCollection","features":[]}`)); err != nil { + if _, err := m.SetCustomGeometrySourceTileData("custom", tileID, []byte(`{"type":"FeatureCollection","features":[]}`)); err != nil { t.Fatalf("SetCustomGeometrySourceTileData(): %v", err) } - if err := m.InvalidateCustomGeometrySourceTile("custom", tileID); err != nil { + if _, err := m.InvalidateCustomGeometrySourceTile("custom", tileID); err != nil { t.Fatalf("InvalidateCustomGeometrySourceTile(): %v", err) } - if err := m.InvalidateCustomGeometrySourceRegion("custom", LatLngBounds{Southwest: LatLng{Latitude: -1, Longitude: -1}, Northeast: LatLng{Latitude: 1, Longitude: 1}}); err != nil { + if _, err := m.InvalidateCustomGeometrySourceRegion("custom", LatLngBounds{Southwest: LatLng{Latitude: -1, Longitude: -1}, Northeast: LatLng{Latitude: 1, Longitude: 1}}); err != nil { t.Fatalf("InvalidateCustomGeometrySourceRegion(): %v", err) } - removed, err := m.RemoveStyleSource("custom") - if err != nil { - t.Fatalf("RemoveStyleSource(custom): %v", err) - } - if !removed { - t.Fatal("RemoveStyleSource(custom) removed=false, want true") - } - if err := m.AddCustomGeometrySource("bad-custom", CustomGeometrySourceOptions{}); !errors.Is(err, ErrInvalidArgument) { + removeID, err := m.RemoveStyleSource("custom") + requireCommandCommitted(t, runtime, removeID, err) + if _, err := m.AddCustomGeometrySource("bad-custom", CustomGeometrySourceOptions{}); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("AddCustomGeometrySource(nil fetch) error = %v, want ErrInvalidArgument", err) } // A native add the style rejects owes no release callback, so the binding // frees the state it built for it before returning. - if err := m.AddCustomGeometrySource("", CustomGeometrySourceOptions{ + if _, err := m.AddCustomGeometrySource("", CustomGeometrySourceOptions{ FetchTile: func(CanonicalTileID) {}, }); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("AddCustomGeometrySource(empty ID) error = %v, want ErrInvalidArgument", err) @@ -117,7 +110,7 @@ func TestCustomGeometrySourceReleasedWhenStyleLoadDropsIt(t *testing.T) { baseline := callback.CustomGeometrySourceLiveCountForTest() loadStyleAndCollect(t, runtime, m, backgroundStyleJSON) - if err := m.AddCustomGeometrySource("custom", CustomGeometrySourceOptions{ + if _, err := m.AddCustomGeometrySource("custom", CustomGeometrySourceOptions{ FetchTile: func(CanonicalTileID) {}, }); err != nil { t.Fatalf("AddCustomGeometrySource(): %v", err) @@ -145,15 +138,14 @@ func TestCustomGeometrySourceReleasedByRemovalAndMapClose(t *testing.T) { loadStyleAndCollect(t, runtime, m, backgroundStyleJSON) for _, sourceID := range []string{"removed", "surviving"} { - if err := m.AddCustomGeometrySource(sourceID, CustomGeometrySourceOptions{ + if _, err := m.AddCustomGeometrySource(sourceID, CustomGeometrySourceOptions{ FetchTile: func(CanonicalTileID) {}, }); err != nil { t.Fatalf("AddCustomGeometrySource(%s): %v", sourceID, err) } } - if removed, err := m.RemoveStyleSource("removed"); err != nil || !removed { - t.Fatalf("RemoveStyleSource(removed) = (%v, %v), want (true, nil)", removed, err) - } + removeID, err := m.RemoveStyleSource("removed") + requireCommandCommitted(t, runtime, removeID, err) if live := liveCustomGeometrySources(t, baseline); live != 1 { t.Fatalf("live callback states after the removal = %d, want 1", live) } @@ -162,6 +154,7 @@ func TestCustomGeometrySourceReleasedByRemovalAndMapClose(t *testing.T) { if err := m.Close(); err != nil { t.Fatalf("Map Close(): %v", err) } + waitForRuntimeBarrier(t, runtime) if live := liveCustomGeometrySources(t, baseline); live != 0 { t.Fatalf("live callback states after the map close = %d, want 0", live) } diff --git a/bindings/go/custom_mvt_vector_test.go b/bindings/go/custom_mvt_vector_test.go index dfae47ffe..ef4649365 100644 --- a/bindings/go/custom_mvt_vector_test.go +++ b/bindings/go/custom_mvt_vector_test.go @@ -22,7 +22,7 @@ func TestCustomMVTVectorSourceDescriptors(t *testing.T) { maxZoom := 2.0 fetches := 0 cancels := 0 - if err := m.AddCustomMVTVectorSource("custom-mvt", CustomMVTVectorSourceOptions{ + if _, err := m.AddCustomMVTVectorSource("custom-mvt", CustomMVTVectorSourceOptions{ FetchTile: func(CanonicalTileID) { fetches++ }, CancelTile: func(CanonicalTileID) { cancels++ }, MinZoom: &minZoom, @@ -34,30 +34,31 @@ func TestCustomMVTVectorSourceDescriptors(t *testing.T) { t.Fatalf("callbacks invoked during registration: fetches=%d cancels=%d", fetches, cancels) } tileID := CanonicalTileID{Z: 0, X: 0, Y: 0} - if err := m.SetCustomMVTVectorSourceTileData("custom-mvt", tileID, nil); err != nil { + if _, err := m.SetCustomMVTVectorSourceTileData("custom-mvt", tileID, nil); err != nil { t.Fatalf("SetCustomMVTVectorSourceTileData(): %v", err) } - if err := m.SetCustomMVTVectorSourceTileError("custom-mvt", tileID, "tile missing"); err != nil { + if _, err := m.SetCustomMVTVectorSourceTileError("custom-mvt", tileID, "tile missing"); err != nil { t.Fatalf("SetCustomMVTVectorSourceTileError(): %v", err) } - if err := m.InvalidateCustomMVTVectorSourceTile("custom-mvt", tileID); err != nil { + if _, err := m.InvalidateCustomMVTVectorSourceTile("custom-mvt", tileID); err != nil { t.Fatalf("InvalidateCustomMVTVectorSourceTile(): %v", err) } - sourceType, found, err := m.StyleSourceType("custom-mvt") - if err != nil || !found || sourceType != StyleSourceTypeCustomMVTVector { - t.Fatalf("StyleSourceType(custom-mvt) = (%v, %v, %v), want CustomMVTVector true nil", sourceType, found, err) - } - if removed, err := m.RemoveStyleSource("custom-mvt"); err != nil || !removed { - t.Fatalf("RemoveStyleSource(custom-mvt) = (%v, %v), want (true, nil)", removed, err) + source, err := awaitForTest(m.StyleSourceInfo("custom-mvt")) + if err != nil || !source.Found || source.Value.Type != StyleSourceTypeCustomMVTVector { + t.Fatalf("StyleSourceInfo(custom-mvt) = (%#v, %v), want a found CustomMVTVector source", source, err) } + removeID, err := m.RemoveStyleSource("custom-mvt") + requireCommandCommitted(t, runtime, removeID, err) if live := liveCustomMVTVectorSources(t, baseline); live != 0 { t.Fatalf("live callback states after removal = %d, want 0", live) } - if err := m.AddCustomMVTVectorSource("bad-custom", CustomMVTVectorSourceOptions{}); !errors.Is(err, ErrInvalidArgument) { + if _, err := m.AddCustomMVTVectorSource("bad-custom", CustomMVTVectorSourceOptions{}); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("AddCustomMVTVectorSource(nil fetch) error = %v, want ErrInvalidArgument", err) } - if err := m.AddCustomMVTVectorSource("", CustomMVTVectorSourceOptions{ + // A native add the style rejects owes no release callback, so the binding + // frees the state it built for it before returning. + if _, err := m.AddCustomMVTVectorSource("", CustomMVTVectorSourceOptions{ FetchTile: func(CanonicalTileID) {}, }); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("AddCustomMVTVectorSource(empty ID) error = %v, want ErrInvalidArgument", err) @@ -75,7 +76,7 @@ func TestCustomMVTVectorSourceReleasedWhenStyleLoadDropsIt(t *testing.T) { baseline := callback.CustomMVTVectorSourceLiveCountForTest() loadStyleAndCollect(t, runtime, m, backgroundStyleJSON) - if err := m.AddCustomMVTVectorSource("custom-mvt", CustomMVTVectorSourceOptions{ + if _, err := m.AddCustomMVTVectorSource("custom-mvt", CustomMVTVectorSourceOptions{ FetchTile: func(CanonicalTileID) {}, }); err != nil { t.Fatalf("AddCustomMVTVectorSource(): %v", err) @@ -102,22 +103,23 @@ func TestCustomMVTVectorSourceReleasedByRemovalAndMapClose(t *testing.T) { loadStyleAndCollect(t, runtime, m, backgroundStyleJSON) for _, sourceID := range []string{"removed", "surviving"} { - if err := m.AddCustomMVTVectorSource(sourceID, CustomMVTVectorSourceOptions{ + if _, err := m.AddCustomMVTVectorSource(sourceID, CustomMVTVectorSourceOptions{ FetchTile: func(CanonicalTileID) {}, }); err != nil { t.Fatalf("AddCustomMVTVectorSource(%s): %v", sourceID, err) } } - if removed, err := m.RemoveStyleSource("removed"); err != nil || !removed { - t.Fatalf("RemoveStyleSource(removed) = (%v, %v), want (true, nil)", removed, err) - } + removeID, err := m.RemoveStyleSource("removed") + requireCommandCommitted(t, runtime, removeID, err) if live := liveCustomMVTVectorSources(t, baseline); live != 1 { t.Fatalf("live callback states after the removal = %d, want 1", live) } + // The map still holds the surviving source, so its teardown frees the state. if err := m.Close(); err != nil { t.Fatalf("Map Close(): %v", err) } + waitForRuntimeBarrier(t, runtime) if live := liveCustomMVTVectorSources(t, baseline); live != 0 { t.Fatalf("live callback states after the map close = %d, want 0", live) } diff --git a/bindings/go/errors.go b/bindings/go/errors.go index 8b20abb43..8ba8acaab 100644 --- a/bindings/go/errors.go +++ b/bindings/go/errors.go @@ -18,14 +18,25 @@ var ( ErrInvalidArgument = errors.New("maplibre: invalid argument") // ErrInvalidState reports valid objects used in an invalid lifecycle state. ErrInvalidState = errors.New("maplibre: invalid state") - // ErrWrongThread reports use of an owner-thread-affine handle from the wrong - // OS thread. + // ErrWrongThread reports use of a thread-affine render handle from the + // wrong OS thread. ErrWrongThread = errors.New("maplibre: wrong thread") // ErrUnsupported reports a backend, platform, or operation unavailable in // the linked native build. ErrUnsupported = errors.New("maplibre: unsupported") // ErrNative reports a MapLibre Native error converted to a C status. ErrNative = errors.New("maplibre: native error") + // ErrCancelled reports a terminal cancelled operation. + ErrCancelled = errors.New("maplibre: cancelled") + // ErrBusy reports a conflicting driver or lifecycle call. + ErrBusy = errors.New("maplibre: busy") + // ErrTargetLost reports irreversible render-target loss. + ErrTargetLost = errors.New("maplibre: target lost") + // ErrNotReady reports that a nonblocking call has no result yet. + ErrNotReady = errors.New("maplibre: not ready") + // ErrNotFound reports a command or operation that named an ID with no live + // object behind it. + ErrNotFound = errors.New("maplibre: not found") // ErrABIVersionMismatch reports that the loaded C ABI version is // incompatible with this binding. ErrABIVersionMismatch = errors.New("maplibre: ABI version mismatch") @@ -121,6 +132,16 @@ func kindForStatus(status int32) error { return ErrUnsupported case int32(C.MLN_STATUS_NATIVE_ERROR): return ErrNative + case int32(C.MLN_STATUS_CANCELLED): + return ErrCancelled + case int32(C.MLN_STATUS_BUSY): + return ErrBusy + case int32(C.MLN_STATUS_TARGET_LOST): + return ErrTargetLost + case int32(C.MLN_STATUS_NOT_READY): + return ErrNotReady + case int32(C.MLN_STATUS_NOT_FOUND): + return ErrNotFound default: return ErrUnknownStatus } diff --git a/bindings/go/feature_state_test.go b/bindings/go/feature_state_test.go new file mode 100644 index 000000000..1e29cdd5e --- /dev/null +++ b/bindings/go/feature_state_test.go @@ -0,0 +1,84 @@ +package maplibre + +import ( + "bytes" + "errors" + "testing" +) + +// Feature state belongs to the map store, so a set is observed by an ordered +// get without a render session or a loaded source. +func TestMapFeatureStateRoundTrip(t *testing.T) { + runtime, m := newRuntimeAndMap(t, nil) + + featureID := "42" + selector := FeatureStateSelector{SourceID: "source", FeatureID: &featureID} + + // Missing feature state reads as an empty JSON object. + before, err := awaitForTest(m.FeatureState(selector)) + if err != nil { + t.Fatalf("FeatureState() before set: %v", err) + } + if !bytes.Equal(before, []byte("{}")) { + t.Fatalf("FeatureState() before set = %q, want an empty object", before) + } + + setID, err := m.SetFeatureState(selector, []byte(`{"hover":true}`)) + requireCommandCommitted(t, runtime, setID, err) + after, err := awaitForTest(m.FeatureState(selector)) + if err != nil { + t.Fatalf("FeatureState() after set: %v", err) + } + if !bytes.Contains(after, []byte("hover")) { + t.Fatalf("FeatureState() after set = %q, want the stored hover key", after) + } + + // Removing one key returns the feature to the empty object. + stateKey := "hover" + removeID, err := m.RemoveFeatureState(FeatureStateSelector{ + SourceID: "source", + FeatureID: &featureID, + StateKey: &stateKey, + }) + requireCommandCommitted(t, runtime, removeID, err) + cleared, err := awaitForTest(m.FeatureState(selector)) + if err != nil { + t.Fatalf("FeatureState() after remove: %v", err) + } + if !bytes.Equal(cleared, []byte("{}")) { + t.Fatalf("FeatureState() after remove = %q, want an empty object", cleared) + } +} + +func TestMapFeatureStateRejectsInvalidSelectors(t *testing.T) { + _, m := newRuntimeAndMap(t, nil) + + // Set and get require a feature ID. + noFeature := FeatureStateSelector{SourceID: "source"} + if _, err := m.SetFeatureState(noFeature, []byte(`{"hover":true}`)); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetFeatureState() without feature ID error = %v, want ErrInvalidArgument", err) + } + if _, err := m.FeatureState(noFeature); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("FeatureState() without feature ID error = %v, want ErrInvalidArgument", err) + } + + // A state key without a feature ID selects nothing removable. + stateKey := "hover" + if _, err := m.RemoveFeatureState(FeatureStateSelector{ + SourceID: "source", + StateKey: &stateKey, + }); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("RemoveFeatureState() with key but no feature ID error = %v, want ErrInvalidArgument", err) + } + + // State must be one JSON object. An empty view and a non-object are both + // rejected before acceptance. + featureID := "42" + withFeature := FeatureStateSelector{SourceID: "source", FeatureID: &featureID} + if _, err := m.SetFeatureState(withFeature, nil); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetFeatureState() with empty state error = %v, want ErrInvalidArgument", err) + } + if _, err := m.SetFeatureState(withFeature, []byte(`[1,2,3]`)); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetFeatureState() with a non-object state error = %v, want ErrInvalidArgument", err) + } +} diff --git a/bindings/go/geojson_source_data.go b/bindings/go/geojson_source_data.go index 229289b25..b7a7a9bd0 100644 --- a/bindings/go/geojson_source_data.go +++ b/bindings/go/geojson_source_data.go @@ -23,7 +23,7 @@ type GeoJSONSourceDataHandle struct { // data holds one complete UTF-8 GeoJSON document and options may be nil for // defaults; both are copied into the prepared data before the call returns. // Preparation touches no runtime or map and is callable from any goroutine, so -// the expensive parse and tiling can run off the map owner thread. +// the expensive parse and tiling can run concurrently with map work. // // AddGeoJSONSourceData and SetGeoJSONSourceData borrow the handle, so one // prepared value may be installed on any number of sources and closed at any @@ -55,15 +55,15 @@ func NewGeoJSONSourceData(data []byte, options *StyleGeoJSONSourceOptions) (*Geo return &GeoJSONSourceDataHandle{state: state}, nil } -func (data *GeoJSONSourceDataHandle) ptr() (nativeGeoJSONSourceData, func(), error) { +func (data *GeoJSONSourceDataHandle) ptr() (nativeGeoJSONSourceData, error) { if data == nil || data.state == nil { - return 0, nil, newBindingError(ErrInvalidArgument, "GeoJSONSourceDataHandle is nil") + return 0, newBindingError(ErrInvalidArgument, "GeoJSONSourceDataHandle is nil") } - borrow, live := data.state.Borrow() + value, live := data.state.Handle() if !live { - return 0, nil, newBindingError(ErrInvalidArgument, "GeoJSONSourceDataHandle is closed") + return 0, newBindingError(ErrInvalidArgument, "GeoJSONSourceDataHandle is closed") } - return borrow.Handle(), borrow.Release, nil + return value, nil } // Close releases this prepared data. Close is callable from any goroutine, and diff --git a/bindings/go/handle_identity_test.go b/bindings/go/handle_identity_test.go index 0e861129e..df38b26c9 100644 --- a/bindings/go/handle_identity_test.go +++ b/bindings/go/handle_identity_test.go @@ -9,30 +9,27 @@ import ( // BND-045: a released map id, replayed after a new map exists, is reported // stale rather than naming the new map. func TestReleasedMapIDReplayedAfterANewMapReportsItStale(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - defer runtime.Close() + defer closeRuntimeForTest(runtime) - first, err := runtime.NewMap() + first, err := awaitForTest(runtime.NewMap()) if err != nil { t.Fatalf("NewMap(): %v", err) } - released, release, err := first.ptr() + released, err := first.ptr() if err != nil { t.Fatalf("ptr(): %v", err) } - release() if err := first.Close(); err != nil { t.Fatalf("first.Close(): %v", err) } // The released slot is the one the next map takes, so the replayed id // names a retired generation of a slot that is live again. - second, err := runtime.NewMap() + second, err := awaitForTest(runtime.NewMap()) if err != nil { t.Fatalf("second NewMap(): %v", err) } @@ -50,71 +47,3 @@ func TestReleasedMapIDReplayedAfterANewMapReportsItStale(t *testing.T) { t.Fatalf("second.Size(): %v", err) } } - -// BND-047: a map id passed to a runtime operation is rejected on its kind. -func TestMapIDPassedToARuntimeOperationReportsInvalidArgument(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - defer runtime.Close() - - m, err := runtime.NewMap() - if err != nil { - t.Fatalf("NewMap(): %v", err) - } - defer m.Close() - - mapID, release, err := m.ptr() - if err != nil { - t.Fatalf("ptr(): %v", err) - } - defer release() - - err = pumpRuntimeWithMapIDForTest(mapID) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("wrong-kind id: err = %v, want invalid argument", err) - } - if message := err.Error(); !strings.Contains(message, "map") || - !strings.Contains(message, "runtime") { - t.Fatalf("diagnostic = %q, want it to name both kinds", message) - } -} - -// BND-049: a live id called from another thread reports wrong-thread rather -// than stale. -func TestLiveMapIDCalledFromAnotherThreadReportsWrongThread(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - defer runtime.Close() - - m, err := runtime.NewMap() - if err != nil { - t.Fatalf("NewMap(): %v", err) - } - defer m.Close() - - live, release, err := m.ptr() - if err != nil { - t.Fatalf("ptr(): %v", err) - } - defer release() - - done := make(chan error, 1) - go func() { done <- mapSizeByIDForTest(live) }() - got := <-done - - // The id is live, so the owner-thread rule decides rather than identity. - if !errors.Is(got, ErrWrongThread) { - t.Fatalf("cross-thread call: err = %v, want wrong thread", got) - } - if strings.Contains(got.Error(), "stale") { - t.Fatalf("diagnostic = %v, want a wrong-thread message", got) - } -} diff --git a/bindings/go/internal/callback/custom_geometry.go b/bindings/go/internal/callback/custom_geometry.go index 26247fd49..06ad43c92 100644 --- a/bindings/go/internal/callback/custom_geometry.go +++ b/bindings/go/internal/callback/custom_geometry.go @@ -16,6 +16,7 @@ import ( "runtime/cgo" "sync" "sync/atomic" + "unsafe" ) // liveCustomGeometrySources counts the callback states this package holds a cgo @@ -76,7 +77,7 @@ func newCustomGeometrySourceState(options CustomGeometrySourceOptions) *CustomGe // AddCustomGeometrySource installs a custom geometry source callback descriptor. // The C API owns the state from a successful add onwards and releases it through // the release callback, so a caller keeps nothing. -func AddCustomGeometrySource(m uint64, sourceID string, options CustomGeometrySourceOptions) int32 { +func AddCustomGeometrySource(m uint64, sourceID string, options CustomGeometrySourceOptions, completion unsafe.Pointer) int32 { if options.FetchTile == nil { return int32(C.MLN_STATUS_INVALID_ARGUMENT) } @@ -102,7 +103,9 @@ func AddCustomGeometrySource(m uint64, sourceID string, options CustomGeometrySo raw.clip = C.bool(options.Clip) raw.wrap = C.bool(options.Wrap) - status := int32(C.mln_map_add_custom_geometry_source(C.mln_map(m), sourceView, &raw)) + status := int32(C.mln_map_add_custom_geometry_source( + C.mln_map(m), sourceView, &raw, (*C.mln_completion)(completion), + )) if status != int32(C.MLN_STATUS_OK) { // A failed add owes no release callback, so this call frees the state. state.release() diff --git a/bindings/go/internal/callback/custom_geometry_trampoline.go b/bindings/go/internal/callback/custom_geometry_trampoline.go index 74b46da83..1f8fabe22 100644 --- a/bindings/go/internal/callback/custom_geometry_trampoline.go +++ b/bindings/go/internal/callback/custom_geometry_trampoline.go @@ -27,9 +27,8 @@ func goMaplibreCustomGeometryFetchTile(userData unsafe.Pointer, tileID C.mln_can } // goMaplibreCustomGeometryReleaseUserData frees one source's callback state. The -// C API invokes it once, on the map owner thread, when it stops referencing the -// state: on explicit removal, when a style load drops the source, or when the map -// is destroyed. +// C API invokes it once on native map execution: on explicit removal, when a +// style load drops the source, or when the map closes. // //export goMaplibreCustomGeometryReleaseUserData func goMaplibreCustomGeometryReleaseUserData(userData unsafe.Pointer) { diff --git a/bindings/go/internal/callback/custom_mvt_vector.go b/bindings/go/internal/callback/custom_mvt_vector.go index 5b5e1dd5e..cb52b55d2 100644 --- a/bindings/go/internal/callback/custom_mvt_vector.go +++ b/bindings/go/internal/callback/custom_mvt_vector.go @@ -16,6 +16,7 @@ import ( "runtime/cgo" "sync" "sync/atomic" + "unsafe" ) // liveCustomMVTVectorSources counts the callback states this package holds a @@ -64,7 +65,7 @@ func newCustomMVTVectorSourceState(options CustomMVTVectorSourceOptions) *Custom // AddCustomMVTVectorSource installs a custom MVT vector source callback descriptor. // The C API owns the state from a successful add onwards and releases it through // the release callback, so a caller keeps nothing. -func AddCustomMVTVectorSource(m uint64, sourceID string, options CustomMVTVectorSourceOptions) int32 { +func AddCustomMVTVectorSource(m uint64, sourceID string, options CustomMVTVectorSourceOptions, completion unsafe.Pointer) int32 { if options.FetchTile == nil { return int32(C.MLN_STATUS_INVALID_ARGUMENT) } @@ -85,7 +86,9 @@ func AddCustomMVTVectorSource(m uint64, sourceID string, options CustomMVTVector raw.min_zoom = C.double(options.MinZoom) raw.max_zoom = C.double(options.MaxZoom) - status := int32(C.mln_map_add_custom_mvt_vector_source(C.mln_map(m), sourceView, &raw)) + status := int32(C.mln_map_add_custom_mvt_vector_source( + C.mln_map(m), sourceView, &raw, (*C.mln_completion)(completion), + )) if status != int32(C.MLN_STATUS_OK) { // A failed add owes no release callback, so this call frees the state. state.release() diff --git a/bindings/go/internal/callback/logging.go b/bindings/go/internal/callback/logging.go index db3939258..1ee4906cd 100644 --- a/bindings/go/internal/callback/logging.go +++ b/bindings/go/internal/callback/logging.go @@ -7,10 +7,12 @@ package callback #include "../cgo_shim.h" extern uint32_t goMaplibreLogCallback(void* user_data, uint32_t severity, uint32_t event, int64_t code, const char* message); +extern void goMaplibreReleaseCallbackState(void* user_data); */ import "C" import ( + "runtime/cgo" "sync" "unsafe" ) @@ -18,25 +20,23 @@ import ( // LogCallback is the internal shape for process-global log callbacks. type LogCallback func(severity uint32, event uint32, code int64, message string) bool -var logState struct { - sync.Mutex - active bool +type LogCallbackState struct { callback LogCallback + handle cgo.Handle + once sync.Once } -var logInstallMu sync.Mutex +func newLogCallbackState(callback LogCallback) *LogCallbackState { + state := &LogCallbackState{callback: callback} + state.handle = cgo.NewHandle(state) + return state +} -var ( - setNativeLogCallback = func() int32 { - return int32(C.mln_log_set_callback( - C.mln_log_callback(C.goMaplibreLogCallback), - nil, - )) - } - clearNativeLogCallback = func() int32 { - return int32(C.mln_log_clear_callback()) +func (state *LogCallbackState) Release() { + if state != nil { + state.once.Do(state.handle.Delete) } -) +} // SetLogCallback installs or replaces the process-global native log callback. func SetLogCallback(callback LogCallback) int32 { @@ -44,13 +44,14 @@ func SetLogCallback(callback LogCallback) int32 { return ClearLogCallback() } - logInstallMu.Lock() - defer logInstallMu.Unlock() - - oldCallback, oldActive := swapCurrentLogCallback(callback, true) - status := setNativeLogCallback() + state := newLogCallbackState(callback) + status := int32(C.mln_log_set_callback( + C.mln_log_callback(C.goMaplibreLogCallback), + C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + C.mln_log_callback_release(C.goMaplibreReleaseCallbackState), + )) if status != int32(C.MLN_STATUS_OK) { - swapCurrentLogCallback(oldCallback, oldActive) + state.Release() return status } return int32(C.MLN_STATUS_OK) @@ -58,16 +59,7 @@ func SetLogCallback(callback LogCallback) int32 { // ClearLogCallback clears the process-global native log callback. func ClearLogCallback() int32 { - logInstallMu.Lock() - defer logInstallMu.Unlock() - - oldCallback, oldActive := swapCurrentLogCallback(nil, false) - status := clearNativeLogCallback() - if status != int32(C.MLN_STATUS_OK) { - swapCurrentLogCallback(oldCallback, oldActive) - return status - } - return int32(C.MLN_STATUS_OK) + return int32(C.mln_log_clear_callback()) } // SetAsyncLogSeverityMask sets the native asynchronous logging severity mask. @@ -75,35 +67,14 @@ func SetAsyncLogSeverityMask(mask uint32) int32 { return int32(C.mln_log_set_async_severity_mask(C.uint32_t(mask))) } -func currentLogCallback() LogCallback { - logState.Lock() - defer logState.Unlock() - if !logState.active { - return nil - } - return logState.callback -} - -func swapCurrentLogCallback(callback LogCallback, active bool) (LogCallback, bool) { - logState.Lock() - defer logState.Unlock() - oldCallback := logState.callback - oldActive := logState.active - logState.callback = callback - logState.active = active - return oldCallback, oldActive -} - func invokeLogCallbackForTest(callback LogCallback) uint32 { - oldCallback, oldActive := swapCurrentLogCallback(callback, callback != nil) - defer func() { - swapCurrentLogCallback(oldCallback, oldActive) - }() + state := newLogCallbackState(callback) + defer state.Release() message := C.CString("test message") defer C.free(unsafe.Pointer(message)) return uint32(goMaplibreLogCallback( - nil, + C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), C.uint32_t(C.MLN_LOG_SEVERITY_INFO), C.uint32_t(C.MLN_LOG_EVENT_GENERAL), 0, diff --git a/bindings/go/internal/callback/logging_trampoline.go b/bindings/go/internal/callback/logging_trampoline.go index 9894b6d2c..ddb9d9f57 100644 --- a/bindings/go/internal/callback/logging_trampoline.go +++ b/bindings/go/internal/callback/logging_trampoline.go @@ -4,7 +4,11 @@ package callback #include */ import "C" -import "unsafe" + +import ( + "runtime/cgo" + "unsafe" +) //export goMaplibreLogCallback func goMaplibreLogCallback(userData unsafe.Pointer, severity C.uint32_t, event C.uint32_t, code C.int64_t, message *C.char) C.uint32_t { @@ -12,11 +16,14 @@ func goMaplibreLogCallback(userData unsafe.Pointer, severity C.uint32_t, event C _ = recover() }() - callback := currentLogCallback() - if callback == nil { + if userData == nil { + return 0 + } + state, ok := cgo.Handle(uintptr(userData)).Value().(*LogCallbackState) + if !ok || state == nil || state.callback == nil { return 0 } - if callback(uint32(severity), uint32(event), int64(code), C.GoString(message)) { + if state.callback(uint32(severity), uint32(event), int64(code), C.GoString(message)) { return 1 } return 0 diff --git a/bindings/go/internal/callback/panic_test.go b/bindings/go/internal/callback/panic_test.go index fe6fd1e2b..af6c6ebf5 100644 --- a/bindings/go/internal/callback/panic_test.go +++ b/bindings/go/internal/callback/panic_test.go @@ -1,9 +1,7 @@ package callback import ( - "sync/atomic" "testing" - "time" ) const ( @@ -52,54 +50,6 @@ func TestLogCallbackTrampolineReturnsConsumed(t *testing.T) { } } -func TestLogCallbackSetSerializesNativeAndGoState(t *testing.T) { - oldSet := setNativeLogCallback - oldClear := clearNativeLogCallback - t.Cleanup(func() { - _ = ClearLogCallback() - setNativeLogCallback = oldSet - clearNativeLogCallback = oldClear - }) - - entered := make(chan struct{}) - release := make(chan struct{}) - var calls atomic.Int32 - setNativeLogCallback = func() int32 { - if calls.Add(1) == 1 { - close(entered) - <-release - } - return 0 - } - clearNativeLogCallback = func() int32 { return 0 } - - firstDone := make(chan int32, 1) - secondDone := make(chan int32, 1) - go func() { - firstDone <- SetLogCallback(func(uint32, uint32, int64, string) bool { return false }) - }() - <-entered - go func() { - secondDone <- SetLogCallback(func(uint32, uint32, int64, string) bool { return true }) - }() - select { - case status := <-secondDone: - close(release) - t.Fatalf("second SetLogCallback completed while first native install was in flight: %d", status) - case <-time.After(25 * time.Millisecond): - } - close(release) - if status := <-firstDone; status != 0 { - t.Fatalf("first SetLogCallback status = %d, want 0", status) - } - if status := <-secondDone; status != 0 { - t.Fatalf("second SetLogCallback status = %d, want 0", status) - } - if calls.Load() != 2 { - t.Fatalf("native set calls = %d, want 2", calls.Load()) - } -} - func TestCustomGeometryReleaseWaitsForActiveCallback(t *testing.T) { entered := make(chan struct{}) unblock := make(chan struct{}) diff --git a/bindings/go/internal/callback/resource.go b/bindings/go/internal/callback/resource.go index 71964fe20..7d666a4ce 100644 --- a/bindings/go/internal/callback/resource.go +++ b/bindings/go/internal/callback/resource.go @@ -9,7 +9,8 @@ package callback extern mln_status goMaplibreResourceTransform(void* user_data, uint32_t kind, const char* url, mln_resource_transform_response* out_response); extern mln_status goMaplibreHttpHeaderTransform(void* user_data, uint32_t kind, const char* url, mln_http_header_transform_response* out_response); -extern uint32_t goMaplibreResourceProvider(void* user_data, const mln_resource_request* request, mln_resource_request_handle* handle); +extern uint32_t goMaplibreResourceProvider(void* user_data, const mln_resource_request* request, mln_resource_request_handle handle); +extern void goMaplibreReleaseCallbackState(void* user_data); */ import "C" @@ -39,30 +40,34 @@ func newResourceTransformState(callback ResourceTransformCallback) *ResourceTran } // SetResourceTransform installs or replaces a runtime-scoped resource transform. -func SetResourceTransform(runtime uint64, callback ResourceTransformCallback) (*ResourceTransformState, int32) { +func SetResourceTransform(runtime uint64, callback ResourceTransformCallback, completion unsafe.Pointer) int32 { if callback == nil { - return nil, int32(C.MLN_STATUS_INVALID_ARGUMENT) + return int32(C.MLN_STATUS_INVALID_ARGUMENT) } state := newResourceTransformState(callback) descriptor := C.mln_resource_transform{ - size: C.uint32_t(unsafe.Sizeof(C.mln_resource_transform{})), - callback: C.mln_resource_transform_callback(C.goMaplibreResourceTransform), - user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + size: C.uint32_t(unsafe.Sizeof(C.mln_resource_transform{})), + callback: C.mln_resource_transform_callback(C.goMaplibreResourceTransform), + user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + release_user_data: C.mln_runtime_callback_release(C.goMaplibreReleaseCallbackState), } status := int32(C.mln_runtime_set_resource_transform( C.mln_runtime(runtime), &descriptor, + (*C.mln_completion)(completion), )) if status != int32(C.MLN_STATUS_OK) { state.Release() - return nil, status + return status } - return state, int32(C.MLN_STATUS_OK) + return int32(C.MLN_STATUS_OK) } // ClearResourceTransform clears the runtime-scoped resource transform. -func ClearResourceTransform(runtime uint64) int32 { - return int32(C.mln_runtime_clear_resource_transform(C.mln_runtime(runtime))) +func ClearResourceTransform(runtime uint64, completion unsafe.Pointer) int32 { + return int32(C.mln_runtime_clear_resource_transform( + C.mln_runtime(runtime), (*C.mln_completion)(completion), + )) } // Release frees callback state after native no longer references it. @@ -135,26 +140,31 @@ func newHttpHeaderTransformState(callback HttpHeaderTransformCallback) *HttpHead return state } -func SetHttpHeaderTransform(runtime uint64, callback HttpHeaderTransformCallback) (*HttpHeaderTransformState, int32) { +func SetHttpHeaderTransform(runtime uint64, callback HttpHeaderTransformCallback, completion unsafe.Pointer) int32 { if callback == nil { - return nil, int32(C.MLN_STATUS_INVALID_ARGUMENT) + return int32(C.MLN_STATUS_INVALID_ARGUMENT) } state := newHttpHeaderTransformState(callback) descriptor := C.mln_http_header_transform{ - size: C.uint32_t(unsafe.Sizeof(C.mln_http_header_transform{})), - callback: C.mln_http_header_transform_callback(C.goMaplibreHttpHeaderTransform), - user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + size: C.uint32_t(unsafe.Sizeof(C.mln_http_header_transform{})), + callback: C.mln_http_header_transform_callback(C.goMaplibreHttpHeaderTransform), + user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + release_user_data: C.mln_runtime_callback_release(C.goMaplibreReleaseCallbackState), } - status := int32(C.mln_runtime_set_http_header_transform(C.mln_runtime(runtime), &descriptor)) + status := int32(C.mln_runtime_set_http_header_transform( + C.mln_runtime(runtime), &descriptor, (*C.mln_completion)(completion), + )) if status != int32(C.MLN_STATUS_OK) { state.Release() - return nil, status + return status } - return state, int32(C.MLN_STATUS_OK) + return int32(C.MLN_STATUS_OK) } -func ClearHttpHeaderTransform(runtime uint64) int32 { - return int32(C.mln_runtime_clear_http_header_transform(C.mln_runtime(runtime))) +func ClearHttpHeaderTransform(runtime uint64, completion unsafe.Pointer) int32 { + return int32(C.mln_runtime_clear_http_header_transform( + C.mln_runtime(runtime), (*C.mln_completion)(completion), + )) } func (state *HttpHeaderTransformState) Release() { @@ -239,30 +249,34 @@ func newResourceProviderState(callback ResourceProviderCallback) *ResourceProvid } // SetResourceProvider installs or replaces a runtime-scoped resource provider. -func SetResourceProvider(runtime uint64, callback ResourceProviderCallback) (*ResourceProviderState, int32) { +func SetResourceProvider(runtime uint64, callback ResourceProviderCallback, completion unsafe.Pointer) int32 { if callback == nil { - return nil, int32(C.MLN_STATUS_INVALID_ARGUMENT) + return int32(C.MLN_STATUS_INVALID_ARGUMENT) } state := newResourceProviderState(callback) descriptor := C.mln_resource_provider{ - size: C.uint32_t(unsafe.Sizeof(C.mln_resource_provider{})), - callback: C.mln_resource_provider_callback(C.goMaplibreResourceProvider), - user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + size: C.uint32_t(unsafe.Sizeof(C.mln_resource_provider{})), + callback: C.mln_resource_provider_callback(C.goMaplibreResourceProvider), + user_data: C.mln_go_handle_to_pointer(C.uintptr_t(state.handle)), + release_user_data: C.mln_runtime_callback_release(C.goMaplibreReleaseCallbackState), } status := int32(C.mln_runtime_set_resource_provider( C.mln_runtime(runtime), &descriptor, + (*C.mln_completion)(completion), )) if status != int32(C.MLN_STATUS_OK) { state.Release() - return nil, status + return status } - return state, int32(C.MLN_STATUS_OK) + return int32(C.MLN_STATUS_OK) } // ClearResourceProvider clears the runtime-scoped resource provider. -func ClearResourceProvider(runtime uint64) int32 { - return int32(C.mln_runtime_clear_resource_provider(C.mln_runtime(runtime))) +func ClearResourceProvider(runtime uint64, completion unsafe.Pointer) int32 { + return int32(C.mln_runtime_clear_resource_provider( + C.mln_runtime(runtime), (*C.mln_completion)(completion), + )) } // Release frees provider callback state after native no longer references it. diff --git a/bindings/go/internal/callback/resource_trampoline.go b/bindings/go/internal/callback/resource_trampoline.go index 52acd649c..e073fa6df 100644 --- a/bindings/go/internal/callback/resource_trampoline.go +++ b/bindings/go/internal/callback/resource_trampoline.go @@ -13,6 +13,18 @@ import ( "unsafe" ) +//export goMaplibreReleaseCallbackState +func goMaplibreReleaseCallbackState(userData unsafe.Pointer) { + if userData == nil { + return + } + defer func() { _ = recover() }() + state, ok := cgo.Handle(uintptr(userData)).Value().(interface{ Release() }) + if ok { + state.Release() + } +} + //export goMaplibreResourceTransform func goMaplibreResourceTransform(userData unsafe.Pointer, kind C.uint32_t, url *C.char, outResponse *C.mln_resource_transform_response) (status C.mln_status) { defer func() { diff --git a/bindings/go/internal/cgo_completion_shim.h b/bindings/go/internal/cgo_completion_shim.h new file mode 100644 index 000000000..0791839ea --- /dev/null +++ b/bindings/go/internal/cgo_completion_shim.h @@ -0,0 +1,33 @@ +#ifndef MAPLIBRE_NATIVE_FFI_GO_COMPLETION_SHIM_H +#define MAPLIBRE_NATIVE_FFI_GO_COMPLETION_SHIM_H + +#include "maplibre_native_c.h" + +extern void mln_go_completion_callback( + void* user_data, mln_completion_result* result +); +extern void mln_go_completion_release(void* user_data); + +static inline void mln_go_completion_trampoline( + void* user_data, const mln_completion_result* result +) { + mln_go_completion_callback(user_data, (mln_completion_result*)result); +} + +static inline mln_completion mln_go_make_completion(void* user_data) { + mln_completion completion = { + .size = sizeof(mln_completion), + .callback = mln_go_completion_trampoline, + .user_data = user_data, + .release_user_data = mln_go_completion_release, + }; + return completion; +} + +static inline mln_completion mln_go_make_completion_from_handle( + uintptr_t handle +) { + return mln_go_make_completion((void*)handle); +} + +#endif diff --git a/bindings/go/internal/cgo_runtime_shim.h b/bindings/go/internal/cgo_runtime_shim.h index 8fda3ab7b..d3b071c2a 100644 --- a/bindings/go/internal/cgo_runtime_shim.h +++ b/bindings/go/internal/cgo_runtime_shim.h @@ -45,13 +45,6 @@ mln_go_runtime_event_offline_region_tile_count_limit( return event->payload.offline_region_tile_count_limit; } -static inline mln_runtime_event_offline_operation_completed -mln_go_runtime_event_offline_operation_completed( - const mln_runtime_event* event -) { - return event->payload.offline_operation_completed; -} - static inline mln_runtime_event_camera_transition_finished mln_go_runtime_event_camera_transition_finished( const mln_runtime_event* event @@ -112,15 +105,6 @@ mln_go_runtime_event_with_offline_region_tile_count_limit( return event; } -static inline mln_runtime_event -mln_go_runtime_event_with_offline_operation_completed( - mln_runtime_event event, mln_runtime_event_offline_operation_completed payload -) { - event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED; - event.payload.offline_operation_completed = payload; - return event; -} - static inline mln_runtime_event mln_go_runtime_event_with_camera_transition_finished( mln_runtime_event event, mln_runtime_event_camera_transition_finished payload diff --git a/bindings/go/internal/cgo_shim.h b/bindings/go/internal/cgo_shim.h index 083f539dd..bb17f43f6 100644 --- a/bindings/go/internal/cgo_shim.h +++ b/bindings/go/internal/cgo_shim.h @@ -32,17 +32,16 @@ static inline void mln_go_opengl_context_set_egl( descriptor->data.egl.client_api = client_api; descriptor->data.egl.get_proc_address = get_proc_address; } - -static inline mln_wgl_context_descriptor* mln_go_opengl_context_wgl( - mln_opengl_context_descriptor* descriptor -) { - return &descriptor->data.wgl; -} - -static inline mln_egl_context_descriptor* mln_go_opengl_context_egl( - mln_opengl_context_descriptor* descriptor +static inline void mln_go_opengl_context_set_webgl( + mln_opengl_context_descriptor* descriptor, mln_webgl_context_kind kind, + int32_t context, const char* canvas_selector, size_t canvas_selector_size ) { - return &descriptor->data.egl; + descriptor->platform = MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + descriptor->data.webgl.size = sizeof(mln_webgl_context_descriptor); + descriptor->data.webgl.kind = kind; + descriptor->data.webgl.context = context; + descriptor->data.webgl.canvas_selector.data = canvas_selector; + descriptor->data.webgl.canvas_selector.size = canvas_selector_size; } #endif diff --git a/bindings/go/internal/handle/state.go b/bindings/go/internal/handle/state.go index 088261783..3e63525c6 100644 --- a/bindings/go/internal/handle/state.go +++ b/bindings/go/internal/handle/state.go @@ -1,7 +1,6 @@ package handle import ( - "errors" "fmt" "log" "runtime" @@ -9,104 +8,37 @@ import ( ) // DestroyFunc releases one owned native handle. A non-OK status leaves the -// handle live so callers can retry on the correct owner thread. +// handle live so callers can retry after correcting the failure. type DestroyFunc[T ~uint64] func(T) int32 -// ErrLiveChildren reports that an owner cannot close while dependent handles are -// still live. -var ErrLiveChildren = errors.New("handle has live children") - // State stores close-once state for one owned native handle. The zero handle -// means closed. +// means closed. The C API leases the native object for each entry point, so +// calls do not need a second binding-side borrow. type State[T ~uint64] struct { - mu sync.Mutex - cond *sync.Cond - handle T - typeName string - parents []any - borrows int - children int - releasing bool -} - -// Borrow serializes one use of a native handle against release, so a close -// waits for in-flight cgo calls. -type Borrow[T ~uint64] struct { - state *State[T] - handle T -} - -// Child tracks one live dependent handle against its parent. -type Child struct { - mu sync.Mutex - parent childCounter - live bool -} - -type childCounter interface { - removeChild() + mu sync.Mutex + handle T + typeName string } // New creates close-once state for an owned native handle. -func New[T ~uint64](handle T, typeName string, parents ...any) (*State[T], error) { +func New[T ~uint64](handle T, typeName string) (*State[T], error) { if handle == 0 { return nil, fmt.Errorf("%s handle is the null handle", typeName) } - state := &State[T]{handle: handle, typeName: typeName, parents: parents} - state.cond = sync.NewCond(&state.mu) + state := &State[T]{handle: handle, typeName: typeName} runtime.SetFinalizer(state, func(state *State[T]) { state.reportLeakIfLive() }) return state, nil } -// Handle returns the native handle and whether it is still live. The result is -// only a snapshot; use Borrow for cgo calls. +// Handle returns the native handle and whether this wrapper still owns it. func (state *State[T]) Handle() (T, bool) { state.mu.Lock() defer state.mu.Unlock() return state.handle, state.handle != 0 } -// Borrow returns the native handle and serializes it against release. Borrow -// fails while release is in progress. -func (state *State[T]) Borrow() (*Borrow[T], bool) { - state.mu.Lock() - defer state.mu.Unlock() - if state.handle == 0 || state.releasing { - return nil, false - } - state.borrows++ - return &Borrow[T]{state: state, handle: state.handle}, true -} - -// Handle returns the borrowed native handle. -func (borrow *Borrow[T]) Handle() T { - if borrow == nil { - var zero T - return zero - } - return borrow.handle -} - -// Release ends this borrow. It is safe to call more than once. -func (borrow *Borrow[T]) Release() { - if borrow == nil || borrow.state == nil { - return - } - state := borrow.state - borrow.state = nil - borrow.handle = 0 - - state.mu.Lock() - state.borrows-- - if state.borrows == 0 { - state.cond.Broadcast() - } - state.mu.Unlock() - state.KeepAlive() -} - // IsClosed reports whether this handle has been successfully closed. func (state *State[T]) IsClosed() bool { state.mu.Lock() @@ -115,91 +47,19 @@ func (state *State[T]) IsClosed() bool { } // Close calls destroy at most once after a successful native release. -func (state *State[T]) Close(destroy DestroyFunc[T], liveChildStatus ...int32) int32 { - status, err := state.CloseChecked(destroy) - if err != nil { - if errors.Is(err, ErrLiveChildren) && len(liveChildStatus) > 0 { - return liveChildStatus[0] - } - return -1 - } - return status -} - -// CloseChecked calls destroy at most once after a successful native release and -// reports binding-owned preconditions separately from native statuses. -func (state *State[T]) CloseChecked(destroy DestroyFunc[T]) (int32, error) { +func (state *State[T]) Close(destroy DestroyFunc[T]) int32 { state.mu.Lock() - for state.releasing { - state.cond.Wait() - } + defer state.mu.Unlock() if state.handle == 0 { - state.mu.Unlock() - return 0, nil - } - state.releasing = true - for state.borrows > 0 { - state.cond.Wait() - } - if state.children > 0 { - state.releasing = false - state.cond.Broadcast() - state.mu.Unlock() - state.KeepAlive() - return 0, ErrLiveChildren - } - handle := state.handle - state.mu.Unlock() - - status := destroy(handle) - state.mu.Lock() - if status == 0 { - state.handle = 0 + return 0 } - state.releasing = false - state.cond.Broadcast() - state.mu.Unlock() - + status := destroy(state.handle) if status != 0 { - state.KeepAlive() - return status, nil + return status } + state.handle = 0 runtime.SetFinalizer(state, nil) - state.KeepAlive() - return 0, nil -} - -// AddChild records one live dependent handle. The returned child should be -// released after the dependent native handle is successfully destroyed. -func (state *State[T]) AddChild() *Child { - state.mu.Lock() - state.children++ - state.mu.Unlock() - return &Child{parent: state, live: true} -} - -// Release removes this dependent handle from its parent. It is safe to call -// more than once. -func (child *Child) Release() { - if child == nil { - return - } - child.mu.Lock() - parent := child.parent - live := child.live - child.live = false - child.parent = nil - child.mu.Unlock() - if live && parent != nil { - parent.removeChild() - } -} - -func (state *State[T]) removeChild() { - state.mu.Lock() - state.children-- - state.cond.Broadcast() - state.mu.Unlock() + return 0 } func (state *State[T]) reportLeakIfLive() { @@ -212,12 +72,9 @@ func (state *State[T]) reportLeakIfLive() { } } -// KeepAlive keeps the handle state and retained parents reachable until this -// point after a cgo call. +// KeepAlive prevents the leak-reporting finalizer from running before a C call +// that used this state has returned. func (state *State[T]) KeepAlive() { - for _, parent := range state.parents { - runtime.KeepAlive(parent) - } runtime.KeepAlive(state) } diff --git a/bindings/go/internal/handle/state_test.go b/bindings/go/internal/handle/state_test.go index 69b847e2d..960e7f0e0 100644 --- a/bindings/go/internal/handle/state_test.go +++ b/bindings/go/internal/handle/state_test.go @@ -7,7 +7,6 @@ import ( "sync" "sync/atomic" "testing" - "time" ) const ( @@ -88,84 +87,6 @@ func TestStateFailedCloseLeavesHandleLiveForRetry(t *testing.T) { } } -// BND-197. -func TestStateCloseWaitsForActiveBorrowBeforeDestroy(t *testing.T) { - state, err := New(testHandle, "test_handle") - if err != nil { - t.Fatal(err) - } - - borrow, live := state.Borrow() - if !live { - t.Fatal("Borrow() failed for live state") - } - if handle := borrow.Handle(); handle != testHandle { - t.Fatalf("borrow Handle() = %#x, want %#x", handle, testHandle) - } - - destroyCalled := make(chan struct{}) - closeDone := make(chan int32) - go func() { - closeDone <- state.Close(func(handle testNativeHandle) int32 { - if handle != testHandle { - t.Errorf("destroy handle = %#x, want %#x", handle, testHandle) - } - close(destroyCalled) - return testStatusOK - }) - }() - - select { - case <-destroyCalled: - t.Fatal("destroy ran before active borrow was released") - default: - } - deadline := time.After(2 * time.Second) - for { - if borrow, live := state.Borrow(); live { - borrow.Release() - select { - case <-destroyCalled: - t.Fatal("destroy ran before active borrow was released") - case <-deadline: - t.Fatal("Close did not enter releasing state") - case <-time.After(100 * time.Microsecond): - continue - } - } - break - } - - borrow.Release() - if status := <-closeDone; status != testStatusOK { - t.Fatalf("Close status = %d, want OK", status) - } - select { - case <-destroyCalled: - default: - t.Fatal("destroy did not run after borrow release") - } -} - -func TestStateFailedCloseAllowsBorrowRetry(t *testing.T) { - state, err := New(testHandle, "test_handle") - if err != nil { - t.Fatal(err) - } - - if status := state.Close(func(testNativeHandle) int32 { return testStatusWrongThread }); status != testStatusWrongThread { - t.Fatalf("Close status = %d, want wrong-thread", status) - } - borrow, live := state.Borrow() - if !live { - t.Fatal("Borrow() failed after failed close") - } - if handle := borrow.Handle(); handle != testHandle { - t.Fatalf("borrow Handle() = %#x, want %#x", handle, testHandle) - } - borrow.Release() -} - func TestStateConcurrentCloseDestroysOnce(t *testing.T) { state, err := New(testHandle, "test_handle") if err != nil { @@ -197,46 +118,6 @@ func TestStateConcurrentCloseDestroysOnce(t *testing.T) { } } -func TestStateCloseFailsWithLiveChildrenAndRetries(t *testing.T) { - state, err := New(testHandle, "test_handle") - if err != nil { - t.Fatal(err) - } - - child := state.AddChild() - if status := state.Close(func(testNativeHandle) int32 { - t.Fatal("destroy called while child is live") - return testStatusOK - }, testStatusWrongThread); status != testStatusWrongThread { - t.Fatalf("Close status = %d, want live-child status", status) - } - if borrow, live := state.Borrow(); !live { - t.Fatal("Borrow() failed after live-child close failure") - } else { - borrow.Release() - } - - child.Release() - if status := state.Close(func(testNativeHandle) int32 { return testStatusOK }, testStatusWrongThread); status != testStatusOK { - t.Fatalf("Close after child release status = %d, want OK", status) - } -} - -func TestStateKeepsParentsReachable(t *testing.T) { - const parentHandle testNativeHandle = 0x0100_0000_0000_0007 - parent := parentHandle - native := testHandle - state, err := New(native, "test_handle", parent) - if err != nil { - t.Fatal(err) - } - - state.KeepAlive() - if got := state.TypeName(); got != "test_handle" { - t.Fatalf("TypeName() = %q, want test_handle", got) - } -} - func TestStateLeakReportDoesNotDestroyHandle(t *testing.T) { state, err := New(testHandle, "test_handle") if err != nil { diff --git a/bindings/go/internal/testsupport/metal_darwin.go b/bindings/go/internal/testsupport/metal_darwin.go new file mode 100644 index 000000000..5124b9d41 --- /dev/null +++ b/bindings/go/internal/testsupport/metal_darwin.go @@ -0,0 +1,29 @@ +//go:build darwin && cgo + +package testsupport + +/* +#cgo darwin LDFLAGS: -framework Metal -lobjc +#include + +void* MTLCreateSystemDefaultDevice(void); +void objc_release(void* object); + +static inline void mln_go_test_release_metal_device(uintptr_t device) { + if (device != 0) { + objc_release((void*)device); + } +} +*/ +import "C" + +// DefaultMetalDevice returns a default Metal device with one caller-owned +// reference. The caller must balance it with ReleaseMetalDevice. +func DefaultMetalDevice() uintptr { + return uintptr(C.MTLCreateSystemDefaultDevice()) +} + +// ReleaseMetalDevice balances DefaultMetalDevice's retained return value. +func ReleaseMetalDevice(device uintptr) { + C.mln_go_test_release_metal_device(C.uintptr_t(device)) +} diff --git a/bindings/go/map.go b/bindings/go/map.go index f42caa5f9..7429ee185 100644 --- a/bindings/go/map.go +++ b/bindings/go/map.go @@ -8,6 +8,7 @@ package maplibre import "C" import ( + "bytes" "errors" "unsafe" @@ -44,16 +45,12 @@ func (options MapDebugOptions) Has(requested MapDebugOptions) bool { // MapOptions configures map creation. type MapOptions struct { - // Width is the initial logical width in UI pixels, replaced by the extent of - // the first attached render session. + // Width is the initial logical width in UI pixels. Width uint32 - // Height is the initial logical height in UI pixels, replaced by the extent - // of the first attached render session. + // Height is the initial logical height in UI pixels. Height uint32 - // ScaleFactor is the UI-to-device pixel scale, fixed for the lifetime of the - // map. It selects sprites, glyphs, and raster tiles. A render session whose - // own scale factor differs logs a warning and renders imagery chosen for - // this density. + // ScaleFactor is the initial UI-to-device pixel scale. It selects sprites, + // glyphs, and raster tiles. Resize may update it with the logical size. ScaleFactor float64 Mode MapMode // FastPFOREnabled decodes MapLibre Tile (MLT) tiles whose integer streams @@ -96,28 +93,78 @@ func defaultMapEventMask() RuntimeEventMask { return RuntimeEventMask(C.mln_map_options_default().event_mask) } +// LogicalExtent is a map's logical size and device-pixel scale. +type LogicalExtent struct { + Width uint32 + Height uint32 + ScaleFactor float64 +} + +// MapSnapshot is a copied immutable map state generation. Every committed map +// command publishes a new generation in its completion, so a snapshot whose +// Generation is at or past that value observes the commit. +type MapSnapshot struct { + Generation uint64 + // DebugOptions is the committed debug overlay mask. + DebugOptions MapDebugOptions + Camera CameraOptions + LogicalExtent LogicalExtent + ProjectionMode ProjectionModeOptions + Viewport ViewportOptions + Tile TileOptions + Bounds BoundOptions + FreeCamera FreeCameraOptions + // FullyLoaded reports whether every requested style and tile resource + // finished loading. + FullyLoaded bool + RenderingStatsViewEnabled bool + RepaintDemand bool + EventMask RuntimeEventMask + LatestRenderUpdateGeneration uint64 +} + +// CameraSnapshot is a copied camera and the immutable map generation that +// supplied it. +type CameraSnapshot struct { + Generation uint64 + Camera CameraOptions +} + // MapHandle owns map state for one RuntimeHandle. type MapHandle struct { - state *handle.State[nativeMap] - runtime *RuntimeHandle - runtimeChild *handle.Child + state *handle.State[nativeMap] + runtime *RuntimeHandle // The map's native handle, which also serves as its public identity. id MapID } -var destroyMapHandle = func(native nativeMap) int32 { - return int32(C.mln_map_destroy(C.mln_map(native))) -} +// Test seam for synthetic handles. Production close uses closeNativeMap. +var destroyMapHandle func(nativeMap) int32 -func (m *MapHandle) ptr() (nativeMap, func(), error) { +func (m *MapHandle) ptr() (nativeMap, error) { if m == nil || m.state == nil { - return 0, nil, newBindingError(ErrInvalidArgument, "MapHandle is nil") + return 0, newBindingError(ErrInvalidArgument, "MapHandle is nil") } - borrow, live := m.state.Borrow() + value, live := m.state.Handle() if !live { - return 0, nil, newBindingError(ErrInvalidArgument, "MapHandle is closed") + return 0, newBindingError(ErrInvalidArgument, "MapHandle is closed") } - return borrow.Handle(), borrow.Release, nil + return value, nil +} + +func startMapCompletion[T any]( + m *MapHandle, + start func(C.mln_map, *C.mln_completion) int32, + convert func(*C.mln_completion_result) (T, error), +) (*Future[T], error) { + ptr, err := m.ptr() + if err != nil { + return nil, err + } + defer m.state.KeepAlive() + return startCompletion(func(completion *C.mln_completion) int32 { + return start(C.mln_map(ptr), completion) + }, convert) } func validateCStringArgument(name string, value string) error { @@ -133,587 +180,293 @@ func validateCStringArgument(name string, value string) error { // ID returns this map's event source identity, which matches // RuntimeEventSource.MapID on runtime events this map raises. func (m *MapHandle) ID() (MapID, error) { - _, release, err := m.ptr() + _, err := m.ptr() if err != nil { return 0, err } - defer release() + return m.id, nil } -// SetEventMask selects which map-originated event types this map queues. It -// accepts RuntimeEventMaskAll, reads the bits in RuntimeEventMaskAllMapEvents, -// and returns ErrInvalidArgument for a bit outside RuntimeEventMaskAll. -// -// Select every event type the caller reads. Render-update-available is the map's -// only invalidation report, the two still-image types are the only reports that -// a still-image request finished, and loading-failed and render-error carry -// native failure text. Narrowing gates later events and keeps queued ones, so a -// caller drains what it already caused. -func (m *MapHandle) SetEventMask(mask RuntimeEventMask) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - - return checkNative(func() int32 { - return int32(C.mln_map_set_event_mask(C.mln_map(ptr), C.uint64_t(mask))) - }) +// SetEventMask submits a command that selects map-originated event types and +// . +func (m *MapHandle) SetEventMask(mask RuntimeEventMask) (*Future[CommandCompletion], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_event_mask(raw, C.uint64_t(mask), completion)) + }, completionCommand) } -// EventMask reports which map-originated event types this map queues. A map that -// has not been narrowed reports RuntimeEventMaskAll. +// EventMask returns the mask copied in the latest immutable map snapshot. func (m *MapHandle) EventMask() (RuntimeEventMask, error) { - ptr, release, err := m.ptr() + snapshot, err := m.Snapshot() if err != nil { return 0, err } - defer release() - defer m.state.KeepAlive() - - var raw C.uint64_t - if err := checkNative(func() int32 { - return int32(C.mln_map_get_event_mask(C.mln_map(ptr), &raw)) - }); err != nil { - return 0, err - } - return RuntimeEventMask(raw), nil + return snapshot.EventMask, nil } -// RequestRepaint requests a repaint for a continuous map. -func (m *MapHandle) RequestRepaint() error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { return int32(C.mln_map_request_repaint(C.mln_map(ptr))) }) +// RequestRepaint submits a repaint command for a continuous map. +func (m *MapHandle) RequestRepaint() (*Future[CommandCompletion], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_request_repaint(raw, completion)) + }, completionCommand) } -// RequestStillImage requests one still image for a static or tile map. -func (m *MapHandle) RequestStillImage() error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { return int32(C.mln_map_request_still_image(C.mln_map(ptr))) }) +// RequestStillImage starts one noncoalescing still-image operation for a static +// or tile map. +func (m *MapHandle) RequestStillImage() (*Future[struct{}], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_request_still_image(raw, completion)) + }, completionUnit) } -// SetStyleURL loads a style URL. Loading is asynchronous: a style that is -// missing, unreachable, or malformed still returns success here and reports -// through a map-loading-failed runtime event. A well-formed style that MapLibre -// rejects semantically produces neither an error nor an event. -func (m *MapHandle) SetStyleURL(url string) error { +// SetStyleURL submits a style URL command. Loading +// failures arrive through runtime events. +func (m *MapHandle) SetStyleURL(url string) (*Future[CommandCompletion], error) { if err := validateCStringArgument("style URL", url); err != nil { - return err - } - ptr, release, err := m.ptr() - if err != nil { - return err + return nil, err } - defer release() - defer m.state.KeepAlive() cURL := C.CString(url) defer C.free(unsafe.Pointer(cURL)) - return checkNative(func() int32 { return int32(C.mln_map_set_style_url(C.mln_map(ptr), cURL)) }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_url(raw, cURL, completion)) + }, completionCommand) } -// SetStyleJSON loads inline style JSON. Malformed JSON is reported twice: this -// call returns the parse error, and the same message arrives as a -// map-loading-failed runtime event. A well-formed style that MapLibre rejects -// semantically produces neither an error nor an event. -func (m *MapHandle) SetStyleJSON(json []byte) error { - ptr, release, err := m.ptr() - if err != nil { - return err +// SetStyleJSON submits an inline style command. +// Loading failures arrive through runtime events. +func (m *MapHandle) SetStyleJSON(json []byte) (*Future[CommandCompletion], error) { + if bytes.IndexByte(json, 0) >= 0 { + return nil, newBindingError(ErrInvalidArgument, "style JSON contains a NUL byte") } - defer release() - defer m.state.KeepAlive() jsonView := newCBufferView(json) defer jsonView.free() - return checkNative(func() int32 { return int32(C.mln_map_set_style_json(C.mln_map(ptr), jsonView.raw())) }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_json(raw, jsonView.raw(), completion)) + }, completionCommand) } -// SetFeatureState sets per-feature state on this map. The state value is -// copied before the call returns. -func (m *MapHandle) SetFeatureState(selector FeatureStateSelector, state []byte) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +// SetFeatureState submits a copied per-feature-state command. The selector +// and state value are copied before the call returns. +func (m *MapHandle) SetFeatureState(selector FeatureStateSelector, state []byte) (*Future[CommandCompletion], error) { rawSelector := newCFeatureStateSelector(selector) defer rawSelector.free() rawState := newCBufferView(state) defer rawState.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_feature_state(C.mln_map(ptr), &rawSelector.raw, rawState.raw())) - }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_feature_state(raw, &rawSelector.raw, rawState.raw(), completion)) + }, completionCommand) } -// FeatureState returns copied per-feature state from this map. -func (m *MapHandle) FeatureState(selector FeatureStateSelector) ([]byte, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() +// FeatureState starts an ordered read of copied per-feature state from this +// map's store. Missing feature state is reported as an empty JSON object. +func (m *MapHandle) FeatureState(selector FeatureStateSelector) (*Future[[]byte], error) { rawSelector := newCFeatureStateSelector(selector) defer rawSelector.free() - var buffer C.mln_buffer - if err := checkNative(func() int32 { - return int32(C.mln_map_get_feature_state(C.mln_map(ptr), &rawSelector.raw, &buffer)) - }); err != nil { - return nil, err - } - return goOwnedBuffer(buffer) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_feature_state(raw, &rawSelector.raw, completion)) + }, completionBuffer) } -// RemoveFeatureState removes per-feature state from this map. -func (m *MapHandle) RemoveFeatureState(selector FeatureStateSelector) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +// RemoveFeatureState submits a per-feature-state removal command. The selector +// is copied before the call returns. +func (m *MapHandle) RemoveFeatureState(selector FeatureStateSelector) (*Future[CommandCompletion], error) { rawSelector := newCFeatureStateSelector(selector) defer rawSelector.free() - return checkNative(func() int32 { - return int32(C.mln_map_remove_feature_state(C.mln_map(ptr), &rawSelector.raw)) - }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_remove_feature_state(raw, &rawSelector.raw, completion)) + }, completionCommand) } -// LoadedStyleJSON returns the style document this map's style was last parsed -// from, byte for byte, rather than a serialization of the live style. Runtime -// mutations such as adding a layer do not change it, and a failed parse leaves -// the previously parsed document in place. The result is empty only when no -// document has been parsed. -func (m *MapHandle) LoadedStyleJSON() ([]byte, error) { - return m.copyMapBytes(func(rawMap C.mln_map, data *C.uint8_t, capacity C.size_t, size *C.size_t) int32 { - return int32(C.mln_map_copy_loaded_style_json(rawMap, data, capacity, size)) - }) +// LoadedStyleJSON returns an ordered copy of the last loaded style document. +func (m *MapHandle) LoadedStyleJSON() (*Future[[]byte], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_loaded_style_json(raw, completion)) + }, completionBuffer) } -// StyleURL returns the URL this map's style was last requested from. -// SetStyleURL records the URL when the request is made, before the response -// arrives or the document parses, and SetStyleJSON clears it, so this and -// LoadedStyleJSON can disagree while a load is in flight or after one fails. -// -// The result is empty for a style loaded from inline JSON, a map that has -// loaded no style, and a URL load requested with an empty string alike. -func (m *MapHandle) StyleURL() (string, error) { - return m.copyMapText(func(rawMap C.mln_map, text *C.char, capacity C.size_t, size *C.size_t) int32 { - return int32(C.mln_map_copy_style_url(rawMap, text, capacity, size)) +// StyleURL returns an ordered copy of the last requested style URL. +func (m *MapHandle) StyleURL() (*Future[string], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_style_url(raw, completion)) + }, func(result *C.mln_completion_result) (string, error) { + value, err := completionBuffer(result) + return string(value), err }) } -// SetDebugOptions applies MapLibre debug overlay mask bits to a map. -func (m *MapHandle) SetDebugOptions(options MapDebugOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_set_debug_options(C.mln_map(ptr), C.uint32_t(options))) - }) -} - -// DebugOptions returns the current MapLibre debug overlay mask bits. -func (m *MapHandle) DebugOptions() (MapDebugOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return 0, err - } - defer release() - defer m.state.KeepAlive() - var raw C.uint32_t - if err := checkNative(func() int32 { - return int32(C.mln_map_get_debug_options(C.mln_map(ptr), &raw)) - }); err != nil { - return 0, err - } - return MapDebugOptions(raw), nil +// SetDebugOptions applies MapLibre debug overlay mask bits to a map. The +// committed mask is visible through Snapshot as MapSnapshot.DebugOptions. +func (m *MapHandle) SetDebugOptions(options MapDebugOptions) (*Future[CommandCompletion], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_debug_options(raw, C.uint32_t(options), completion)) + }, completionCommand) } // SetRenderingStatsViewEnabled enables or disables MapLibre's rendering stats -// overlay view. -func (m *MapHandle) SetRenderingStatsViewEnabled(enabled bool) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_set_rendering_stats_view_enabled(C.mln_map(ptr), C.bool(enabled))) - }) +// overlay view. The committed value is visible through Snapshot as +// MapSnapshot.RenderingStatsViewEnabled. +func (m *MapHandle) SetRenderingStatsViewEnabled(enabled bool) (*Future[CommandCompletion], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_rendering_stats_view_enabled(raw, C.bool(enabled), completion)) + }, completionCommand) } -// RenderingStatsViewEnabled reports whether MapLibre's rendering stats overlay -// view is enabled. -func (m *MapHandle) RenderingStatsViewEnabled() (bool, error) { - ptr, release, err := m.ptr() +// Snapshot returns a copy of the latest immutable map state. +func (m *MapHandle) Snapshot() (MapSnapshot, error) { + ptr, err := m.ptr() if err != nil { - return false, err + return MapSnapshot{}, err } - defer release() - defer m.state.KeepAlive() - var enabled C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_rendering_stats_view_enabled(C.mln_map(ptr), &enabled)) - }); err != nil { - return false, err - } - return bool(enabled), nil -} -// IsFullyLoaded reports whether MapLibre currently considers the map fully -// loaded. -func (m *MapHandle) IsFullyLoaded() (bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return false, err - } - defer release() defer m.state.KeepAlive() - var loaded C.bool + raw := C.mln_map_snapshot{size: C.uint32_t(unsafe.Sizeof(C.mln_map_snapshot{}))} if err := checkNative(func() int32 { - return int32(C.mln_map_is_fully_loaded(C.mln_map(ptr), &loaded)) + return int32(C.mln_map_snapshot_get(C.mln_map(ptr), &raw)) }); err != nil { - return false, err - } - return bool(loaded), nil -} - -// Size returns the map's logical viewport size in UI pixels and its scale -// factor. The scale factor is independent of any render target's. + return MapSnapshot{}, err + } + return MapSnapshot{ + Generation: uint64(raw.generation), + DebugOptions: MapDebugOptions(raw.debug_options), + Camera: goCameraOptions(raw.camera), + LogicalExtent: LogicalExtent{ + Width: uint32(raw.logical_extent.width), + Height: uint32(raw.logical_extent.height), + ScaleFactor: float64(raw.logical_extent.scale_factor), + }, + ProjectionMode: goProjectionModeOptions(raw.projection_mode), + Viewport: goViewportOptions(raw.viewport), + Tile: goTileOptions(raw.tile), + Bounds: goBoundOptions(raw.bounds), + FreeCamera: goFreeCameraOptions(raw.free_camera), + FullyLoaded: bool(raw.fully_loaded), + RenderingStatsViewEnabled: bool(raw.rendering_stats_view_enabled), + RepaintDemand: bool(raw.repaint_demand), + EventMask: RuntimeEventMask(raw.event_mask), + LatestRenderUpdateGeneration: uint64(raw.latest_render_update_generation), + }, nil +} + +// Size returns the latest logical extent copied from the map snapshot. func (m *MapHandle) Size() (width uint32, height uint32, scaleFactor float64, err error) { - ptr, release, err := m.ptr() + snapshot, err := m.Snapshot() if err != nil { return 0, 0, 0, err } - defer release() - defer m.state.KeepAlive() - var rawWidth, rawHeight C.uint32_t - var rawScaleFactor C.double - if err := checkNative(func() int32 { - return int32(C.mln_map_get_size(C.mln_map(ptr), &rawWidth, &rawHeight, &rawScaleFactor)) - }); err != nil { - return 0, 0, 0, err + extent := snapshot.LogicalExtent + return extent.Width, extent.Height, extent.ScaleFactor, nil +} + +// Resize submits a logical extent update. +func (m *MapHandle) Resize(extent LogicalExtent) (*Future[CommandCompletion], error) { + raw := C.mln_logical_extent{ + width: C.uint32_t(extent.Width), + height: C.uint32_t(extent.Height), + scale_factor: C.double(extent.ScaleFactor), } - return uint32(rawWidth), uint32(rawHeight), float64(rawScaleFactor), nil + return startMapCompletion(m, func(handle C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_resize(handle, raw, completion)) + }, completionCommand) } // DumpDebugLogs dumps map debug logs through MapLibre Native logging. -func (m *MapHandle) DumpDebugLogs() error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { return int32(C.mln_map_dump_debug_logs(C.mln_map(ptr))) }) +func (m *MapHandle) DumpDebugLogs() (*Future[CommandCompletion], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_dump_debug_logs(raw, completion)) + }, completionCommand) } -// Camera returns the current camera snapshot. +// Camera returns a copy of the latest published camera. func (m *MapHandle) Camera() (CameraOptions, error) { - ptr, release, err := m.ptr() + snapshot, err := m.CameraSnapshot() if err != nil { return CameraOptions{}, err } - defer release() - defer m.state.KeepAlive() - var raw C.mln_camera_options = C.mln_camera_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_camera(C.mln_map(ptr), &raw)) - }); err != nil { - return CameraOptions{}, err - } - return goCameraOptions(raw), nil -} - -// JumpTo applies a camera jump command. -func (m *MapHandle) JumpTo(camera CameraOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawCamera := cCameraOptions(camera) - return checkNative(func() int32 { - return int32(C.mln_map_jump_to(C.mln_map(ptr), &rawCamera)) - }) -} - -// EaseTo applies a camera ease transition command. A nil animation, or one with -// no Duration, uses the native default duration of zero, so the camera reaches -// the target immediately; set Duration explicitly to animate. -func (m *MapHandle) EaseTo(camera CameraOptions, animation *AnimationOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawCamera := cCameraOptions(camera) - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_ease_to(C.mln_map(ptr), &rawCamera, rawAnimationPtr)) - }) -} - -// FlyTo applies a camera fly transition command. A nil animation, or one with -// no Duration, flies at a default velocity of 1.2 ρ-screenfuls per second, so -// the duration scales with the distance travelled. -func (m *MapHandle) FlyTo(camera CameraOptions, animation *AnimationOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawCamera := cCameraOptions(camera) - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_fly_to(C.mln_map(ptr), &rawCamera, rawAnimationPtr)) - }) -} - -// MoveBy applies a screen-space pan command. -func (m *MapHandle) MoveBy(delta ScreenPoint) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_move_by(C.mln_map(ptr), C.double(delta.X), C.double(delta.Y))) - }) + return snapshot.Camera, nil } -// MoveByAnimated applies an animated screen-space pan command. A nil -// animation, or one with no Duration, applies the change instantly; see EaseTo. -func (m *MapHandle) MoveByAnimated(delta ScreenPoint, animation *AnimationOptions) error { - ptr, release, err := m.ptr() +// CameraSnapshot returns a copied camera and its map snapshot generation. +func (m *MapHandle) CameraSnapshot() (CameraSnapshot, error) { + ptr, err := m.ptr() if err != nil { - return err + return CameraSnapshot{}, err } - defer release() - defer m.state.KeepAlive() - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_move_by_animated( - C.mln_map(ptr), - C.double(delta.X), - C.double(delta.Y), - rawAnimationPtr, - )) - }) -} -// ScaleBy applies a screen-space zoom command. Passing nil anchor uses the -// native default zoom anchor. -func (m *MapHandle) ScaleBy(scale float64, anchor *ScreenPoint) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() defer m.state.KeepAlive() - var rawAnchor C.mln_screen_point - var rawAnchorPtr *C.mln_screen_point - if anchor != nil { - rawAnchor = cScreenPoint(*anchor) - rawAnchorPtr = &rawAnchor - } - return checkNative(func() int32 { - return int32(C.mln_map_scale_by(C.mln_map(ptr), C.double(scale), rawAnchorPtr)) - }) -} - -// ScaleByAnimated applies an animated screen-space zoom command. Passing nil -// anchor or animation uses the native default for that option. -func (m *MapHandle) ScaleByAnimated(scale float64, anchor *ScreenPoint, animation *AnimationOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - var rawAnchor C.mln_screen_point - var rawAnchorPtr *C.mln_screen_point - if anchor != nil { - rawAnchor = cScreenPoint(*anchor) - rawAnchorPtr = &rawAnchor + raw := C.mln_camera_options_default() + var generation C.uint64_t + if err := checkNative(func() int32 { + return int32(C.mln_map_camera_snapshot_get(C.mln_map(ptr), &raw, &generation)) + }); err != nil { + return CameraSnapshot{}, err } - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_scale_by_animated( - C.mln_map(ptr), - C.double(scale), - rawAnchorPtr, - rawAnimationPtr, - )) - }) + return CameraSnapshot{Generation: uint64(generation), Camera: goCameraOptions(raw)}, nil } -// RotateBy applies a screen-space rotate command. -func (m *MapHandle) RotateBy(first ScreenPoint, second ScreenPoint) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_rotate_by(C.mln_map(ptr), cScreenPoint(first), cScreenPoint(second))) - }) -} - -// RotateByAnimated applies an animated screen-space rotate command. A nil -// animation, or one with no Duration, applies the change instantly; see EaseTo. -func (m *MapHandle) RotateByAnimated(first ScreenPoint, second ScreenPoint, animation *AnimationOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_rotate_by_animated( - C.mln_map(ptr), - cScreenPoint(first), - cScreenPoint(second), - rawAnimationPtr, - )) +// QueryCamera starts an ordered camera read that observes every previously +// committed command. +func (m *MapHandle) QueryCamera() (*Future[CameraSnapshot], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_camera_query(raw, completion)) + }, func(result *C.mln_completion_result) (CameraSnapshot, error) { + raw, err := completionValue[C.mln_camera_query_result](result) + if err != nil { + return CameraSnapshot{}, err + } + return CameraSnapshot{Generation: uint64(raw.generation), Camera: goCameraOptions(raw.camera)}, nil }) } -// PitchBy applies a pitch delta command. -func (m *MapHandle) PitchBy(pitch float64) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_pitch_by(C.mln_map(ptr), C.double(pitch))) - }) +// UpdateCamera submits one atomic camera update. +func (m *MapHandle) UpdateCamera(update CameraUpdate) (*Future[CommandCompletion], error) { + raw := cCameraUpdate(update) + return startMapCompletion(m, func(handle C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_update_camera(handle, &raw, completion)) + }, completionCommand) } -// PitchByAnimated applies an animated pitch delta command. A nil animation, or -// one with no Duration, applies the change instantly; see EaseTo. -func (m *MapHandle) PitchByAnimated(pitch float64, animation *AnimationOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawAnimation, rawAnimationPtr := cAnimationOptionsPointer(animation) - _ = rawAnimation - return checkNative(func() int32 { - return int32(C.mln_map_pitch_by_animated(C.mln_map(ptr), C.double(pitch), rawAnimationPtr)) - }) +// ApplyCameraDelta submits one relative camera operation. +func (m *MapHandle) ApplyCameraDelta(delta CameraDelta) (*Future[CommandCompletion], error) { + raw := cCameraDelta(delta) + return startMapCompletion(m, func(handle C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_apply_camera_delta(handle, &raw, completion)) + }, completionCommand) } -// CancelTransitions cancels active camera transitions. -func (m *MapHandle) CancelTransitions() error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { return int32(C.mln_map_cancel_transitions(C.mln_map(ptr))) }) +// JumpTo submits an atomic camera jump. +func (m *MapHandle) JumpTo(camera CameraOptions) (*Future[CommandCompletion], error) { + return m.UpdateCamera(CameraUpdate{Mode: CameraUpdateModeJump, Camera: camera}) } -// SetGestureInProgress marks whether a host-driven gesture is in progress. The -// flag stays set until the host clears it, so pair every true with a false. -func (m *MapHandle) SetGestureInProgress(inProgress bool) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_map_set_gesture_in_progress(C.mln_map(ptr), C.bool(inProgress))) - }) +// EaseTo submits an atomic eased camera transition. +func (m *MapHandle) EaseTo(camera CameraOptions, animation *AnimationOptions) (*Future[CommandCompletion], error) { + return m.UpdateCamera(CameraUpdate{Mode: CameraUpdateModeEase, Camera: camera, Animation: animation}) } -// IsGestureInProgress reports whether a host-driven gesture is currently in -// progress. -func (m *MapHandle) IsGestureInProgress() (bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return false, err - } - defer release() - defer m.state.KeepAlive() - var inProgress C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_is_gesture_in_progress(C.mln_map(ptr), &inProgress)) - }); err != nil { - return false, err - } - return bool(inProgress), nil +// FlyTo submits an atomic flying camera transition. +func (m *MapHandle) FlyTo(camera CameraOptions, animation *AnimationOptions) (*Future[CommandCompletion], error) { + return m.UpdateCamera(CameraUpdate{Mode: CameraUpdateModeFly, Camera: camera, Animation: animation}) } // CameraForLatLngBounds computes a camera that fits geographic bounds. Passing // nil fitOptions uses native default fitting options. -func (m *MapHandle) CameraForLatLngBounds(bounds LatLngBounds, fitOptions *CameraFitOptions) (CameraOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return CameraOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_camera_options = C.mln_camera_options_default() +func (m *MapHandle) CameraForLatLngBounds(bounds LatLngBounds, fitOptions *CameraFitOptions) (*Future[CameraOptions], error) { rawFitOptions, rawFitOptionsPtr := cCameraFitOptionsPointer(fitOptions) _ = rawFitOptions - if err := checkNative(func() int32 { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { return int32(C.mln_map_camera_for_lat_lng_bounds( - C.mln_map(ptr), - cLatLngBounds(bounds), - rawFitOptionsPtr, - &raw, + raw, cLatLngBounds(bounds), rawFitOptionsPtr, completion, )) - }); err != nil { - return CameraOptions{}, err - } - return goCameraOptions(raw), nil + }, func(result *C.mln_completion_result) (CameraOptions, error) { + raw, err := completionValue[C.mln_camera_options](result) + return goCameraOptions(raw), err + }) } // CameraForLatLngs computes a camera that fits geographic coordinates. Passing // nil fitOptions uses native default fitting options. -func (m *MapHandle) CameraForLatLngs(coordinates []LatLng, fitOptions *CameraFitOptions) (CameraOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return CameraOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_camera_options = C.mln_camera_options_default() +func (m *MapHandle) CameraForLatLngs(coordinates []LatLng, fitOptions *CameraFitOptions) (*Future[CameraOptions], error) { rawCoordinates := cLatLngSlice(coordinates) var rawCoordinatesPtr *C.mln_lat_lng if len(rawCoordinates) > 0 { @@ -721,45 +474,32 @@ func (m *MapHandle) CameraForLatLngs(coordinates []LatLng, fitOptions *CameraFit } rawFitOptions, rawFitOptionsPtr := cCameraFitOptionsPointer(fitOptions) _ = rawFitOptions - if err := checkNative(func() int32 { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { return int32(C.mln_map_camera_for_lat_lngs( - C.mln_map(ptr), - rawCoordinatesPtr, - C.size_t(len(rawCoordinates)), - rawFitOptionsPtr, - &raw, + raw, rawCoordinatesPtr, C.size_t(len(rawCoordinates)), + rawFitOptionsPtr, completion, )) - }); err != nil { - return CameraOptions{}, err - } - return goCameraOptions(raw), nil + }, func(result *C.mln_completion_result) (CameraOptions, error) { + value, err := completionValue[C.mln_camera_options](result) + return goCameraOptions(value), err + }) } // CameraForGeometry computes a camera that fits a geometry. Passing nil // fitOptions uses native default fitting options. -func (m *MapHandle) CameraForGeometry(geometry []byte, fitOptions *CameraFitOptions) (CameraOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return CameraOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_camera_options = C.mln_camera_options_default() +func (m *MapHandle) CameraForGeometry(geometry []byte, fitOptions *CameraFitOptions) (*Future[CameraOptions], error) { rawGeometry := newCBufferView(geometry) defer rawGeometry.free() rawFitOptions, rawFitOptionsPtr := cCameraFitOptionsPointer(fitOptions) _ = rawFitOptions - if err := checkNative(func() int32 { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { return int32(C.mln_map_camera_for_geometry( - C.mln_map(ptr), - rawGeometry.raw(), - rawFitOptionsPtr, - &raw, + raw, rawGeometry.raw(), rawFitOptionsPtr, completion, )) - }); err != nil { - return CameraOptions{}, err - } - return goCameraOptions(raw), nil + }, func(result *C.mln_completion_result) (CameraOptions, error) { + value, err := completionValue[C.mln_camera_options](result) + return goCameraOptions(value), err + }) } // LatLngBoundsForCamera computes geographic bounds for a camera from two @@ -769,21 +509,14 @@ func (m *MapHandle) CameraForGeometry(geometry []byte, fitOptions *CameraFitOpti // camera in the current viewport. When bearing and pitch are zero, the box // equals the visible area. Those corners are the northwest and southeast of // the viewport. Longitudes stay in -180 to 180. -func (m *MapHandle) LatLngBoundsForCamera(camera CameraOptions) (LatLngBounds, error) { - ptr, release, err := m.ptr() - if err != nil { - return LatLngBounds{}, err - } - defer release() - defer m.state.KeepAlive() +func (m *MapHandle) LatLngBoundsForCamera(camera CameraOptions) (*Future[LatLngBounds], error) { rawCamera := cCameraOptions(camera) - var raw C.mln_lat_lng_bounds - if err := checkNative(func() int32 { - return int32(C.mln_map_lat_lng_bounds_for_camera(C.mln_map(ptr), &rawCamera, &raw)) - }); err != nil { - return LatLngBounds{}, err - } - return goLatLngBounds(raw), nil + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_lat_lng_bounds_for_camera(raw, &rawCamera, completion)) + }, func(result *C.mln_completion_result) (LatLngBounds, error) { + value, err := completionValue[C.mln_lat_lng_bounds](result) + return goLatLngBounds(value), err + }) } // LatLngBoundsForCameraUnwrapped computes geographic bounds for a camera from @@ -793,335 +526,189 @@ func (m *MapHandle) LatLngBoundsForCamera(camera CameraOptions) (LatLngBounds, e // the projected viewport. Longitudes unwrap onto the shortest path through the // center. A viewport that crosses the antimeridian reports values outside -180 // to 180. -func (m *MapHandle) LatLngBoundsForCameraUnwrapped(camera CameraOptions) (LatLngBounds, error) { - ptr, release, err := m.ptr() - if err != nil { - return LatLngBounds{}, err - } - defer release() - defer m.state.KeepAlive() +func (m *MapHandle) LatLngBoundsForCameraUnwrapped(camera CameraOptions) (*Future[LatLngBounds], error) { rawCamera := cCameraOptions(camera) - var raw C.mln_lat_lng_bounds - if err := checkNative(func() int32 { - return int32(C.mln_map_lat_lng_bounds_for_camera_unwrapped(C.mln_map(ptr), &rawCamera, &raw)) - }); err != nil { - return LatLngBounds{}, err - } - return goLatLngBounds(raw), nil -} - -// Bounds returns map camera constraint options. -func (m *MapHandle) Bounds() (BoundOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return BoundOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_bound_options = C.mln_bound_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_bounds(C.mln_map(ptr), &raw)) - }); err != nil { - return BoundOptions{}, err - } - return goBoundOptions(raw), nil -} - -// SetBounds applies selected map camera constraint options. -func (m *MapHandle) SetBounds(options BoundOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - rawOptions, err := cBoundOptions(options) - if err != nil { - return err - } - return checkNative(func() int32 { - return int32(C.mln_map_set_bounds(C.mln_map(ptr), &rawOptions)) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_lat_lng_bounds_for_camera_unwrapped(raw, &rawCamera, completion)) + }, func(result *C.mln_completion_result) (LatLngBounds, error) { + value, err := completionValue[C.mln_lat_lng_bounds](result) + return goLatLngBounds(value), err }) } -// FreeCameraOptions returns current free camera position and orientation. -func (m *MapHandle) FreeCameraOptions() (FreeCameraOptions, error) { - ptr, release, err := m.ptr() +// SetBounds applies selected map camera constraint options. The committed +// constraints are visible through Snapshot as MapSnapshot.Bounds. +func (m *MapHandle) SetBounds(options BoundOptions) (*Future[CommandCompletion], error) { + rawOptions, err := cBoundOptions(options) if err != nil { - return FreeCameraOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_free_camera_options = C.mln_free_camera_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_free_camera_options(C.mln_map(ptr), &raw)) - }); err != nil { - return FreeCameraOptions{}, err + return nil, err } - return goFreeCameraOptions(raw), nil + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_bounds(raw, &rawOptions, completion)) + }, completionCommand) } // SetFreeCameraOptions applies selected free camera position and orientation -// fields. -func (m *MapHandle) SetFreeCameraOptions(options FreeCameraOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +// fields. The committed options are visible through Snapshot as +// MapSnapshot.FreeCamera. +func (m *MapHandle) SetFreeCameraOptions(options FreeCameraOptions) (*Future[CommandCompletion], error) { rawOptions := cFreeCameraOptions(options) - return checkNative(func() int32 { - return int32(C.mln_map_set_free_camera_options(C.mln_map(ptr), &rawOptions)) - }) -} - -// ViewportOptions returns live map viewport and render-transform controls. -func (m *MapHandle) ViewportOptions() (ViewportOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return ViewportOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_map_viewport_options = C.mln_map_viewport_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_viewport_options(C.mln_map(ptr), &raw)) - }); err != nil { - return ViewportOptions{}, err - } - return goViewportOptions(raw), nil + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_free_camera_options(raw, &rawOptions, completion)) + }, completionCommand) } // SetViewportOptions applies selected live map viewport and render-transform -// controls. -func (m *MapHandle) SetViewportOptions(options ViewportOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +// controls. The committed options are visible through Snapshot as +// MapSnapshot.Viewport. +func (m *MapHandle) SetViewportOptions(options ViewportOptions) (*Future[CommandCompletion], error) { rawOptions := cViewportOptions(options) - return checkNative(func() int32 { - return int32(C.mln_map_set_viewport_options(C.mln_map(ptr), &rawOptions)) - }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_viewport_options(raw, &rawOptions, completion)) + }, completionCommand) } -// TileOptions returns tile prefetch and LOD tuning controls. -func (m *MapHandle) TileOptions() (TileOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return TileOptions{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_map_tile_options = C.mln_map_tile_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_tile_options(C.mln_map(ptr), &raw)) - }); err != nil { - return TileOptions{}, err - } - return goTileOptions(raw), nil -} - -// SetTileOptions applies selected tile prefetch and LOD tuning controls. -func (m *MapHandle) SetTileOptions(options TileOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +// SetTileOptions applies selected tile prefetch and LOD tuning controls. The +// committed options are visible through Snapshot as MapSnapshot.Tile. +func (m *MapHandle) SetTileOptions(options TileOptions) (*Future[CommandCompletion], error) { rawOptions := cTileOptions(options) - return checkNative(func() int32 { - return int32(C.mln_map_set_tile_options(C.mln_map(ptr), &rawOptions)) - }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_tile_options(raw, &rawOptions, completion)) + }, completionCommand) } -// ProjectionMode returns current axonometric rendering options. +// ProjectionMode returns the latest published axonometric rendering options. func (m *MapHandle) ProjectionMode() (ProjectionModeOptions, error) { - ptr, release, err := m.ptr() + snapshot, err := m.Snapshot() if err != nil { return ProjectionModeOptions{}, err } - defer release() - defer m.state.KeepAlive() - var raw C.mln_projection_mode = C.mln_projection_mode_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_projection_mode(C.mln_map(ptr), &raw)) - }); err != nil { - return ProjectionModeOptions{}, err - } - return goProjectionModeOptions(raw), nil + return snapshot.ProjectionMode, nil } // SetProjectionMode applies axonometric rendering option fields. -func (m *MapHandle) SetProjectionMode(options ProjectionModeOptions) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() +func (m *MapHandle) SetProjectionMode(options ProjectionModeOptions) (*Future[CommandCompletion], error) { rawOptions := cProjectionModeOptions(options) - return checkNative(func() int32 { - return int32(C.mln_map_set_projection_mode(C.mln_map(ptr), &rawOptions)) - }) + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_projection_mode(raw, &rawOptions, completion)) + }, completionCommand) } // PixelForLatLng converts a geographic coordinate to a logical screen point for // the current map. -func (m *MapHandle) PixelForLatLng(coordinate LatLng) (ScreenPoint, error) { - ptr, release, err := m.ptr() - if err != nil { - return ScreenPoint{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_screen_point - if err := checkNative(func() int32 { - return int32(C.mln_map_pixel_for_lat_lng(C.mln_map(ptr), cLatLng(coordinate), &raw)) - }); err != nil { - return ScreenPoint{}, err - } - return goScreenPoint(raw), nil +func (m *MapHandle) PixelForLatLng(coordinate LatLng) (*Future[ScreenPoint], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_pixel_for_lat_lng(raw, cLatLng(coordinate), completion)) + }, func(result *C.mln_completion_result) (ScreenPoint, error) { + value, err := completionValue[C.mln_screen_point](result) + return goScreenPoint(value), err + }) } // LatLngForPixel converts a logical screen point to a geographic coordinate for // the current map. -func (m *MapHandle) LatLngForPixel(point ScreenPoint) (LatLng, error) { - ptr, release, err := m.ptr() - if err != nil { - return LatLng{}, err - } - defer release() - defer m.state.KeepAlive() - var raw C.mln_lat_lng - if err := checkNative(func() int32 { - return int32(C.mln_map_lat_lng_for_pixel(C.mln_map(ptr), cScreenPoint(point), &raw)) - }); err != nil { - return LatLng{}, err - } - return goLatLng(raw), nil +func (m *MapHandle) LatLngForPixel(point ScreenPoint) (*Future[LatLng], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_lat_lng_for_pixel(raw, cScreenPoint(point), completion)) + }, func(result *C.mln_completion_result) (LatLng, error) { + value, err := completionValue[C.mln_lat_lng](result) + return goLatLng(value), err + }) } // PixelsForLatLngs converts geographic coordinates to logical screen points for // the current map. -func (m *MapHandle) PixelsForLatLngs(coordinates []LatLng) ([]ScreenPoint, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() +func (m *MapHandle) PixelsForLatLngs(coordinates []LatLng) (*Future[[]ScreenPoint], error) { rawCoordinates := cLatLngSlice(coordinates) - rawPoints := make([]C.mln_screen_point, len(coordinates)) var rawCoordinatesPtr *C.mln_lat_lng - var rawPointsPtr *C.mln_screen_point if len(coordinates) > 0 { rawCoordinatesPtr = &rawCoordinates[0] - rawPointsPtr = &rawPoints[0] } - if err := checkNative(func() int32 { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { return int32(C.mln_map_pixels_for_lat_lngs( - C.mln_map(ptr), - rawCoordinatesPtr, - C.size_t(len(coordinates)), - rawPointsPtr, + raw, rawCoordinatesPtr, C.size_t(len(coordinates)), completion, )) - }); err != nil { - return nil, err - } - return goScreenPointSlice(rawPoints), nil + }, func(result *C.mln_completion_result) ([]ScreenPoint, error) { + values, err := completionSlice[C.mln_screen_point](result) + return goScreenPointSlice(values), err + }) } // LatLngsForPixels converts logical screen points to geographic coordinates for // the current map. -func (m *MapHandle) LatLngsForPixels(points []ScreenPoint) ([]LatLng, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() +func (m *MapHandle) LatLngsForPixels(points []ScreenPoint) (*Future[[]LatLng], error) { rawPoints := cScreenPointSlice(points) - rawCoordinates := make([]C.mln_lat_lng, len(points)) var rawPointsPtr *C.mln_screen_point - var rawCoordinatesPtr *C.mln_lat_lng if len(points) > 0 { rawPointsPtr = &rawPoints[0] - rawCoordinatesPtr = &rawCoordinates[0] } - if err := checkNative(func() int32 { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { return int32(C.mln_map_lat_lngs_for_pixels( - C.mln_map(ptr), - rawPointsPtr, - C.size_t(len(points)), - rawCoordinatesPtr, + raw, rawPointsPtr, C.size_t(len(points)), completion, )) - }); err != nil { - return nil, err - } - return goLatLngSlice(rawCoordinates), nil + }, func(result *C.mln_completion_result) ([]LatLng, error) { + values, err := completionSlice[C.mln_lat_lng](result) + return goLatLngSlice(values), err + }) } -// Close destroys this map. A successful close makes later calls no-ops. A -// failed close leaves the native handle live so callers can retry on the owner -// thread. Close discards this map's queued runtime events and its recorded -// loading failure without a flush and without a terminal event, and releases the -// callback state of every custom geometry source the map still holds. +// Close releases this map's public native handle. Use CloseAsync when later +// work depends on the end of native teardown. func (m *MapHandle) Close() error { + _, err := m.CloseAsync() + return err +} + +// CloseAsync releases this map's public native handle and returns the future +// for native teardown. +func (m *MapHandle) CloseAsync() (*Future[struct{}], error) { if m == nil || m.state == nil { - return newBindingError(ErrInvalidArgument, "MapHandle is nil") + return nil, newBindingError(ErrInvalidArgument, "MapHandle is nil") } defer func() { if m.runtime != nil && m.runtime.state != nil { m.runtime.state.KeepAlive() } }() - var bindingErr error - if err := checkNative(func() int32 { - status, err := m.state.CloseChecked(func(native nativeMap) int32 { - return destroyMapHandle(native) - }) - if err != nil { - if errors.Is(err, handle.ErrLiveChildren) { - bindingErr = newBindingError(ErrInvalidState, "MapHandle has live child handles") - return int32(C.MLN_STATUS_OK) + var closeErr error + teardown := completedFuture(struct{}{}) + _ = m.state.Close(func(native nativeMap) int32 { + if destroyMapHandle != nil { + status := destroyMapHandle(native) + if status != int32(C.MLN_STATUS_OK) { + closeErr = &Error{ + kind: kindForStatus(status), + rawStatus: status, + hasStatus: true, + diagnostic: "synthetic map close failure", + } } - bindingErr = newBindingError(ErrInvalidState, err.Error()) - return int32(C.MLN_STATUS_OK) + return status } - return status - }); err != nil { - return err - } - if bindingErr != nil { - return bindingErr + var err error + teardown, err = startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_release(C.mln_map(native), completion)) + }, completionUnit) + if err != nil { + closeErr = err + return statusFromError(err) + } + return int32(C.MLN_STATUS_OK) + }) + if closeErr != nil { + return nil, closeErr } if m.runtime != nil { m.runtime.unregisterMap(m) } - m.runtimeChild.Release() - return nil + return teardown, nil } -// mapSizeByIDForTest calls the C size accessor with a raw map id, so a test can -// replay a released id or use one from another thread. The safe API expresses -// neither. +// mapSizeByIDForTest calls the C snapshot accessor with a raw map id, so a test +// can replay a released id. The safe API cannot express a raw id. func mapSizeByIDForTest(id nativeMap) error { - var width, height C.uint32_t - var scale C.double - return checkNative(func() int32 { - return int32(C.mln_map_get_size(C.mln_map(id), &width, &height, &scale)) - }) -} - -// pumpRuntimeWithMapIDForTest passes a map id where a runtime id belongs, which -// the distinct Go types make unexpressible in the safe API. -func pumpRuntimeWithMapIDForTest(id nativeMap) error { + raw := C.mln_map_snapshot{size: C.uint32_t(unsafe.Sizeof(C.mln_map_snapshot{}))} return checkNative(func() int32 { - return int32(C.mln_runtime_pump(C.mln_runtime(id), 0, -1)) + return int32(C.mln_map_snapshot_get(C.mln_map(id), &raw)) }) } diff --git a/bindings/go/map_geometry_test.go b/bindings/go/map_geometry_test.go index f7c93ec91..c88afceb0 100644 --- a/bindings/go/map_geometry_test.go +++ b/bindings/go/map_geometry_test.go @@ -6,60 +6,58 @@ import ( ) func TestMapCameraGeometryAndCoordinateConversions(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() geometry := []byte(`{"type":"LineString","coordinates":[[0,0],[1,1]]}`) - camera, err := m.CameraForGeometry(geometry, nil) + camera, err := awaitForTest(m.CameraForGeometry(geometry, nil)) if err != nil { t.Fatalf("CameraForGeometry(): %v", err) } if camera.Center == nil || camera.Zoom == nil { t.Fatalf("CameraForGeometry() = %+v, want center and zoom", camera) } - point, err := m.PixelForLatLng(LatLng{Latitude: 0, Longitude: 0}) + point, err := awaitForTest(m.PixelForLatLng(LatLng{Latitude: 0, Longitude: 0})) if err != nil { t.Fatalf("PixelForLatLng(): %v", err) } - coordinate, err := m.LatLngForPixel(point) + coordinate, err := awaitForTest(m.LatLngForPixel(point)) if err != nil { t.Fatalf("LatLngForPixel(): %v", err) } if coordinate.Latitude < -90 || coordinate.Latitude > 90 || coordinate.Longitude < -180 || coordinate.Longitude > 180 { t.Fatalf("LatLngForPixel(PixelForLatLng()) = %+v, want valid coordinate", coordinate) } - points, err := m.PixelsForLatLngs([]LatLng{{Latitude: 0, Longitude: 0}, {Latitude: 1, Longitude: 1}}) + points, err := awaitForTest(m.PixelsForLatLngs([]LatLng{{Latitude: 0, Longitude: 0}, {Latitude: 1, Longitude: 1}})) if err != nil { t.Fatalf("PixelsForLatLngs(): %v", err) } if len(points) != 2 { t.Fatalf("PixelsForLatLngs() length = %d, want 2", len(points)) } - coordinates, err := m.LatLngsForPixels(points) + coordinates, err := awaitForTest(m.LatLngsForPixels(points)) if err != nil { t.Fatalf("LatLngsForPixels(): %v", err) } if len(coordinates) != 2 { t.Fatalf("LatLngsForPixels() length = %d, want 2", len(coordinates)) } - projection, err := m.NewProjection() + projection, err := awaitForTest(m.NewProjection()) if err != nil { t.Fatalf("Projection(): %v", err) } diff --git a/bindings/go/map_test.go b/bindings/go/map_test.go index d5ff40ffd..221b8a2eb 100644 --- a/bindings/go/map_test.go +++ b/bindings/go/map_test.go @@ -2,70 +2,63 @@ package maplibre import ( "errors" - stdruntime "runtime" - "sync/atomic" + "math" "testing" - "time" - - "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" ) func TestRuntimeMapLifecycle(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } - if err := runtime.Close(); !errors.Is(err, ErrInvalidState) { + if err := closeRuntimeForTest(runtime); !errors.Is(err, ErrInvalidState) { _ = m.Close() - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("Close() with live map error = %v, want ErrInvalidState", err) } else { var bindingErr *Error - if !errors.As(err, &bindingErr) || bindingErr.Diagnostic() != "RuntimeHandle has live child handles" { + if !errors.As(err, &bindingErr) || + bindingErr.Diagnostic() != "handle still owns live or pending children" { _ = m.Close() - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("Close() with live map diagnostic = %v", err) } } if err := m.Close(); err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("Map Close(): %v", err) } if err := m.Close(); err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("second Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Fatalf("Runtime Close(): %v", err) } } func TestMapIDIdentifiesEachMapUntilClose(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - first, err := runtime.NewMap() + first, err := awaitForTest(runtime.NewMap()) if err != nil { t.Fatalf("NewMap(): %v", err) } - second, err := runtime.NewMap() + second, err := awaitForTest(runtime.NewMap()) if err != nil { _ = first.Close() t.Fatalf("second NewMap(): %v", err) @@ -103,262 +96,225 @@ func TestMapIDIdentifiesEachMapUntilClose(t *testing.T) { } } -func TestMapCloseFailedDestroyLeavesHandleRetryable(t *testing.T) { - state, err := handle.New(nativeMap(0x0200_0000_0000_002a), "MapHandle") - if err != nil { - t.Fatal(err) - } - m := &MapHandle{state: state} - - oldDestroy := destroyMapHandle - defer func() { - destroyMapHandle = oldDestroy - }() - var calls atomic.Int32 - destroyMapHandle = func(nativeMap) int32 { - if calls.Add(1) == 1 { - return -3 - } - return 0 - } - - if err := m.Close(); !errors.Is(err, ErrWrongThread) { - t.Fatalf("first Close() error = %v, want ErrWrongThread", err) - } - ptr, release, err := m.ptr() - if err != nil { - t.Fatalf("ptr() after failed Close(): %v", err) - } - if ptr == 0 { - t.Fatal("ptr() after failed Close() returned the null handle") - } - release() - if err := m.Close(); err != nil { - t.Fatalf("second Close(): %v", err) - } - if err := m.Close(); err != nil { - t.Fatalf("third Close(): %v", err) - } - if got := calls.Load(); got != 2 { - t.Fatalf("destroy calls = %d, want 2", got) - } -} - -func TestMapCloseWaitsForActiveBorrow(t *testing.T) { - state, err := handle.New(nativeMap(0x0200_0000_0000_002a), "MapHandle") - if err != nil { - t.Fatal(err) - } - m := &MapHandle{state: state} - - ptr, release, err := m.ptr() - if err != nil { - t.Fatalf("ptr(): %v", err) - } - if ptr == 0 { - t.Fatal("ptr() returned the null handle") - } - - oldDestroy := destroyMapHandle - defer func() { - destroyMapHandle = oldDestroy - }() - destroyCalled := make(chan struct{}) - destroyMapHandle = func(nativeMap) int32 { - close(destroyCalled) - return 0 - } - - closeDone := make(chan error, 1) - go func() { - closeDone <- m.Close() - }() - - deadline := time.After(2 * time.Second) - for { - if _, extraRelease, err := m.ptr(); err == nil { - extraRelease() - select { - case <-destroyCalled: - t.Fatal("destroy ran before active borrow was released") - case <-deadline: - t.Fatal("Close did not enter releasing state") - case <-time.After(100 * time.Microsecond): - continue - } - } - break - } - - release() - if err := <-closeDone; err != nil { - t.Fatalf("Close(): %v", err) - } - select { - case <-destroyCalled: - default: - t.Fatal("destroy did not run after borrow release") - } -} - func TestMapCommandsAndStyleLoadingUseNativeABI(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.RequestRepaint(); err != nil { + if _, err := m.RequestRepaint(); err != nil { t.Fatalf("RequestRepaint(): %v", err) } - if err := m.RequestStillImage(); !errors.Is(err, ErrInvalidState) { + if _, err := m.RequestStillImage(); !errors.Is(err, ErrInvalidState) { t.Fatalf("RequestStillImage() on continuous map error = %v, want ErrInvalidState", err) } - if err := m.SetStyleJSON([]byte(minimalStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(minimalStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } - if err := m.SetStyleURL("http://example.com/style.json"); err != nil { + if _, err := m.SetStyleURL("http://example.com/style.json"); err != nil { t.Fatalf("SetStyleURL(): %v", err) } } func TestMapReportsLoadedStyleDocumentAndURL(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if document, err := m.LoadedStyleJSON(); err != nil || len(document) != 0 { + if document, err := awaitForTest(m.LoadedStyleJSON()); err != nil || len(document) != 0 { t.Fatalf("LoadedStyleJSON() before load = %q, %v, want \"\", nil", document, err) } - if url, err := m.StyleURL(); err != nil || url != "" { + if url, err := awaitForTest(m.StyleURL()); err != nil || url != "" { t.Fatalf("StyleURL() before load = %q, %v, want \"\", nil", url, err) } - if err := m.SetStyleJSON([]byte(minimalStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(minimalStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } - document, err := m.LoadedStyleJSON() + document, err := awaitForTest(m.LoadedStyleJSON()) if err != nil { t.Fatalf("LoadedStyleJSON(): %v", err) } if string(document) != minimalStyleJSON { t.Fatalf("LoadedStyleJSON() = %q, want %q", document, minimalStyleJSON) } - if url, err := m.StyleURL(); err != nil || url != "" { + if url, err := awaitForTest(m.StyleURL()); err != nil || url != "" { t.Fatalf("StyleURL() after inline JSON = %q, %v, want \"\", nil", url, err) } // The URL is request state, recorded before the load can succeed, while the // document still reports the style that last parsed. const styleURL = "http://example.com/style.json" - if err := m.SetStyleURL(styleURL); err != nil { + if _, err := m.SetStyleURL(styleURL); err != nil { t.Fatalf("SetStyleURL(): %v", err) } - url, err := m.StyleURL() + url, err := awaitForTest(m.StyleURL()) if err != nil { t.Fatalf("StyleURL(): %v", err) } if url != styleURL { t.Fatalf("StyleURL() = %q, want %q", url, styleURL) } - if document, err := m.LoadedStyleJSON(); err != nil || string(document) != minimalStyleJSON { + if document, err := awaitForTest(m.LoadedStyleJSON()); err != nil || string(document) != minimalStyleJSON { t.Fatalf("LoadedStyleJSON() after URL request = %q, %v, want the previously parsed document", document, err) } } -func TestMapDebugAndStatusHelpersUseNativeABI(t *testing.T) { - lockOSThreadForTest(t) - +// A committed command's terminal event carries the published map snapshot +// generation, so a snapshot at or past that generation shows the committed +// value. +func TestMapSnapshotObservesCommittedCommands(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() options := MapDebugTileBorders | MapDebugCollision - if err := m.SetDebugOptions(options); err != nil { - t.Fatalf("SetDebugOptions(): %v", err) - } - got, err := m.DebugOptions() + completion, err := m.SetDebugOptions(options) + committed := requireCommandCommitted(t, runtime, completion, err) + snapshot, err := m.Snapshot() if err != nil { - t.Fatalf("DebugOptions(): %v", err) + t.Fatalf("Snapshot(): %v", err) } - if !got.Has(options) { - t.Fatalf("DebugOptions() = %v, want bits %v", got, options) + if snapshot.Generation < committed { + t.Fatalf("Snapshot() generation = %d, want at least the committed %d", snapshot.Generation, committed) } - if err := m.SetRenderingStatsViewEnabled(true); err != nil { - t.Fatalf("SetRenderingStatsViewEnabled(true): %v", err) + if !snapshot.DebugOptions.Has(options) { + t.Fatalf("Snapshot() DebugOptions = %v, want bits %v", snapshot.DebugOptions, options) } - if got, err := m.RenderingStatsViewEnabled(); err != nil || !got { - t.Fatalf("RenderingStatsViewEnabled() = %v, %v; want true, nil", got, err) + + completion, err = m.SetRenderingStatsViewEnabled(true) + committed = requireCommandCommitted(t, runtime, completion, err) + snapshot, err = m.Snapshot() + if err != nil { + t.Fatalf("Snapshot(): %v", err) } - if _, err := m.IsFullyLoaded(); err != nil { - t.Fatalf("IsFullyLoaded(): %v", err) + if snapshot.Generation < committed || !snapshot.RenderingStatsViewEnabled { + t.Fatalf("Snapshot() = (generation %d, stats view %v), want at least %d and true", + snapshot.Generation, snapshot.RenderingStatsViewEnabled, committed) } - if err := m.DumpDebugLogs(); err != nil { + if _, err := m.DumpDebugLogs(); err != nil { t.Fatalf("DumpDebugLogs(): %v", err) } } -func TestMapSizeReportsCreationExtentAndPixelRatio(t *testing.T) { - lockOSThreadForTest(t) +// The tile, bounds, and free-camera snapshot fields round-trip through their +// set commands. +func TestMapSnapshotRoundTripsOptionCommands(t *testing.T) { + runtime, err := NewRuntime() + if err != nil { + t.Fatalf("NewRuntime(): %v", err) + } + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(256, 256, 1))) + if err != nil { + _ = closeRuntimeForTest(runtime) + t.Fatalf("NewMapWithOptions(): %v", err) + } + defer func() { + if err := m.Close(); err != nil { + t.Errorf("Map Close(): %v", err) + } + if err := closeRuntimeForTest(runtime); err != nil { + t.Errorf("Runtime Close(): %v", err) + } + }() + + completion, err := m.SetTileOptions(TileOptions{}.WithPrefetchZoomDelta(3)) + committed := requireCommandCommitted(t, runtime, completion, err) + snapshot, err := m.Snapshot() + if err != nil { + t.Fatalf("Snapshot(): %v", err) + } + if snapshot.Generation < committed { + t.Fatalf("Snapshot() generation = %d, want at least the committed %d", snapshot.Generation, committed) + } + if snapshot.Tile.PrefetchZoomDelta == nil || *snapshot.Tile.PrefetchZoomDelta != 3 { + t.Fatalf("Snapshot() Tile = %#v, want prefetch zoom delta 3", snapshot.Tile) + } + completion, err = m.SetBounds(BoundOptions{}.WithMinZoom(2).WithMaxZoom(15)) + committed = requireCommandCommitted(t, runtime, completion, err) + snapshot, err = m.Snapshot() + if err != nil { + t.Fatalf("Snapshot(): %v", err) + } + if snapshot.Generation < committed || + snapshot.Bounds.MinZoom == nil || *snapshot.Bounds.MinZoom != 2 || + snapshot.Bounds.MaxZoom == nil || *snapshot.Bounds.MaxZoom != 15 { + t.Fatalf("Snapshot() Bounds = %#v, want zoom range 2 to 15", snapshot.Bounds) + } + + position := Vec3{X: 0.25, Y: 0.25, Z: 0.1} + completion, err = m.SetFreeCameraOptions(FreeCameraOptions{}.WithPosition(position)) + committed = requireCommandCommitted(t, runtime, completion, err) + snapshot, err = m.Snapshot() + if err != nil { + t.Fatalf("Snapshot(): %v", err) + } + if snapshot.Generation < committed || snapshot.FreeCamera.Position == nil { + t.Fatalf("Snapshot() FreeCamera = %#v, want a position", snapshot.FreeCamera) + } + got := *snapshot.FreeCamera.Position + if math.Abs(got.X-position.X) > 1e-6 || math.Abs(got.Y-position.Y) > 1e-6 || math.Abs(got.Z-position.Z) > 1e-6 { + t.Fatalf("Snapshot() FreeCamera position = %#v, want %#v", got, position) + } +} + +func TestMapSizeReportsCreationExtentAndPixelRatio(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 256, 2)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 256, 2))) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() @@ -373,8 +329,6 @@ func TestMapSizeReportsCreationExtentAndPixelRatio(t *testing.T) { } func TestMapAcceptsFastPFORDecoding(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) @@ -384,104 +338,100 @@ func TestMapAcceptsFastPFORDecoding(t *testing.T) { t.Fatalf("NewMapOptions().FastPFOREnabled = true; want false") } options.FastPFOREnabled = true - m, err := runtime.NewMapWithOptions(options) + m, err := awaitForTest(runtime.NewMapWithOptions(options)) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } } -func TestMapDebugOptionsRejectUnknownBits(t *testing.T) { - lockOSThreadForTest(t) - +func TestMapDebugOptionsRejectUnknownBitsBeforeSubmission(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetDebugOptions(MapDebugOptions(1 << 31)); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetDebugOptions(unknown) error = %v, want ErrInvalidArgument", err) + future, err := m.SetDebugOptions(MapDebugOptions(1 << 31)) + if future != nil || !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetDebugOptions(unknown) = (%v, %v), want nil and ErrInvalidArgument", future, err) } } func TestMapStyleStringsRejectEmbeddedNUL(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleURL("http://example.com/\x00style.json"); !errors.Is(err, ErrInvalidArgument) { + if _, err := m.SetStyleURL("http://example.com/\x00style.json"); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("SetStyleURL embedded NUL error = %v, want ErrInvalidArgument", err) } - if err := m.SetStyleJSON([]byte("{\x00}")); err == nil { + if _, err := m.SetStyleJSON([]byte("{\x00}")); err == nil { t.Fatal("SetStyleJSON embedded NUL error = nil") } } -func TestMapWrongThreadReturnsWrongThread(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - +func TestMapCommandsCanMigrateAcrossGoroutines(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() + defer closeRuntimeForTest(runtime) + defer m.Close() - errCh := make(chan error, 1) + result := make(chan struct { + generation uint64 + err error + }, 1) go func() { - errCh <- m.RequestRepaint() + completion, err := awaitForTest(m.RequestRepaint()) + result <- struct { + generation uint64 + err error + }{generation: completion.Generation, err: err} }() - if err := <-errCh; !errors.Is(err, ErrWrongThread) { - t.Fatalf("RequestRepaint() from another thread error = %v, want ErrWrongThread", err) + got := <-result + if got.err != nil { + t.Fatalf("RequestRepaint() from another goroutine: %v", got.err) } - if err := m.RequestRepaint(); err != nil { - t.Fatalf("RequestRepaint() on owner thread after wrong-thread call: %v", err) + if got.generation == 0 { + t.Fatal("RequestRepaint() completed without a published generation") } } diff --git a/bindings/go/maplibre.go b/bindings/go/maplibre.go index 08b580718..8e0f436b1 100644 --- a/bindings/go/maplibre.go +++ b/bindings/go/maplibre.go @@ -1,10 +1,9 @@ // Package maplibre provides low-level Go bindings for the MapLibre Native C // API. // -// Runtime, map, projection, render-session, acquired-frame, and offline -// operation handles follow the C API owner-thread model. Call -// runtime.LockOSThread before creating a RuntimeHandle and keep that runtime's -// lifecycle on the locked goroutine. +// Runtime, map, and projection handles are usable from any goroutine. +// Render-session and acquired-frame graphics calls retain their native thread +// affinity. Futures may be awaited from any goroutine. package maplibre /* diff --git a/bindings/go/maplibre_test.go b/bindings/go/maplibre_test.go index f85640f42..fec4e38e1 100644 --- a/bindings/go/maplibre_test.go +++ b/bindings/go/maplibre_test.go @@ -1,9 +1,21 @@ package maplibre -import ( - stdruntime "runtime" - "testing" -) +import "context" + +func awaitForTest[T any](future *Future[T], err error) (T, error) { + var zero T + if err != nil { + return zero, err + } + return future.Await(context.Background()) +} + +// closeRuntimeForTest closes a runtime and waits for its native teardown, so a +// test leaves no native thread running past its own end. +func closeRuntimeForTest(runtime *RuntimeHandle) error { + _, err := awaitForTest(runtime.Close()) + return err +} const minimalStyleJSON = `{ "version": 8, @@ -13,9 +25,3 @@ const minimalStyleJSON = `{ {"id":"background","type":"background","paint":{"background-color":"#d8f1ff"}} ] }` - -func lockOSThreadForTest(t *testing.T) { - t.Helper() - stdruntime.LockOSThread() - t.Cleanup(stdruntime.UnlockOSThread) -} diff --git a/bindings/go/mise.toml b/bindings/go/mise.toml index f5b7f3ec9..3afb9a1f7 100644 --- a/bindings/go/mise.toml +++ b/bindings/go/mise.toml @@ -26,6 +26,7 @@ mkdir -p build # Document the public maplibre package only; omit ./... so internal/ # packages are not published under the docs site. PKG_CONFIG_PATH="$MISE_MONOREPO_ROOT/build/$usage_preset/install/share/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" \ + GOCACHE="$MISE_MONOREPO_ROOT/build/$usage_preset/go-cache" \ doc2go \ -out build/doc2go \ -home github.com/maplibre/maplibre-native-ffi/bindings/go \ @@ -50,6 +51,7 @@ extends = "ffi:preset" run = ''' native_install_dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" export PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export GOCACHE="$MISE_MONOREPO_ROOT/build/$usage_preset/go-cache" # A host build runs the suite; a cross build proves the package compiles and # links for the target. @@ -68,8 +70,8 @@ PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CO [tasks.test] extends = "ffi:preset" -# The preset selects the runner: Android and OpenHarmony x64 presets push to an -# emulator, and host presets run in process. +# The preset selects the runner: Android presets and OpenHarmony x64 presets +# push to an emulator, and host presets run in process. run = ''' case "$usage_preset" in android-* | ohos-*) @@ -79,6 +81,7 @@ esac native_install_dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" export PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export GOCACHE="$MISE_MONOREPO_ROOT/build/$usage_preset/go-cache" go test ./... go vet ./... ''' diff --git a/bindings/go/native_handles.go b/bindings/go/native_handles.go index 8a4e0b0e9..e27b155b0 100644 --- a/bindings/go/native_handles.go +++ b/bindings/go/native_handles.go @@ -6,5 +6,4 @@ type ( nativeRuntime uint64 nativeMap uint64 nativeRenderSession uint64 - nativeWakeSource uint64 ) diff --git a/bindings/go/offline.go b/bindings/go/offline.go index a3a8335ee..91a13d7d4 100644 --- a/bindings/go/offline.go +++ b/bindings/go/offline.go @@ -142,24 +142,75 @@ func metadataPointer(metadata []byte) *C.uint8_t { return (*C.uint8_t)(unsafe.Pointer(&metadata[0])) } -// StartCreateOfflineRegion starts creating an offline region. -func (runtime *RuntimeHandle) StartCreateOfflineRegion(definition OfflineRegionDefinition, metadata []byte) (*OfflineOperationHandle[OfflineRegionInfo], error) { +func startRuntimeCompletion[T any]( + runtime *RuntimeHandle, + start func(C.mln_runtime, *C.mln_completion) int32, + convert func(*C.mln_completion_result) (T, error), +) (*Future[T], error) { + ptr, err := runtime.ptr() + if err != nil { + return nil, err + } + defer runtime.state.KeepAlive() + return startCompletion(func(completion *C.mln_completion) int32 { + return start(C.mln_runtime(ptr), completion) + }, convert) +} + +func completionOfflineRegion(result *C.mln_completion_result) (OfflineRegionInfo, error) { + raw, err := completionValue[C.mln_offline_region_info](result) + if err != nil { + return OfflineRegionInfo{}, err + } + return offlineRegionInfoFromC(raw) +} + +func completionOptionalOfflineRegion(result *C.mln_completion_result) (*OfflineRegionInfo, error) { + if result.value_count == 0 { + return nil, nil + } + value, err := completionOfflineRegion(result) + return &value, err +} + +func completionOfflineRegions(result *C.mln_completion_result) ([]OfflineRegionInfo, error) { + raw, err := completionSlice[C.mln_offline_region_info](result) + if err != nil { + return nil, err + } + regions := make([]OfflineRegionInfo, len(raw)) + for index := range raw { + regions[index], err = offlineRegionInfoFromC(raw[index]) + if err != nil { + return nil, err + } + } + return regions, nil +} + +func completionOfflineStatus(result *C.mln_completion_result) (OfflineRegionStatus, error) { + raw, err := completionValue[C.mln_offline_region_status](result) + return offlineRegionStatusFromC(raw), err +} + +// CreateOfflineRegion starts creating an offline region. +func (runtime *RuntimeHandle) CreateOfflineRegion(definition OfflineRegionDefinition, metadata []byte) (*Future[OfflineRegionInfo], error) { switch region := definition.(type) { case OfflineTilePyramidRegionDefinition: if err := region.validate(); err != nil { return nil, err } - return startOfflineOperation[OfflineRegionInfo](runtime, OfflineOperationRegionCreate, OfflineOperationResultRegion, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { rawDefinition := newCOfflineTilePyramidRegionDefinition(region) defer rawDefinition.free() - return int32(C.mln_runtime_offline_region_create_start( - C.mln_runtime(handle), + return int32(C.mln_runtime_offline_region_create( + handle, &rawDefinition.raw, metadataPointer(metadata), C.size_t(len(metadata)), out, )) - }) + }, completionOfflineRegion) case OfflineGeometryRegionDefinition: if err := region.validate(); err != nil { return nil, err @@ -169,265 +220,99 @@ func (runtime *RuntimeHandle) StartCreateOfflineRegion(definition OfflineRegionD return nil, err } defer rawDefinition.free() - return startOfflineOperation[OfflineRegionInfo](runtime, OfflineOperationRegionCreate, OfflineOperationResultRegion, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_create_start( - C.mln_runtime(handle), + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_create( + handle, &rawDefinition.raw, metadataPointer(metadata), C.size_t(len(metadata)), out, )) - }) + }, completionOfflineRegion) default: return nil, newBindingError(ErrInvalidArgument, "unsupported offline region definition") } } -// StartOfflineRegion starts getting an offline region snapshot by ID. -func (runtime *RuntimeHandle) StartOfflineRegion(id OfflineRegionID) (*OfflineOperationHandle[*OfflineRegionInfo], error) { - return startOfflineOperation[*OfflineRegionInfo](runtime, OfflineOperationRegionGet, OfflineOperationResultOptionalRegion, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_get_start(C.mln_runtime(handle), C.mln_offline_region_id(id), out)) - }) +// OfflineRegion starts getting an offline region snapshot by ID. +func (runtime *RuntimeHandle) OfflineRegion(id OfflineRegionID) (*Future[*OfflineRegionInfo], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_get(handle, C.mln_offline_region_id(id), out)) + }, completionOptionalOfflineRegion) } -// StartOfflineRegions starts listing offline regions. -func (runtime *RuntimeHandle) StartOfflineRegions() (*OfflineOperationHandle[[]OfflineRegionInfo], error) { - return startOfflineOperation[[]OfflineRegionInfo](runtime, OfflineOperationRegionsList, OfflineOperationResultRegionList, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_regions_list_start(C.mln_runtime(handle), out)) - }) +// OfflineRegions starts listing offline regions. +func (runtime *RuntimeHandle) OfflineRegions() (*Future[[]OfflineRegionInfo], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_regions_list(handle, out)) + }, completionOfflineRegions) } -// StartMergeOfflineRegionsDatabase starts merging offline regions from another +// MergeOfflineRegionsDatabase starts merging offline regions from another // database path. -func (runtime *RuntimeHandle) StartMergeOfflineRegionsDatabase(path string) (*OfflineOperationHandle[[]OfflineRegionInfo], error) { +func (runtime *RuntimeHandle) MergeOfflineRegionsDatabase(path string) (*Future[[]OfflineRegionInfo], error) { if err := validateCStringArgument("offline side database path", path); err != nil { return nil, err } - return startOfflineOperation[[]OfflineRegionInfo](runtime, OfflineOperationRegionsMergeDatabase, OfflineOperationResultRegionList, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { rawPath := C.CString(path) defer C.free(unsafe.Pointer(rawPath)) - return int32(C.mln_runtime_offline_regions_merge_database_start(C.mln_runtime(handle), rawPath, out)) - }) + return int32(C.mln_runtime_offline_regions_merge_database(handle, rawPath, out)) + }, completionOfflineRegions) } -// StartUpdateOfflineRegionMetadata starts updating offline region metadata. -func (runtime *RuntimeHandle) StartUpdateOfflineRegionMetadata(id OfflineRegionID, metadata []byte) (*OfflineOperationHandle[OfflineRegionInfo], error) { - return startOfflineOperation[OfflineRegionInfo](runtime, OfflineOperationRegionUpdateMetadata, OfflineOperationResultRegion, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_update_metadata_start( - C.mln_runtime(handle), +// UpdateOfflineRegionMetadata starts updating offline region metadata. +func (runtime *RuntimeHandle) UpdateOfflineRegionMetadata(id OfflineRegionID, metadata []byte) (*Future[OfflineRegionInfo], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_update_metadata( + handle, C.mln_offline_region_id(id), metadataPointer(metadata), C.size_t(len(metadata)), out, )) - }) + }, completionOfflineRegion) } -// StartOfflineRegionStatus starts getting offline region status. -func (runtime *RuntimeHandle) StartOfflineRegionStatus(id OfflineRegionID) (*OfflineOperationHandle[OfflineRegionStatus], error) { - return startOfflineOperation[OfflineRegionStatus](runtime, OfflineOperationRegionGetStatus, OfflineOperationResultRegionStatus, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_get_status_start(C.mln_runtime(handle), C.mln_offline_region_id(id), out)) - }) +// OfflineRegionStatus starts getting offline region status. +func (runtime *RuntimeHandle) OfflineRegionStatus(id OfflineRegionID) (*Future[OfflineRegionStatus], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_get_status(handle, C.mln_offline_region_id(id), out)) + }, completionOfflineStatus) } -// StartSetOfflineRegionObserved starts setting offline event observation state. -func (runtime *RuntimeHandle) StartSetOfflineRegionObserved(id OfflineRegionID, observed bool) (*OfflineOperationHandle[struct{}], error) { - operation, err := startOfflineOperation[struct{}](runtime, OfflineOperationRegionSetObserved, OfflineOperationResultNone, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_set_observed_start(C.mln_runtime(handle), C.mln_offline_region_id(id), C.bool(observed), out)) - }) - return operation, err +// SetOfflineRegionObserved starts setting offline event observation state. +func (runtime *RuntimeHandle) SetOfflineRegionObserved(id OfflineRegionID, observed bool) (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_set_observed(handle, C.mln_offline_region_id(id), C.bool(observed), out)) + }, completionUnit) } -// StartSetOfflineRegionDownloadState starts setting offline region download +// SetOfflineRegionDownloadState starts setting offline region download // state. -func (runtime *RuntimeHandle) StartSetOfflineRegionDownloadState(id OfflineRegionID, state OfflineRegionDownloadState) (*OfflineOperationHandle[struct{}], error) { +func (runtime *RuntimeHandle) SetOfflineRegionDownloadState(id OfflineRegionID, state OfflineRegionDownloadState) (*Future[struct{}], error) { raw, err := rawOfflineRegionDownloadState(state) if err != nil { return nil, err } - return startOfflineOperation[struct{}](runtime, OfflineOperationRegionSetDownloadState, OfflineOperationResultNone, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_set_download_state_start(C.mln_runtime(handle), C.mln_offline_region_id(id), C.uint32_t(raw), out)) - }) + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_set_download_state(handle, C.mln_offline_region_id(id), C.uint32_t(raw), out)) + }, completionUnit) } -// StartInvalidateOfflineRegion starts invalidating cached resources for a +// InvalidateOfflineRegion starts invalidating cached resources for a // region. -func (runtime *RuntimeHandle) StartInvalidateOfflineRegion(id OfflineRegionID) (*OfflineOperationHandle[struct{}], error) { - return startOfflineOperation[struct{}](runtime, OfflineOperationRegionInvalidate, OfflineOperationResultNone, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_invalidate_start(C.mln_runtime(handle), C.mln_offline_region_id(id), out)) - }) -} - -// StartDeleteOfflineRegion starts deleting an offline region. -func (runtime *RuntimeHandle) StartDeleteOfflineRegion(id OfflineRegionID) (*OfflineOperationHandle[struct{}], error) { - operation, err := startOfflineOperation[struct{}](runtime, OfflineOperationRegionDelete, OfflineOperationResultNone, func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - return int32(C.mln_runtime_offline_region_delete_start(C.mln_runtime(handle), C.mln_offline_region_id(id), out)) - }) - return operation, err -} - -// Take consumes a completed offline operation result and copies it into Go -// values. If native reports that the result is not ready, the handle remains -// live so callers can retry later or discard it. -func (operation *OfflineOperationHandle[T]) Take() (T, error) { - var zero T - if operation == nil || operation.runtime == nil { - return zero, newBindingError(ErrInvalidArgument, "OfflineOperationHandle is nil") - } - operation.mu.Lock() - if !operation.live { - operation.mu.Unlock() - return zero, newBindingError(ErrInvalidArgument, "OfflineOperationHandle is closed") - } - id := operation.id - kind := operation.kind - resultKind := operation.resultKind - - ptr, release, err := operation.runtime.ptr() - if err != nil { - operation.mu.Unlock() - return zero, err - } - defer release() - defer operation.runtime.state.KeepAlive() - - result, consumed, err := takeOfflineOperationResult[T](ptr, C.mln_offline_operation_id(id), kind, resultKind) - child := operation.child - if consumed { - operation.live = false - operation.discarded = false - operation.child = nil - } - operation.mu.Unlock() - if consumed { - child.Release() - } - if err != nil { - return zero, err - } - return result, nil -} - -func takeOfflineOperationResult[T any](runtime nativeRuntime, id C.mln_offline_operation_id, kind OfflineOperationKind, resultKind OfflineOperationResultKind) (T, bool, error) { - var zero T - rawRuntime := C.mln_runtime(runtime) - switch resultKind { - case OfflineOperationResultNone: - return zero, false, newBindingError(ErrInvalidState, "offline operation does not produce a take result; drain its completion event and discard it") - case OfflineOperationResultRegion: - var snapshot C.mln_offline_region_snapshot - var err error - switch kind { - case OfflineOperationRegionCreate: - err = checkNative(func() int32 { - return int32(C.mln_runtime_offline_region_create_take_result(rawRuntime, id, &snapshot)) - }) - case OfflineOperationRegionUpdateMetadata: - err = checkNative(func() int32 { - return int32(C.mln_runtime_offline_region_update_metadata_take_result(rawRuntime, id, &snapshot)) - }) - default: - return zero, false, newBindingError(ErrInvalidState, "offline operation result kind mismatch") - } - if err != nil { - return zero, false, err - } - info, err := offlineRegionSnapshotInfo(snapshot) - if err != nil { - return zero, true, err - } - if result, ok := any(info).(T); ok { - return result, true, nil - } - return zero, true, newBindingError(ErrInvalidState, "offline operation result type mismatch") - case OfflineOperationResultOptionalRegion: - var snapshot C.mln_offline_region_snapshot - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_runtime_offline_region_get_take_result(rawRuntime, id, &snapshot, &found)) - }); err != nil { - return zero, false, err - } - var result *OfflineRegionInfo - if bool(found) { - info, err := offlineRegionSnapshotInfo(snapshot) - if err != nil { - return zero, true, err - } - result = &info - } - if typed, ok := any(result).(T); ok { - return typed, true, nil - } - return zero, true, newBindingError(ErrInvalidState, "offline operation result type mismatch") - case OfflineOperationResultRegionList: - var list C.mln_offline_region_list - var err error - switch kind { - case OfflineOperationRegionsList: - err = checkNative(func() int32 { - return int32(C.mln_runtime_offline_regions_list_take_result(rawRuntime, id, &list)) - }) - case OfflineOperationRegionsMergeDatabase: - err = checkNative(func() int32 { - return int32(C.mln_runtime_offline_regions_merge_database_take_result(rawRuntime, id, &list)) - }) - default: - return zero, false, newBindingError(ErrInvalidState, "offline operation result kind mismatch") - } - if err != nil { - return zero, false, err - } - regions, err := offlineRegionListInfos(list) - if err != nil { - return zero, true, err - } - if result, ok := any(regions).(T); ok { - return result, true, nil - } - return zero, true, newBindingError(ErrInvalidState, "offline operation result type mismatch") - case OfflineOperationResultRegionStatus: - raw := C.mln_offline_region_status{size: C.uint32_t(unsafe.Sizeof(C.mln_offline_region_status{}))} - if err := checkNative(func() int32 { return int32(C.mln_runtime_offline_region_get_status_take_result(rawRuntime, id, &raw)) }); err != nil { - return zero, false, err - } - if result, ok := any(offlineRegionStatusFromC(raw)).(T); ok { - return result, true, nil - } - return zero, true, newBindingError(ErrInvalidState, "offline operation result type mismatch") - default: - return zero, false, newBindingError(ErrInvalidState, "unknown offline operation result kind") - } -} - -func offlineRegionSnapshotInfo(snapshot C.mln_offline_region_snapshot) (OfflineRegionInfo, error) { - defer C.mln_offline_region_snapshot_destroy(snapshot) - raw := C.mln_offline_region_info{size: C.uint32_t(unsafe.Sizeof(C.mln_offline_region_info{}))} - if err := checkNative(func() int32 { return int32(C.mln_offline_region_snapshot_get(snapshot, &raw)) }); err != nil { - return OfflineRegionInfo{}, err - } - return offlineRegionInfoFromC(raw) +func (runtime *RuntimeHandle) InvalidateOfflineRegion(id OfflineRegionID) (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_invalidate(handle, C.mln_offline_region_id(id), out)) + }, completionUnit) } -func offlineRegionListInfos(list C.mln_offline_region_list) ([]OfflineRegionInfo, error) { - defer C.mln_offline_region_list_destroy(list) - var count C.size_t - if err := checkNative(func() int32 { return int32(C.mln_offline_region_list_count(list, &count)) }); err != nil { - return nil, err - } - regions := make([]OfflineRegionInfo, int(count)) - for index := range regions { - raw := C.mln_offline_region_info{size: C.uint32_t(unsafe.Sizeof(C.mln_offline_region_info{}))} - if err := checkNative(func() int32 { return int32(C.mln_offline_region_list_get(list, C.size_t(index), &raw)) }); err != nil { - return nil, err - } - info, err := offlineRegionInfoFromC(raw) - if err != nil { - return nil, err - } - regions[index] = info - } - return regions, nil +// DeleteOfflineRegion starts deleting an offline region. +func (runtime *RuntimeHandle) DeleteOfflineRegion(id OfflineRegionID) (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_offline_region_delete(handle, C.mln_offline_region_id(id), out)) + }, completionUnit) } func offlineRegionInfoFromC(info C.mln_offline_region_info) (OfflineRegionInfo, error) { @@ -489,21 +374,3 @@ func rawOfflineRegionDownloadState(state OfflineRegionDownloadState) (uint32, er return 0, newBindingError(ErrInvalidArgument, "unknown offline region download state cannot be set") } } - -func startOfflineOperation[T any](runtime *RuntimeHandle, kind OfflineOperationKind, resultKind OfflineOperationResultKind, start func(nativeRuntime, *C.mln_offline_operation_id) int32) (*OfflineOperationHandle[T], error) { - ptr, release, err := runtime.ptr() - if err != nil { - return nil, err - } - defer release() - defer runtime.state.KeepAlive() - - var id C.mln_offline_operation_id - if err := checkNative(func() int32 { return start(ptr, &id) }); err != nil { - return nil, err - } - if id == 0 { - return nil, newBindingError(ErrInvalidState, "offline operation did not return an ID") - } - return newOfflineOperationHandle[T](runtime, uint64(id), kind, resultKind), nil -} diff --git a/bindings/go/offline_test.go b/bindings/go/offline_test.go index 906e4b19d..e074b302b 100644 --- a/bindings/go/offline_test.go +++ b/bindings/go/offline_test.go @@ -1,13 +1,9 @@ package maplibre import ( + "context" "errors" - stdruntime "runtime" - "sync/atomic" "testing" - "time" - - "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" ) func testOfflineTileDefinition() OfflineTilePyramidRegionDefinition { @@ -35,195 +31,140 @@ func testOfflineGeometryDefinition() OfflineGeometryRegionDefinition { } } -func requireDiscardOfflineOperation[T any](t *testing.T, operation *OfflineOperationHandle[T], kind OfflineOperationKind, resultKind OfflineOperationResultKind) { - t.Helper() - if operation.ID() == 0 { - t.Fatal("operation ID is zero") - } - if operation.Kind() != kind || operation.ResultKind() != resultKind { - t.Fatalf("operation kind/result = %v/%v, want %v/%v", operation.Kind(), operation.ResultKind(), kind, resultKind) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } +func requireReleaseOperation[T any](_ *testing.T, _ *Future[T]) { } func TestOfflineRegionStartOperationsReturnTypedHandles(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() - create, err := runtime.StartCreateOfflineRegion(testOfflineTileDefinition(), []byte{1, 2, 3}) + create, err := runtime.CreateOfflineRegion(testOfflineTileDefinition(), []byte{1, 2, 3}) if err != nil { - t.Fatalf("StartCreateOfflineRegion(): %v", err) + t.Fatalf("CreateOfflineRegion(): %v", err) } - requireDiscardOfflineOperation(t, create, OfflineOperationRegionCreate, OfflineOperationResultRegion) + requireReleaseOperation(t, create) - createGeometry, err := runtime.StartCreateOfflineRegion(testOfflineGeometryDefinition(), []byte{1, 2, 3}) + createGeometry, err := runtime.CreateOfflineRegion(testOfflineGeometryDefinition(), []byte{1, 2, 3}) if err != nil { - t.Fatalf("StartCreateOfflineRegion(geometry): %v", err) + t.Fatalf("CreateOfflineRegion(geometry): %v", err) } - requireDiscardOfflineOperation(t, createGeometry, OfflineOperationRegionCreate, OfflineOperationResultRegion) + requireReleaseOperation(t, createGeometry) - get, err := runtime.StartOfflineRegion(1) + get, err := runtime.OfflineRegion(1) if err != nil { - t.Fatalf("StartOfflineRegion(): %v", err) + t.Fatalf("OfflineRegion(): %v", err) } - requireDiscardOfflineOperation(t, get, OfflineOperationRegionGet, OfflineOperationResultOptionalRegion) + requireReleaseOperation(t, get) - list, err := runtime.StartOfflineRegions() + list, err := runtime.OfflineRegions() if err != nil { - t.Fatalf("StartOfflineRegions(): %v", err) + t.Fatalf("OfflineRegions(): %v", err) } - requireDiscardOfflineOperation(t, list, OfflineOperationRegionsList, OfflineOperationResultRegionList) + requireReleaseOperation(t, list) - update, err := runtime.StartUpdateOfflineRegionMetadata(1, []byte{4, 5, 6}) + update, err := runtime.UpdateOfflineRegionMetadata(1, []byte{4, 5, 6}) if err != nil { - t.Fatalf("StartUpdateOfflineRegionMetadata(): %v", err) + t.Fatalf("UpdateOfflineRegionMetadata(): %v", err) } - requireDiscardOfflineOperation(t, update, OfflineOperationRegionUpdateMetadata, OfflineOperationResultRegion) + requireReleaseOperation(t, update) - status, err := runtime.StartOfflineRegionStatus(1) + status, err := runtime.OfflineRegionStatus(1) if err != nil { - t.Fatalf("StartOfflineRegionStatus(): %v", err) + t.Fatalf("OfflineRegionStatus(): %v", err) } - requireDiscardOfflineOperation(t, status, OfflineOperationRegionGetStatus, OfflineOperationResultRegionStatus) + requireReleaseOperation(t, status) - observed, err := runtime.StartSetOfflineRegionObserved(1, true) + observed, err := runtime.SetOfflineRegionObserved(1, true) if err != nil { - t.Fatalf("StartSetOfflineRegionObserved(): %v", err) + t.Fatalf("SetOfflineRegionObserved(): %v", err) } - requireDiscardOfflineOperation(t, observed, OfflineOperationRegionSetObserved, OfflineOperationResultNone) + requireReleaseOperation(t, observed) - download, err := runtime.StartSetOfflineRegionDownloadState(1, OfflineRegionDownloadInactive) + download, err := runtime.SetOfflineRegionDownloadState(1, OfflineRegionDownloadInactive) if err != nil { - t.Fatalf("StartSetOfflineRegionDownloadState(): %v", err) + t.Fatalf("SetOfflineRegionDownloadState(): %v", err) } - requireDiscardOfflineOperation(t, download, OfflineOperationRegionSetDownloadState, OfflineOperationResultNone) + requireReleaseOperation(t, download) - invalidate, err := runtime.StartInvalidateOfflineRegion(1) + invalidate, err := runtime.InvalidateOfflineRegion(1) if err != nil { - t.Fatalf("StartInvalidateOfflineRegion(): %v", err) + t.Fatalf("InvalidateOfflineRegion(): %v", err) } - requireDiscardOfflineOperation(t, invalidate, OfflineOperationRegionInvalidate, OfflineOperationResultNone) + requireReleaseOperation(t, invalidate) - deleteOperation, err := runtime.StartDeleteOfflineRegion(1) + deleteOperation, err := runtime.DeleteOfflineRegion(1) if err != nil { - t.Fatalf("StartDeleteOfflineRegion(): %v", err) + t.Fatalf("DeleteOfflineRegion(): %v", err) } - requireDiscardOfflineOperation(t, deleteOperation, OfflineOperationRegionDelete, OfflineOperationResultNone) + requireReleaseOperation(t, deleteOperation) } -func TestRuntimeOptionsEventMaskSuppressesCompletionEventsNotResults(t *testing.T) { - lockOSThreadForTest(t) - +func TestOfflineOperationResultDoesNotUseRuntimeEventQueue(t *testing.T) { options := NewRuntimeOptions("", ":memory:") - options.EventMask = RuntimeEventMaskAll &^ RuntimeEventMaskOfflineOperationCompleted + options.EventMask = RuntimeEventMaskNone runtime, err := NewRuntimeWithOptions(options) if err != nil { t.Fatalf("NewRuntimeWithOptions(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() - if got, err := runtime.EventMask(); err != nil || got != options.EventMask { - t.Fatalf("EventMask() = (%#x, %v), want %#x", uint64(got), err, uint64(options.EventMask)) - } - - // An operation records its result before the mask is consulted, so the - // take-result call still reports it while the event stays out of the queue. - operation, err := runtime.StartOfflineRegions() + operation, err := runtime.OfflineRegions() if err != nil { - t.Fatalf("StartOfflineRegions(): %v", err) + t.Fatalf("OfflineRegions(): %v", err) } - var completions int - took := false - for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) - if err != nil { - t.Fatalf("DrainEvents(): %v", err) - } - for _, event := range batch.Events { - if event.Type == RuntimeEventOfflineOperationCompleted { - completions++ - } - } - if _, err := operation.Take(); err == nil { - took = true - break - } else if !errors.Is(err, ErrInvalidState) { - t.Fatalf("Take(): %v", err) - } - time.Sleep(time.Millisecond) + regions := waitTakeOperation(t, runtime, operation) + if regions == nil { + t.Fatal("Take() returned a nil region list") } - if !took { - t.Fatal("the offline operation never reported a result") + batch, err := runtime.DrainEvents() + if err != nil { + t.Fatalf("DrainEvents(): %v", err) } - if completions != 0 { - t.Fatalf("drained %d offline completion events, want none", completions) + if len(batch.Events) != 0 { + t.Fatalf("DrainEvents() returned %d events with an empty mask", len(batch.Events)) } } -func waitTakeOfflineOperation[T any](t *testing.T, runtime *RuntimeHandle, operation *OfflineOperationHandle[T]) T { +func waitTakeOperation[T any](t *testing.T, _ *RuntimeHandle, future *Future[T]) T { t.Helper() - for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - result, err := operation.Take() - if err == nil { - return result - } - if !errors.Is(err, ErrInvalidState) { - t.Fatalf("Take(): %v", err) - } - time.Sleep(time.Millisecond) + result, err := future.Await(context.Background()) + if err != nil { + t.Fatalf("Await(): %v", err) } - t.Fatal("offline operation did not complete") - var zero T - return zero + return result } func TestOfflineCreateAndListTakeResultsCopyNativeData(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() metadata := []byte{9, 8, 7} - create, err := runtime.StartCreateOfflineRegion(testOfflineTileDefinition(), metadata) + create, err := runtime.CreateOfflineRegion(testOfflineTileDefinition(), metadata) if err != nil { - t.Fatalf("StartCreateOfflineRegion(): %v", err) + t.Fatalf("CreateOfflineRegion(): %v", err) } - info := waitTakeOfflineOperation(t, runtime, create) + info := waitTakeOperation(t, runtime, create) if info.ID == 0 { t.Fatal("created offline region ID is zero") } - if !errors.Is(create.Discard(), ErrInvalidArgument) { - t.Fatal("Discard() after Take did not report closed operation") - } if got := info.Metadata; len(got) != len(metadata) || got[0] != metadata[0] || got[1] != metadata[1] || got[2] != metadata[2] { t.Fatalf("metadata = %v, want %v", got, metadata) } @@ -235,137 +176,60 @@ func TestOfflineCreateAndListTakeResultsCopyNativeData(t *testing.T) { t.Fatalf("StyleURL = %q, want %q", tile.StyleURL, testOfflineTileDefinition().StyleURL) } - list, err := runtime.StartOfflineRegions() + list, err := runtime.OfflineRegions() if err != nil { - t.Fatalf("StartOfflineRegions(): %v", err) + t.Fatalf("OfflineRegions(): %v", err) } - regions := waitTakeOfflineOperation(t, runtime, list) + regions := waitTakeOperation(t, runtime, list) if len(regions) == 0 { t.Fatal("offline region list is empty after creating a region") } } -func TestOfflineOperationCompletedEventCopiesPayload(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - defer func() { - if err := runtime.Close(); err != nil { - t.Errorf("Close(): %v", err) - } - }() - - operation, err := runtime.StartOfflineRegions() - if err != nil { - t.Fatalf("StartOfflineRegions(): %v", err) - } - for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) - if err != nil { - t.Fatalf("DrainEvents(): %v", err) - } - for _, event := range batch.Events { - payload, ok := event.Payload.(RuntimeEventOfflineOperationCompletedPayload) - if !ok || payload.OperationID != operation.ID() { - continue - } - if payload.OperationKind != OfflineOperationRegionsList || payload.ResultKind != OfflineOperationResultRegionList || payload.ResultStatus != 0 { - t.Fatalf("payload = %#v", payload) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } - return - } - time.Sleep(time.Millisecond) - } - t.Fatal("offline completion event was not reported") -} - func TestSetMaximumAmbientCacheSizeReportsCompletion(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - runtime, err := NewRuntimeWithOptions(NewRuntimeOptions("", ":memory:")) if err != nil { t.Fatalf("NewRuntimeWithOptions(): %v", err) } - defer func() { - if err := runtime.Close(); err != nil { - t.Errorf("Close(): %v", err) - } - }() + defer closeRuntimeForTest(runtime) - operation, err := runtime.StartSetMaximumAmbientCacheSize(8 << 20) - if err != nil { - t.Fatalf("StartSetMaximumAmbientCacheSize(): %v", err) + future, err := runtime.SetMaximumAmbientCacheSize(8 << 20) + if _, err := awaitForTest(future, err); err != nil { + t.Fatalf("SetMaximumAmbientCacheSize completion: %v", err) } - for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) - if err != nil { - t.Fatalf("DrainEvents(): %v", err) - } - for _, event := range batch.Events { - payload, ok := event.Payload.(RuntimeEventOfflineOperationCompletedPayload) - if !ok || payload.OperationID != operation.ID() { - continue - } - if payload.OperationKind != OfflineOperationSetMaximumAmbientCacheSize || payload.ResultKind != OfflineOperationResultNone || payload.ResultStatus != 0 { - t.Fatalf("payload = %#v", payload) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } - return - } - time.Sleep(time.Millisecond) - } - t.Fatal("maximum ambient cache size completion event was not reported") } func TestOfflineRegionStartOperationsValidateGoInputs(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() definition := testOfflineTileDefinition() definition.StyleURL = "http://example.com/\x00style.json" - if _, err := runtime.StartCreateOfflineRegion(definition, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartCreateOfflineRegion embedded NUL error = %v, want ErrInvalidArgument", err) + if _, err := runtime.CreateOfflineRegion(definition, nil); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("CreateOfflineRegion embedded NUL error = %v, want ErrInvalidArgument", err) } geometryDefinition := testOfflineGeometryDefinition() geometryDefinition.StyleURL = "http://example.com/\x00style.json" - if _, err := runtime.StartCreateOfflineRegion(geometryDefinition, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartCreateOfflineRegion geometry embedded NUL error = %v, want ErrInvalidArgument", err) + if _, err := runtime.CreateOfflineRegion(geometryDefinition, nil); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("CreateOfflineRegion geometry embedded NUL error = %v, want ErrInvalidArgument", err) } geometryDefinition = testOfflineGeometryDefinition() geometryDefinition.Geometry = []byte(`{"type":"Unsupported"}`) - if _, err := runtime.StartCreateOfflineRegion(geometryDefinition, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartCreateOfflineRegion bad geometry error = %v, want ErrInvalidArgument", err) + if _, err := runtime.CreateOfflineRegion(geometryDefinition, nil); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("CreateOfflineRegion bad geometry error = %v, want ErrInvalidArgument", err) } - if _, err := runtime.StartMergeOfflineRegionsDatabase("/tmp/\x00side.db"); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartMergeOfflineRegionsDatabase embedded NUL error = %v, want ErrInvalidArgument", err) + if _, err := runtime.MergeOfflineRegionsDatabase("/tmp/\x00side.db"); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("MergeOfflineRegionsDatabase embedded NUL error = %v, want ErrInvalidArgument", err) } - if _, err := runtime.StartSetOfflineRegionDownloadState(1, OfflineRegionDownloadState(999_999)); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartSetOfflineRegionDownloadState unknown error = %v, want ErrInvalidArgument", err) + if _, err := runtime.SetOfflineRegionDownloadState(1, OfflineRegionDownloadState(999_999)); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetOfflineRegionDownloadState unknown error = %v, want ErrInvalidArgument", err) } } @@ -392,202 +256,3 @@ func TestOfflineGeometryDefinitionMaterializesAndCopies(t *testing.T) { t.Fatalf("copied geometry = %q, want %q", copied.Geometry, definition.Geometry) } } - -func TestOfflineOperationDiscardIsIdempotentAfterSuccess(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - var calls int - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - calls++ - return 0 - }) - defer restore() - - operation := newOfflineOperationHandle[struct{}](runtime, 1, OfflineOperationRegionSetObserved, OfflineOperationResultNone) - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } - if err := operation.Discard(); err != nil { - t.Fatalf("second Discard(): %v", err) - } - if calls != 1 { - t.Fatalf("discard calls = %d, want 1", calls) - } -} - -func TestOfflineOperationDiscardFailureLeavesHandleRetryable(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - statuses := []int32{-2, 0, 0} - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - status := statuses[0] - statuses = statuses[1:] - return status - }) - defer restore() - - operation := newOfflineOperationHandle[struct{}](runtime, 1, OfflineOperationRegionSetObserved, OfflineOperationResultNone) - if err := operation.Discard(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("Discard() failure = %v, want ErrInvalidState", err) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard() retry: %v", err) - } -} - -func TestOfflineOperationBlocksRuntimeCloseUntilReleased(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - return 0 - }) - defer restore() - restoreRuntimeDestroy := replaceRuntimeDestroyForTest(func(nativeRuntime) int32 { - return 0 - }) - defer restoreRuntimeDestroy() - - operation := newOfflineOperationHandle[struct{}](runtime, 1, OfflineOperationRegionSetObserved, OfflineOperationResultNone) - if err := runtime.Close(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("Runtime Close() with live offline operation = %v, want ErrInvalidState", err) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Fatalf("Runtime Close() after Discard(): %v", err) - } -} - -func TestOfflineOperationDiscardSerializesConcurrentCalls(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - entered := make(chan struct{}) - release := make(chan struct{}) - var calls atomic.Int32 - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - if calls.Add(1) == 1 { - close(entered) - <-release - } - return 0 - }) - defer restore() - - operation := newOfflineOperationHandle[struct{}](runtime, 1, OfflineOperationRegionSetObserved, OfflineOperationResultNone) - firstDone := make(chan error, 1) - secondDone := make(chan error, 1) - go func() { firstDone <- operation.Discard() }() - <-entered - secondStarted := make(chan struct{}) - go func() { - close(secondStarted) - secondDone <- operation.Discard() - }() - <-secondStarted - select { - case err := <-secondDone: - t.Fatalf("second Discard() completed while first discard was in flight: %v", err) - case <-time.After(25 * time.Millisecond): - } - close(release) - if err := <-firstDone; err != nil { - t.Fatalf("first Discard(): %v", err) - } - if err := <-secondDone; err != nil { - t.Fatalf("second Discard(): %v", err) - } - if calls.Load() != 1 { - t.Fatalf("native discard calls = %d, want 1", calls.Load()) - } -} - -func TestOfflineOperationTakeRejectsNoResultOperationWithoutDiscarding(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - var calls int - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - calls++ - return 0 - }) - defer restore() - - operation := newOfflineOperationHandle[struct{}](runtime, 1, OfflineOperationRegionSetObserved, OfflineOperationResultNone) - if _, err := operation.Take(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("Take() no-result operation = %v, want ErrInvalidState", err) - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard() after rejected Take: %v", err) - } - if calls != 1 { - t.Fatalf("discard calls = %d, want 1", calls) - } -} - -func TestOfflineOperationTakePreConsumeMismatchRemainsRetryable(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - restore := replaceOfflineOperationDiscardForTest(func(ptr nativeRuntime, id uint64) int32 { - return 0 - }) - defer restore() - - preConsume := newOfflineOperationHandle[struct{}](runtime, 2, OfflineOperationRegionSetObserved, OfflineOperationResultKind(999_999)) - if _, err := preConsume.Take(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("Take() pre-consume mismatch = %v, want ErrInvalidState", err) - } - if err := preConsume.Discard(); err != nil { - t.Fatalf("Discard() after pre-consume mismatch: %v", err) - } -} - -func TestStartOfflineOperationRejectsZeroID(t *testing.T) { - runtime := newFakeRuntimeHandle(t) - defer closeFakeRuntimeHandle(t, runtime) - - operation, err := startOfflineOperationZeroIDForTest(runtime) - if !errors.Is(err, ErrInvalidState) { - t.Fatalf("startOfflineOperation() error = %v, want ErrInvalidState", err) - } - if operation != nil { - t.Fatalf("startOfflineOperation() operation = %#v, want nil", operation) - } -} - -func newFakeRuntimeHandle(t *testing.T) *RuntimeHandle { - t.Helper() - state, err := handle.New(nativeRuntime(0x0100_0000_0000_002a), "RuntimeHandle") - if err != nil { - t.Fatal(err) - } - return &RuntimeHandle{state: state} -} - -func closeFakeRuntimeHandle(t *testing.T, runtime *RuntimeHandle) { - t.Helper() - if status := runtime.state.Close(func(nativeRuntime) int32 { return 0 }); status != 0 { - t.Fatalf("fake runtime close status = %d, want 0", status) - } -} - -func replaceOfflineOperationDiscardForTest(discard func(nativeRuntime, uint64) int32) func() { - previous := offlineOperationDiscard - offlineOperationDiscard = discard - return func() { - offlineOperationDiscard = previous - } -} - -func replaceRuntimeDestroyForTest(destroy func(nativeRuntime) int32) func() { - previous := destroyRuntimeHandle - destroyRuntimeHandle = destroy - return func() { - destroyRuntimeHandle = previous - } -} diff --git a/bindings/go/offline_testhook.go b/bindings/go/offline_testhook.go deleted file mode 100644 index bc924d234..000000000 --- a/bindings/go/offline_testhook.go +++ /dev/null @@ -1,18 +0,0 @@ -package maplibre - -/* -#include "maplibre_native_c.h" -*/ -import "C" - -func startOfflineOperationZeroIDForTest(runtime *RuntimeHandle) (*OfflineOperationHandle[struct{}], error) { - return startOfflineOperation[struct{}]( - runtime, - OfflineOperationRegionSetObserved, - OfflineOperationResultNone, - func(handle nativeRuntime, out *C.mln_offline_operation_id) int32 { - *out = 0 - return 0 - }, - ) -} diff --git a/bindings/go/projection.go b/bindings/go/projection.go index f7ab55ba6..bd73163b8 100644 --- a/bindings/go/projection.go +++ b/bindings/go/projection.go @@ -5,79 +5,78 @@ package maplibre */ import "C" -import ( - "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" -) +import "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" type nativeProjection uint64 -// MapProjectionHandle owns an any-thread standalone projection snapshot. +// MapProjectionHandle owns an any-thread standalone projection snapshot. Every +// call after creation is synchronous, runs on the calling goroutine, and is +// internally serialized, so a projection is safe to share across goroutines. A +// projection never observes map changes made after its creation. type MapProjectionHandle struct { state *handle.State[nativeProjection] } -// NewProjection creates a standalone projection helper from this map's current -// transform. Later map changes do not update the helper. -func (m *MapHandle) NewProjection() (*MapProjectionHandle, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var projection nativeProjection - if err := checkNative(func() int32 { - var raw C.mln_map_projection - status := int32(C.mln_map_projection_create(C.mln_map(ptr), &raw)) - if status == int32(C.MLN_STATUS_OK) { - projection = nativeProjection(raw) +// NewProjection creates a standalone projection helper that copies this map's +// transform state after every earlier map command. Later map changes do not +// update the helper. +func (m *MapHandle) NewProjection() (*Future[*MapProjectionHandle], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_projection_create(raw, completion)) + }, func(result *C.mln_completion_result) (*MapProjectionHandle, error) { + raw, err := completionValue[C.mln_map_projection](result) + if err != nil { + return nil, err } - return status - }); err != nil { - return nil, err - } - state, err := handle.New(projection, "MapProjectionHandle") - if err != nil { - return nil, newBindingError(ErrInvalidArgument, err.Error()) - } - return &MapProjectionHandle{state: state}, nil + state, err := handle.New(nativeProjection(raw), "MapProjectionHandle") + if err != nil { + return nil, newBindingError(ErrInvalidArgument, err.Error()) + } + return &MapProjectionHandle{state: state}, nil + }) } -func (projection *MapProjectionHandle) ptr() (nativeProjection, func(), error) { +func (projection *MapProjectionHandle) ptr() (nativeProjection, error) { if projection == nil || projection.state == nil { - return 0, nil, newBindingError(ErrInvalidArgument, "MapProjectionHandle is nil") + return 0, newBindingError(ErrInvalidArgument, "MapProjectionHandle is nil") } - borrow, live := projection.state.Borrow() + value, live := projection.state.Handle() if !live { - return 0, nil, newBindingError(ErrInvalidArgument, "MapProjectionHandle is closed") + return 0, newBindingError(ErrInvalidArgument, "MapProjectionHandle is closed") } - return borrow.Handle(), borrow.Release, nil + return value, nil } -// Close destroys this projection helper. A successful close makes later calls -// no-ops. A failed close leaves the native handle live so callers can retry. +// Close destroys this projection helper synchronously. A successful close +// makes later calls fail. A failed close leaves the native handle live so +// callers can retry. func (projection *MapProjectionHandle) Close() error { if projection == nil || projection.state == nil { return newBindingError(ErrInvalidArgument, "MapProjectionHandle is nil") } - return checkNative(func() int32 { - return projection.state.Close(func(native nativeProjection) int32 { - return int32(C.mln_map_projection_destroy(C.mln_map_projection(native))) - }) + var closeErr error + _ = projection.state.Close(func(native nativeProjection) int32 { + if err := checkNative(func() int32 { + return int32(C.mln_map_projection_close(C.mln_map_projection(native))) + }); err != nil { + closeErr = err + return statusFromError(err) + } + return int32(C.MLN_STATUS_OK) }) + return closeErr } -// Camera returns this projection helper's camera snapshot. +// Camera returns a copy of the projection camera. The result observes every +// earlier projection setter. func (projection *MapProjectionHandle) Camera() (CameraOptions, error) { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return CameraOptions{}, err } - defer release() - defer projection.state.KeepAlive() - var camera C.mln_camera_options = C.mln_camera_options_default() + defer projection.state.KeepAlive() + camera := C.mln_camera_options_default() if err := checkNative(func() int32 { return int32(C.mln_map_projection_get_camera(C.mln_map_projection(ptr), &camera)) }); err != nil { @@ -86,82 +85,68 @@ func (projection *MapProjectionHandle) Camera() (CameraOptions, error) { return goCameraOptions(camera), nil } -// SetCamera applies selected camera fields to this projection helper. +// SetCamera applies selected camera fields synchronously, so a later read or +// conversion observes them. The map's camera is unaffected. func (projection *MapProjectionHandle) SetCamera(camera CameraOptions) error { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return err } - defer release() - defer projection.state.KeepAlive() - rawCamera := cCameraOptions(camera) + defer projection.state.KeepAlive() + raw := cCameraOptions(camera) return checkNative(func() int32 { - return int32(C.mln_map_projection_set_camera(C.mln_map_projection(ptr), &rawCamera)) + return int32(C.mln_map_projection_set_camera(C.mln_map_projection(ptr), &raw)) }) } -// SetVisibleCoordinates updates this projection helper's camera to fit -// coordinates inside padding. +// SetVisibleCoordinates applies a camera fitted to geographic coordinates +// synchronously, so a later read or conversion observes it. func (projection *MapProjectionHandle) SetVisibleCoordinates(coordinates []LatLng, padding EdgeInsets) error { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return err } - defer release() - defer projection.state.KeepAlive() - rawCoordinates := cLatLngSlice(coordinates) - var rawCoordinatesPtr *C.mln_lat_lng - if len(rawCoordinates) > 0 { - rawCoordinatesPtr = &rawCoordinates[0] + defer projection.state.KeepAlive() + raw := cLatLngSlice(coordinates) + var data *C.mln_lat_lng + if len(raw) > 0 { + data = &raw[0] } return checkNative(func() int32 { - return int32(C.mln_map_projection_set_visible_coordinates( - C.mln_map_projection(ptr), - rawCoordinatesPtr, - C.size_t(len(rawCoordinates)), - cEdgeInsets(padding), - )) + return int32(C.mln_map_projection_set_visible_coordinates(C.mln_map_projection(ptr), data, C.size_t(len(raw)), cEdgeInsets(padding))) }) } -// SetVisibleGeometry updates this projection helper's camera to fit geometry -// inside padding. +// SetVisibleGeometry applies a camera fitted to GeoJSON Geometry bytes +// synchronously, so a later read or conversion observes it. func (projection *MapProjectionHandle) SetVisibleGeometry(geometry []byte, padding EdgeInsets) error { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return err } - defer release() + defer projection.state.KeepAlive() - rawGeometry := newCBufferView(geometry) - defer rawGeometry.free() + raw := newCBufferView(geometry) + defer raw.free() return checkNative(func() int32 { - return int32(C.mln_map_projection_set_visible_geometry( - C.mln_map_projection(ptr), - rawGeometry.raw(), - cEdgeInsets(padding), - )) + return int32(C.mln_map_projection_set_visible_geometry(C.mln_map_projection(ptr), raw.raw(), cEdgeInsets(padding))) }) } // PixelForLatLng converts a geographic coordinate to a logical screen point. +// The result observes every earlier projection setter. func (projection *MapProjectionHandle) PixelForLatLng(coordinate LatLng) (ScreenPoint, error) { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return ScreenPoint{}, err } - defer release() - defer projection.state.KeepAlive() + defer projection.state.KeepAlive() var point C.mln_screen_point if err := checkNative(func() int32 { - return int32(C.mln_map_projection_pixel_for_lat_lng( - C.mln_map_projection(ptr), - cLatLng(coordinate), - &point, - )) + return int32(C.mln_map_projection_pixel_for_lat_lng(C.mln_map_projection(ptr), cLatLng(coordinate), &point)) }); err != nil { return ScreenPoint{}, err } @@ -169,46 +154,36 @@ func (projection *MapProjectionHandle) PixelForLatLng(coordinate LatLng) (Screen } // LatLngForPixel converts a logical screen point to a geographic coordinate. +// The result observes every earlier projection setter. func (projection *MapProjectionHandle) LatLngForPixel(point ScreenPoint) (LatLng, error) { - ptr, release, err := projection.ptr() + ptr, err := projection.ptr() if err != nil { return LatLng{}, err } - defer release() - defer projection.state.KeepAlive() + defer projection.state.KeepAlive() var coordinate C.mln_lat_lng if err := checkNative(func() int32 { - return int32(C.mln_map_projection_lat_lng_for_pixel( - C.mln_map_projection(ptr), - cScreenPoint(point), - &coordinate, - )) + return int32(C.mln_map_projection_lat_lng_for_pixel(C.mln_map_projection(ptr), cScreenPoint(point), &coordinate)) }); err != nil { return LatLng{}, err } return goLatLng(coordinate), nil } -// ProjectedMetersForLatLng converts a geographic coordinate to Spherical -// Mercator projected meters. +// ProjectedMetersForLatLng converts a geographic coordinate to Spherical Mercator projected meters. func ProjectedMetersForLatLng(coordinate LatLng) (ProjectedMeters, error) { var meters C.mln_projected_meters - if err := checkNative(func() int32 { - return int32(C.mln_projected_meters_for_lat_lng(cLatLng(coordinate), &meters)) - }); err != nil { + if err := checkNative(func() int32 { return int32(C.mln_projected_meters_for_lat_lng(cLatLng(coordinate), &meters)) }); err != nil { return ProjectedMeters{}, err } return goProjectedMeters(meters), nil } -// LatLngForProjectedMeters converts Spherical Mercator projected meters to a -// geographic coordinate. +// LatLngForProjectedMeters converts Spherical Mercator projected meters to a geographic coordinate. func LatLngForProjectedMeters(meters ProjectedMeters) (LatLng, error) { var coordinate C.mln_lat_lng - if err := checkNative(func() int32 { - return int32(C.mln_lat_lng_for_projected_meters(cProjectedMeters(meters), &coordinate)) - }); err != nil { + if err := checkNative(func() int32 { return int32(C.mln_lat_lng_for_projected_meters(cProjectedMeters(meters), &coordinate)) }); err != nil { return LatLng{}, err } return goLatLng(coordinate), nil diff --git a/bindings/go/projection_test.go b/bindings/go/projection_test.go index b942135c7..581d78d46 100644 --- a/bindings/go/projection_test.go +++ b/bindings/go/projection_test.go @@ -2,26 +2,24 @@ package maplibre import ( "errors" - stdruntime "runtime" + "math" "testing" ) func TestMapProjectionCameraAndVisibleCoordinates(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } - projection, err := m.NewProjection() + projection, err := awaitForTest(m.NewProjection()) if err != nil { _ = m.Close() - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewProjection(): %v", err) } defer func() { @@ -31,11 +29,16 @@ func TestMapProjectionCameraAndVisibleCoordinates(t *testing.T) { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() + // A setter is synchronous, so the conversions right after it observe it. + before, err := projection.PixelForLatLng(LatLng{Latitude: 2, Longitude: 3}) + if err != nil { + t.Fatalf("PixelForLatLng(): %v", err) + } camera := CameraOptions{}. WithCenter(LatLng{Latitude: 2, Longitude: 3}). WithZoom(2) @@ -49,6 +52,17 @@ func TestMapProjectionCameraAndVisibleCoordinates(t *testing.T) { if gotCamera.Center == nil || gotCamera.Zoom == nil { t.Fatalf("Camera() missing expected fields: %#v", gotCamera) } + after, err := projection.PixelForLatLng(LatLng{Latitude: 2, Longitude: 3}) + if err != nil { + t.Fatalf("PixelForLatLng() after SetCamera(): %v", err) + } + if math.Abs(after.X-before.X) < 1e-9 && math.Abs(after.Y-before.Y) < 1e-9 { + t.Fatalf("PixelForLatLng() = %#v before and after SetCamera(), want the setter to move the conversion", after) + } + // The centered coordinate lands at the middle of the 512x512 viewport. + if math.Abs(after.X-256) > 1e-6 || math.Abs(after.Y-256) > 1e-6 { + t.Fatalf("PixelForLatLng(center) = %#v, want the viewport center", after) + } if err := projection.SetVisibleCoordinates([]LatLng{{Latitude: -1, Longitude: -1}, {Latitude: 1, Longitude: 1}}, EdgeInsets{}); err != nil { t.Fatalf("SetVisibleCoordinates(): %v", err) } @@ -60,43 +74,77 @@ func TestMapProjectionCameraAndVisibleCoordinates(t *testing.T) { } } -func TestMapProjectionSnapshotOutlivesMap(t *testing.T) { - lockOSThreadForTest(t) +func TestMapProjectionObservesEarlierMapCommands(t *testing.T) { + runtime, err := NewRuntime() + if err != nil { + t.Fatalf("NewRuntime(): %v", err) + } + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) + if err != nil { + _ = closeRuntimeForTest(runtime) + t.Fatalf("NewMapWithOptions(): %v", err) + } + defer func() { + if err := m.Close(); err != nil { + t.Errorf("Map Close(): %v", err) + } + if err := closeRuntimeForTest(runtime); err != nil { + t.Errorf("Runtime Close(): %v", err) + } + }() + + if _, err := m.JumpTo(CameraOptions{}.WithCenter(LatLng{Latitude: 10, Longitude: 20}).WithZoom(3)); err != nil { + t.Fatalf("JumpTo(): %v", err) + } + projection, err := awaitForTest(m.NewProjection()) + if err != nil { + t.Fatalf("NewProjection(): %v", err) + } + defer func() { + if err := projection.Close(); err != nil { + t.Errorf("Projection Close(): %v", err) + } + }() + camera, err := projection.Camera() + if err != nil { + t.Fatalf("Camera(): %v", err) + } + if camera.Center == nil || + math.Abs(camera.Center.Latitude-10) > 1e-9 || + math.Abs(camera.Center.Longitude-20) > 1e-9 { + t.Fatalf("Camera() center = %#v, want the camera the map committed before creation", camera.Center) + } +} +func TestMapProjectionOutlivesMapAndRuntime(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } - projection, err := m.NewProjection() + projection, err := awaitForTest(m.NewProjection()) if err != nil { _ = m.Close() - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewProjection(): %v", err) } + coordinate := LatLng{Latitude: 0, Longitude: 0} if err := m.Close(); err != nil { - _ = projection.Close() - _ = runtime.Close() t.Fatalf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { - _ = projection.Close() + if err := closeRuntimeForTest(runtime); err != nil { t.Fatalf("Runtime Close(): %v", err) } - - coordinate := LatLng{Latitude: 0, Longitude: 0} point, err := projection.PixelForLatLng(coordinate) if err != nil { - _ = projection.Close() t.Fatalf("PixelForLatLng(): %v", err) } roundTripped, err := projection.LatLngForPixel(point) if err != nil { - _ = projection.Close() t.Fatalf("LatLngForPixel(): %v", err) } if diff := roundTripped.Latitude - coordinate.Latitude; diff < -1e-7 || diff > 1e-7 { @@ -105,6 +153,7 @@ func TestMapProjectionSnapshotOutlivesMap(t *testing.T) { if diff := roundTripped.Longitude - coordinate.Longitude; diff < -1e-7 || diff > 1e-7 { t.Fatalf("longitude round trip = %f, want %f", roundTripped.Longitude, coordinate.Longitude) } + // Close is synchronous: when it returns, the independent handle is retired. if err := projection.Close(); err != nil { t.Fatalf("Projection Close(): %v", err) } @@ -116,49 +165,56 @@ func TestMapProjectionSnapshotOutlivesMap(t *testing.T) { } } -func TestMapProjectionCanBeUsedAndClosedFromAnotherThread(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - +func TestMapProjectionCanMigrateAcrossGoroutines(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(512, 512, 1))) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMapWithOptions(): %v", err) } - projection, err := m.NewProjection() + projection, err := awaitForTest(m.NewProjection()) if err != nil { _ = m.Close() - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewProjection(): %v", err) } - defer func() { - if err := projection.Close(); err != nil { - t.Errorf("Projection Close(): %v", err) - } + defer closeRuntimeForTest(runtime) + defer m.Close() + defer projection.Close() + + // A setter on this goroutine is observed by a conversion on another one. + if err := projection.SetCamera(CameraOptions{}.WithCenter(LatLng{Latitude: 5, Longitude: 6}).WithZoom(2)); err != nil { + t.Fatalf("SetCamera(): %v", err) + } + type conversion struct { + point ScreenPoint + err error + } + result := make(chan conversion, 1) + go func() { + point, err := projection.PixelForLatLng(LatLng{Latitude: 5, Longitude: 6}) + result <- conversion{point: point, err: err} }() - if err := m.Close(); err != nil { - t.Fatalf("Map Close(): %v", err) + got := <-result + if got.err != nil { + t.Fatalf("Projection PixelForLatLng() from another goroutine: %v", got.err) } - if err := runtime.Close(); err != nil { - t.Fatalf("Runtime Close(): %v", err) + if math.Abs(got.point.X-256) > 1e-6 || math.Abs(got.point.Y-256) > 1e-6 { + t.Fatalf("PixelForLatLng(center) from another goroutine = %#v, want the viewport center", got.point) } + // A close on another goroutine retires the handle for every goroutine. errCh := make(chan error, 1) go func() { - if _, err := projection.Camera(); err != nil { - errCh <- err - return - } errCh <- projection.Close() }() if err := <-errCh; err != nil { - t.Fatalf("Projection use from another thread: %v", err) + t.Fatalf("Projection Close() from another goroutine: %v", err) } if _, err := projection.Camera(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("Projection Camera() after cross-thread close error = %v, want ErrInvalidArgument", err) + t.Fatalf("Projection Camera() after cross-goroutine close error = %v, want ErrInvalidArgument", err) } } diff --git a/bindings/go/query.go b/bindings/go/query.go index 9704c3de0..d3a13851b 100644 --- a/bindings/go/query.go +++ b/bindings/go/query.go @@ -282,15 +282,24 @@ func (options *cSourceFeatureQueryOptions) free() { } } -// QueryRenderedFeatures queries rendered features from the latest render session -// state and returns copied hits. -func (session *RenderSessionHandle) QueryRenderedFeatures(geometry RenderedQueryGeometry, options *RenderedFeatureQueryOptions) ([]QueriedFeature, error) { - ptr, release, err := session.ptr() +func startRenderCompletion[T any]( + session *RenderSessionHandle, + start func(C.mln_render_session, *C.mln_completion) int32, + convert func(*C.mln_completion_result) (T, error), +) (*Future[T], error) { + ptr, err := session.ptr() if err != nil { return nil, err } - defer release() defer session.state.KeepAlive() + return startCompletion(func(completion *C.mln_completion) int32 { + return start(C.mln_render_session(ptr), completion) + }, convert) +} + +// QueryRenderedFeatures starts a query against the latest driver state. +// The completed operation yields copied hits. +func (session *RenderSessionHandle) QueryRenderedFeatures(geometry RenderedQueryGeometry, options *RenderedFeatureQueryOptions) (*Future[[]QueriedFeature], error) { rawGeometry := newCRenderedQueryGeometry(geometry) defer rawGeometry.free() rawOptions, err := newCRenderedFeatureQueryOptions(options) @@ -298,58 +307,44 @@ func (session *RenderSessionHandle) QueryRenderedFeatures(geometry RenderedQuery return nil, newBindingError(ErrInvalidArgument, err.Error()) } defer rawOptions.free() - var result C.mln_queried_feature_list - if err := session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_query_rendered_features(C.mln_render_session(ptr), rawGeometry.ptr(), rawOptions.ptr(), &result)) - }) - }); err != nil { - return nil, err - } - return queriedFeatureList(result) -} - -// QuerySourceFeatures queries source features from the latest render session -// state and returns copied hits. -func (session *RenderSessionHandle) QuerySourceFeatures(sourceID string, options *SourceFeatureQueryOptions) ([]QueriedFeature, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, err - } - defer release() - defer session.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() + return startRenderCompletion(session, func(s C.mln_render_session, completion *C.mln_completion) int32 { + return int32(C.mln_render_session_query_rendered_features( + s, + rawGeometry.ptr(), + rawOptions.ptr(), + completion, + )) + }, completionQueriedFeatures) +} + +// QuerySourceFeatures starts a source query against the latest driver +// state. The completed operation yields copied hits. +func (session *RenderSessionHandle) QuerySourceFeatures(sourceID string, options *SourceFeatureQueryOptions) (*Future[[]QueriedFeature], error) { + source := newCStringView(sourceID) + defer source.free() rawOptions, err := newCSourceFeatureQueryOptions(options) if err != nil { return nil, newBindingError(ErrInvalidArgument, err.Error()) } defer rawOptions.free() - var result C.mln_queried_feature_list - if err := session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_query_source_features(C.mln_render_session(ptr), sourceView.raw(), rawOptions.ptr(), &result)) - }) - }); err != nil { - return nil, err - } - return queriedFeatureList(result) -} - -func queriedFeatureList(list C.mln_queried_feature_list) ([]QueriedFeature, error) { - defer C.mln_queried_feature_list_destroy(list) - var count C.size_t - if err := checkNative(func() int32 { return int32(C.mln_queried_feature_list_count(list, &count)) }); err != nil { + return startRenderCompletion(session, func(s C.mln_render_session, completion *C.mln_completion) int32 { + return int32(C.mln_render_session_query_source_features( + s, + source.raw(), + rawOptions.ptr(), + completion, + )) + }, completionQueriedFeatures) +} + +func completionQueriedFeatures(result *C.mln_completion_result) ([]QueriedFeature, error) { + raw, err := completionSlice[C.mln_queried_feature](result) + if err != nil { return nil, err } - features := make([]QueriedFeature, int(count)) + features := make([]QueriedFeature, len(raw)) for i := range features { - hit := C.mln_queried_feature_default() - if err := checkNative(func() int32 { - return int32(C.mln_queried_feature_list_get(list, C.size_t(i), &hit)) - }); err != nil { - return nil, err - } + hit := raw[i] feature, ok := goByteSlice(hit.feature.data, hit.feature.size) if !ok { return nil, newBindingError(ErrNative, "native queried feature data is invalid") @@ -375,48 +370,30 @@ func queriedFeatureList(list C.mln_queried_feature_list) ([]QueriedFeature, erro return features, nil } -// QueryFeatureExtensions queries a feature extension from the latest render -// session state. -// -// feature contains one UTF-8 GeoJSON Feature. arguments, when present, contains -// a UTF-8 JSON object. The result contains either JSON or GeoJSON bytes. -func (session *RenderSessionHandle) QueryFeatureExtensions(sourceID string, feature []byte, extension string, extensionField string, arguments []byte) ([]byte, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, err - } - defer release() - defer session.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - extensionView := newCStringView(extension) - defer extensionView.free() - extensionFieldView := newCStringView(extensionField) - defer extensionFieldView.free() +// QueryFeatureExtensions starts a feature-extension query. +func (session *RenderSessionHandle) QueryFeatureExtensions(sourceID string, feature []byte, extension, extensionField string, arguments []byte) (*Future[[]byte], error) { + source, ext, field := newCStringView(sourceID), newCStringView(extension), newCStringView(extensionField) + defer source.free() + defer ext.free() + defer field.free() featureView := newCBufferView(feature) defer featureView.free() - var argumentsView cBufferView - var rawArguments *C.mln_buffer_view + var argumentView cBufferView + var argument *C.mln_buffer_view if arguments != nil { - argumentsView = newCBufferView(arguments) - defer argumentsView.free() - rawArguments = argumentsView.ptr() - } - var result C.mln_buffer - if err := session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_query_feature_extensions( - C.mln_render_session(ptr), - sourceView.raw(), - featureView.raw(), - extensionView.raw(), - extensionFieldView.raw(), - rawArguments, - &result, - )) - }) - }); err != nil { - return nil, err - } - return goOwnedBuffer(result) + argumentView = newCBufferView(arguments) + defer argumentView.free() + argument = argumentView.ptr() + } + return startRenderCompletion(session, func(s C.mln_render_session, completion *C.mln_completion) int32 { + return int32(C.mln_render_session_query_feature_extensions( + s, + source.raw(), + featureView.raw(), + ext.raw(), + field.raw(), + argument, + completion, + )) + }, completionBuffer) } diff --git a/bindings/go/render.go b/bindings/go/render.go index 7b1192075..044594e9e 100644 --- a/bindings/go/render.go +++ b/bindings/go/render.go @@ -7,9 +7,7 @@ package maplibre import "C" import ( - "fmt" "math" - "runtime" "sync" "unsafe" @@ -66,26 +64,12 @@ const ( // into. Wait for a host event that changes the render target, or back off // and retry. RenderResultTargetNotReady RenderResult = RenderResult(C.MLN_RENDER_RESULT_TARGET_NOT_READY) + RenderResultSuperseded RenderResult = RenderResult(C.MLN_RENDER_RESULT_SUPERSEDED) + RenderResultDeadlineMissed RenderResult = RenderResult(C.MLN_RENDER_RESULT_DEADLINE_MISSED) ) -// RenderUpdate is the outcome of a successful RenderSessionHandle.RenderUpdate -// call. -type RenderUpdate struct { - // Result is what the call produced; each value names the wake a host - // waits for before it calls again. - Result RenderResult - // NeedsRepaint reports whether the map asked for another frame while it - // rendered this one, as during an ongoing camera transition. It is true - // only when Result is RenderResultRendered and reads false for every other - // outcome. This is the same signal a RuntimeEventMapRenderFrameFinished - // event carries in its NeedsRepaint field, delivered here without the - // event round trip, so a host can re-arm its frame loop before it drains - // events. - NeedsRepaint bool -} - -// OpenGLContextOwnership names how a session's OpenGL context relates to the -// thread that attached it. +// OpenGLContextOwnership names how a session's OpenGL context relates to its +// driver thread and host graphics state. type OpenGLContextOwnership uint32 const ( @@ -95,10 +79,9 @@ const ( // the descriptor, so a host may hand the session a texture and sample it // from its own context. OpenGLContextOwnershipShared OpenGLContextOwnership = OpenGLContextOwnership(C.MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED) - // OpenGLContextOwnershipDedicated gives the session its thread. It makes - // its context current once and keeps it current between renders, and it - // joins no share group. Use this when a thread exists to drive one render - // session and runs no other graphics work. + // OpenGLContextOwnershipDedicated gives the session its driver thread's + // context. It keeps the context current between renders and joins no share + // group. The driver may be a native core worker or a dedicated host thread. OpenGLContextOwnershipDedicated OpenGLContextOwnership = OpenGLContextOwnership(C.MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED) ) @@ -150,6 +133,44 @@ type VulkanContextDescriptor struct { GetDeviceProcAddr NativePointer } +// WebGPUContextDescriptor contains WebGPU backend context handles. +type WebGPUContextDescriptor struct { + Instance NativePointer + Device NativePointer + Queue NativePointer +} + +// WebGPUSurfaceDescriptor describes a WebGPU surface target. +type WebGPUSurfaceDescriptor struct { + Extent RenderTargetExtent + Context WebGPUContextDescriptor + Surface NativePointer + Format uint32 +} + +// WebGPUOwnedTextureDescriptor describes a session-owned WebGPU texture ring. +type WebGPUOwnedTextureDescriptor struct { + Extent RenderTargetExtent + Context WebGPUContextDescriptor +} + +// WebGPUBorrowedTextureDescriptor describes a caller-owned WebGPU texture. +type WebGPUBorrowedTextureDescriptor struct { + Extent RenderTargetExtent + PhysicalWidth uint32 + PhysicalHeight uint32 + Context WebGPUContextDescriptor + Texture NativePointer + TextureView NativePointer + Format uint32 +} + +// TextureReadback owns premultiplied RGBA8 bytes and its image layout. +type TextureReadback struct { + Data []byte + Info TextureImageInfo +} + // MetalSurfaceDescriptor describes a Metal-backed surface render target. The // session retains the layer and optional device while attached. type MetalSurfaceDescriptor struct { @@ -194,20 +215,43 @@ type EGLContextDescriptor struct { GetProcAddress NativePointer } +// WebGLContextKind selects an existing agent-local context or a transferred canvas. +type WebGLContextKind uint32 + +const ( + WebGLContextExisting WebGLContextKind = WebGLContextKind(C.MLN_WEBGL_CONTEXT_EXISTING) + WebGLContextTransferredCanvas WebGLContextKind = WebGLContextKind(C.MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS) +) + +// WebGLContextDescriptor describes browser WebGL context placement. +type WebGLContextDescriptor struct { + Kind WebGLContextKind + Context int32 + CanvasSelector string +} + // OpenGLContextDescriptor contains one OpenGL platform context provider. type OpenGLContextDescriptor struct { - WGL *WGLContextDescriptor - EGL *EGLContextDescriptor - // Ownership is whether the session shares its thread with host graphics - // work. The zero value is OpenGLContextOwnershipShared. WGL and EGL - // surface sessions support both. A WebGL session renders through the - // host's own context and a texture session hands its texture to the host, - // so both are shared only. + WGL *WGLContextDescriptor + EGL *EGLContextDescriptor + WebGL *WebGLContextDescriptor + // A private EGL owned texture and a transferred canvas are dedicated to + // their core worker. Ownership OpenGLContextOwnership } func (context OpenGLContextDescriptor) validate() error { - if (context.WGL == nil) == (context.EGL == nil) { + count := 0 + if context.WGL != nil { + count++ + } + if context.EGL != nil { + count++ + } + if context.WebGL != nil { + count++ + } + if count != 1 { return newBindingError(ErrInvalidArgument, "OpenGL context descriptor must specify exactly one platform") } return nil @@ -283,112 +327,254 @@ type OpenGLBorrowedTextureDescriptor struct { Target uint32 } -// RenderSessionHandle owns a map render session. +// RenderDriver selects native render execution placement. +type RenderDriver uint32 + +const ( + RenderDriverCoreWorker RenderDriver = RenderDriver(C.MLN_RENDER_DRIVER_CORE_WORKER) + RenderDriverCallerGraphicsThread RenderDriver = RenderDriver(C.MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) +) + +// RenderSessionAttachOptions configures execution placement and an owned-texture ring. +type RenderSessionAttachOptions struct { + Driver RenderDriver + RequestedTextureRingDepth uint32 +} + +// NewRenderSessionAttachOptions returns core-worker attachment defaults. +func NewRenderSessionAttachOptions() RenderSessionAttachOptions { + return RenderSessionAttachOptions{Driver: RenderDriverCoreWorker} +} + +func (options RenderSessionAttachOptions) toC() C.mln_render_session_attach_options { + raw := C.mln_render_session_attach_options_default() + if options.Driver != 0 { + raw.driver = C.uint32_t(options.Driver) + } + raw.requested_texture_ring_depth = C.uint32_t(options.RequestedTextureRingDepth) + return raw +} + +// RenderSessionCapability is a render-session capability bit. +type RenderSessionCapability uint32 + +const ( + RenderSessionCapabilityFrameAcquisition RenderSessionCapability = RenderSessionCapability(C.MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION) + RenderSessionCapabilityReadback RenderSessionCapability = RenderSessionCapability(C.MLN_RENDER_SESSION_CAPABILITY_READBACK) + RenderSessionCapabilityConsumerSync RenderSessionCapability = RenderSessionCapability(C.MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC) + RenderSessionCapabilityPresentation RenderSessionCapability = RenderSessionCapability(C.MLN_RENDER_SESSION_CAPABILITY_PRESENTATION) +) + +// RenderSessionCapabilities are immutable negotiated attachment capabilities. +type RenderSessionCapabilities struct { + Driver RenderDriver + TextureRingDepth uint32 + Flags RenderSessionCapability +} + +// RenderSessionState is the published session lifecycle state. +type RenderSessionState uint32 + +const ( + RenderSessionAttaching RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_ATTACHING) + RenderSessionAttached RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_ATTACHED) + RenderSessionDetaching RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_DETACHING) + RenderSessionDetached RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_DETACHED) + RenderSessionTargetLost RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_TARGET_LOST) + RenderSessionAbandoned RenderSessionState = RenderSessionState(C.MLN_RENDER_SESSION_STATE_ABANDONED) +) + +// RenderSessionSnapshot is an immutable any-goroutine session snapshot. +type RenderSessionSnapshot struct { + State RenderSessionState + RawState uint32 + Driver RenderDriver + LatestResult RenderResult + Extent RenderTargetExtent + Generation, MapUpdateGeneration uint64 + RenderedUpdateGeneration uint64 + ExtentGeneration, FrameGeneration uint64 + LatestDemandToken uint64 + PendingDemandCount, AcquiredFrameCount uint32 + TargetReady, PendingChanges bool +} + +// FrameDemandFlag controls one nonblocking frame demand. +type FrameDemandFlag uint32 + +const ( + FrameDemandIfNeeded FrameDemandFlag = FrameDemandFlag(C.MLN_FRAME_DEMAND_IF_NEEDED) + FrameDemandPresent FrameDemandFlag = FrameDemandFlag(C.MLN_FRAME_DEMAND_PRESENT) +) + +// FrameDemand is copied by RequestFrame. +type FrameDemand struct { + Flags FrameDemandFlag + Token, CoalescingBoundary uint64 + TimeoutNS uint64 +} + +func NewFrameDemand() FrameDemand { + return FrameDemand{Flags: FrameDemandIfNeeded} +} + +func (d FrameDemand) toC() C.mln_frame_demand { + raw := C.mln_frame_demand_default() + raw.flags = C.uint32_t(d.Flags) + raw.token = C.uint64_t(d.Token) + raw.coalescing_boundary = C.uint64_t(d.CoalescingBoundary) + raw.timeout_ns = C.uint64_t(d.TimeoutNS) + return raw +} + +// RenderFrameResult is one terminal result for an accepted demand. +type RenderFrameResult struct { + Disposition RenderResult + Token uint64 + MapUpdateGeneration uint64 + ExtentGeneration uint64 + FrameGeneration uint64 + // NeedsRepaint reports whether the map asked for another frame while it + // rendered this one, as during an ongoing camera transition. It is true + // only when Disposition is RenderResultRendered and reads false for every + // other outcome. This is the same signal a + // RuntimeEventMapRenderFrameFinished event carries in its NeedsRepaint + // field, delivered with the frame result so a host can re-arm its frame + // loop without the runtime event round trip. + NeedsRepaint bool +} + +func (sync GPUSync) toC() C.mln_gpu_sync { + raw := C.mln_gpu_sync_default() + raw.kind = C.uint32_t(sync.Kind) + raw.object = cPointer(sync.Object) + raw.value = C.uint64_t(sync.Value) + return raw +} + +func frameResultFromC(raw C.mln_render_frame_result) RenderFrameResult { + return RenderFrameResult{ + Disposition: RenderResult(raw.disposition), + Token: uint64(raw.token), + MapUpdateGeneration: uint64(raw.map_update_generation), + ExtentGeneration: uint64(raw.extent_generation), + FrameGeneration: uint64(raw.frame_generation), + NeedsRepaint: bool(raw.needs_repaint), + } +} + +// GPUSyncKind identifies a producer or consumer completion primitive. +type GPUSyncKind uint32 + +const ( + GPUSyncCPUComplete GPUSyncKind = GPUSyncKind(C.MLN_GPU_SYNC_CPU_COMPLETE) + GPUSyncMetalSharedEvent GPUSyncKind = GPUSyncKind(C.MLN_GPU_SYNC_METAL_SHARED_EVENT) + GPUSyncVulkanTimelineSemaphore GPUSyncKind = GPUSyncKind(C.MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE) + GPUSyncOpenGLFence GPUSyncKind = GPUSyncKind(C.MLN_GPU_SYNC_OPENGL_FENCE) + GPUSyncWebGPUToken GPUSyncKind = GPUSyncKind(C.MLN_GPU_SYNC_WEBGPU_TOKEN) +) + +// GPUSync is a copied backend synchronization payload. +type GPUSync struct { + Kind GPUSyncKind + Object NativePointer + Value uint64 +} + +// RenderSessionHandle owns a map render session. Control methods are safe from +// any goroutine; only ServiceDriverWork and OpenGL frame access require the +// caller graphics thread. type RenderSessionHandle struct { - state *handle.State[nativeRenderSession] - parent *MapHandle - parentChild *handle.Child - mu sync.Mutex - frame bool + closeMu sync.Mutex + state *handle.State[nativeRenderSession] + parent *MapHandle } var destroyRenderSessionHandle = func(native nativeRenderSession) int32 { return int32(C.mln_render_session_destroy(C.mln_render_session(native))) } -var detachRenderSessionHandle = func(native nativeRenderSession) int32 { - return int32(C.mln_render_session_detach(C.mln_render_session(native))) +// RenderAbandonDisposition reports whether graphics resources were quarantined. +type RenderAbandonDisposition uint32 + +const ( + RenderAbandonClean RenderAbandonDisposition = RenderAbandonDisposition(C.MLN_RENDER_ABANDON_DISPOSITION_CLEAN) + RenderAbandonQuarantined RenderAbandonDisposition = RenderAbandonDisposition(C.MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED) +) + +// RenderAbandonResult is the result of irreversible CPU-side abandonment. +type RenderAbandonResult struct { + Disposition RenderAbandonDisposition + QuarantinedResourceCount uint32 } -type metalOwnedTextureFrameState struct { - session *RenderSessionHandle - raw C.mln_metal_owned_texture_frame - mu sync.Mutex - closed bool +// RenderFrameBatch owns drained immutable frame-result records. +type RenderFrameBatch struct { + mu sync.Mutex + handle uint64 + closed bool } -type vulkanOwnedTextureFrameState struct { - session *RenderSessionHandle - raw C.mln_vulkan_owned_texture_frame - mu sync.Mutex - closed bool +// AcquiredFrame leases one owned-texture ring slot. Release consumes the lease. +type AcquiredFrame struct { + mu sync.Mutex + handle uint64 + closed bool } -type openglOwnedTextureFrameState struct { - session *RenderSessionHandle - raw C.mln_opengl_owned_texture_frame - mu sync.Mutex - closed bool +// WebGPUOwnedTextureFrameInfo contains backend-native WebGPU frame metadata. +type WebGPUOwnedTextureFrameInfo struct { + Generation uint64 + Width uint32 + Height uint32 + ScaleFactor float64 + FrameID uint64 + Texture NativePointer + TextureView NativePointer + Device NativePointer + Format uint32 } -// MetalOwnedTextureFrameInfo contains copied metadata for an acquired -// session-owned texture frame. +// MetalOwnedTextureFrameInfo contains backend-native Metal frame metadata. type MetalOwnedTextureFrameInfo struct { Generation uint64 Width uint32 Height uint32 ScaleFactor float64 + FrameID uint64 + Texture NativePointer + Device NativePointer PixelFormat uint64 } -// MetalOwnedTextureFrame is an acquired session-owned Metal texture frame. -// Backend handles are borrowed and remain valid only while the frame is active. -// Close the frame on the render session owner thread before resizing, rendering, -// reading back, detaching, closing the session, or acquiring another frame. -type MetalOwnedTextureFrame struct { - info MetalOwnedTextureFrameInfo - texture NativePointer - device NativePointer - state *metalOwnedTextureFrameState -} - -// VulkanOwnedTextureFrameInfo contains copied metadata for an acquired -// session-owned texture frame. +// VulkanOwnedTextureFrameInfo contains backend-native Vulkan frame metadata. type VulkanOwnedTextureFrameInfo struct { Generation uint64 Width uint32 Height uint32 ScaleFactor float64 + FrameID uint64 + Image NativePointer + ImageView NativePointer + Device NativePointer Format uint32 Layout uint32 } -// VulkanOwnedTextureFrame is an acquired session-owned Vulkan texture frame. -// Backend handles are borrowed and remain valid only while the frame is active. -// Close the frame on the render session owner thread before resizing, rendering, -// reading back, detaching, closing the session, or acquiring another frame. -type VulkanOwnedTextureFrame struct { - info VulkanOwnedTextureFrameInfo - image NativePointer - imageView NativePointer - device NativePointer - state *vulkanOwnedTextureFrameState -} - -// OpenGLOwnedTextureFrameInfo contains copied metadata for an -// acquired session-owned texture frame. +// OpenGLOwnedTextureFrameInfo contains backend-native OpenGL frame metadata. type OpenGLOwnedTextureFrameInfo struct { Generation uint64 Width uint32 Height uint32 ScaleFactor float64 + FrameID uint64 + Texture uint32 + Target uint32 InternalFormat uint32 Format uint32 Type uint32 } -// OpenGLOwnedTextureFrame is an acquired session-owned OpenGL texture frame. -// Backend object names are borrowed and remain valid only while the frame is -// active. Close the frame on the render session owner thread before resizing, -// rendering, reading back, detaching, closing the session, or acquiring another -// frame. -type OpenGLOwnedTextureFrame struct { - info OpenGLOwnedTextureFrameInfo - texture uint32 - target uint32 - state *openglOwnedTextureFrameState -} - func (extent RenderTargetExtent) validate() error { if math.IsNaN(extent.ScaleFactor) || math.IsInf(extent.ScaleFactor, 0) || extent.ScaleFactor <= 0 { return newBindingError(ErrInvalidArgument, "render target scale factor must be positive and finite") @@ -496,6 +682,43 @@ func (descriptor OpenGLOwnedTextureDescriptor) toC() C.mln_opengl_owned_texture_ return raw } +func (context WebGPUContextDescriptor) toC() C.mln_webgpu_context_descriptor { + return C.mln_webgpu_context_descriptor{ + size: C.uint32_t(unsafe.Sizeof(C.mln_webgpu_context_descriptor{})), + instance: cPointer(context.Instance), + device: cPointer(context.Device), + queue: cPointer(context.Queue), + } +} + +func (descriptor WebGPUSurfaceDescriptor) toC() C.mln_webgpu_surface_descriptor { + raw := C.mln_webgpu_surface_descriptor_default() + raw.extent = descriptor.Extent.toC() + raw.context = descriptor.Context.toC() + raw.surface = cPointer(descriptor.Surface) + raw.format = C.uint32_t(descriptor.Format) + return raw +} + +func (descriptor WebGPUOwnedTextureDescriptor) toC() C.mln_webgpu_owned_texture_descriptor { + raw := C.mln_webgpu_owned_texture_descriptor_default() + raw.extent = descriptor.Extent.toC() + raw.context = descriptor.Context.toC() + return raw +} + +func (descriptor WebGPUBorrowedTextureDescriptor) toC() C.mln_webgpu_borrowed_texture_descriptor { + raw := C.mln_webgpu_borrowed_texture_descriptor_default() + raw.extent = descriptor.Extent.toC() + raw.physical_width = C.uint32_t(descriptor.PhysicalWidth) + raw.physical_height = C.uint32_t(descriptor.PhysicalHeight) + raw.context = descriptor.Context.toC() + raw.texture = cPointer(descriptor.Texture) + raw.texture_view = cPointer(descriptor.TextureView) + raw.format = C.uint32_t(descriptor.Format) + return raw +} + func (descriptor OpenGLBorrowedTextureDescriptor) toC() C.mln_opengl_borrowed_texture_descriptor { raw := C.mln_opengl_borrowed_texture_descriptor_default() raw.extent = descriptor.Extent.toC() @@ -531,971 +754,21 @@ func (context OpenGLContextDescriptor) toC() C.mln_opengl_context_descriptor { C.mln_go_opengl_context_set_egl(&raw, cPointer(context.EGL.Display), cPointer(context.EGL.Config), cPointer(context.EGL.ShareContext), C.mln_opengl_client_api(context.EGL.ClientAPI), cPointer(context.EGL.GetProcAddress)) return raw } + if context.WebGL != nil { + var selector *C.char + if context.WebGL.CanvasSelector != "" { + selector = (*C.char)(unsafe.Pointer(unsafe.StringData(context.WebGL.CanvasSelector))) + } + C.mln_go_opengl_context_set_webgl(&raw, C.mln_webgl_context_kind(context.WebGL.Kind), C.int32_t(context.WebGL.Context), selector, C.size_t(len(context.WebGL.CanvasSelector))) + return raw + } return raw } func newRenderSessionHandle(parent *MapHandle, session nativeRenderSession) (*RenderSessionHandle, error) { - state, err := handle.New(session, "RenderSessionHandle", parent) + state, err := handle.New(session, "RenderSessionHandle") if err != nil { return nil, newBindingError(ErrInvalidArgument, err.Error()) } - return &RenderSessionHandle{state: state, parent: parent, parentChild: parent.state.AddChild()}, nil -} - -// AttachMetalSurface attaches a Metal native surface render target to this map. -// The returned session is affine to the OS thread that attaches it, which need -// not be the map owner thread. -func (m *MapHandle) AttachMetalSurface(descriptor MetalSurfaceDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_metal_surface_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachVulkanSurface attaches a Vulkan native surface render target to this -// map. The returned session is affine to the OS thread that attaches it, which -// need not be the map owner thread, and borrowed Vulkan handles must outlive -// detach or session close. -func (m *MapHandle) AttachVulkanSurface(descriptor VulkanSurfaceDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_vulkan_surface_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachOpenGLSurface attaches an OpenGL native surface render target to this -// map. The returned session is affine to the OS thread that attaches it, which -// need not be the map owner thread, and borrowed OpenGL context handles must -// outlive detach or session close. -func (m *MapHandle) AttachOpenGLSurface(descriptor OpenGLSurfaceDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - if err := descriptor.Context.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_opengl_surface_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachMetalOwnedTexture attaches a Metal session-owned texture render target. -func (m *MapHandle) AttachMetalOwnedTexture(descriptor MetalOwnedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_metal_owned_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachMetalBorrowedTexture attaches a Metal caller-owned texture render -// target. The caller keeps the texture valid until detach or session close and -// synchronizes external use. -func (m *MapHandle) AttachMetalBorrowedTexture(descriptor MetalBorrowedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_metal_borrowed_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachVulkanOwnedTexture attaches a Vulkan session-owned texture render target. -func (m *MapHandle) AttachVulkanOwnedTexture(descriptor VulkanOwnedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_vulkan_owned_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachVulkanBorrowedTexture attaches a Vulkan caller-owned texture render -// target. The caller owns image lifetime, queue-family ownership, image layout -// transitions, and external synchronization around each RenderUpdate. -func (m *MapHandle) AttachVulkanBorrowedTexture(descriptor VulkanBorrowedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_vulkan_borrowed_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachOpenGLOwnedTexture attaches an OpenGL session-owned texture render target. -func (m *MapHandle) AttachOpenGLOwnedTexture(descriptor OpenGLOwnedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - if err := descriptor.Context.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_opengl_owned_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -// AttachOpenGLBorrowedTexture attaches an OpenGL caller-owned texture render target. -func (m *MapHandle) AttachOpenGLBorrowedTexture(descriptor OpenGLBorrowedTextureDescriptor) (*RenderSessionHandle, error) { - if err := descriptor.Extent.validate(); err != nil { - return nil, err - } - if err := descriptor.Context.validate(); err != nil { - return nil, err - } - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - - var session C.mln_render_session - rawDescriptor := descriptor.toC() - if err := checkNative(func() int32 { - return int32(C.mln_opengl_borrowed_texture_attach(C.mln_map(ptr), &rawDescriptor, &session)) - }); err != nil { - return nil, err - } - return newRenderSessionHandle(m, nativeRenderSession(session)) -} - -func (session *RenderSessionHandle) ptr() (nativeRenderSession, func(), error) { - if session == nil || session.state == nil { - return 0, nil, newBindingError(ErrInvalidArgument, "RenderSessionHandle is nil") - } - borrow, live := session.state.Borrow() - if !live { - return 0, nil, newBindingError(ErrInvalidArgument, "RenderSessionHandle is closed") - } - return borrow.Handle(), borrow.Release, nil -} - -func (session *RenderSessionHandle) withNoAcquiredFrame(call func() error) error { - session.mu.Lock() - defer session.mu.Unlock() - if session.frame { - return newBindingError(ErrInvalidState, "texture frame is still acquired") - } - return call() -} - -func (session *RenderSessionHandle) markFrameReleased() { - session.mu.Lock() - session.frame = false - session.mu.Unlock() -} - -// Resize changes the render session target extent. -// -// Surface and owned-texture sessions resize in place. Borrowed texture targets -// are sized by their owner and report an unsupported-feature error; hand over a -// texture at the new size with SetMetalBorrowedTextureTarget, -// SetVulkanBorrowedTextureTarget, or SetOpenGLBorrowedTextureTarget instead. -// -// The session keeps its renderer across a resize, along with the tile pyramid, -// glyph and image atlases, and symbol placement. A scale factor change retires -// the renderer instead, because shaders are compiled for one pixel ratio. -// Map-owned feature state survives either way. -func (session *RenderSessionHandle) Resize(extent RenderTargetExtent) error { - if err := extent.validate(); err != nil { - return err - } - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_resize(C.mln_render_session(ptr), C.uint32_t(extent.Width), C.uint32_t(extent.Height), C.double(extent.ScaleFactor))) - }) - }) -} - -// SetMetalSurfaceTarget presents this attached surface session through a new -// Metal surface. Replacing the surface in place keeps this session's renderer, -// and with it the tile pyramid, glyph and image atlases, symbol placement, and -// feature state. The extent applies as Resize applies one, scale factor change -// included. -// -// A descriptor whose Context.Device is neither zero nor this session's device -// reports an invalid-argument error and leaves this session rendering into the -// surface it has. The session assigns the layer its own device and pixel -// format. -func (session *RenderSessionHandle) SetMetalSurfaceTarget(descriptor MetalSurfaceDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_metal_surface_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// SetVulkanSurfaceTarget presents this attached surface session through a new -// Vulkan surface. -// -// See SetMetalSurfaceTarget for what replacing a surface preserves. The -// outgoing VkSurfaceKHR must still be valid, because this session holds a -// swapchain built from it; a host that must release its surface first closes -// this session and attaches again afterward. -// -// The replacement must support the color format and surface transform this -// session compiled its render pass and shaders for. One that does not reports -// an unsupported-feature error, leaving this session rendering into the surface -// it has. -func (session *RenderSessionHandle) SetVulkanSurfaceTarget(descriptor VulkanSurfaceDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_vulkan_surface_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// SetOpenGLSurfaceTarget presents this attached surface session through a new -// OpenGL surface. -// -// See SetMetalSurfaceTarget for what replacing a surface preserves. The new -// surface is made current on the next render, so a host may hand over a -// replacement for one it has already destroyed. An unusable surface accepted -// here reports a native error from the next RenderUpdate rather than this call. -func (session *RenderSessionHandle) SetOpenGLSurfaceTarget(descriptor OpenGLSurfaceDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - if err := descriptor.Context.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_opengl_surface_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// SetMetalBorrowedTextureTarget renders this attached texture session into a new -// caller-owned Metal texture. -// -// Handing over a replacement keeps this session's renderer, unlike Resize, -// which reports an unsupported-feature error for a caller-owned texture. A -// scale factor change still starts a new renderer for the new pixel ratio. -// -// The replacement must belong to the device this session attached with, which -// reports an invalid-argument error otherwise, and carry the pixel format it -// attached with, which reports an unsupported-feature error otherwise; both -// leave this session rendering into the texture it has. The caller keeps the -// replacement valid until the next replacement, detach, or session close. This -// session never retains or releases the outgoing texture. -func (session *RenderSessionHandle) SetMetalBorrowedTextureTarget(descriptor MetalBorrowedTextureDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_metal_borrowed_texture_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// SetVulkanBorrowedTextureTarget renders this attached texture session into a -// new caller-owned Vulkan image. -// -// See SetMetalBorrowedTextureTarget for what replacing a target preserves. The -// replacement must carry the format and both layouts this session attached -// with, because its render pass was built around them. -func (session *RenderSessionHandle) SetVulkanBorrowedTextureTarget(descriptor VulkanBorrowedTextureDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_vulkan_borrowed_texture_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// SetOpenGLBorrowedTextureTarget renders this attached texture session into a -// new caller-owned OpenGL texture. -// -// See SetMetalBorrowedTextureTarget for what replacing a target preserves. The -// replacement must belong to the context this session attached with or one in -// its share group, and that context must be current on the calling thread. -func (session *RenderSessionHandle) SetOpenGLBorrowedTextureTarget(descriptor OpenGLBorrowedTextureDescriptor) error { - if err := descriptor.Extent.validate(); err != nil { - return err - } - if err := descriptor.Context.validate(); err != nil { - return err - } - rawDescriptor := descriptor.toC() - return session.setTarget(func(ptr nativeRenderSession) int32 { - return int32(C.mln_opengl_borrowed_texture_set_target(C.mln_render_session(ptr), &rawDescriptor)) - }) -} - -// setTarget runs the shared body of the target replacement methods. The C API -// borrows the descriptor the caller closes over only for the native call. -func (session *RenderSessionHandle) setTarget(call func(nativeRenderSession) int32) error { - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { return call(ptr) }) - }) -} - -// RenderUpdate renders the latest available map render update into the attached -// render target. -// -// The map retains its latest update, so repeated calls re-render it and report -// RenderResultRendered again. Every other result names the wake to wait for: -// RenderResultNoUpdate and RenderResultSizePending resolve on a -// RuntimeEventMapRenderUpdateAvailable event, and RenderResultTargetNotReady -// resolves when the host changes the render target. -func (session *RenderSessionHandle) RenderUpdate() (RenderUpdate, error) { - ptr, release, err := session.ptr() - if err != nil { - return RenderUpdate{}, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - var result C.mln_render_result - var needsRepaint C.bool - err = session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_render_update(C.mln_render_session(ptr), &result, &needsRepaint)) - }) - }) - if err != nil { - return RenderUpdate{}, err - } - return RenderUpdate{Result: RenderResult(result), NeedsRepaint: bool(needsRepaint)}, nil -} - -// Detach detaches the render target from the session. The session stays live -// and still needs Close, but it no longer holds its map: the map can be closed -// while this session is open, and every session operation that needs an -// attached target reports a native error. A detached session stays detached. -func (session *RenderSessionHandle) Detach() error { - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - if err := checkNative(func() int32 { - return detachRenderSessionHandle(ptr) - }); err != nil { - return err - } - session.parentChild.Release() - return nil - }) -} - -// ReduceMemoryUse asks the render session to release cached render resources. -func (session *RenderSessionHandle) ReduceMemoryUse() error { - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_reduce_memory_use(C.mln_render_session(ptr))) - }) - }) -} - -// ClearData clears render-session data. -func (session *RenderSessionHandle) ClearData() error { - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_clear_data(C.mln_render_session(ptr))) - }) - }) -} - -// DumpDebugLogs dumps render-session debug logs. -func (session *RenderSessionHandle) DumpDebugLogs() error { - ptr, release, err := session.ptr() - if err != nil { - return err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - return checkNative(func() int32 { - return int32(C.mln_render_session_dump_debug_logs(C.mln_render_session(ptr))) - }) - }) -} - -// ReadPremultipliedRGBA8 reads the latest session-owned texture frame into a -// new byte slice. -func (session *RenderSessionHandle) ReadPremultipliedRGBA8() ([]byte, TextureImageInfo, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, TextureImageInfo{}, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - - var buffer []byte - var info TextureImageInfo - err = session.withNoAcquiredFrame(func() error { - // A null buffer with zero capacity is a size probe. - rawInfo := C.mln_texture_image_info_default() - if probeErr := checkNative(func() int32 { - return int32(C.mln_texture_read_premultiplied_rgba8(C.mln_render_session(ptr), nil, 0, &rawInfo)) - }); probeErr != nil { - info = textureImageInfoFromC(rawInfo) - return probeErr - } - info = textureImageInfoFromC(rawInfo) - buffer = make([]byte, info.ByteLength) - var readErr error - info, readErr = session.readPremultipliedRGBA8IntoLocked(ptr, buffer) - return readErr - }) - if err != nil { - return nil, info, err - } - return buffer, info, nil -} - -// ReadPremultipliedRGBA8Into reads the latest session-owned texture frame into -// caller-owned storage. -func (session *RenderSessionHandle) ReadPremultipliedRGBA8Into(buffer []byte) (TextureImageInfo, error) { - ptr, release, err := session.ptr() - if err != nil { - return TextureImageInfo{}, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - var info TextureImageInfo - err = session.withNoAcquiredFrame(func() error { - var readErr error - info, readErr = session.readPremultipliedRGBA8IntoLocked(ptr, buffer) - return readErr - }) - return info, err -} - -func (session *RenderSessionHandle) readPremultipliedRGBA8IntoLocked(ptr nativeRenderSession, buffer []byte) (TextureImageInfo, error) { - rawInfo := C.mln_texture_image_info_default() - var rawBuffer *C.uint8_t - if len(buffer) > 0 { - rawBuffer = (*C.uint8_t)(unsafe.Pointer(&buffer[0])) - } - if err := checkNative(func() int32 { - return int32(C.mln_texture_read_premultiplied_rgba8(C.mln_render_session(ptr), rawBuffer, C.size_t(len(buffer)), &rawInfo)) - }); err != nil { - runtime.KeepAlive(buffer) - return textureImageInfoFromC(rawInfo), err - } - runtime.KeepAlive(buffer) - info := textureImageInfoFromC(rawInfo) - // An empty destination reaches native code as a size probe, which succeeds - // without copying, so report it as too small unless the frame is empty. - if len(buffer) == 0 && info.ByteLength > 0 { - return info, newBindingError(ErrInvalidArgument, - fmt.Sprintf("buffer length 0 is smaller than the required %d bytes", info.ByteLength)) - } - return info, nil -} - -// AcquireMetalTextureFrame acquires the latest Metal session-owned texture -// frame. While the frame is live, resize, render update, detach, readback, -// session close, and another frame acquire are invalid. -func (session *RenderSessionHandle) AcquireMetalTextureFrame() (*MetalOwnedTextureFrame, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - session.mu.Lock() - defer session.mu.Unlock() - if session.frame { - return nil, newBindingError(ErrInvalidState, "texture frame is still acquired") - } - rawFrame := C.mln_metal_owned_texture_frame{size: C.uint32_t(unsafe.Sizeof(C.mln_metal_owned_texture_frame{}))} - if err := checkNative(func() int32 { - return int32(C.mln_metal_owned_texture_acquire_frame(C.mln_render_session(ptr), &rawFrame)) - }); err != nil { - return nil, err - } - session.frame = true - return &MetalOwnedTextureFrame{ - info: MetalOwnedTextureFrameInfo{ - Generation: uint64(rawFrame.generation), - Width: uint32(rawFrame.width), - Height: uint32(rawFrame.height), - ScaleFactor: float64(rawFrame.scale_factor), - PixelFormat: uint64(rawFrame.pixel_format), - }, - texture: NativePointer(uintptr(rawFrame.texture)), - device: NativePointer(uintptr(rawFrame.device)), - state: &metalOwnedTextureFrameState{session: session, raw: rawFrame}, - }, nil -} - -// AcquireVulkanTextureFrame acquires the latest Vulkan session-owned texture -// frame. While the frame is live, resize, render update, detach, readback, -// session close, and another frame acquire are invalid. -func (session *RenderSessionHandle) AcquireVulkanTextureFrame() (*VulkanOwnedTextureFrame, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - session.mu.Lock() - defer session.mu.Unlock() - if session.frame { - return nil, newBindingError(ErrInvalidState, "texture frame is still acquired") - } - rawFrame := C.mln_vulkan_owned_texture_frame{size: C.uint32_t(unsafe.Sizeof(C.mln_vulkan_owned_texture_frame{}))} - if err := checkNative(func() int32 { - return int32(C.mln_vulkan_owned_texture_acquire_frame(C.mln_render_session(ptr), &rawFrame)) - }); err != nil { - return nil, err - } - session.frame = true - return &VulkanOwnedTextureFrame{ - info: VulkanOwnedTextureFrameInfo{ - Generation: uint64(rawFrame.generation), - Width: uint32(rawFrame.width), - Height: uint32(rawFrame.height), - ScaleFactor: float64(rawFrame.scale_factor), - Format: uint32(rawFrame.format), - Layout: uint32(rawFrame.layout), - }, - image: NativePointer(uintptr(rawFrame.image)), - imageView: NativePointer(uintptr(rawFrame.image_view)), - device: NativePointer(uintptr(rawFrame.device)), - state: &vulkanOwnedTextureFrameState{session: session, raw: rawFrame}, - }, nil -} - -// AcquireOpenGLTextureFrame acquires the latest OpenGL session-owned texture -// frame. While the frame is live, resize, render update, detach, readback, -// session close, and another frame acquire are invalid. -func (session *RenderSessionHandle) AcquireOpenGLTextureFrame() (*OpenGLOwnedTextureFrame, error) { - ptr, release, err := session.ptr() - if err != nil { - return nil, err - } - defer release() - defer session.state.KeepAlive() - defer session.parent.state.KeepAlive() - session.mu.Lock() - defer session.mu.Unlock() - if session.frame { - return nil, newBindingError(ErrInvalidState, "texture frame is still acquired") - } - rawFrame := C.mln_opengl_owned_texture_frame{size: C.uint32_t(unsafe.Sizeof(C.mln_opengl_owned_texture_frame{}))} - if err := checkNative(func() int32 { - return int32(C.mln_opengl_owned_texture_acquire_frame(C.mln_render_session(ptr), &rawFrame)) - }); err != nil { - return nil, err - } - session.frame = true - return &OpenGLOwnedTextureFrame{ - info: OpenGLOwnedTextureFrameInfo{ - Generation: uint64(rawFrame.generation), - Width: uint32(rawFrame.width), - Height: uint32(rawFrame.height), - ScaleFactor: float64(rawFrame.scale_factor), - InternalFormat: uint32(rawFrame.internal_format), - Format: uint32(rawFrame.format), - Type: uint32(rawFrame._type), - }, - texture: uint32(rawFrame.texture), - target: uint32(rawFrame.target), - state: &openglOwnedTextureFrameState{session: session, raw: rawFrame}, - }, nil -} - -// WithInfo passes copied Metal frame metadata after verifying the frame is live. -func (frame *MetalOwnedTextureFrame) WithInfo(fn func(MetalOwnedTextureFrameInfo) error) error { - if frame == nil || frame.state == nil { - return newBindingError(ErrInvalidArgument, "MetalOwnedTextureFrame is nil") - } - if fn == nil { - return newBindingError(ErrInvalidArgument, "MetalOwnedTextureFrame WithInfo callback is nil") - } - frame.state.mu.Lock() - if frame.state.closed { - frame.state.mu.Unlock() - return newBindingError(ErrInvalidState, "MetalOwnedTextureFrame is closed") - } - info := frame.info - frame.state.mu.Unlock() - return fn(info) -} - -// Texture returns the borrowed Metal texture while the frame remains live. -func (frame *MetalOwnedTextureFrame) Texture() (NativePointer, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "MetalOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "MetalOwnedTextureFrame is closed") - } - return frame.texture, nil -} - -// Device returns the borrowed Metal device while the frame remains live. -func (frame *MetalOwnedTextureFrame) Device() (NativePointer, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "MetalOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "MetalOwnedTextureFrame is closed") - } - return frame.device, nil -} - -// WithInfo passes copied Vulkan frame metadata after verifying the frame is live. -func (frame *VulkanOwnedTextureFrame) WithInfo(fn func(VulkanOwnedTextureFrameInfo) error) error { - if frame == nil || frame.state == nil { - return newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame is nil") - } - if fn == nil { - return newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame WithInfo callback is nil") - } - frame.state.mu.Lock() - if frame.state.closed { - frame.state.mu.Unlock() - return newBindingError(ErrInvalidState, "VulkanOwnedTextureFrame is closed") - } - info := frame.info - frame.state.mu.Unlock() - return fn(info) -} - -// Image returns the borrowed Vulkan image while the frame remains live. -func (frame *VulkanOwnedTextureFrame) Image() (NativePointer, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "VulkanOwnedTextureFrame is closed") - } - return frame.image, nil -} - -// ImageView returns the borrowed Vulkan image view while the frame remains live. -func (frame *VulkanOwnedTextureFrame) ImageView() (NativePointer, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "VulkanOwnedTextureFrame is closed") - } - return frame.imageView, nil -} - -// Device returns the borrowed Vulkan device while the frame remains live. -func (frame *VulkanOwnedTextureFrame) Device() (NativePointer, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "VulkanOwnedTextureFrame is closed") - } - return frame.device, nil -} - -// WithInfo passes copied OpenGL frame metadata after verifying the frame is live. -func (frame *OpenGLOwnedTextureFrame) WithInfo(fn func(OpenGLOwnedTextureFrameInfo) error) error { - if frame == nil || frame.state == nil { - return newBindingError(ErrInvalidArgument, "OpenGLOwnedTextureFrame is nil") - } - if fn == nil { - return newBindingError(ErrInvalidArgument, "OpenGLOwnedTextureFrame WithInfo callback is nil") - } - frame.state.mu.Lock() - if frame.state.closed { - frame.state.mu.Unlock() - return newBindingError(ErrInvalidState, "OpenGLOwnedTextureFrame is closed") - } - info := frame.info - frame.state.mu.Unlock() - return fn(info) -} - -// Texture returns the borrowed OpenGL texture object name while the frame -// remains live. -func (frame *OpenGLOwnedTextureFrame) Texture() (uint32, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "OpenGLOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "OpenGLOwnedTextureFrame is closed") - } - return frame.texture, nil -} - -// Target returns the borrowed OpenGL texture target while the frame remains -// live. -func (frame *OpenGLOwnedTextureFrame) Target() (uint32, error) { - if frame == nil || frame.state == nil { - return 0, newBindingError(ErrInvalidArgument, "OpenGLOwnedTextureFrame is nil") - } - frame.state.mu.Lock() - defer frame.state.mu.Unlock() - if frame.state.closed { - return 0, newBindingError(ErrInvalidState, "OpenGLOwnedTextureFrame is closed") - } - return frame.target, nil -} - -// Close releases this acquired Metal texture frame on the session owner thread. -// A second Close is a no-op after a successful release; failed releases remain -// retryable. -func (frame *MetalOwnedTextureFrame) Close() error { - if frame == nil || frame.state == nil || frame.state.session == nil { - return newBindingError(ErrInvalidArgument, "MetalOwnedTextureFrame is nil") - } - state := frame.state - state.mu.Lock() - defer state.mu.Unlock() - if state.closed { - return nil - } - ptr, release, err := state.session.ptr() - if err != nil { - return err - } - defer release() - defer state.session.state.KeepAlive() - defer state.session.parent.state.KeepAlive() - if err := checkNative(func() int32 { - return int32(C.mln_metal_owned_texture_release_frame(C.mln_render_session(ptr), &state.raw)) - }); err != nil { - return err - } - state.closed = true - state.session.markFrameReleased() - return nil -} - -// Close releases this acquired Vulkan texture frame on the session owner -// thread. A second Close is a no-op after a successful release; failed releases -// remain retryable. -func (frame *VulkanOwnedTextureFrame) Close() error { - if frame == nil || frame.state == nil || frame.state.session == nil { - return newBindingError(ErrInvalidArgument, "VulkanOwnedTextureFrame is nil") - } - state := frame.state - state.mu.Lock() - defer state.mu.Unlock() - if state.closed { - return nil - } - ptr, release, err := state.session.ptr() - if err != nil { - return err - } - defer release() - defer state.session.state.KeepAlive() - defer state.session.parent.state.KeepAlive() - if err := checkNative(func() int32 { - return int32(C.mln_vulkan_owned_texture_release_frame(C.mln_render_session(ptr), &state.raw)) - }); err != nil { - return err - } - state.closed = true - state.session.markFrameReleased() - return nil -} - -// Close releases this acquired OpenGL texture frame on the session owner -// thread. A second Close is a no-op after a successful release; failed releases -// remain retryable. -func (frame *OpenGLOwnedTextureFrame) Close() error { - if frame == nil || frame.state == nil || frame.state.session == nil { - return newBindingError(ErrInvalidArgument, "OpenGLOwnedTextureFrame is nil") - } - state := frame.state - state.mu.Lock() - defer state.mu.Unlock() - if state.closed { - return nil - } - ptr, release, err := state.session.ptr() - if err != nil { - return err - } - defer release() - defer state.session.state.KeepAlive() - defer state.session.parent.state.KeepAlive() - if err := checkNative(func() int32 { - return int32(C.mln_opengl_owned_texture_release_frame(C.mln_render_session(ptr), &state.raw)) - }); err != nil { - return err - } - state.closed = true - state.session.markFrameReleased() - return nil -} - -// Close destroys this render session. A successful close makes later calls -// no-ops. A failed close leaves the native handle live so callers can retry on -// the owner thread. -func (session *RenderSessionHandle) Close() error { - if session == nil || session.state == nil { - return newBindingError(ErrInvalidArgument, "RenderSessionHandle is nil") - } - defer session.parent.state.KeepAlive() - return session.withNoAcquiredFrame(func() error { - if err := checkNative(func() int32 { - return session.state.Close(func(native nativeRenderSession) int32 { - return destroyRenderSessionHandle(native) - }) - }); err != nil { - return err - } - session.parentChild.Release() - return nil - }) + return &RenderSessionHandle{state: state, parent: parent}, nil } diff --git a/bindings/go/render_cgo_helpers.go b/bindings/go/render_cgo_helpers.go deleted file mode 100644 index d99ed3e39..000000000 --- a/bindings/go/render_cgo_helpers.go +++ /dev/null @@ -1,112 +0,0 @@ -package maplibre - -/* -#include "maplibre_native_c.h" -#include "internal/cgo_shim.h" -*/ -import "C" - -import "unsafe" - -// Descriptor inspection helpers live outside _test.go because Go test files -// cannot import C directly. -type vulkanContextDescriptorFields struct { - Size uint32 - Instance uintptr - PhysicalDevice uintptr - Device uintptr - GraphicsQueue uintptr - GraphicsQueueFamilyIndex uint32 - GetInstanceProcAddr uintptr - GetDeviceProcAddr uintptr -} - -type wglContextDescriptorFields struct { - OpenGLSize uint32 - Platform uint32 - Ownership uint32 - WGLSize uint32 - DeviceContext uintptr - ShareContext uintptr - GetProcAddress uintptr -} - -type eglContextDescriptorFields struct { - OpenGLSize uint32 - Platform uint32 - Ownership uint32 - EGLSize uint32 - Display uintptr - Config uintptr - ShareContext uintptr - ClientAPI uint32 - GetProcAddress uintptr -} - -func testVulkanContextDescriptorFields(descriptor VulkanContextDescriptor) vulkanContextDescriptorFields { - raw := descriptor.toC() - return vulkanContextDescriptorFields{ - Size: uint32(raw.size), - Instance: uintptr(raw.instance), - PhysicalDevice: uintptr(raw.physical_device), - Device: uintptr(raw.device), - GraphicsQueue: uintptr(raw.graphics_queue), - GraphicsQueueFamilyIndex: uint32(raw.graphics_queue_family_index), - GetInstanceProcAddr: uintptr(raw.get_instance_proc_addr), - GetDeviceProcAddr: uintptr(raw.get_device_proc_addr), - } -} - -func testVulkanContextDescriptorSize() uintptr { - return unsafe.Sizeof(C.mln_vulkan_context_descriptor{}) -} - -func testWGLContextDescriptorFields(descriptor OpenGLContextDescriptor) wglContextDescriptorFields { - raw := descriptor.toC() - wgl := C.mln_go_opengl_context_wgl(&raw) - return wglContextDescriptorFields{ - OpenGLSize: uint32(raw.size), - Platform: uint32(raw.platform), - Ownership: uint32(raw.ownership), - WGLSize: uint32(wgl.size), - DeviceContext: uintptr(wgl.device_context), - ShareContext: uintptr(wgl.share_context), - GetProcAddress: uintptr(wgl.get_proc_address), - } -} - -func testEGLContextDescriptorFields(descriptor OpenGLContextDescriptor) eglContextDescriptorFields { - raw := descriptor.toC() - egl := C.mln_go_opengl_context_egl(&raw) - return eglContextDescriptorFields{ - OpenGLSize: uint32(raw.size), - Platform: uint32(raw.platform), - Ownership: uint32(raw.ownership), - EGLSize: uint32(egl.size), - Display: uintptr(egl.display), - Config: uintptr(egl.config), - ShareContext: uintptr(egl.share_context), - ClientAPI: uint32(egl.client_api), - GetProcAddress: uintptr(egl.get_proc_address), - } -} - -func testOpenGLContextDescriptorSize() uintptr { - return unsafe.Sizeof(C.mln_opengl_context_descriptor{}) -} - -func testWGLContextDescriptorSize() uintptr { - return unsafe.Sizeof(C.mln_wgl_context_descriptor{}) -} - -func testEGLContextDescriptorSize() uintptr { - return unsafe.Sizeof(C.mln_egl_context_descriptor{}) -} - -func testOpenGLContextPlatformWGL() uint32 { - return uint32(C.MLN_OPENGL_CONTEXT_PLATFORM_WGL) -} - -func testOpenGLContextPlatformEGL() uint32 { - return uint32(C.MLN_OPENGL_CONTEXT_PLATFORM_EGL) -} diff --git a/bindings/go/render_descriptor_test.go b/bindings/go/render_descriptor_test.go index d12203d7c..41d9bedfc 100644 --- a/bindings/go/render_descriptor_test.go +++ b/bindings/go/render_descriptor_test.go @@ -1,233 +1,14 @@ package maplibre -import ( - "errors" - "math" - "testing" -) +import "testing" -func TestRenderBackendAndOpenGLProviderMasks(t *testing.T) { - backends := RenderBackendMetal | RenderBackendVulkan | RenderBackendOpenGL | RenderBackendWebGPU | RenderBackendMask(1<<30) - if !backends.Has(RenderBackendOpenGL) || !backends.Has(RenderBackendWebGPU) { - t.Fatalf("backend mask %08x should include OpenGL and WebGPU", uint32(backends)) - } - if !backends.Has(RenderBackendMask(1 << 30)) { - t.Fatalf("backend mask should preserve unknown future bits") - } - - providers := OpenGLContextProviderWGL | OpenGLContextProviderEGL | OpenGLContextProviderWebGL | OpenGLContextProviderMask(1<<30) - if !providers.Has(OpenGLContextProviderWGL) || !providers.Has(OpenGLContextProviderEGL) || - !providers.Has(OpenGLContextProviderWebGL) { - t.Fatalf("provider mask %08x should include WGL, EGL, and WebGL", uint32(providers)) - } - if !providers.Has(OpenGLContextProviderMask(1 << 30)) { - t.Fatalf("provider mask should preserve unknown future bits") - } -} - -func TestVulkanContextDescriptorMaterialization(t *testing.T) { - descriptor := VulkanContextDescriptor{ - Instance: 0x101, - PhysicalDevice: 0x202, - Device: 0x303, - GraphicsQueue: 0x404, - GraphicsQueueFamilyIndex: 7, - GetInstanceProcAddr: 0x505, - GetDeviceProcAddr: 0x606, - } - - fields := testVulkanContextDescriptorFields(descriptor) - if fields.Size != uint32(testVulkanContextDescriptorSize()) { - t.Fatalf("size = %d, want %d", fields.Size, testVulkanContextDescriptorSize()) - } - if fields.Instance != uintptr(descriptor.Instance) || - fields.PhysicalDevice != uintptr(descriptor.PhysicalDevice) || - fields.Device != uintptr(descriptor.Device) || - fields.GraphicsQueue != uintptr(descriptor.GraphicsQueue) || - fields.GraphicsQueueFamilyIndex != descriptor.GraphicsQueueFamilyIndex || - fields.GetInstanceProcAddr != uintptr(descriptor.GetInstanceProcAddr) || - fields.GetDeviceProcAddr != uintptr(descriptor.GetDeviceProcAddr) { - t.Fatalf("Vulkan context descriptor fields = %#v", fields) - } -} - -func TestOpenGLContextDescriptorMaterialization(t *testing.T) { - wglDescriptor := OpenGLContextDescriptor{WGL: &WGLContextDescriptor{ - DeviceContext: 0x111, - ShareContext: 0x222, - GetProcAddress: 0x333, - }} - wgl := testWGLContextDescriptorFields(wglDescriptor) - if wgl.OpenGLSize != uint32(testOpenGLContextDescriptorSize()) { - t.Fatalf("OpenGL size = %d, want %d", wgl.OpenGLSize, testOpenGLContextDescriptorSize()) - } - if wgl.Platform != testOpenGLContextPlatformWGL() { - t.Fatalf("WGL platform = %d, want %d", wgl.Platform, testOpenGLContextPlatformWGL()) - } - if wgl.Ownership != uint32(OpenGLContextOwnershipShared) { - t.Fatalf("default WGL ownership = %d, want shared", wgl.Ownership) - } - if wgl.WGLSize != uint32(testWGLContextDescriptorSize()) || - wgl.DeviceContext != uintptr(wglDescriptor.WGL.DeviceContext) || - wgl.ShareContext != uintptr(wglDescriptor.WGL.ShareContext) || - wgl.GetProcAddress != uintptr(wglDescriptor.WGL.GetProcAddress) { - t.Fatalf("WGL context descriptor fields = %#v", wgl) - } - - eglDescriptor := OpenGLContextDescriptor{EGL: &EGLContextDescriptor{ - Display: 0x444, - Config: 0x555, - ShareContext: 0x666, - GetProcAddress: 0x777, - }} - egl := testEGLContextDescriptorFields(eglDescriptor) - if egl.OpenGLSize != uint32(testOpenGLContextDescriptorSize()) { - t.Fatalf("OpenGL size = %d, want %d", egl.OpenGLSize, testOpenGLContextDescriptorSize()) - } - if egl.Platform != testOpenGLContextPlatformEGL() { - t.Fatalf("EGL platform = %d, want %d", egl.Platform, testOpenGLContextPlatformEGL()) - } - if egl.Ownership != uint32(OpenGLContextOwnershipShared) { - t.Fatalf("default EGL ownership = %d, want shared", egl.Ownership) - } - if egl.EGLSize != uint32(testEGLContextDescriptorSize()) || - egl.Display != uintptr(eglDescriptor.EGL.Display) || - egl.Config != uintptr(eglDescriptor.EGL.Config) || - egl.ShareContext != uintptr(eglDescriptor.EGL.ShareContext) || - egl.ClientAPI != uint32(OpenGLClientAPIUnspecified) || - egl.GetProcAddress != uintptr(eglDescriptor.EGL.GetProcAddress) { - t.Fatalf("EGL context descriptor fields = %#v", egl) - } - - dedicated := testEGLContextDescriptorFields(OpenGLContextDescriptor{ - EGL: &EGLContextDescriptor{Display: 0x888, Config: 0x999, ClientAPI: OpenGLClientAPIGLES}, - Ownership: OpenGLContextOwnershipDedicated, - }) - if dedicated.Ownership != uint32(OpenGLContextOwnershipDedicated) || - dedicated.ClientAPI != uint32(OpenGLClientAPIGLES) || - dedicated.ShareContext != 0 { - t.Fatalf("dedicated EGL context descriptor fields = %#v", dedicated) +func TestRenderExtentPhysicalSizeRejectsInvalidScale(t *testing.T) { + if _, _, err := (RenderTargetExtent{Width: 1, Height: 1}).PhysicalSize(); err == nil { + t.Fatal("zero scale factor accepted") } } -func TestRenderDescriptorValidation(t *testing.T) { - for _, extent := range []RenderTargetExtent{ - {Width: 1, Height: 1, ScaleFactor: 0}, - {Width: 1, Height: 1, ScaleFactor: -1}, - {Width: 1, Height: 1, ScaleFactor: math.NaN()}, - {Width: 1, Height: 1, ScaleFactor: math.Inf(1)}, - } { - if err := extent.validate(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("extent.validate() error = %v, want ErrInvalidArgument", err) - } - } - if err := (RenderTargetExtent{Width: 1, Height: 1, ScaleFactor: 1}).validate(); err != nil { - t.Fatalf("valid extent.validate() error = %v", err) - } - - if err := (OpenGLContextDescriptor{}).validate(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("empty OpenGL context validate error = %v, want ErrInvalidArgument", err) - } - if err := (OpenGLContextDescriptor{WGL: &WGLContextDescriptor{}, EGL: &EGLContextDescriptor{}}).validate(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("ambiguous OpenGL context validate error = %v, want ErrInvalidArgument", err) - } - if err := (OpenGLContextDescriptor{WGL: &WGLContextDescriptor{}}).validate(); err != nil { - t.Fatalf("valid WGL context validate error = %v", err) - } - if err := (OpenGLContextDescriptor{EGL: &EGLContextDescriptor{}}).validate(); err != nil { - t.Fatalf("valid EGL context validate error = %v", err) - } -} - -func TestOpenGLFrameNilClose(t *testing.T) { - var nilFrame *OpenGLOwnedTextureFrame - if err := nilFrame.Close(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil OpenGLOwnedTextureFrame Close() error = %v, want ErrInvalidArgument", err) - } -} - -func TestTextureFrameWithInfoRejectsClosedFrames(t *testing.T) { - metal := &MetalOwnedTextureFrame{ - info: MetalOwnedTextureFrameInfo{Generation: 7}, - texture: NativePointer(0xabc), - device: NativePointer(0xdef), - state: &metalOwnedTextureFrameState{}, - } - if err := metal.WithInfo(func(info MetalOwnedTextureFrameInfo) error { - if info.Generation != 7 { - t.Fatalf("Metal info = %+v", info) - } - texture, err := metal.Texture() - if err != nil { - t.Fatalf("Metal Texture(): %v", err) - } - if texture != NativePointer(0xabc) { - t.Fatalf("Metal Texture() = %#x, want 0xabc", uintptr(texture)) - } - return nil - }); err != nil { - t.Fatalf("Metal WithInfo(): %v", err) - } - metal.state.closed = true - if err := metal.WithInfo(func(MetalOwnedTextureFrameInfo) error { return nil }); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Metal WithInfo() error = %v, want ErrInvalidState", err) - } - if _, err := metal.Texture(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Metal Texture() error = %v, want ErrInvalidState", err) - } - if _, err := metal.Device(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Metal Device() error = %v, want ErrInvalidState", err) - } - - vulkan := &VulkanOwnedTextureFrame{ - info: VulkanOwnedTextureFrameInfo{Generation: 8}, - image: NativePointer(0x111), - imageView: NativePointer(0x222), - device: NativePointer(0x333), - state: &vulkanOwnedTextureFrameState{}, - } - image, err := vulkan.Image() - if err != nil { - t.Fatalf("Vulkan Image(): %v", err) - } - if image != NativePointer(0x111) { - t.Fatalf("Vulkan Image() = %#x, want 0x111", uintptr(image)) - } - vulkan.state.closed = true - if err := vulkan.WithInfo(func(VulkanOwnedTextureFrameInfo) error { return nil }); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Vulkan WithInfo() error = %v, want ErrInvalidState", err) - } - if _, err := vulkan.Image(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Vulkan Image() error = %v, want ErrInvalidState", err) - } - if _, err := vulkan.ImageView(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Vulkan ImageView() error = %v, want ErrInvalidState", err) - } - if _, err := vulkan.Device(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed Vulkan Device() error = %v, want ErrInvalidState", err) - } - - opengl := &OpenGLOwnedTextureFrame{ - info: OpenGLOwnedTextureFrameInfo{Generation: 9}, - texture: 11, - target: 12, - state: &openglOwnedTextureFrameState{}, - } - texture, err := opengl.Texture() - if err != nil { - t.Fatalf("OpenGL Texture(): %v", err) - } - if texture != 11 { - t.Fatalf("OpenGL Texture() = %d, want 11", texture) - } - opengl.state.closed = true - if err := opengl.WithInfo(func(OpenGLOwnedTextureFrameInfo) error { return nil }); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed OpenGL WithInfo() error = %v, want ErrInvalidState", err) - } - if _, err := opengl.Texture(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed OpenGL Texture() error = %v, want ErrInvalidState", err) - } - if _, err := opengl.Target(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("closed OpenGL Target() error = %v, want ErrInvalidState", err) - } +func TestFrameBatchCloseIsIdempotent(t *testing.T) { + var batch *RenderFrameBatch + batch.Close() } diff --git a/bindings/go/render_metal_darwin_test.go b/bindings/go/render_metal_darwin_test.go new file mode 100644 index 000000000..8a70832ed --- /dev/null +++ b/bindings/go/render_metal_darwin_test.go @@ -0,0 +1,351 @@ +//go:build darwin && cgo + +package maplibre + +import ( + "context" + "errors" + stdruntime "runtime" + "testing" + "time" + + "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/testsupport" +) + +func awaitWithDeadline[T any](t *testing.T, future *Future[T]) T { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + value, err := future.Await(ctx) + if err != nil { + t.Fatalf("Await(): %v", err) + } + return value +} + +func awaitRenderedMetalFrame(t *testing.T, session *RenderSessionHandle) RenderFrameResult { + t.Helper() + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + snapshot, err := session.Snapshot() + if err != nil { + t.Fatalf("Snapshot(): %v", err) + } + token := snapshot.LatestDemandToken + 1 + if err := session.RequestFrame(FrameDemand{Token: token}); err != nil { + t.Fatalf("RequestFrame(): %v", err) + } + for time.Now().Before(deadline) { + batch, err := session.DrainFrameResults() + if errors.Is(err, ErrNotReady) { + time.Sleep(time.Millisecond) + continue + } + if err != nil { + t.Fatalf("DrainFrameResults(): %v", err) + } + results, err := batch.Results() + batch.Close() + if err != nil { + t.Fatalf("RenderFrameBatch.Results(): %v", err) + } + for _, result := range results { + if result.Token != token { + continue + } + if result.Disposition == RenderResultRendered { + return result + } + break + } + break + } + } + t.Fatal("Metal session did not render before the deadline") + return RenderFrameResult{} +} + +func awaitCallerDriverCompletion(t *testing.T, session *RenderSessionHandle, future *Future[struct{}]) { + t.Helper() + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + select { + case <-future.Done(): + awaitWithDeadline(t, future) + return + default: + } + if _, err := session.ServiceDriverWork(64); err != nil { + t.Fatalf("ServiceDriverWork(): %v", err) + } + stdruntime.Gosched() + } + t.Fatal("caller-driver operation did not complete before the deadline") +} + +func TestMetalOwnedTextureCompletionLifecycleDarwin(t *testing.T) { + if !SupportedRenderBackends().Has(RenderBackendMetal) { + t.Skip("Metal is not the configured render backend") + } + device := testsupport.DefaultMetalDevice() + if device == 0 { + t.Skip("the system default Metal device is unavailable") + } + deviceRetained := true + defer func() { + if deviceRetained { + testsupport.ReleaseMetalDevice(device) + } + }() + + runtime, err := NewRuntime() + if err != nil { + t.Fatalf("NewRuntime(): %v", err) + } + var m *MapHandle + var session *RenderSessionHandle + defer func() { + if session != nil { + _, _ = session.Abandon() + _ = session.Close() + } + if m != nil { + _ = m.Close() + } + if runtime != nil { + if teardown, err := runtime.Close(); err == nil { + awaitWithDeadline(t, teardown) + } + } + }() + + mapFuture, err := runtime.NewMapWithOptions(NewMapOptions(32, 16, 1)) + if err != nil { + t.Fatalf("NewMapWithOptions(): %v", err) + } + m = awaitWithDeadline(t, mapFuture) + + extent := RenderTargetExtent{Width: 32, Height: 16, ScaleFactor: 1} + options := NewRenderSessionAttachOptions() + options.RequestedTextureRingDepth = 2 + session, attach, err := m.AttachMetalOwnedTexture( + MetalOwnedTextureDescriptor{ + Extent: extent, + Context: MetalContextDescriptor{Device: NativePointer(device)}, + }, + options, + ) + if err != nil { + t.Fatalf("AttachMetalOwnedTexture(): %v", err) + } + if session == nil || attach == nil { + t.Fatal("AttachMetalOwnedTexture() did not publish both session and completion") + } + // A successful attach has retained its own device reference before return. + // Drop the caller's reference while asynchronous attachment is still pending. + testsupport.ReleaseMetalDevice(device) + deviceRetained = false + awaitWithDeadline(t, attach) + + capabilities, err := session.Capabilities() + if err != nil { + t.Fatalf("Capabilities(): %v", err) + } + wantCapabilities := RenderSessionCapabilityFrameAcquisition | + RenderSessionCapabilityReadback | + RenderSessionCapabilityConsumerSync + if capabilities.Driver != RenderDriverCoreWorker || capabilities.TextureRingDepth != 2 || + capabilities.Flags&wantCapabilities != wantCapabilities { + t.Fatalf("Capabilities() = %#v, want core worker and a two-slot acquirable/readable ring", capabilities) + } + + style, err := m.SetStyleJSON([]byte(minimalStyleJSON)) + if err != nil { + t.Fatalf("SetStyleJSON(): %v", err) + } + awaitWithDeadline(t, style) + barrier, err := runtime.Barrier() + if err != nil { + t.Fatalf("Runtime Barrier(): %v", err) + } + awaitWithDeadline(t, barrier) + + awaitRenderedMetalFrame(t, session) + frame, err := session.AcquireFrame() + if err != nil { + t.Fatalf("AcquireFrame(): %v", err) + } + metadata, err := frame.MetalTexture() + if err != nil { + t.Fatalf("MetalTexture(): %v", err) + } + if metadata.Width != 32 || metadata.Height != 16 || metadata.Texture == 0 || metadata.Device == 0 { + t.Fatalf("MetalTexture() = %#v, want a live 32x16 texture", metadata) + } + if err := frame.Release(GPUSync{Kind: GPUSyncCPUComplete}); err != nil { + t.Fatalf("AcquiredFrame.Release(): %v", err) + } + + readback, err := session.ReadPremultipliedRGBA8() + if err != nil { + t.Fatalf("ReadPremultipliedRGBA8(): %v", err) + } + image := awaitWithDeadline(t, readback) + if image.Info.Width != 32 || image.Info.Height != 16 || len(image.Data) != int(image.Info.ByteLength) { + t.Fatalf("ReadPremultipliedRGBA8() info = %#v, bytes = %d", image.Info, len(image.Data)) + } + + // Leave both old-size ring entries available. Resize must retire them so + // acquisition cannot return an older 32x16 frame ahead of the new one. + awaitRenderedMetalFrame(t, session) + awaitRenderedMetalFrame(t, session) + resized := RenderTargetExtent{Width: 48, Height: 24, ScaleFactor: 1} + resize, err := session.Resize(resized) + if err != nil { + t.Fatalf("Resize(): %v", err) + } + awaitWithDeadline(t, resize) + awaitRenderedMetalFrame(t, session) + resizedFrame, err := session.AcquireFrame() + if err != nil { + t.Fatalf("AcquireFrame() after resize: %v", err) + } + resizedMetadata, err := resizedFrame.MetalTexture() + if err != nil { + t.Fatalf("MetalTexture() after resize: %v", err) + } + if resizedMetadata.Width != 48 || resizedMetadata.Height != 24 { + t.Fatalf("MetalTexture() after resize = %#v, want 48x24", resizedMetadata) + } + if err := resizedFrame.Release(GPUSync{Kind: GPUSyncCPUComplete}); err != nil { + t.Fatalf("resized AcquiredFrame.Release(): %v", err) + } + + detach, err := session.Detach() + if err != nil { + t.Fatalf("Detach(): %v", err) + } + awaitWithDeadline(t, detach) + if err := session.Close(); err != nil { + t.Fatalf("RenderSessionHandle.Close(): %v", err) + } + session = nil + if err := m.Close(); err != nil { + t.Fatalf("MapHandle.Close(): %v", err) + } + m = nil + teardown, err := runtime.Close() + if err != nil { + t.Fatalf("RuntimeHandle.Close(): %v", err) + } + awaitWithDeadline(t, teardown) + runtime = nil +} + +func TestMetalCallerDriverServicesPublishedAttachingSessionDarwin(t *testing.T) { + if !SupportedRenderBackends().Has(RenderBackendMetal) { + t.Skip("Metal is not the configured render backend") + } + stdruntime.LockOSThread() + defer stdruntime.UnlockOSThread() + + device := testsupport.DefaultMetalDevice() + if device == 0 { + t.Skip("the system default Metal device is unavailable") + } + deviceRetained := true + defer func() { + if deviceRetained { + testsupport.ReleaseMetalDevice(device) + } + }() + + runtime, err := NewRuntime() + if err != nil { + t.Fatalf("NewRuntime(): %v", err) + } + var m *MapHandle + var session *RenderSessionHandle + defer func() { + if session != nil { + _, _ = session.Abandon() + _ = session.Close() + } + if m != nil { + _ = m.Close() + } + if runtime != nil { + if teardown, err := runtime.Close(); err == nil { + awaitWithDeadline(t, teardown) + } + } + }() + + mapFuture, err := runtime.NewMapWithOptions(NewMapOptions(32, 16, 1)) + if err != nil { + t.Fatalf("NewMapWithOptions(): %v", err) + } + m = awaitWithDeadline(t, mapFuture) + extent := RenderTargetExtent{Width: 32, Height: 16, ScaleFactor: 1} + options := NewRenderSessionAttachOptions() + options.Driver = RenderDriverCallerGraphicsThread + options.RequestedTextureRingDepth = 2 + session, attach, err := m.AttachMetalOwnedTexture( + MetalOwnedTextureDescriptor{ + Extent: extent, + Context: MetalContextDescriptor{Device: NativePointer(device)}, + }, + options, + ) + if err != nil { + t.Fatalf("AttachMetalOwnedTexture(): %v", err) + } + if session == nil || attach == nil { + t.Fatal("AttachMetalOwnedTexture() did not publish both session and completion") + } + // Caller-driver initialization has not run yet, but acceptance already + // retained the device independently of the descriptor owner. + testsupport.ReleaseMetalDevice(device) + deviceRetained = false + select { + case <-attach.Done(): + t.Fatal("caller-driver attach completed before the host serviced its published session") + default: + } + snapshot, err := session.Snapshot() + if err != nil { + t.Fatalf("Snapshot() while attaching: %v", err) + } + if snapshot.State != RenderSessionAttaching { + t.Fatalf("Snapshot().State = %v, want RenderSessionAttaching", snapshot.State) + } + awaitCallerDriverCompletion(t, session, attach) + + capabilities, err := session.Capabilities() + if err != nil { + t.Fatalf("Capabilities(): %v", err) + } + if capabilities.Driver != RenderDriverCallerGraphicsThread { + t.Fatalf("Capabilities().Driver = %v, want caller graphics thread", capabilities.Driver) + } + + detach, err := session.Detach() + if err != nil { + t.Fatalf("Detach(): %v", err) + } + awaitCallerDriverCompletion(t, session, detach) + if err := session.Close(); err != nil { + t.Fatalf("RenderSessionHandle.Close(): %v", err) + } + session = nil + if err := m.Close(); err != nil { + t.Fatalf("MapHandle.Close(): %v", err) + } + m = nil + teardown, err := runtime.Close() + if err != nil { + t.Fatalf("RuntimeHandle.Close(): %v", err) + } + awaitWithDeadline(t, teardown) + runtime = nil +} diff --git a/bindings/go/render_metal_testhook_darwin.go b/bindings/go/render_metal_testhook_darwin.go deleted file mode 100644 index 410b2d229..000000000 --- a/bindings/go/render_metal_testhook_darwin.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build darwin && cgo - -package maplibre - -/* -#cgo darwin LDFLAGS: -framework Metal -void* MTLCreateSystemDefaultDevice(void); -*/ -import "C" - -func defaultMetalDeviceForTest() uintptr { - return uintptr(C.MTLCreateSystemDefaultDevice()) -} diff --git a/bindings/go/render_session.go b/bindings/go/render_session.go new file mode 100644 index 000000000..9a32928e9 --- /dev/null +++ b/bindings/go/render_session.go @@ -0,0 +1,793 @@ +package maplibre + +/* +#include "maplibre_native_c.h" +*/ +import "C" + +import ( + "runtime" + "unsafe" +) + +func (m *MapHandle) startRenderAttach( + start func(C.mln_map, *C.mln_render_session, *C.mln_completion) int32, +) (*RenderSessionHandle, *Future[struct{}], error) { + ptr, err := m.ptr() + if err != nil { + return nil, nil, err + } + + var session C.mln_render_session + attach, err := startCompletion(func(completion *C.mln_completion) int32 { + return start(C.mln_map(ptr), &session, completion) + }, completionUnit) + if err != nil { + return nil, nil, err + } + if session == 0 { + return nil, nil, newBindingError(ErrInvalidState, "render attach did not return a session") + } + + result, err := newRenderSessionHandle(m, nativeRenderSession(session)) + if err != nil { + var abandoned C.mln_render_abandon_result + abandoned.size = C.uint32_t(unsafe.Sizeof(abandoned)) + _ = C.mln_render_session_abandon(session, &abandoned) + _ = C.mln_render_session_destroy(session) + return nil, nil, err + } + attach.retain(result) + return result, attach, nil +} + +func (m *MapHandle) AttachMetalSurface( + descriptor MetalSurfaceDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_metal_surface_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachVulkanSurface( + descriptor VulkanSurfaceDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_vulkan_surface_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachOpenGLSurface( + descriptor OpenGLSurfaceDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + if err := descriptor.Context.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + defer runtime.KeepAlive(descriptor) + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_opengl_surface_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachMetalOwnedTexture( + descriptor MetalOwnedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_metal_owned_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachMetalBorrowedTexture( + descriptor MetalBorrowedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_metal_borrowed_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachVulkanOwnedTexture( + descriptor VulkanOwnedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_vulkan_owned_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachVulkanBorrowedTexture( + descriptor VulkanBorrowedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_vulkan_borrowed_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachOpenGLOwnedTexture( + descriptor OpenGLOwnedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + if err := descriptor.Context.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + defer runtime.KeepAlive(descriptor) + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_opengl_owned_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachOpenGLBorrowedTexture( + descriptor OpenGLBorrowedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + if err := descriptor.Context.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + defer runtime.KeepAlive(descriptor) + return m.startRenderAttach(func(m C.mln_map, session *C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_opengl_borrowed_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachWebGPUSurface( + descriptor WebGPUSurfaceDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func( + m C.mln_map, + session *C.mln_render_session, + operation *C.mln_completion, + ) int32 { + return int32(C.mln_webgpu_surface_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachWebGPUOwnedTexture( + descriptor WebGPUOwnedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func( + m C.mln_map, + session *C.mln_render_session, + operation *C.mln_completion, + ) int32 { + return int32(C.mln_webgpu_owned_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (m *MapHandle) AttachWebGPUBorrowedTexture( + descriptor WebGPUBorrowedTextureDescriptor, + options RenderSessionAttachOptions, +) (*RenderSessionHandle, *Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, nil, err + } + rawDescriptor, rawOptions := descriptor.toC(), options.toC() + return m.startRenderAttach(func( + m C.mln_map, + session *C.mln_render_session, + operation *C.mln_completion, + ) int32 { + return int32(C.mln_webgpu_borrowed_texture_attach(m, &rawDescriptor, &rawOptions, session, operation)) + }) +} + +func (s *RenderSessionHandle) ptr() (nativeRenderSession, error) { + if s == nil || s.state == nil { + return 0, newBindingError(ErrInvalidArgument, "RenderSessionHandle is nil") + } + value, live := s.state.Handle() + if !live { + return 0, newBindingError(ErrInvalidArgument, "RenderSessionHandle is closed") + } + return value, nil +} + +func (s *RenderSessionHandle) startOperation( + start func(C.mln_render_session, *C.mln_completion) int32, +) (*Future[struct{}], error) { + return startRenderCompletion(s, start, completionUnit) +} + +func (s *RenderSessionHandle) Capabilities() (RenderSessionCapabilities, error) { + ptr, err := s.ptr() + if err != nil { + return RenderSessionCapabilities{}, err + } + + var raw C.mln_render_session_capabilities + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_render_session_get_capabilities(C.mln_render_session(ptr), &raw)) + }); err != nil { + return RenderSessionCapabilities{}, err + } + return RenderSessionCapabilities{ + Driver: RenderDriver(raw.driver), + TextureRingDepth: uint32(raw.texture_ring_depth), + Flags: RenderSessionCapability(raw.flags), + }, nil +} + +func (s *RenderSessionHandle) Snapshot() (RenderSessionSnapshot, error) { + ptr, err := s.ptr() + if err != nil { + return RenderSessionSnapshot{}, err + } + + var raw C.mln_render_session_snapshot + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_render_session_get_snapshot(C.mln_render_session(ptr), &raw)) + }); err != nil { + return RenderSessionSnapshot{}, err + } + return RenderSessionSnapshot{ + State: RenderSessionState(raw.state), + RawState: uint32(raw.state), + Driver: RenderDriver(raw.driver), + LatestResult: RenderResult(raw.latest_result), + Extent: RenderTargetExtent{ + Width: uint32(raw.extent.width), + Height: uint32(raw.extent.height), + ScaleFactor: float64(raw.extent.scale_factor), + }, + Generation: uint64(raw.generation), + MapUpdateGeneration: uint64(raw.map_update_generation), + RenderedUpdateGeneration: uint64(raw.rendered_update_generation), + ExtentGeneration: uint64(raw.extent_generation), + FrameGeneration: uint64(raw.frame_generation), + LatestDemandToken: uint64(raw.latest_demand_token), + PendingDemandCount: uint32(raw.pending_demand_count), + AcquiredFrameCount: uint32(raw.acquired_frame_count), + TargetReady: bool(raw.target_ready), + PendingChanges: bool(raw.pending_changes), + }, nil +} + +func (s *RenderSessionHandle) RequestFrame(demand FrameDemand) error { + ptr, err := s.ptr() + if err != nil { + return err + } + + raw := demand.toC() + return checkNative(func() int32 { + return int32(C.mln_render_session_request_frame(C.mln_render_session(ptr), &raw)) + }) +} + +func (s *RenderSessionHandle) ServiceDriverWork(maxWork int) (int, error) { + if maxWork < 0 { + return 0, newBindingError(ErrInvalidArgument, "max work is negative") + } + ptr, err := s.ptr() + if err != nil { + return 0, err + } + + var serviced C.size_t + err = checkNative(func() int32 { + return int32(C.mln_render_session_service_driver_work( + C.mln_render_session(ptr), + C.size_t(maxWork), + &serviced, + )) + }) + return int(serviced), err +} + +func (s *RenderSessionHandle) DrainFrameResults() (*RenderFrameBatch, error) { + ptr, err := s.ptr() + if err != nil { + return nil, err + } + + var batch C.mln_render_frame_batch + if err := checkNative(func() int32 { + return int32(C.mln_render_session_drain_frame_results( + C.mln_render_session(ptr), + &batch, + )) + }); err != nil { + return nil, err + } + return &RenderFrameBatch{handle: uint64(batch)}, nil +} + +func (b *RenderFrameBatch) Results() ([]RenderFrameResult, error) { + if b == nil { + return nil, newBindingError(ErrInvalidArgument, "RenderFrameBatch is nil") + } + b.mu.Lock() + defer b.mu.Unlock() + if b.closed { + return nil, newBindingError(ErrInvalidArgument, "RenderFrameBatch is closed") + } + + var count C.size_t + if err := checkNative(func() int32 { + return int32(C.mln_render_frame_batch_count(C.mln_render_frame_batch(b.handle), &count)) + }); err != nil { + return nil, err + } + results := make([]RenderFrameResult, int(count)) + for i := range results { + var raw C.mln_render_frame_result + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_render_frame_batch_get( + C.mln_render_frame_batch(b.handle), + C.size_t(i), + &raw, + )) + }); err != nil { + return nil, err + } + results[i] = frameResultFromC(raw) + } + return results, nil +} + +func (b *RenderFrameBatch) Close() { + if b == nil { + return + } + b.mu.Lock() + defer b.mu.Unlock() + if b.closed { + return + } + C.mln_render_frame_batch_release(C.mln_render_frame_batch(b.handle)) + b.closed = true + b.handle = 0 +} + +func (s *RenderSessionHandle) AcquireFrame() (*AcquiredFrame, error) { + ptr, err := s.ptr() + if err != nil { + return nil, err + } + + var frame C.mln_acquired_frame + if err := checkNative(func() int32 { + return int32(C.mln_render_session_acquire_frame(C.mln_render_session(ptr), &frame)) + }); err != nil { + return nil, err + } + if frame == 0 { + return nil, newBindingError(ErrInvalidState, "frame acquisition did not return a frame") + } + return &AcquiredFrame{handle: uint64(frame)}, nil +} + +func (f *AcquiredFrame) use(call func(C.mln_acquired_frame) error) error { + if f == nil { + return newBindingError(ErrInvalidArgument, "AcquiredFrame is nil") + } + f.mu.Lock() + defer f.mu.Unlock() + if f.closed { + return newBindingError(ErrInvalidArgument, "AcquiredFrame is closed") + } + return call(C.mln_acquired_frame(f.handle)) +} + +func (f *AcquiredFrame) Result() (RenderFrameResult, error) { + var result RenderFrameResult + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_render_frame_result + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_result(frame, &raw)) + }); err != nil { + return err + } + result = frameResultFromC(raw) + return nil + }) + return result, err +} + +func (f *AcquiredFrame) ProducerSync() (GPUSync, error) { + var sync GPUSync + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_gpu_sync + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_producer_sync(frame, &raw)) + }); err != nil { + return err + } + sync = GPUSync{ + Kind: GPUSyncKind(raw.kind), + Object: NativePointer(uintptr(raw.object)), + Value: uint64(raw.value), + } + return nil + }) + return sync, err +} + +func (f *AcquiredFrame) Release(sync GPUSync) error { + if f == nil { + return newBindingError(ErrInvalidArgument, "AcquiredFrame is nil") + } + f.mu.Lock() + defer f.mu.Unlock() + if f.closed { + return newBindingError(ErrInvalidArgument, "AcquiredFrame is closed") + } + + frame := C.mln_acquired_frame(f.handle) + rawSync := sync.toC() + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_release(&frame, &rawSync)) + }); err != nil { + return err + } + if frame != 0 { + return newBindingError(ErrInvalidState, "frame release did not consume the frame") + } + f.closed = true + f.handle = 0 + return nil +} + +func (f *AcquiredFrame) MetalTexture() (MetalOwnedTextureFrameInfo, error) { + var info MetalOwnedTextureFrameInfo + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_metal_owned_texture_frame + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_metal_texture(frame, &raw)) + }); err != nil { + return err + } + info = MetalOwnedTextureFrameInfo{ + Generation: uint64(raw.generation), + Width: uint32(raw.width), + Height: uint32(raw.height), + ScaleFactor: float64(raw.scale_factor), + FrameID: uint64(raw.frame_id), + Texture: NativePointer(uintptr(raw.texture)), + Device: NativePointer(uintptr(raw.device)), + PixelFormat: uint64(raw.pixel_format), + } + return nil + }) + return info, err +} + +func (f *AcquiredFrame) VulkanTexture() (VulkanOwnedTextureFrameInfo, error) { + var info VulkanOwnedTextureFrameInfo + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_vulkan_owned_texture_frame + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_vulkan_texture(frame, &raw)) + }); err != nil { + return err + } + info = VulkanOwnedTextureFrameInfo{ + Generation: uint64(raw.generation), + Width: uint32(raw.width), + Height: uint32(raw.height), + ScaleFactor: float64(raw.scale_factor), + FrameID: uint64(raw.frame_id), + Image: NativePointer(uintptr(raw.image)), + ImageView: NativePointer(uintptr(raw.image_view)), + Device: NativePointer(uintptr(raw.device)), + Format: uint32(raw.format), + Layout: uint32(raw.layout), + } + return nil + }) + return info, err +} + +func (f *AcquiredFrame) OpenGLTexture() (OpenGLOwnedTextureFrameInfo, error) { + var info OpenGLOwnedTextureFrameInfo + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_opengl_owned_texture_frame + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_opengl_texture(frame, &raw)) + }); err != nil { + return err + } + info = OpenGLOwnedTextureFrameInfo{ + Generation: uint64(raw.generation), + Width: uint32(raw.width), + Height: uint32(raw.height), + ScaleFactor: float64(raw.scale_factor), + FrameID: uint64(raw.frame_id), + Texture: uint32(raw.texture), + Target: uint32(raw.target), + InternalFormat: uint32(raw.internal_format), + Format: uint32(raw.format), + Type: uint32(raw._type), + } + return nil + }) + return info, err +} + +func (f *AcquiredFrame) WebGPUTexture() (WebGPUOwnedTextureFrameInfo, error) { + var info WebGPUOwnedTextureFrameInfo + err := f.use(func(frame C.mln_acquired_frame) error { + var raw C.mln_webgpu_owned_texture_frame + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_acquired_frame_get_webgpu_texture(frame, &raw)) + }); err != nil { + return err + } + info = WebGPUOwnedTextureFrameInfo{ + Generation: uint64(raw.generation), + Width: uint32(raw.width), + Height: uint32(raw.height), + ScaleFactor: float64(raw.scale_factor), + FrameID: uint64(raw.frame_id), + Texture: NativePointer(uintptr(raw.texture)), + TextureView: NativePointer(uintptr(raw.texture_view)), + Device: NativePointer(uintptr(raw.device)), + Format: uint32(raw.format), + } + return nil + }) + return info, err +} + +// Resize starts a render target extent update. +// +// The session keeps its renderer across a resize, along with the tile pyramid, +// glyph and image atlases, and symbol placement. A scale factor change retires +// the renderer instead, because shaders are compiled for one pixel ratio. +// Map-owned feature state survives either way. +func (s *RenderSessionHandle) Resize(extent RenderTargetExtent) (*Future[struct{}], error) { + if err := extent.validate(); err != nil { + return nil, err + } + raw := extent.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_resize(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) Barrier() (*Future[struct{}], error) { + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_barrier(session, operation)) + }) +} + +func (s *RenderSessionHandle) ReduceMemoryUse() (*Future[struct{}], error) { + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_reduce_memory_use(session, operation)) + }) +} + +func (s *RenderSessionHandle) ClearData() (*Future[struct{}], error) { + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_clear_data(session, operation)) + }) +} + +func (s *RenderSessionHandle) DumpDebugLogs() (*Future[struct{}], error) { + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_dump_debug_logs(session, operation)) + }) +} + +func (s *RenderSessionHandle) Detach() (*Future[struct{}], error) { + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_render_session_detach(session, operation)) + }) +} + +func (s *RenderSessionHandle) ReadPremultipliedRGBA8() (*Future[TextureReadback], error) { + return startRenderCompletion(s, func(session C.mln_render_session, completion *C.mln_completion) int32 { + return int32(C.mln_texture_read_premultiplied_rgba8(session, completion)) + }, func(result *C.mln_completion_result) (TextureReadback, error) { + raw, err := completionValue[C.mln_texture_readback_result](result) + if err != nil { + return TextureReadback{}, err + } + data, ok := goByteSlice(raw.data.data, raw.data.size) + if !ok { + return TextureReadback{}, newBindingError(ErrNative, "native texture readback is invalid") + } + return TextureReadback{Data: data, Info: textureImageInfoFromC(raw.info)}, nil + }) +} + +// Abandon irreversibly closes control and mailboxes without a graphics call. +// It waits for the map's in-flight tile work before returning, so no library +// thread touches the session's target or device afterward and the host may +// destroy its graphics objects immediately. Do not call it from a MapLibre +// worker callback. +func (s *RenderSessionHandle) Abandon() (RenderAbandonResult, error) { + ptr, err := s.ptr() + if err != nil { + return RenderAbandonResult{}, err + } + + var raw C.mln_render_abandon_result + raw.size = C.uint32_t(unsafe.Sizeof(raw)) + if err := checkNative(func() int32 { + return int32(C.mln_render_session_abandon(C.mln_render_session(ptr), &raw)) + }); err != nil { + return RenderAbandonResult{}, err + } + return RenderAbandonResult{ + Disposition: RenderAbandonDisposition(raw.disposition), + QuarantinedResourceCount: uint32(raw.quarantined_resource_count), + }, nil +} + +func (s *RenderSessionHandle) Close() error { + if s == nil || s.state == nil { + return newBindingError(ErrInvalidArgument, "RenderSessionHandle is nil") + } + s.closeMu.Lock() + defer s.closeMu.Unlock() + if err := checkNative(func() int32 { + return s.state.Close(destroyRenderSessionHandle) + }); err != nil { + return err + } + return nil +} + +func (s *RenderSessionHandle) SetOpenGLBorrowedTextureTarget( + descriptor OpenGLBorrowedTextureDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + if err := descriptor.Context.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + defer runtime.KeepAlive(descriptor) + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_opengl_borrowed_texture_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetOpenGLSurfaceTarget( + descriptor OpenGLSurfaceDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + if err := descriptor.Context.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + defer runtime.KeepAlive(descriptor) + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_opengl_surface_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetMetalSurfaceTarget( + descriptor MetalSurfaceDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_metal_surface_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetVulkanSurfaceTarget( + descriptor VulkanSurfaceDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_vulkan_surface_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetMetalBorrowedTextureTarget( + descriptor MetalBorrowedTextureDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_metal_borrowed_texture_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetVulkanBorrowedTextureTarget( + descriptor VulkanBorrowedTextureDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_vulkan_borrowed_texture_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetWebGPUSurfaceTarget( + descriptor WebGPUSurfaceDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_webgpu_surface_set_target(session, &raw, operation)) + }) +} + +func (s *RenderSessionHandle) SetWebGPUBorrowedTextureTarget( + descriptor WebGPUBorrowedTextureDescriptor, +) (*Future[struct{}], error) { + if err := descriptor.Extent.validate(); err != nil { + return nil, err + } + raw := descriptor.toC() + return s.startOperation(func(session C.mln_render_session, operation *C.mln_completion) int32 { + return int32(C.mln_webgpu_borrowed_texture_set_target(session, &raw, operation)) + }) +} diff --git a/bindings/go/render_set_target_darwin_test.go b/bindings/go/render_set_target_darwin_test.go deleted file mode 100644 index 0a1bb8fb2..000000000 --- a/bindings/go/render_set_target_darwin_test.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build darwin && cgo - -package maplibre - -import ( - "errors" - "testing" -) - -// A session-owned texture session is the only session this suite can create, so -// it covers only the target kinds SetTarget rejects; replacing a host-owned -// target would need a host-allocated surface or texture. -func TestSetTargetRejectsOtherTargetKindsDarwin(t *testing.T) { - if !SupportedRenderBackends().Has(RenderBackendMetal) { - t.Skip("Metal texture sessions are not supported by this build") - } - - lockOSThreadForTest(t) - - device := defaultMetalDeviceForTest() - if device == 0 { - t.Skip("Metal system default device is unavailable") - } - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(64, 64, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - extent := RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1} - session, err := m.AttachMetalOwnedTexture(MetalOwnedTextureDescriptor{ - Extent: extent, - Context: MetalContextDescriptor{Device: NativePointer(device)}, - }) - if err != nil { - _ = m.Close() - _ = runtime.Close() - t.Fatalf("AttachMetalOwnedTexture(): %v", err) - } - defer func() { - if err := session.Close(); err != nil { - t.Errorf("RenderSession Close(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - if err := session.SetMetalSurfaceTarget(MetalSurfaceDescriptor{ - Extent: extent, - Context: MetalContextDescriptor{Device: NativePointer(device)}, - }); !errors.Is(err, ErrUnsupported) { - t.Fatalf("SetMetalSurfaceTarget() on a texture session error = %v, want ErrUnsupported", err) - } - if err := session.SetMetalBorrowedTextureTarget(MetalBorrowedTextureDescriptor{ - Extent: extent, - PhysicalWidth: 64, - PhysicalHeight: 64, - }); !errors.Is(err, ErrUnsupported) { - t.Fatalf("SetMetalBorrowedTextureTarget() on a session-owned texture session error = %v, want ErrUnsupported", err) - } - - // Both rejections leave the session rendering into the target it has, which - // holds no style yet. - update, err := session.RenderUpdate() - if err != nil { - t.Fatalf("RenderUpdate() after rejected target replacement: %v", err) - } - if update.Result != RenderResultNoUpdate { - t.Fatalf("RenderUpdate() after rejected target replacement = %v, want RenderResultNoUpdate", update.Result) - } - if update.NeedsRepaint { - t.Fatal("RenderUpdate() NeedsRepaint = true, want false when Result is not RenderResultRendered") - } -} diff --git a/bindings/go/render_test.go b/bindings/go/render_test.go deleted file mode 100644 index a70e93518..000000000 --- a/bindings/go/render_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package maplibre - -import ( - "errors" - "sync/atomic" - "testing" - - "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" -) - -func TestRenderSessionNilHandleAndInvalidSurfaceDescriptor(t *testing.T) { - lockOSThreadForTest(t) - - var nilSession *RenderSessionHandle - if err := nilSession.Close(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil RenderSessionHandle Close() error = %v, want ErrInvalidArgument", err) - } - if _, err := nilSession.RenderUpdate(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil RenderSessionHandle RenderUpdate() error = %v, want ErrInvalidArgument", err) - } - if _, err := nilSession.ReadPremultipliedRGBA8Into(nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil RenderSessionHandle ReadPremultipliedRGBA8Into() error = %v, want ErrInvalidArgument", err) - } - var nilMetalFrame *MetalOwnedTextureFrame - if err := nilMetalFrame.Close(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil MetalOwnedTextureFrame Close() error = %v, want ErrInvalidArgument", err) - } - var nilVulkanFrame *VulkanOwnedTextureFrame - if err := nilVulkanFrame.Close(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil VulkanOwnedTextureFrame Close() error = %v, want ErrInvalidArgument", err) - } - var nilOpenGLFrame *OpenGLOwnedTextureFrame - if err := nilOpenGLFrame.Close(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("nil OpenGLOwnedTextureFrame Close() error = %v, want ErrInvalidArgument", err) - } - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(64, 64, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - defer func() { - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - _, err = m.AttachMetalSurface(MetalSurfaceDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrUnsupported) { - t.Fatalf("AttachMetalSurface(invalid descriptor) error = %v, want ErrInvalidArgument or ErrUnsupported", err) - } - _, err = m.AttachMetalOwnedTexture(MetalOwnedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrUnsupported) { - t.Fatalf("AttachMetalOwnedTexture(invalid descriptor) error = %v, want ErrInvalidArgument or ErrUnsupported", err) - } - _, err = m.AttachMetalBorrowedTexture(MetalBorrowedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrUnsupported) { - t.Fatalf("AttachMetalBorrowedTexture(invalid descriptor) error = %v, want ErrInvalidArgument or ErrUnsupported", err) - } - _, err = m.AttachVulkanOwnedTexture(VulkanOwnedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrUnsupported) { - t.Fatalf("AttachVulkanOwnedTexture(invalid descriptor) error = %v, want ErrInvalidArgument or ErrUnsupported", err) - } - _, err = m.AttachVulkanBorrowedTexture(VulkanBorrowedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrUnsupported) { - t.Fatalf("AttachVulkanBorrowedTexture(invalid descriptor) error = %v, want ErrInvalidArgument or ErrUnsupported", err) - } - _, err = m.AttachOpenGLSurface(OpenGLSurfaceDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AttachOpenGLSurface(invalid descriptor) error = %v, want ErrInvalidArgument", err) - } - _, err = m.AttachOpenGLOwnedTexture(OpenGLOwnedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AttachOpenGLOwnedTexture(invalid descriptor) error = %v, want ErrInvalidArgument", err) - } - _, err = m.AttachOpenGLBorrowedTexture(OpenGLBorrowedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - }) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AttachOpenGLBorrowedTexture(invalid descriptor) error = %v, want ErrInvalidArgument", err) - } -} - -func TestMapCloseFailsWhileRenderSessionIsLive(t *testing.T) { - mapState, err := handle.New(nativeMap(0x0200_0000_0000_002a), "MapHandle") - if err != nil { - t.Fatal(err) - } - m := &MapHandle{state: mapState} - session, err := newRenderSessionHandle(m, nativeRenderSession(0x0400_0000_0000_002a)) - if err != nil { - t.Fatalf("newRenderSessionHandle(): %v", err) - } - - oldDestroyMap := destroyMapHandle - oldDestroySession := destroyRenderSessionHandle - defer func() { - destroyMapHandle = oldDestroyMap - destroyRenderSessionHandle = oldDestroySession - }() - var mapDestroyCalls atomic.Int32 - var sessionDestroyCalls atomic.Int32 - destroyMapHandle = func(nativeMap) int32 { - mapDestroyCalls.Add(1) - return 0 - } - destroyRenderSessionHandle = func(nativeRenderSession) int32 { - sessionDestroyCalls.Add(1) - return 0 - } - - if err := m.Close(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("Map Close() with live render session error = %v, want ErrInvalidState", err) - } - if got := mapDestroyCalls.Load(); got != 0 { - t.Fatalf("map destroy calls before session close = %d, want 0", got) - } - if err := session.Close(); err != nil { - t.Fatalf("RenderSession Close(): %v", err) - } - if err := m.Close(); err != nil { - t.Fatalf("Map Close() after render session close: %v", err) - } - if got := sessionDestroyCalls.Load(); got != 1 { - t.Fatalf("render session destroy calls = %d, want 1", got) - } - if got := mapDestroyCalls.Load(); got != 1 { - t.Fatalf("map destroy calls = %d, want 1", got) - } -} - -func TestMapCloseSucceedsAfterRenderSessionDetach(t *testing.T) { - mapState, err := handle.New(nativeMap(0x0200_0000_0000_002a), "MapHandle") - if err != nil { - t.Fatal(err) - } - m := &MapHandle{state: mapState} - session, err := newRenderSessionHandle(m, nativeRenderSession(0x0400_0000_0000_002a)) - if err != nil { - t.Fatalf("newRenderSessionHandle(): %v", err) - } - - oldDestroyMap := destroyMapHandle - oldDestroySession := destroyRenderSessionHandle - oldDetachSession := detachRenderSessionHandle - defer func() { - destroyMapHandle = oldDestroyMap - destroyRenderSessionHandle = oldDestroySession - detachRenderSessionHandle = oldDetachSession - }() - var mapDestroyCalls atomic.Int32 - var sessionDestroyCalls atomic.Int32 - var detachCalls atomic.Int32 - destroyMapHandle = func(nativeMap) int32 { - mapDestroyCalls.Add(1) - return 0 - } - destroyRenderSessionHandle = func(nativeRenderSession) int32 { - sessionDestroyCalls.Add(1) - return 0 - } - detachRenderSessionHandle = func(nativeRenderSession) int32 { - detachCalls.Add(1) - return 0 - } - - if err := session.Detach(); err != nil { - t.Fatalf("RenderSession Detach(): %v", err) - } - // Detaching releases the map, so the map closes while the session is open. - if err := m.Close(); err != nil { - t.Fatalf("Map Close() after detach: %v", err) - } - if got := mapDestroyCalls.Load(); got != 1 { - t.Fatalf("map destroy calls after detach = %d, want 1", got) - } - if err := session.Close(); err != nil { - t.Fatalf("RenderSession Close() after map close: %v", err) - } - if got := sessionDestroyCalls.Load(); got != 1 { - t.Fatalf("render session destroy calls = %d, want 1", got) - } - if got := detachCalls.Load(); got != 1 { - t.Fatalf("detach calls = %d, want 1", got) - } -} - -func newActiveFrameTestSession(t *testing.T) *RenderSessionHandle { - t.Helper() - - mapState, err := handle.New(nativeMap(0x0200_0000_0000_002a), "MapHandle") - if err != nil { - t.Fatal(err) - } - m := &MapHandle{state: mapState} - session, err := newRenderSessionHandle(m, nativeRenderSession(0x0400_0000_0000_002a)) - if err != nil { - t.Fatalf("newRenderSessionHandle(): %v", err) - } - session.frame = true - - oldDestroyMap := destroyMapHandle - oldDestroySession := destroyRenderSessionHandle - destroyMapHandle = func(nativeMap) int32 { return 0 } - destroyRenderSessionHandle = func(nativeRenderSession) int32 { return 0 } - t.Cleanup(func() { - session.frame = false - if err := session.Close(); err != nil { - t.Errorf("RenderSession Close(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - destroyMapHandle = oldDestroyMap - destroyRenderSessionHandle = oldDestroySession - }) - - return session -} - -// activeFrameTestExtent is a valid extent, so the active-frame guard is what -// rejects each operation rather than descriptor validation ahead of it. -var activeFrameTestExtent = RenderTargetExtent{Width: 1, Height: 1, ScaleFactor: 1} - -func TestRenderSessionOperationsRejectActiveFrame(t *testing.T) { - session := newActiveFrameTestSession(t) - - for _, tt := range []struct { - name string - call func() error - }{ - {name: "Resize", call: func() error { return session.Resize(RenderTargetExtent{Width: 1, Height: 1, ScaleFactor: 1}) }}, - {name: "SetMetalSurfaceTarget", call: func() error { - return session.SetMetalSurfaceTarget(MetalSurfaceDescriptor{Extent: activeFrameTestExtent}) - }}, - {name: "SetVulkanSurfaceTarget", call: func() error { - return session.SetVulkanSurfaceTarget(VulkanSurfaceDescriptor{Extent: activeFrameTestExtent}) - }}, - {name: "SetOpenGLSurfaceTarget", call: func() error { - return session.SetOpenGLSurfaceTarget(OpenGLSurfaceDescriptor{ - Extent: activeFrameTestExtent, - Context: OpenGLContextDescriptor{EGL: &EGLContextDescriptor{}}, - }) - }}, - {name: "SetMetalBorrowedTextureTarget", call: func() error { - return session.SetMetalBorrowedTextureTarget(MetalBorrowedTextureDescriptor{ - Extent: activeFrameTestExtent, - PhysicalWidth: 1, - PhysicalHeight: 1, - }) - }}, - {name: "SetVulkanBorrowedTextureTarget", call: func() error { - return session.SetVulkanBorrowedTextureTarget(VulkanBorrowedTextureDescriptor{ - Extent: activeFrameTestExtent, - PhysicalWidth: 1, - PhysicalHeight: 1, - }) - }}, - {name: "SetOpenGLBorrowedTextureTarget", call: func() error { - return session.SetOpenGLBorrowedTextureTarget(OpenGLBorrowedTextureDescriptor{ - Extent: activeFrameTestExtent, - PhysicalWidth: 1, - PhysicalHeight: 1, - Context: OpenGLContextDescriptor{EGL: &EGLContextDescriptor{}}, - }) - }}, - {name: "RenderUpdate", call: func() error { - _, err := session.RenderUpdate() - return err - }}, - {name: "Detach", call: session.Detach}, - {name: "ReduceMemoryUse", call: session.ReduceMemoryUse}, - {name: "ClearData", call: session.ClearData}, - {name: "DumpDebugLogs", call: session.DumpDebugLogs}, - {name: "ReadPremultipliedRGBA8", call: func() error { - _, _, err := session.ReadPremultipliedRGBA8() - return err - }}, - {name: "ReadPremultipliedRGBA8Into", call: func() error { - _, err := session.ReadPremultipliedRGBA8Into(make([]byte, 4)) - return err - }}, - {name: "Close", call: session.Close}, - } { - if err := tt.call(); !errors.Is(err, ErrInvalidState) { - t.Fatalf("%s() error = %v, want ErrInvalidState", tt.name, err) - } - } -} diff --git a/bindings/go/render_wrong_thread_darwin_test.go b/bindings/go/render_wrong_thread_darwin_test.go deleted file mode 100644 index 2396359ae..000000000 --- a/bindings/go/render_wrong_thread_darwin_test.go +++ /dev/null @@ -1,62 +0,0 @@ -//go:build darwin && cgo - -package maplibre - -import ( - "errors" - stdruntime "runtime" - "testing" -) - -func TestRenderSessionWrongThreadReturnsWrongThreadDarwin(t *testing.T) { - if !SupportedRenderBackends().Has(RenderBackendMetal) { - t.Skip("Metal texture sessions are not supported by this build") - } - - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - - device := defaultMetalDeviceForTest() - if device == 0 { - t.Skip("Metal system default device is unavailable") - } - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - m, err := runtime.NewMapWithOptions(NewMapOptions(64, 64, 1)) - if err != nil { - _ = runtime.Close() - t.Fatalf("NewMapWithOptions(): %v", err) - } - session, err := m.AttachMetalOwnedTexture(MetalOwnedTextureDescriptor{ - Extent: RenderTargetExtent{Width: 64, Height: 64, ScaleFactor: 1}, - Context: MetalContextDescriptor{Device: NativePointer(device)}, - }) - if err != nil { - _ = m.Close() - _ = runtime.Close() - t.Fatalf("AttachMetalOwnedTexture(): %v", err) - } - defer func() { - if err := session.Close(); err != nil { - t.Errorf("RenderSession Close(): %v", err) - } - if err := m.Close(); err != nil { - t.Errorf("Map Close(): %v", err) - } - if err := runtime.Close(); err != nil { - t.Errorf("Runtime Close(): %v", err) - } - }() - - errCh := make(chan error, 1) - go func() { - _, err := session.RenderUpdate() - errCh <- err - }() - if err := <-errCh; !errors.Is(err, ErrWrongThread) { - t.Fatalf("RenderUpdate() from another thread error = %v, want ErrWrongThread", err) - } -} diff --git a/bindings/go/resource_test.go b/bindings/go/resource_test.go index 3e45b2257..d16f76137 100644 --- a/bindings/go/resource_test.go +++ b/bindings/go/resource_test.go @@ -21,14 +21,12 @@ func countingResourceProvider(calls *atomic.Int64) ResourceProviderCallback { // event naming that URL proves the request reached the network file source. func loadProbeStyle(t *testing.T, runtime *RuntimeHandle, m *MapHandle, styleURL string) { t.Helper() - if err := m.SetStyleURL(styleURL); err != nil { + if _, err := m.SetStyleURL(styleURL); err != nil { t.Fatalf("SetStyleURL(%q): %v", styleURL, err) } for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } @@ -43,28 +41,26 @@ func loadProbeStyle(t *testing.T, runtime *RuntimeHandle, m *MapHandle, styleURL } func TestRuntimeResourceProviderInstallsReplacesAndClears(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() var firstCalls, secondCalls atomic.Int64 - if err := runtime.SetResourceProvider(countingResourceProvider(&firstCalls)); err != nil { + if _, err := runtime.SetResourceProvider(countingResourceProvider(&firstCalls)); err != nil { t.Fatalf("SetResourceProvider(): %v", err) } loadProbeStyle(t, runtime, m, "jar:file:/packaged/first.json") @@ -72,7 +68,7 @@ func TestRuntimeResourceProviderInstallsReplacesAndClears(t *testing.T) { t.Fatalf("installed provider calls = %d, want at least 1", got) } - if err := runtime.SetResourceProvider(countingResourceProvider(&secondCalls)); err != nil { + if _, err := runtime.SetResourceProvider(countingResourceProvider(&secondCalls)); err != nil { t.Fatalf("SetResourceProvider(replace): %v", err) } firstCallsAfterReplace := firstCalls.Load() @@ -84,7 +80,7 @@ func TestRuntimeResourceProviderInstallsReplacesAndClears(t *testing.T) { t.Fatalf("replaced provider calls = %d, want %d", got, firstCallsAfterReplace) } - if err := runtime.ClearResourceProvider(); err != nil { + if _, err := runtime.ClearResourceProvider(); err != nil { t.Fatalf("ClearResourceProvider(): %v", err) } secondCallsAfterClear := secondCalls.Load() @@ -96,7 +92,7 @@ func TestRuntimeResourceProviderInstallsReplacesAndClears(t *testing.T) { t.Fatalf("cleared provider calls = %d, want %d", got, secondCallsAfterClear) } - if err := runtime.ClearResourceProvider(); err != nil { + if _, err := runtime.ClearResourceProvider(); err != nil { t.Fatalf("second ClearResourceProvider(): %v", err) } } @@ -105,8 +101,6 @@ func TestRuntimeResourceProviderInstallsReplacesAndClears(t *testing.T) { // reaches the provider as the alias, alongside the HTTPS URL the built-in // network path would have fetched. func TestResourceProviderSeesSchemeAliasAndItsResolvedURL(t *testing.T) { - lockOSThreadForTest(t) - const emptyStyle = `{"version":8,"sources":{},"layers":[]}` var resolvedURL atomic.Value @@ -114,7 +108,7 @@ func TestResourceProviderSeesSchemeAliasAndItsResolvedURL(t *testing.T) { if err != nil { t.Fatalf("NewRuntime(): %v", err) } - if err := runtime.SetResourceProvider(func(request ResourceRequest, handle *ResourceRequestHandle) ResourceProviderDecision { + if _, err := runtime.SetResourceProvider(func(request ResourceRequest, handle *ResourceRequestHandle) ResourceProviderDecision { if request.RequestedURL != "maplibre://maps/style" { return ResourceProviderDecisionPassThrough } @@ -124,24 +118,24 @@ func TestResourceProviderSeesSchemeAliasAndItsResolvedURL(t *testing.T) { } return ResourceProviderDecisionHandle }); err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("SetResourceProvider(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleURL("maplibre://maps/style"); err != nil { + if _, err := m.SetStyleURL("maplibre://maps/style"); err != nil { t.Fatalf("SetStyleURL(): %v", err) } waitForRuntimeEvent(t, runtime, RuntimeEventMapStyleLoaded) @@ -197,69 +191,63 @@ func TestResourceResponseAcceptsKnownEnums(t *testing.T) { } func TestRuntimeResourceProviderRejectsNilCallback(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() - if err := runtime.SetResourceProvider(nil); !errors.Is(err, ErrInvalidArgument) { + if _, err := runtime.SetResourceProvider(nil); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("SetResourceProvider(nil) error = %v, want ErrInvalidArgument", err) } } func TestRuntimeResourceTransformLifecycle(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - if err := runtime.SetResourceTransform(func(request ResourceTransformRequest) (string, bool) { + if _, err := runtime.SetResourceTransform(func(request ResourceTransformRequest) (string, bool) { return request.URL + "?first", true }); err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("SetResourceTransform(): %v", err) } - if err := runtime.SetResourceTransform(func(request ResourceTransformRequest) (string, bool) { + if _, err := runtime.SetResourceTransform(func(request ResourceTransformRequest) (string, bool) { return "", false }); err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("SetResourceTransform(replace): %v", err) } - if err := runtime.ClearResourceTransform(); err != nil { - _ = runtime.Close() + if _, err := runtime.ClearResourceTransform(); err != nil { + _ = closeRuntimeForTest(runtime) t.Fatalf("ClearResourceTransform(): %v", err) } - if err := runtime.ClearResourceTransform(); err != nil { - _ = runtime.Close() + if _, err := runtime.ClearResourceTransform(); err != nil { + _ = closeRuntimeForTest(runtime) t.Fatalf("second ClearResourceTransform(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Fatalf("Close(): %v", err) } } func TestRuntimeResourceTransformRejectsNilCallback(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() - if err := runtime.SetResourceTransform(nil); !errors.Is(err, ErrInvalidArgument) { + if _, err := runtime.SetResourceTransform(nil); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("SetResourceTransform(nil) error = %v, want ErrInvalidArgument", err) } } diff --git a/bindings/go/runtime.go b/bindings/go/runtime.go index 5db0bd94a..b68dcf294 100644 --- a/bindings/go/runtime.go +++ b/bindings/go/runtime.go @@ -9,9 +9,7 @@ import "C" import ( "errors" - stdruntime "runtime" "sync" - "time" "unsafe" "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/callback" @@ -37,123 +35,6 @@ const ( AmbientCacheOperationClear AmbientCacheOperation = AmbientCacheOperation(C.MLN_AMBIENT_CACHE_OPERATION_CLEAR) ) -// OfflineOperationKind identifies a native offline operation kind. -type OfflineOperationKind uint32 - -const ( - OfflineOperationAmbientCache OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_AMBIENT_CACHE) - OfflineOperationRegionCreate OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_CREATE) - OfflineOperationRegionGet OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_GET) - OfflineOperationRegionsList OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGIONS_LIST) - OfflineOperationRegionsMergeDatabase OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE) - OfflineOperationRegionUpdateMetadata OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA) - OfflineOperationRegionGetStatus OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_GET_STATUS) - OfflineOperationRegionSetObserved OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED) - OfflineOperationRegionSetDownloadState OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE) - OfflineOperationRegionInvalidate OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_INVALIDATE) - OfflineOperationRegionDelete OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_REGION_DELETE) - OfflineOperationSetMaximumAmbientCacheSize OfflineOperationKind = OfflineOperationKind(C.MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE) -) - -// OfflineOperationResultKind identifies the expected result shape for an -// offline operation. -type OfflineOperationResultKind uint32 - -const ( - OfflineOperationResultNone OfflineOperationResultKind = OfflineOperationResultKind(C.MLN_OFFLINE_OPERATION_RESULT_NONE) - OfflineOperationResultRegion OfflineOperationResultKind = OfflineOperationResultKind(C.MLN_OFFLINE_OPERATION_RESULT_REGION) - OfflineOperationResultOptionalRegion OfflineOperationResultKind = OfflineOperationResultKind(C.MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION) - OfflineOperationResultRegionList OfflineOperationResultKind = OfflineOperationResultKind(C.MLN_OFFLINE_OPERATION_RESULT_REGION_LIST) - OfflineOperationResultRegionStatus OfflineOperationResultKind = OfflineOperationResultKind(C.MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS) -) - -// OfflineOperationHandle owns a runtime-scoped offline operation token. -type OfflineOperationHandle[T any] struct { - runtime *RuntimeHandle - child *handle.Child - id uint64 - kind OfflineOperationKind - resultKind OfflineOperationResultKind - mu sync.Mutex - live bool - discarded bool -} - -func newOfflineOperationHandle[T any](runtime *RuntimeHandle, id uint64, kind OfflineOperationKind, resultKind OfflineOperationResultKind) *OfflineOperationHandle[T] { - var child *handle.Child - if runtime != nil && runtime.state != nil { - child = runtime.state.AddChild() - } - return &OfflineOperationHandle[T]{runtime: runtime, child: child, id: id, kind: kind, resultKind: resultKind, live: true} -} - -// ID returns the native offline operation ID. -func (operation *OfflineOperationHandle[T]) ID() uint64 { - if operation == nil { - return 0 - } - operation.mu.Lock() - defer operation.mu.Unlock() - return operation.id -} - -// Kind returns the native offline operation kind. -func (operation *OfflineOperationHandle[T]) Kind() OfflineOperationKind { - if operation == nil { - return 0 - } - operation.mu.Lock() - defer operation.mu.Unlock() - return operation.kind -} - -// ResultKind returns the expected native result shape. -func (operation *OfflineOperationHandle[T]) ResultKind() OfflineOperationResultKind { - if operation == nil { - return 0 - } - operation.mu.Lock() - defer operation.mu.Unlock() - return operation.resultKind -} - -// Discard drops runtime-owned state for this operation. The operation remains -// retryable when native discard fails. -func (operation *OfflineOperationHandle[T]) Discard() error { - if operation == nil || operation.runtime == nil { - return newBindingError(ErrInvalidArgument, "OfflineOperationHandle is nil") - } - operation.mu.Lock() - if !operation.live { - discarded := operation.discarded - operation.mu.Unlock() - if discarded { - return nil - } - return newBindingError(ErrInvalidArgument, "OfflineOperationHandle is closed") - } - id := operation.id - - ptr, release, err := operation.runtime.ptr() - if err != nil { - operation.mu.Unlock() - return err - } - defer release() - defer operation.runtime.state.KeepAlive() - if err := checkNative(func() int32 { return offlineOperationDiscard(ptr, id) }); err != nil { - operation.mu.Unlock() - return err - } - operation.live = false - operation.discarded = true - child := operation.child - operation.child = nil - operation.mu.Unlock() - child.Release() - return nil -} - // RuntimeOptions configures runtime creation. type RuntimeOptions struct { AssetPath string @@ -231,13 +112,11 @@ const ( RuntimeEventOfflineRegionStatusChanged RuntimeEventType = RuntimeEventType(C.MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED) RuntimeEventOfflineRegionResponseError RuntimeEventType = RuntimeEventType(C.MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR) RuntimeEventOfflineRegionTileCountLimitExceeded RuntimeEventType = RuntimeEventType(C.MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED) - RuntimeEventOfflineOperationCompleted RuntimeEventType = RuntimeEventType(C.MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED) RuntimeEventMapCameraTransitionFinished RuntimeEventType = RuntimeEventType(C.MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED) ) // RuntimeEventMask selects which event types a map or a runtime queues. An event -// whose type is unselected is never built and never queued, so it neither -// reaches a batch nor raises the runtime's wake flag. +// whose type is unselected is never built, queued, or reported ready. // // Every bit value comes from the C API, so a mask cannot drift from the // RuntimeEventType constants. Masks combine with the bitwise operators: | adds @@ -269,7 +148,6 @@ const ( RuntimeEventMaskOfflineRegionStatusChanged RuntimeEventMask = RuntimeEventMask(C.MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED) RuntimeEventMaskOfflineRegionResponseError RuntimeEventMask = RuntimeEventMask(C.MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR) RuntimeEventMaskOfflineRegionTileCountLimitExceeded RuntimeEventMask = RuntimeEventMask(C.MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED) - RuntimeEventMaskOfflineOperationCompleted RuntimeEventMask = RuntimeEventMask(C.MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED) // RuntimeEventMaskAllMapEvents selects every map-originated event type this // binding version defines. RuntimeEventMaskAllMapEvents RuntimeEventMask = RuntimeEventMask(C.MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS) @@ -305,7 +183,6 @@ const ( RuntimeEventPayloadOfflineRegionStatus RuntimeEventPayloadType = RuntimeEventPayloadType(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS) RuntimeEventPayloadOfflineRegionResponseError RuntimeEventPayloadType = RuntimeEventPayloadType(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR) RuntimeEventPayloadOfflineRegionTileCountLimit RuntimeEventPayloadType = RuntimeEventPayloadType(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT) - RuntimeEventPayloadOfflineOperationCompleted RuntimeEventPayloadType = RuntimeEventPayloadType(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED) RuntimeEventPayloadCameraTransitionFinished RuntimeEventPayloadType = RuntimeEventPayloadType(C.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED) ) @@ -335,9 +212,8 @@ type RuntimeEvent struct { Source RuntimeEventSource // Code is a secondary event detail whose meaning Type selects: a // CameraChangeMode for the camera-will-change and camera-did-change events, - // the ordinal of MapLibre Native's internal map load error kind for - // map-loading-failed, and the result status for - // offline-operation-completed. Every other event type reports 0. + // or the ordinal of MapLibre Native's internal map load error kind for + // map-loading-failed. Every other event type reports 0. Code int32 PayloadType RuntimeEventPayloadType // Message is the event's text: a failure description, a missing style image @@ -356,9 +232,6 @@ type RuntimeEvent struct { // the next drain. type RuntimeEventBatch struct { Events []RuntimeEvent - // RemainingCount is the number of events still queued after this batch. A - // nonzero value means another drain reports more. - RemainingCount uint64 } // CameraChangeMode reports whether a camera change belongs to an animated @@ -446,6 +319,16 @@ type RuntimeEventCameraTransitionFinishedPayload struct { TransitionID uint64 } +// CommandDisposition identifies a command's terminal disposition. +type CommandDisposition uint32 + +const ( + CommandDispositionCommitted CommandDisposition = CommandDisposition(C.MLN_COMMAND_DISPOSITION_COMMITTED) + CommandDispositionSuperseded CommandDisposition = CommandDisposition(C.MLN_COMMAND_DISPOSITION_SUPERSEDED) + CommandDispositionFailed CommandDisposition = CommandDisposition(C.MLN_COMMAND_DISPOSITION_FAILED) + CommandDispositionCancelled CommandDisposition = CommandDisposition(C.MLN_COMMAND_DISPOSITION_CANCELLED) +) + // RuntimeEventOfflineRegionStatusPayload is a copied offline status event payload. type RuntimeEventOfflineRegionStatusPayload struct { RegionID OfflineRegionID @@ -467,16 +350,6 @@ type RuntimeEventOfflineRegionTileCountLimitPayload struct { Limit uint64 } -// RuntimeEventOfflineOperationCompletedPayload is a copied offline operation -// completion event payload. -type RuntimeEventOfflineOperationCompletedPayload struct { - OperationID uint64 - OperationKind OfflineOperationKind - ResultKind OfflineOperationResultKind - ResultStatus int32 - Found bool -} - // RuntimeEventUnknownPayload contains copied bytes for a payload type unknown to // this Go binding version. Bytes is the event's whole payload window, which is // the batch's event stride minus this binding's payload offset. @@ -484,27 +357,23 @@ type RuntimeEventUnknownPayload struct { Bytes []byte } -// RuntimeHandle owns scheduler state and event storage for one owner thread. +// RuntimeHandle owns autonomous scheduler state and event storage. type RuntimeHandle struct { state *handle.State[nativeRuntime] - resourceTransformMu sync.Mutex - resourceTransform *callback.ResourceTransformState - httpHeaderTransformMu sync.Mutex - httpHeaderTransform *callback.HttpHeaderTransformState - resourceProviderMu sync.Mutex - resourceProvider *callback.ResourceProviderState - mapsMu sync.Mutex + mapsMu sync.Mutex // Resolves an event's source id to the public wrapper. maps map[MapID]*MapHandle } -var destroyRuntimeHandle = func(native nativeRuntime) int32 { - return int32(C.mln_runtime_destroy(C.mln_runtime(native))) -} - -var offlineOperationDiscard = func(ptr nativeRuntime, id uint64) int32 { - return int32(C.mln_runtime_offline_operation_discard(C.mln_runtime(ptr), C.mln_offline_operation_id(id))) +func statusFromError(err error) int32 { + var native *Error + if errors.As(err, &native) { + if status, ok := native.RawStatus(); ok { + return status + } + } + return int32(C.MLN_STATUS_NATIVE_ERROR) } // String returns a diagnostic name for the status. @@ -550,216 +419,98 @@ func rawNetworkStatusForSet(status NetworkStatus) (uint32, error) { } } -// NewRuntime creates a runtime on the current OS thread using native defaults. +// NewRuntime creates a runtime using native defaults. Native worker threads +// make progress independently of the calling goroutine. func NewRuntime() (*RuntimeHandle, error) { - return createRuntime(CVersion(), func(out *nativeRuntime) int32 { - var raw C.mln_runtime - status := int32(C.mln_runtime_create(nil, &raw)) - if status == int32(C.MLN_STATUS_OK) { - *out = nativeRuntime(raw) - } - return status - }) + return NewRuntimeWithOptions(NewRuntimeOptions("", "")) } -// NewRuntimeWithOptions creates a runtime on the current OS thread using -// explicit options. Start from NewRuntimeOptions to keep every event type -// selected; a zero-value RuntimeOptions queues no event. +// NewRuntimeWithOptions creates a runtime using explicit options. Start from +// NewRuntimeOptions to keep every event type selected; a zero-value +// RuntimeOptions queues no event. func NewRuntimeWithOptions(options RuntimeOptions) (*RuntimeHandle, error) { if err := options.validate(); err != nil { return nil, err } - return createRuntime(CVersion(), func(out *nativeRuntime) int32 { - rawOptions := C.mln_runtime_options_default() - assetPath := C.CString(options.AssetPath) - defer C.free(unsafe.Pointer(assetPath)) - cachePath := C.CString(options.CachePath) - defer C.free(unsafe.Pointer(cachePath)) - rawOptions.asset_path = assetPath - rawOptions.cache_path = cachePath - rawOptions.event_mask = C.uint64_t(options.EventMask) - - var raw C.mln_runtime - status := int32(C.mln_runtime_create(&rawOptions, &raw)) - if status == int32(C.MLN_STATUS_OK) { - *out = nativeRuntime(raw) - } - return status - }) -} - -type runtimeStateFactory func(nativeRuntime) (*handle.State[nativeRuntime], error) - -func createRuntime(actualCABI uint32, create func(*nativeRuntime) int32) (*RuntimeHandle, error) { - return createRuntimeWithStateFactory(actualCABI, create, newRuntimeState) -} - -func createRuntimeWithStateFactory(actualCABI uint32, create func(*nativeRuntime) int32, newState runtimeStateFactory) (*RuntimeHandle, error) { - if err := checkCompatibleCABI(actualCABI); err != nil { + if err := checkCompatibleCABI(CVersion()); err != nil { return nil, err } + rawOptions := C.mln_runtime_options_default() + assetPath := C.CString(options.AssetPath) + defer C.free(unsafe.Pointer(assetPath)) + cachePath := C.CString(options.CachePath) + defer C.free(unsafe.Pointer(cachePath)) + rawOptions.asset_path = assetPath + rawOptions.cache_path = cachePath + rawOptions.event_mask = C.uint64_t(options.EventMask) - var runtime nativeRuntime - if err := checkNative(func() int32 { return create(&runtime) }); err != nil { + var raw C.mln_runtime + if err := checkNative(func() int32 { + return int32(C.mln_runtime_create(&rawOptions, &raw)) + }); err != nil { return nil, err } - state, err := newState(runtime) + state, err := newRuntimeState(nativeRuntime(raw)) if err != nil { + _ = closeNativeRuntime(nativeRuntime(raw)) return nil, newBindingError(ErrInvalidArgument, err.Error()) } return &RuntimeHandle{state: state}, nil } -func newRuntimeState(runtime nativeRuntime) (*handle.State[nativeRuntime], error) { - return handle.New(runtime, "RuntimeHandle") +// closeNativeRuntime releases a native runtime whose Go wrapper never became +// visible, so no caller can await its teardown future. +func closeNativeRuntime(runtime nativeRuntime) error { + _, err := startNativeRuntimeRelease(runtime) + return err } -func (runtime *RuntimeHandle) ptr() (nativeRuntime, func(), error) { - if runtime == nil || runtime.state == nil { - return 0, nil, newBindingError(ErrInvalidArgument, "RuntimeHandle is nil") - } - borrow, live := runtime.state.Borrow() - if !live { - return 0, nil, newBindingError(ErrInvalidArgument, "RuntimeHandle is closed") - } - return borrow.Handle(), borrow.Release, nil -} - -// Pump advances this runtime. It parks the owner thread when timeout allows, -// then drains the owner-thread task queues, including tasks the drained ones -// enqueue. Take the queued runtime events with DrainEvents afterwards. -// -// timeout sets the park bound: zero drains and returns, a positive value parks -// for up to that long, and a negative value parks until a wake arrives. A -// parking call returns as soon as the runtime's wake flag is set and clears it, -// and returns without parking while unread runtime events are queued. Timers -// and ready file descriptors set the flag only when they queue owner-thread -// work, so pass a bounded timeout to cap how long a call waits. -// -// budget bounds the drain: a negative value drains without a bound, and zero -// or a positive value stops the drain at the first task boundary after that -// long, measured from the start of the drain. The first queued task always -// runs, so a bounded pump always makes progress, and tasks left behind set the -// wake flag so the next Pump returns without parking and continues them. The -// budget bounds the task queues alone: expired timers and ready file -// descriptors are serviced regardless, and a task runs to completion once -// started, so one long task can overrun the budget. -// -// A non-zero timeout blocks the calling goroutine and its OS thread. Call it -// outside any lock that a goroutine signalling a WakeSource takes. -func (runtime *RuntimeHandle) Pump(timeout time.Duration, budget time.Duration) error { - ptr, release, err := runtime.ptr() - if err != nil { - return err - } - defer release() - defer runtime.state.KeepAlive() - - timeoutMS := int64(-1) - if timeout >= 0 { - timeoutMS = int64(timeout / time.Millisecond) - } - budgetMS := int64(-1) - if budget >= 0 { - budgetMS = int64(budget / time.Millisecond) - } - return checkNative(func() int32 { - return int32(C.mln_runtime_pump( - C.mln_runtime(ptr), C.int64_t(timeoutMS), C.int64_t(budgetMS), - )) - }) +// startNativeRuntimeRelease releases a native runtime and returns the future +// that completes after its teardown finishes. +func startNativeRuntimeRelease(runtime nativeRuntime) (*Future[struct{}], error) { + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_runtime_release(C.mln_runtime(runtime), completion)) + }, completionUnit) } -// WakeSource acquires a wake source that releases this runtime's parked owner -// thread. The returned source is usable from any goroutine, and the caller -// closes it. -func (runtime *RuntimeHandle) WakeSource() (*WakeSource, error) { - ptr, release, err := runtime.ptr() - if err != nil { - return nil, err - } - defer release() - defer runtime.state.KeepAlive() - - var raw C.mln_wake_source - if err := checkNative(func() int32 { - return int32(C.mln_runtime_wake_source_acquire(C.mln_runtime(ptr), &raw)) - }); err != nil { - return nil, err - } - state, err := handle.New(nativeWakeSource(raw), "WakeSource") - if err != nil { - C.mln_wake_source_destroy(raw) - return nil, newBindingError(ErrInvalidArgument, err.Error()) - } - source := &WakeSource{state: state} - stdruntime.SetFinalizer(source, func(source *WakeSource) { source.Close() }) - return source, nil -} - -// WakeSource releases a runtime owner thread parked in RuntimeHandle.Pump. It -// is usable from any goroutine. Signalling it after its runtime closes does -// nothing. -type WakeSource struct { - state *handle.State[nativeWakeSource] -} - -var destroyWakeSource = func(native nativeWakeSource) int32 { - C.mln_wake_source_destroy(C.mln_wake_source(native)) - return int32(C.MLN_STATUS_OK) +func newRuntimeState(runtime nativeRuntime) (*handle.State[nativeRuntime], error) { + return handle.New(runtime, "RuntimeHandle") } -// Signal sets the runtime's wake flag and releases the parked owner thread. A -// signal raised while the owner thread runs leaves the flag set, so the next -// Pump returns without parking. -func (source *WakeSource) Signal() error { - if source == nil || source.state == nil { - return newBindingError(ErrInvalidArgument, "WakeSource is nil") +func (runtime *RuntimeHandle) ptr() (nativeRuntime, error) { + if runtime == nil || runtime.state == nil { + return 0, newBindingError(ErrInvalidArgument, "RuntimeHandle is nil") } - borrow, live := source.state.Borrow() + value, live := runtime.state.Handle() if !live { - return newBindingError(ErrInvalidArgument, "WakeSource is closed") + return 0, newBindingError(ErrInvalidArgument, "RuntimeHandle is closed") } - defer borrow.Release() - defer source.state.KeepAlive() - return checkNative(func() int32 { - return int32(C.mln_wake_source_signal(C.mln_wake_source(borrow.Handle()))) - }) + return value, nil } -// Close releases the wake source. Later signals report a closed handle. -func (source *WakeSource) Close() { - if source == nil || source.state == nil { - return - } - source.state.Close(destroyWakeSource) +// Barrier starts an ordered runtime operation that completes after every +// command accepted before it reaches a terminal disposition. +func (runtime *RuntimeHandle) Barrier() (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_barrier(handle, out)) + }, completionUnit) } -// DrainEvents takes this runtime's queued events as one batch of copied values. -// Events arrive in queue order, and the batch reports how many events stayed -// queued. -// -// maxEvents bounds the drain: zero takes every queued event, and a positive -// value takes at most that many and leaves the rest queued. A negative value -// returns ErrInvalidArgument. -// -// Call Pump first to advance the runtime, then drain the events that pump -// produced. -func (runtime *RuntimeHandle) DrainEvents(maxEvents int) (RuntimeEventBatch, error) { - if maxEvents < 0 { - return RuntimeEventBatch{}, newBindingError(ErrInvalidArgument, "maxEvents is negative") - } - ptr, release, err := runtime.ptr() +// DrainEvents takes this runtime's whole event queue as one batch of copied +// values in queue order. +func (runtime *RuntimeHandle) DrainEvents() (RuntimeEventBatch, error) { + ptr, err := runtime.ptr() if err != nil { return RuntimeEventBatch{}, err } - defer release() + defer runtime.state.KeepAlive() - rawBatch, err := drainRawEvents(ptr, maxEvents) + rawBatch, ownedBatch, err := drainRawEvents(ptr) if err != nil { return RuntimeEventBatch{}, err } + defer C.mln_event_batch_release(ownedBatch) return runtime.copyEventBatch(rawBatch), nil } @@ -769,15 +520,13 @@ func (runtime *RuntimeHandle) DrainEvents(maxEvents int) (RuntimeEventBatch, err // outside RuntimeEventMaskAll. // // Narrowing gates later events and keeps queued ones, so a caller drains what it -// already caused. An offline operation records its result before this mask is -// consulted, so the matching take-result call reports the result of an operation -// whose completion event this mask cleared. +// already caused. func (runtime *RuntimeHandle) SetEventMask(mask RuntimeEventMask) error { - ptr, release, err := runtime.ptr() + ptr, err := runtime.ptr() if err != nil { return err } - defer release() + defer runtime.state.KeepAlive() return checkNative(func() int32 { @@ -788,11 +537,11 @@ func (runtime *RuntimeHandle) SetEventMask(mask RuntimeEventMask) error { // EventMask reports which runtime-originated event types this runtime queues. A // runtime that has not been narrowed reports RuntimeEventMaskAll. func (runtime *RuntimeHandle) EventMask() (RuntimeEventMask, error) { - ptr, release, err := runtime.ptr() + ptr, err := runtime.ptr() if err != nil { return 0, err } - defer release() + defer runtime.state.KeepAlive() var raw C.uint64_t @@ -804,14 +553,19 @@ func (runtime *RuntimeHandle) EventMask() (RuntimeEventMask, error) { return RuntimeEventMask(raw), nil } -func drainRawEvents(ptr nativeRuntime, maxEvents int) (C.mln_runtime_event_batch, error) { - batch := C.mln_runtime_event_batch_default() +func drainRawEvents(ptr nativeRuntime) (C.mln_runtime_event_batch_view, C.mln_event_batch, error) { + var batch C.mln_event_batch if err := checkNative(func() int32 { - return int32(C.mln_runtime_drain_events(C.mln_runtime(ptr), C.size_t(maxEvents), &batch)) + return int32(C.mln_runtime_drain_events(C.mln_runtime(ptr), &batch)) }); err != nil { - return C.mln_runtime_event_batch{}, err + return C.mln_runtime_event_batch_view{}, 0, err + } + view := C.mln_runtime_event_batch_view{size: C.uint32_t(C.sizeof_mln_runtime_event_batch_view)} + if err := checkNative(func() int32 { return int32(C.mln_event_batch_get(batch, &view)) }); err != nil { + C.mln_event_batch_release(batch) + return C.mln_runtime_event_batch_view{}, 0, err } - return batch, nil + return view, batch, nil } // runtimeEventPayloadOffset is where the payload union starts inside an event @@ -819,10 +573,10 @@ func drainRawEvents(ptr nativeRuntime, maxEvents int) (C.mln_runtime_event_batch // batch's event stride minus this offset is the payload window. var runtimeEventPayloadOffset = unsafe.Offsetof(C.mln_runtime_event{}.payload) -// copyEventBatch copies a borrowed native batch into owned Go values. It takes -// the map registry lock once for the whole batch. -func (runtime *RuntimeHandle) copyEventBatch(raw C.mln_runtime_event_batch) RuntimeEventBatch { - batch := RuntimeEventBatch{RemainingCount: uint64(raw.remaining_count)} +// copyEventBatch copies an owned native batch view into owned Go values. It +// takes the map registry lock once for the whole batch. +func (runtime *RuntimeHandle) copyEventBatch(raw C.mln_runtime_event_batch_view) RuntimeEventBatch { + batch := RuntimeEventBatch{} count := int(raw.event_count) if count <= 0 || raw.events == nil { return batch @@ -850,15 +604,17 @@ func (runtime *RuntimeHandle) copyEventBatch(raw C.mln_runtime_event_batch) Runt source.MapID = sourceMap.id } } - events = append(events, RuntimeEvent{ + payload := runtimeEventPayloadFromC(rawEvent, unsafe.Add(eventPtr, runtimeEventPayloadOffset), payloadWindow) + event := RuntimeEvent{ Type: RuntimeEventType(rawEvent._type), SourceType: source.Type, Source: source, Code: int32(rawEvent.code), PayloadType: RuntimeEventPayloadType(rawEvent.payload_type), Message: runtimeEventMessage(raw, rawEvent), - Payload: runtimeEventPayloadFromC(rawEvent, unsafe.Add(eventPtr, runtimeEventPayloadOffset), payloadWindow), - }) + Payload: payload, + } + events = append(events, event) } runtime.mapsMu.Unlock() @@ -866,7 +622,7 @@ func (runtime *RuntimeHandle) copyEventBatch(raw C.mln_runtime_event_batch) Runt return batch } -func runtimeEventMessage(batch C.mln_runtime_event_batch, event *C.mln_runtime_event) string { +func runtimeEventMessage(batch C.mln_runtime_event_batch_view, event *C.mln_runtime_event) string { if event.message_size == 0 || batch.messages == nil { return "" } @@ -938,15 +694,6 @@ func runtimeEventPayloadFromC(event *C.mln_runtime_event, window unsafe.Pointer, case uint32(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT): payload := C.mln_go_runtime_event_offline_region_tile_count_limit(event) return RuntimeEventOfflineRegionTileCountLimitPayload{RegionID: OfflineRegionID(payload.region_id), Limit: uint64(payload.limit)} - case uint32(C.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED): - payload := C.mln_go_runtime_event_offline_operation_completed(event) - return RuntimeEventOfflineOperationCompletedPayload{ - OperationID: uint64(payload.operation_id), - OperationKind: OfflineOperationKind(payload.operation_kind), - ResultKind: OfflineOperationResultKind(payload.result_kind), - ResultStatus: int32(payload.result_status), - Found: bool(payload.found), - } default: bytes, ok := goByteSlice(window, C.size_t(windowSize)) if !ok { @@ -991,71 +738,40 @@ func offlineRegionStatusFromC(status C.mln_offline_region_status) OfflineRegionS } } -// StartAmbientCacheOperation starts a native ambient cache maintenance +// AmbientCacheOperation starts a native ambient cache maintenance // operation. -func (runtime *RuntimeHandle) StartAmbientCacheOperation(operation AmbientCacheOperation) (*OfflineOperationHandle[struct{}], error) { - ptr, release, err := runtime.ptr() - if err != nil { - return nil, err - } - defer release() - defer runtime.state.KeepAlive() - - var id C.mln_offline_operation_id - if err := checkNative(func() int32 { - return int32(C.mln_runtime_run_ambient_cache_operation_start(C.mln_runtime(ptr), C.uint32_t(operation), &id)) - }); err != nil { - return nil, err - } - if id == 0 { - return nil, newBindingError(ErrInvalidState, "ambient cache operation did not return an ID") - } - return newOfflineOperationHandle[struct{}](runtime, uint64(id), OfflineOperationAmbientCache, OfflineOperationResultNone), nil +func (runtime *RuntimeHandle) AmbientCacheOperation(operation AmbientCacheOperation) (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_run_ambient_cache_operation(handle, C.uint32_t(operation), out)) + }, completionUnit) } -// StartSetMaximumAmbientCacheSize starts a change to this runtime's maximum +// SetMaximumAmbientCacheSize starts a change to this runtime's maximum // ambient cache size. Lowering it evicts ambient resources to fit the new // budget; offline regions are unaffected. -func (runtime *RuntimeHandle) StartSetMaximumAmbientCacheSize(size uint64) (*OfflineOperationHandle[struct{}], error) { - ptr, release, err := runtime.ptr() - if err != nil { - return nil, err - } - defer release() - defer runtime.state.KeepAlive() - - var id C.mln_offline_operation_id - if err := checkNative(func() int32 { - return int32(C.mln_runtime_set_maximum_ambient_cache_size_start(C.mln_runtime(ptr), C.uint64_t(size), &id)) - }); err != nil { - return nil, err - } - if id == 0 { - return nil, newBindingError(ErrInvalidState, "maximum ambient cache size operation did not return an ID") - } - return newOfflineOperationHandle[struct{}](runtime, uint64(id), OfflineOperationSetMaximumAmbientCacheSize, OfflineOperationResultNone), nil +func (runtime *RuntimeHandle) SetMaximumAmbientCacheSize(size uint64) (*Future[struct{}], error) { + return startRuntimeCompletion(runtime, func(handle C.mln_runtime, out *C.mln_completion) int32 { + return int32(C.mln_runtime_set_maximum_ambient_cache_size(handle, C.uint64_t(size), out)) + }, completionUnit) } -// SetResourceProvider installs or replaces the runtime-scoped network resource -// provider, and may be called while maps are live. Native code may invoke the -// provider on worker or network threads, so callbacks must be thread-safe and -// must not call MapLibre map or runtime APIs. Once this call returns, a -// replaced provider is no longer invoked; requests it already took a handle for -// keep that handle, so complete or close each one as usual. -func (runtime *RuntimeHandle) SetResourceProvider(provider ResourceProviderCallback) error { +// SetResourceProvider submits a runtime-scoped network resource provider. +// Native code may invoke the provider on worker or network threads. The +// callback must be thread-safe and must not call map or runtime APIs. The +// The returned future resolves after the provider change becomes terminal. +func (runtime *RuntimeHandle) SetResourceProvider(provider ResourceProviderCallback) (*Future[CommandCompletion], error) { if provider == nil { - return newBindingError(ErrInvalidArgument, "ResourceProviderCallback is nil") + return nil, newBindingError(ErrInvalidArgument, "ResourceProviderCallback is nil") } - ptr, release, err := runtime.ptr() + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() + defer runtime.state.KeepAlive() - var replacement *callback.ResourceProviderState - if err := checkNative(func() int32 { - state, status := callback.SetResourceProvider(uint64(ptr), func(request callback.ResourceRequest, handle *callback.ResourceRequestHandle) uint32 { + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.SetResourceProvider(uint64(ptr), func(request callback.ResourceRequest, handle *callback.ResourceRequestHandle) uint32 { decision := provider(ResourceRequest{ RequestedURL: request.RequestedURL, ResolvedURL: request.ResolvedURL, @@ -1076,256 +792,181 @@ func (runtime *RuntimeHandle) SetResourceProvider(provider ResourceProviderCallb PriorData: request.PriorData, }, newResourceRequestHandle(handle)) return rawResourceProviderDecision(decision) - }) - replacement = state - return status - }); err != nil { - return err - } - - runtime.resourceProviderMu.Lock() - previous := runtime.resourceProvider - runtime.resourceProvider = replacement - runtime.resourceProviderMu.Unlock() - previous.Release() - return nil + }, unsafe.Pointer(completion)) + }, completionCommand) } -// ClearResourceProvider clears the runtime-scoped network resource provider, so -// later requests go to MapLibre's online file source. Once this call returns, -// the cleared provider is no longer invoked; requests it already took a handle -// for keep that handle, so complete or close each one as usual. -func (runtime *RuntimeHandle) ClearResourceProvider() error { - ptr, release, err := runtime.ptr() +// ClearResourceProvider submits removal of the runtime-scoped network resource +// provider. +func (runtime *RuntimeHandle) ClearResourceProvider() (*Future[CommandCompletion], error) { + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() - defer runtime.state.KeepAlive() - if err := checkNative(func() int32 { return callback.ClearResourceProvider(uint64(ptr)) }); err != nil { - return err - } - runtime.releaseResourceProvider() - return nil -} + defer runtime.state.KeepAlive() -func (runtime *RuntimeHandle) releaseResourceProvider() { - runtime.resourceProviderMu.Lock() - previous := runtime.resourceProvider - runtime.resourceProvider = nil - runtime.resourceProviderMu.Unlock() - previous.Release() + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.ClearResourceProvider(uint64(ptr), unsafe.Pointer(completion)) + }, completionCommand) } -// SetResourceTransform installs or replaces the runtime-scoped network URL -// transform. Native code may invoke the transform on worker or network threads, -// so callbacks must be thread-safe and must not call MapLibre map/runtime APIs. -func (runtime *RuntimeHandle) SetResourceTransform(transform ResourceTransformCallback) error { +// SetResourceTransform submits a runtime-scoped network URL transform. Native +// code may invoke the transform on worker or network threads. The callback must +// be thread-safe and must not call map or runtime APIs. +func (runtime *RuntimeHandle) SetResourceTransform(transform ResourceTransformCallback) (*Future[CommandCompletion], error) { if transform == nil { - return newBindingError(ErrInvalidArgument, "ResourceTransformCallback is nil") + return nil, newBindingError(ErrInvalidArgument, "ResourceTransformCallback is nil") } - ptr, release, err := runtime.ptr() + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() + defer runtime.state.KeepAlive() - var replacement *callback.ResourceTransformState - if err := checkNative(func() int32 { - state, status := callback.SetResourceTransform(uint64(ptr), func(kind uint32, url string) (string, bool) { + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.SetResourceTransform(uint64(ptr), func(kind uint32, url string) (string, bool) { return transform(ResourceTransformRequest{Kind: ResourceKind(kind), RawKind: kind, URL: url}) - }) - replacement = state - return status - }); err != nil { - return err - } - - runtime.resourceTransformMu.Lock() - previous := runtime.resourceTransform - runtime.resourceTransform = replacement - runtime.resourceTransformMu.Unlock() - previous.Release() - return nil + }, unsafe.Pointer(completion)) + }, completionCommand) } -// ClearResourceTransform clears the runtime-scoped network URL transform. -func (runtime *RuntimeHandle) ClearResourceTransform() error { - ptr, release, err := runtime.ptr() +// ClearResourceTransform submits removal of the runtime-scoped network URL +// transform. +func (runtime *RuntimeHandle) ClearResourceTransform() (*Future[CommandCompletion], error) { + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() - defer runtime.state.KeepAlive() - if err := checkNative(func() int32 { return callback.ClearResourceTransform(uint64(ptr)) }); err != nil { - return err - } - runtime.releaseResourceTransform() - return nil -} + defer runtime.state.KeepAlive() -func (runtime *RuntimeHandle) releaseResourceTransform() { - runtime.resourceTransformMu.Lock() - previous := runtime.resourceTransform - runtime.resourceTransform = nil - runtime.resourceTransformMu.Unlock() - previous.Release() + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.ClearResourceTransform(uint64(ptr), unsafe.Pointer(completion)) + }, completionCommand) } -// SetHttpHeaderTransform installs or replaces the runtime-scoped outgoing HTTP -// header transform. -func (runtime *RuntimeHandle) SetHttpHeaderTransform(transform HttpHeaderTransformCallback) error { +// SetHttpHeaderTransform submits a runtime-scoped outgoing HTTP header +// transform. +func (runtime *RuntimeHandle) SetHttpHeaderTransform(transform HttpHeaderTransformCallback) (*Future[CommandCompletion], error) { if transform == nil { - return newBindingError(ErrInvalidArgument, "HttpHeaderTransformCallback is nil") + return nil, newBindingError(ErrInvalidArgument, "HttpHeaderTransformCallback is nil") } - ptr, release, err := runtime.ptr() + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() + defer runtime.state.KeepAlive() - var replacement *callback.HttpHeaderTransformState - if err := checkNative(func() int32 { - state, status := callback.SetHttpHeaderTransform(uint64(ptr), func(kind uint32, url string) []callback.HttpHeader { + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.SetHttpHeaderTransform(uint64(ptr), func(kind uint32, url string) []callback.HttpHeader { provided := transform(HttpHeaderTransformRequest{Kind: ResourceKind(kind), RawKind: kind, URL: url}) headers := make([]callback.HttpHeader, len(provided)) for index, header := range provided { headers[index] = callback.HttpHeader{Name: header.Name, Value: header.Value} } return headers - }) - replacement = state - return status - }); err != nil { - return err - } - runtime.httpHeaderTransformMu.Lock() - previous := runtime.httpHeaderTransform - runtime.httpHeaderTransform = replacement - runtime.httpHeaderTransformMu.Unlock() - previous.Release() - return nil + }, unsafe.Pointer(completion)) + }, completionCommand) } -// ClearHttpHeaderTransform clears the runtime-scoped outgoing HTTP header transform. -func (runtime *RuntimeHandle) ClearHttpHeaderTransform() error { - ptr, release, err := runtime.ptr() +// ClearHttpHeaderTransform submits removal of the runtime-scoped outgoing HTTP +// header transform. +func (runtime *RuntimeHandle) ClearHttpHeaderTransform() (*Future[CommandCompletion], error) { + ptr, err := runtime.ptr() if err != nil { - return err + return nil, err } - defer release() + defer runtime.state.KeepAlive() - if err := checkNative(func() int32 { return callback.ClearHttpHeaderTransform(uint64(ptr)) }); err != nil { - return err - } - runtime.releaseHttpHeaderTransform() - return nil -} -func (runtime *RuntimeHandle) releaseHttpHeaderTransform() { - runtime.httpHeaderTransformMu.Lock() - previous := runtime.httpHeaderTransform - runtime.httpHeaderTransform = nil - runtime.httpHeaderTransformMu.Unlock() - previous.Release() + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.ClearHttpHeaderTransform(uint64(ptr), unsafe.Pointer(completion)) + }, completionCommand) } -// NewMap creates a map owned by this runtime with native default options. -func (runtime *RuntimeHandle) NewMap() (*MapHandle, error) { - return runtime.createMap(func(ptr nativeRuntime, out *nativeMap) int32 { - var raw C.mln_map - status := int32(C.mln_map_create(C.mln_runtime(ptr), nil, &raw)) - if status == int32(C.MLN_STATUS_OK) { - *out = nativeMap(raw) - } - return status - }) +// NewMap creates a 512 by 512 logical map with native default options. +func (runtime *RuntimeHandle) NewMap() (*Future[*MapHandle], error) { + return runtime.NewMapWithOptions(NewMapOptions(512, 512, 1)) } // NewMapWithOptions creates a map owned by this runtime with explicit options. // Start from NewMapOptions to keep every map-originated event type selected; a // zero-value MapOptions queues no event. -func (runtime *RuntimeHandle) NewMapWithOptions(options MapOptions) (*MapHandle, error) { - return runtime.createMap(func(ptr nativeRuntime, out *nativeMap) int32 { - rawOptions := C.mln_map_options_default() - rawOptions.width = C.uint32_t(options.Width) - rawOptions.height = C.uint32_t(options.Height) - rawOptions.scale_factor = C.double(options.ScaleFactor) - rawOptions.map_mode = C.uint32_t(options.Mode) - rawOptions.fast_pfor_enabled = C.bool(options.FastPFOREnabled) - rawOptions.event_mask = C.uint64_t(options.EventMask) - - var raw C.mln_map - status := int32(C.mln_map_create(C.mln_runtime(ptr), &rawOptions, &raw)) - if status == int32(C.MLN_STATUS_OK) { - *out = nativeMap(raw) - } - return status - }) -} - -func (runtime *RuntimeHandle) createMap(create func(nativeRuntime, *nativeMap) int32) (*MapHandle, error) { - ptr, release, err := runtime.ptr() +func (runtime *RuntimeHandle) NewMapWithOptions(options MapOptions) (*Future[*MapHandle], error) { + ptr, err := runtime.ptr() if err != nil { return nil, err } - defer release() + defer runtime.state.KeepAlive() - var rawMap nativeMap - if err := checkNative(func() int32 { return create(ptr, &rawMap) }); err != nil { - return nil, err + rawOptions := C.mln_map_options_default() + rawOptions.initial_extent = C.mln_logical_extent{ + width: C.uint32_t(options.Width), + height: C.uint32_t(options.Height), + scale_factor: C.double(options.ScaleFactor), } - state, err := handle.New(rawMap, "MapHandle", runtime) - if err != nil { - return nil, newBindingError(ErrInvalidArgument, err.Error()) - } - m := &MapHandle{ - state: state, - runtime: runtime, - runtimeChild: runtime.state.AddChild(), - id: MapID(rawMap), - } - runtime.registerMap(m) - return m, nil + rawOptions.map_mode = C.uint32_t(options.Mode) + rawOptions.fast_pfor_enabled = C.bool(options.FastPFOREnabled) + rawOptions.event_mask = C.uint64_t(options.EventMask) + + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_create(C.mln_runtime(ptr), &rawOptions, completion)) + }, func(result *C.mln_completion_result) (*MapHandle, error) { + raw, err := completionValue[C.mln_map](result) + if err != nil { + return nil, err + } + state, err := handle.New(nativeMap(raw), "MapHandle") + if err != nil { + _ = closeNativeMap(nativeMap(raw)) + return nil, newBindingError(ErrInvalidArgument, err.Error()) + } + m := &MapHandle{state: state, runtime: runtime, id: MapID(raw)} + runtime.registerMap(m) + return m, nil + }) } -// Close destroys this runtime. A successful close makes later calls no-ops. A -// failed close leaves the native handle live so callers can retry on the owner -// thread. -func (runtime *RuntimeHandle) Close() error { +func closeNativeMap(m nativeMap) error { + _, err := startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_release(C.mln_map(m), completion)) + }, completionUnit) + return err +} + +// Close releases this runtime's public native handle and returns the future for +// its native teardown. Native teardown continues in submission order after this +// method returns; the future completes once every accepted submission, +// including released maps' teardown, has finished and the runtime's threads and +// resources are gone. A host that awaits it may exit the process without racing +// native teardown, and a host that outlives its runtimes may drop it. Closing an +// already closed runtime returns a future that has already completed. A failed +// close returns no future and leaves the handle live so callers can correct the +// native precondition and retry. +func (runtime *RuntimeHandle) Close() (*Future[struct{}], error) { if runtime == nil || runtime.state == nil { - return newBindingError(ErrInvalidArgument, "RuntimeHandle is nil") + return nil, newBindingError(ErrInvalidArgument, "RuntimeHandle is nil") } - var bindingErr error - if err := checkNative(func() int32 { - status, err := runtime.state.CloseChecked(func(native nativeRuntime) int32 { - return destroyRuntimeHandle(native) - }) + var closeErr error + teardown := completedFuture(struct{}{}) + _ = runtime.state.Close(func(native nativeRuntime) int32 { + future, err := startNativeRuntimeRelease(native) if err != nil { - if errors.Is(err, handle.ErrLiveChildren) { - bindingErr = newBindingError(ErrInvalidState, "RuntimeHandle has live child handles") - return int32(C.MLN_STATUS_OK) - } - bindingErr = newBindingError(ErrInvalidState, err.Error()) - return int32(C.MLN_STATUS_OK) + closeErr = err + return statusFromError(err) } - return status - }); err != nil { - return err - } - if bindingErr != nil { - return bindingErr + teardown = future + return int32(C.MLN_STATUS_OK) + }) + if closeErr != nil { + return nil, closeErr } - runtime.releaseResourceTransform() - runtime.releaseHttpHeaderTransform() - runtime.releaseResourceProvider() runtime.mapsMu.Lock() runtime.maps = nil runtime.mapsMu.Unlock() - return nil + return teardown, nil } diff --git a/bindings/go/runtime_event_test.go b/bindings/go/runtime_event_test.go index 28e71cc33..08bb68623 100644 --- a/bindings/go/runtime_event_test.go +++ b/bindings/go/runtime_event_test.go @@ -8,16 +8,22 @@ import ( "time" ) -// drainAllRuntimeEvents pumps until the runtime stops producing events, and -// returns everything it drained in queue order. +func waitForRuntimeBarrier(t *testing.T, runtime *RuntimeHandle) { + t.Helper() + operation, err := runtime.Barrier() + if _, err := awaitForTest(operation, err); err != nil { + t.Fatalf("Barrier completion: %v", err) + } +} + +// drainAllRuntimeEvents waits for autonomous work to settle, then returns +// everything it drained in queue order. func drainAllRuntimeEvents(t *testing.T, runtime *RuntimeHandle) []RuntimeEvent { t.Helper() var events []RuntimeEvent for range make([]struct{}, 100) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } @@ -30,17 +36,15 @@ func drainAllRuntimeEvents(t *testing.T, runtime *RuntimeHandle) []RuntimeEvent return nil } -// collectRuntimeEventsUntil pumps and drains until every wanted event type has -// arrived, and returns every event it saw. +// collectRuntimeEventsUntil drains until every wanted event type has arrived, +// and returns every event it saw. func collectRuntimeEventsUntil(t *testing.T, runtime *RuntimeHandle, wanted ...RuntimeEventType) []RuntimeEvent { t.Helper() var events []RuntimeEvent seen := make(map[RuntimeEventType]bool, len(wanted)) for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } @@ -64,8 +68,8 @@ func collectRuntimeEventsUntil(t *testing.T, runtime *RuntimeHandle, wanted ...R return nil } -// waitForRuntimeEvent pumps and drains until one event of the wanted type -// arrives, and returns it. +// waitForRuntimeEvent drains until one event of the wanted type arrives, and +// returns it. func waitForRuntimeEvent(t *testing.T, runtime *RuntimeHandle, eventType RuntimeEventType) RuntimeEvent { t.Helper() events := collectRuntimeEventsUntil(t, runtime, eventType) @@ -89,7 +93,6 @@ const emptyStyleJSON = `{"version":8,"sources":{},"layers":[]}` // registers their close. func newRuntimeAndMap(t *testing.T, options *MapOptions) (*RuntimeHandle, *MapHandle) { t.Helper() - lockOSThreadForTest(t) runtime, err := NewRuntime() if err != nil { @@ -97,108 +100,78 @@ func newRuntimeAndMap(t *testing.T, options *MapOptions) (*RuntimeHandle, *MapHa } var m *MapHandle if options == nil { - m, err = runtime.NewMap() + m, err = awaitForTest(runtime.NewMap()) } else { - m, err = runtime.NewMapWithOptions(*options) + m, err = awaitForTest(runtime.NewMapWithOptions(*options)) } if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } t.Cleanup(func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }) return runtime, m } -func TestRuntimeDrainReportsOneStyleLoadAsOneBatch(t *testing.T) { +func TestRuntimeDrainReportsStyleLoadInQueueOrder(t *testing.T) { runtime, m := newRuntimeAndMap(t, nil) drainAllRuntimeEvents(t, runtime) - if err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } - // One style load queues several events, and one drain reports all of them in - // queue order. - var loaded RuntimeEventBatch + // A drain never waits for worker progress, so collect successive owned + // batches until the style load finishes and preserve their queue order. + var events []RuntimeEvent for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } + events = append(events, batch.Events...) if slices.Contains(eventTypes(batch.Events), RuntimeEventMapStyleLoaded) { - loaded = batch break } time.Sleep(time.Millisecond) } - types := eventTypes(loaded.Events) + types := eventTypes(events) if len(types) < 2 { - t.Fatalf("style load batch = %v, want more than one event", types) + t.Fatalf("style load events = %v, want more than one event", types) } started := slices.Index(types, RuntimeEventMapLoadingStarted) styleLoaded := slices.Index(types, RuntimeEventMapStyleLoaded) if started < 0 || started > styleLoaded { - t.Fatalf("style load batch = %v, want loading-started before style-loaded", types) - } - if loaded.RemainingCount != 0 { - t.Fatalf("unbounded drain remaining = %d, want 0", loaded.RemainingCount) - } -} - -func TestRuntimeBoundedDrainLeavesTheRestQueued(t *testing.T) { - runtime, m := newRuntimeAndMap(t, nil) - if err := m.JumpTo(CameraOptions{}.WithZoom(2)); err != nil { - t.Fatalf("JumpTo(): %v", err) - } - - bounded, err := runtime.DrainEvents(1) - if err != nil { - t.Fatalf("DrainEvents(1): %v", err) - } - if len(bounded.Events) != 1 || bounded.RemainingCount == 0 { - t.Fatalf("bounded drain = %d events, %d remaining, want 1 event and a remainder", len(bounded.Events), bounded.RemainingCount) - } - - rest, err := runtime.DrainEvents(0) - if err != nil { - t.Fatalf("DrainEvents(0): %v", err) - } - if uint64(len(rest.Events)) != bounded.RemainingCount || rest.RemainingCount != 0 { - t.Fatalf("unbounded drain = %d events, %d remaining, want %d events and none left", len(rest.Events), rest.RemainingCount, bounded.RemainingCount) + t.Fatalf( + "style load events = %v, want loading-started before style-loaded", + types, + ) } } -func TestRuntimeDrainRejectsNegativeBoundAndEmptiesFreshRuntime(t *testing.T) { - lockOSThreadForTest(t) - +func TestRuntimeDrainEmptiesFreshRuntime(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() - if _, err := runtime.DrainEvents(-1); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("DrainEvents(-1) error = %v, want ErrInvalidArgument", err) - } - batch, err := runtime.DrainEvents(0) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } - if len(batch.Events) != 0 || batch.RemainingCount != 0 { - t.Fatalf("fresh runtime batch = %d events, %d remaining", len(batch.Events), batch.RemainingCount) + if len(batch.Events) != 0 { + t.Fatalf("fresh runtime batch = %d events", len(batch.Events)) } } @@ -218,7 +191,7 @@ func TestRuntimeEventMasksRoundTripAndRejectUnknownBits(t *testing.T) { t.Fatalf("default masks = (%#x, %#x), want %#x", uint64(mapMask), uint64(runtimeMask), uint64(RuntimeEventMaskAll)) } - if err := m.SetEventMask(RuntimeEventMaskAll); err != nil { + if _, err := m.SetEventMask(RuntimeEventMaskAll); err != nil { t.Fatalf("map SetEventMask(all): %v", err) } if err := runtime.SetEventMask(RuntimeEventMaskAll); err != nil { @@ -227,9 +200,10 @@ func TestRuntimeEventMasksRoundTripAndRejectUnknownBits(t *testing.T) { // A read-modify-write keeps every other bit. narrowed := mapMask &^ RuntimeEventMaskMapTileAction - if err := m.SetEventMask(narrowed); err != nil { + if _, err := m.SetEventMask(narrowed); err != nil { t.Fatalf("map SetEventMask(narrowed): %v", err) } + waitForRuntimeBarrier(t, runtime) readBack, err := m.EventMask() if err != nil { t.Fatalf("map EventMask(): %v", err) @@ -245,15 +219,16 @@ func TestRuntimeEventMasksRoundTripAndRejectUnknownBits(t *testing.T) { } restored := readBack | RuntimeEventMaskMapTileAction - if err := m.SetEventMask(restored); err != nil { + if _, err := m.SetEventMask(restored); err != nil { t.Fatalf("map SetEventMask(restored): %v", err) } + waitForRuntimeBarrier(t, runtime) if readBack, err = m.EventMask(); err != nil || readBack != RuntimeEventMaskAll { t.Fatalf("map mask after restore = (%#x, %v), want %#x", uint64(readBack), err, uint64(RuntimeEventMaskAll)) } unknown := RuntimeEventMaskAll | RuntimeEventMask(1)<<63 - if err := m.SetEventMask(unknown); !errors.Is(err, ErrInvalidArgument) { + if _, err := m.SetEventMask(unknown); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("map SetEventMask(unknown bit) error = %v, want ErrInvalidArgument", err) } if err := runtime.SetEventMask(unknown); !errors.Is(err, ErrInvalidArgument) { @@ -268,13 +243,11 @@ func TestRuntimeEventMasksRoundTripAndRejectUnknownBits(t *testing.T) { // Both option constructors validate the mask before native sees it, so an // unknown bit is rejected at creation as well as by the setters. func TestOptionsEventMaskRejectsUnknownBits(t *testing.T) { - lockOSThreadForTest(t) - runtimeOptions := NewRuntimeOptions("", ":memory:") runtimeOptions.EventMask = RuntimeEventMaskAll | RuntimeEventMask(1)<<63 if runtime, err := NewRuntimeWithOptions(runtimeOptions); !errors.Is(err, ErrInvalidArgument) { if err == nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) } t.Fatalf("NewRuntimeWithOptions(unknown bit) error = %v, want ErrInvalidArgument", err) } @@ -282,10 +255,7 @@ func TestOptionsEventMaskRejectsUnknownBits(t *testing.T) { runtime, _ := newRuntimeAndMap(t, nil) mapOptions := NewMapOptions(64, 64, 1) mapOptions.EventMask = RuntimeEventMaskAll | RuntimeEventMask(1)<<63 - if m, err := runtime.NewMapWithOptions(mapOptions); !errors.Is(err, ErrInvalidArgument) { - if err == nil { - _ = m.Close() - } + if _, err := runtime.NewMapWithOptions(mapOptions); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("NewMapWithOptions(unknown bit) error = %v, want ErrInvalidArgument", err) } } @@ -303,7 +273,7 @@ func TestMapOptionsEventMaskSuppressesClearedTypesFromCreation(t *testing.T) { t.Fatalf("EventMask() = %#x, want the style-loaded bit cleared", uint64(mask)) } - if err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } // Loading-started and style-loaded arrive in one batch, so a run that saw the @@ -315,24 +285,19 @@ func TestMapOptionsEventMaskSuppressesClearedTypesFromCreation(t *testing.T) { } } -func TestRuntimeDrainAndMaskSettersReportWrongThread(t *testing.T) { +func TestRuntimeDrainAndMaskSettersMigrateAcrossGoroutines(t *testing.T) { runtime, m := newRuntimeAndMap(t, nil) - - type result struct { - name string - err error - } - results := make(chan result, 3) + results := make(chan error, 3) go func() { - _, err := runtime.DrainEvents(0) - results <- result{"DrainEvents", err} - results <- result{"runtime SetEventMask", runtime.SetEventMask(RuntimeEventMaskAll)} - results <- result{"map SetEventMask", m.SetEventMask(RuntimeEventMaskAll)} + _, err := runtime.DrainEvents() + results <- err + results <- runtime.SetEventMask(RuntimeEventMaskAll) + _, err = m.SetEventMask(RuntimeEventMaskAll) + results <- err }() - for range make([]struct{}, 3) { - got := <-results - if !errors.Is(got.err, ErrWrongThread) { - t.Fatalf("%s from another thread error = %v, want ErrWrongThread", got.name, got.err) + for i := 0; i < 3; i++ { + if err := <-results; err != nil { + t.Fatalf("any-thread runtime/map call %d: %v", i, err) } } } @@ -341,15 +306,13 @@ func TestRuntimeEventCopiesSurviveTheNextDrain(t *testing.T) { runtime, m := newRuntimeAndMap(t, nil) // A failed style load carries text, so this batch holds arena-backed values. - if err := m.SetStyleURL("unsupported://style.json"); err != nil { + if _, err := m.SetStyleURL("unsupported://style.json"); err != nil { t.Fatalf("SetStyleURL(): %v", err) } var kept RuntimeEventBatch for range make([]struct{}, 5000) { - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - batch, err := runtime.DrainEvents(0) + time.Sleep(time.Millisecond) + batch, err := runtime.DrainEvents() if err != nil { t.Fatalf("DrainEvents(): %v", err) } @@ -370,11 +333,11 @@ func TestRuntimeEventCopiesSurviveTheNextDrain(t *testing.T) { } // Two more style loads reuse the runtime's event and message storage. - if err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(emptyStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } collectRuntimeEventsUntil(t, runtime, RuntimeEventMapStyleLoaded) - if err := m.SetStyleURL("also-unsupported://style.json"); err != nil { + if _, err := m.SetStyleURL("also-unsupported://style.json"); err != nil { t.Fatalf("SetStyleURL(): %v", err) } collectRuntimeEventsUntil(t, runtime, RuntimeEventMapLoadingFailed) @@ -393,14 +356,13 @@ func TestRuntimeEventCopiesSurviveTheNextDrain(t *testing.T) { } func TestRuntimeEventDecoderUsesTheBatchStride(t *testing.T) { - lockOSThreadForTest(t) runtime, _ := newRuntimeAndMap(t, nil) stride := runtimeEventSizeForTest() // A batch whose stride exceeds the compiled event size is what a later C API // version looks like, and every event after the first misdecodes when the // decoder strides by its own struct size. - batch := newRuntimeEventBatchForTest(stride+16, 0, []runtimeEventForTest{ + batch := newRuntimeEventBatchForTest(stride+16, []runtimeEventForTest{ newRuntimeEventForTest(RuntimeEventMapRenderFrameFinished, RuntimeEventSourceMap, 0, 0). withRenderFrame(RuntimeEventRenderFramePayload{RawMode: uint32(RenderModeFull), NeedsRepaint: true}), newRuntimeEventForTest(RuntimeEventMapTileAction, RuntimeEventSourceMap, 0, 0). @@ -409,14 +371,9 @@ func TestRuntimeEventDecoderUsesTheBatchStride(t *testing.T) { RawOperation: uint32(TileOperationLoadFromCache), TileID: TileID{OverscaledZ: 9, Wrap: -1, CanonicalZ: 8, CanonicalX: 7, CanonicalY: 6}, }), - newRuntimeEventForTest(RuntimeEventOfflineOperationCompleted, RuntimeEventSourceRuntime, 0, 3). - withMessage("operation failed"). - withOfflineOperationCompleted(RuntimeEventOfflineOperationCompletedPayload{ - OperationID: 7, - OperationKind: OfflineOperationRegionGet, - ResultKind: OfflineOperationResultOptionalRegion, - ResultStatus: 3, - Found: true, + newRuntimeEventForTest(RuntimeEventMapCameraTransitionFinished, RuntimeEventSourceMap, 0, 0). + withCameraTransitionFinished(RuntimeEventCameraTransitionFinishedPayload{ + TransitionID: 7, }), }) defer batch.free() @@ -436,24 +393,19 @@ func TestRuntimeEventDecoderUsesTheBatchStride(t *testing.T) { if decoded.Events[1].Message != "roads" { t.Fatalf("tile action message = %q, want the source ID", decoded.Events[1].Message) } - completion, ok := decoded.Events[2].Payload.(RuntimeEventOfflineOperationCompletedPayload) - if !ok || completion.OperationID != 7 || completion.ResultStatus != 3 || !completion.Found { - t.Fatalf("offline completion payload = %+v", decoded.Events[2].Payload) - } - if decoded.Events[2].Message != "operation failed" || decoded.Events[2].Code != 3 { - t.Fatalf("offline completion event = %+v", decoded.Events[2]) + transition, ok := decoded.Events[2].Payload.(RuntimeEventCameraTransitionFinishedPayload) + if !ok || transition.TransitionID != 7 { + t.Fatalf("camera transition payload = %+v", decoded.Events[2].Payload) } } func TestRuntimeEventKnownPayloadsDecodeFromTheUnion(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() @@ -461,7 +413,7 @@ func TestRuntimeEventKnownPayloadsDecodeFromTheUnion(t *testing.T) { status := offlineRegionStatusForTest(uint32(OfflineRegionDownloadActive)) status.CompletedTileCount = 12 status.Complete = true - batch := newRuntimeEventBatchForTest(0, 0, []runtimeEventForTest{ + batch := newRuntimeEventBatchForTest(0, []runtimeEventForTest{ newRuntimeEventForTest(RuntimeEventMapRenderMapFinished, RuntimeEventSourceMap, 0, 0). withRenderMap(RuntimeEventRenderMapPayload{RawMode: uint32(RenderModePartial)}), newRuntimeEventForTest(RuntimeEventMapCameraTransitionFinished, RuntimeEventSourceMap, 0, 0). @@ -514,14 +466,12 @@ func TestRuntimeEventKnownPayloadsDecodeFromTheUnion(t *testing.T) { } func TestRuntimeEventUnknownDomainsPreserveRawValues(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } defer func() { - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Close(): %v", err) } }() @@ -530,7 +480,7 @@ func TestRuntimeEventUnknownDomainsPreserveRawValues(t *testing.T) { for index := range window { window[index] = byte(index + 1) } - batch := newRuntimeEventBatchForTest(0, 0, []runtimeEventForTest{ + batch := newRuntimeEventBatchForTest(0, []runtimeEventForTest{ newRuntimeEventForTest(RuntimeEventType(0x7fff_0001), RuntimeEventSourceType(0x7fff_0002), 0x7fff_0004, 0). withRawPayload(RuntimeEventPayloadType(0x7fff_0003), window), }) @@ -579,7 +529,7 @@ func TestRuntimeEventMapSourceUsesRuntimeLocalID(t *testing.T) { if err != nil { t.Fatalf("ID(): %v", err) } - batch := newRuntimeEventBatchForTest(0, 0, []runtimeEventForTest{ + batch := newRuntimeEventBatchForTest(0, []runtimeEventForTest{ newRuntimeEventForTest(RuntimeEventMapIdle, RuntimeEventSourceMap, uint64(mapID), 0), newRuntimeEventForTest(RuntimeEventMapIdle, RuntimeEventSourceMap, uint64(mapID)+1, 0), }) diff --git a/bindings/go/runtime_event_testhook.go b/bindings/go/runtime_event_testhook.go index c25b50cdc..7e1efae12 100644 --- a/bindings/go/runtime_event_testhook.go +++ b/bindings/go/runtime_event_testhook.go @@ -112,17 +112,6 @@ func (event runtimeEventForTest) withOfflineRegionTileCountLimit(payload Runtime return event } -func (event runtimeEventForTest) withOfflineOperationCompleted(payload RuntimeEventOfflineOperationCompletedPayload) runtimeEventForTest { - event.raw = C.mln_go_runtime_event_with_offline_operation_completed(event.raw, C.mln_runtime_event_offline_operation_completed{ - operation_id: C.mln_offline_operation_id(payload.OperationID), - operation_kind: C.uint32_t(payload.OperationKind), - result_kind: C.uint32_t(payload.ResultKind), - result_status: C.int32_t(payload.ResultStatus), - found: C.bool(payload.Found), - }) - return event -} - // withRawPayload writes opaque bytes into this event's payload window, which is // how a test synthesizes a payload type this binding version does not define. func (event runtimeEventForTest) withRawPayload(payloadType RuntimeEventPayloadType, bytes []byte) runtimeEventForTest { @@ -141,13 +130,13 @@ type runtimeEventBatchForTest struct { events unsafe.Pointer messages unsafe.Pointer stride uintptr - raw C.mln_runtime_event_batch + raw C.mln_runtime_event_batch_view } // newRuntimeEventBatchForTest lays events out stride bytes apart. A stride wider // than this binding's compiled event size is what proves the decoder reads the // batch's own stride. -func newRuntimeEventBatchForTest(stride uintptr, remainingCount uint64, events []runtimeEventForTest) *runtimeEventBatchForTest { +func newRuntimeEventBatchForTest(stride uintptr, events []runtimeEventForTest) *runtimeEventBatchForTest { if stride < runtimeEventSizeForTest() { stride = runtimeEventSizeForTest() } @@ -159,7 +148,7 @@ func newRuntimeEventBatchForTest(stride uintptr, remainingCount uint64, events [ for index, event := range events { raw := event.raw if len(event.message) > 0 { - raw.message_offset = C.uint32_t(len(arena)) + raw.message_offset = C.uint64_t(len(arena)) raw.message_size = C.uint32_t(len(event.message)) arena = append(arena, event.message...) arena = append(arena, 0) @@ -169,14 +158,13 @@ func newRuntimeEventBatchForTest(stride uintptr, remainingCount uint64, events [ if len(arena) > 0 { batch.messages = C.CBytes(arena) } - batch.raw = C.mln_runtime_event_batch{ - size: C.uint32_t(unsafe.Sizeof(C.mln_runtime_event_batch{})), - event_size: C.uint32_t(stride), - events: (*C.mln_runtime_event)(batch.events), - event_count: C.size_t(len(events)), - messages: (*C.char)(batch.messages), - messages_size: C.size_t(len(arena)), - remaining_count: C.size_t(remainingCount), + batch.raw = C.mln_runtime_event_batch_view{ + size: C.uint32_t(unsafe.Sizeof(C.mln_runtime_event_batch_view{})), + event_size: C.uint32_t(stride), + events: (*C.mln_runtime_event)(batch.events), + event_count: C.size_t(len(events)), + messages: (*C.char)(batch.messages), + messages_size: C.size_t(len(arena)), } return batch } diff --git a/bindings/go/runtime_test.go b/bindings/go/runtime_test.go index a5ef89940..12e46698f 100644 --- a/bindings/go/runtime_test.go +++ b/bindings/go/runtime_test.go @@ -1,24 +1,26 @@ package maplibre import ( + "context" "errors" - stdruntime "runtime" "testing" "time" - - "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/handle" ) -func TestRuntimeCreateWithOptions(t *testing.T) { - lockOSThreadForTest(t) - +func TestRuntimeCreateWithOptionsAndClose(t *testing.T) { runtime, err := NewRuntimeWithOptions(NewRuntimeOptions("", ":memory:")) if err != nil { t.Fatalf("NewRuntimeWithOptions(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Fatalf("Close(): %v", err) } + if err := closeRuntimeForTest(runtime); err != nil { + t.Fatalf("second Close(): %v", err) + } + if _, err := runtime.NewMap(); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("NewMap() after Close error = %v, want ErrInvalidArgument", err) + } } func TestRuntimeOptionsRejectEmbeddedNUL(t *testing.T) { @@ -28,254 +30,53 @@ func TestRuntimeOptionsRejectEmbeddedNUL(t *testing.T) { } } -func TestRuntimeCreationRejectsABIMismatchBeforeNativeCreateOrHandleStore(t *testing.T) { - createCalled := false - storeCalled := false - - runtime, err := createRuntimeWithStateFactory( - ExpectedCABIVersion+1, - func(out *nativeRuntime) int32 { - createCalled = true - return 0 - }, - func(runtime nativeRuntime) (*handle.State[nativeRuntime], error) { - storeCalled = true - return nil, nil - }, - ) - - if runtime != nil { - t.Fatalf("createRuntimeWithStateFactory() runtime = %v, want nil", runtime) - } - if !errors.Is(err, ErrABIVersionMismatch) { - t.Fatalf("createRuntimeWithStateFactory() error = %v, want ErrABIVersionMismatch", err) - } - if createCalled { - t.Fatal("runtime create hook was called after ABI mismatch") - } - if storeCalled { - t.Fatal("runtime handle store hook was called after ABI mismatch") - } -} - -func TestRuntimeAmbientCacheOperationDiscard(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - defer func() { - if err := runtime.Close(); err != nil { - t.Errorf("Close(): %v", err) - } - }() - - operation, err := runtime.StartAmbientCacheOperation(AmbientCacheOperationClear) - if err != nil { - t.Fatalf("StartAmbientCacheOperation(): %v", err) - } - if operation.ID() == 0 { - t.Fatal("operation ID is zero") - } - if err := operation.Discard(); err != nil { - t.Fatalf("Discard(): %v", err) - } - if err := operation.Discard(); err != nil { - t.Fatalf("second Discard(): %v", err) - } -} - -func TestRuntimeAmbientCacheOperationRejectsUnknownOperation(t *testing.T) { - lockOSThreadForTest(t) - +func TestRuntimeBarrierProgressesAutonomously(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - defer func() { - if err := runtime.Close(); err != nil { - t.Errorf("Close(): %v", err) - } - }() - - _, err = runtime.StartAmbientCacheOperation(AmbientCacheOperation(999_999)) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StartAmbientCacheOperation(unknown) error = %v, want ErrInvalidArgument", err) - } -} - -func TestRuntimeCreatePumpAndClose(t *testing.T) { - lockOSThreadForTest(t) + defer closeRuntimeForTest(runtime) - runtime, err := NewRuntime() + future, err := runtime.Barrier() if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - if err := runtime.Pump(0, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - if batch, err := runtime.DrainEvents(0); err != nil { - t.Fatalf("DrainEvents(): %v", err) - } else if batch.RemainingCount != 0 { - t.Fatalf("DrainEvents(0) left %d events queued", batch.RemainingCount) - } - if err := runtime.Close(); err != nil { - t.Fatalf("Close(): %v", err) + t.Fatalf("Barrier(): %v", err) } - if err := runtime.Close(); err != nil { - t.Fatalf("second Close(): %v", err) - } - if _, err := runtime.NewMap(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("NewMap() after Close error = %v, want ErrInvalidArgument", err) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if _, err := future.Await(ctx); err != nil { + t.Fatalf("Await(): %v", err) } } -func TestRuntimeCloseWrongThreadLeavesHandleRetryable(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - +func TestRuntimeLifecycleMigratesAcrossGoroutines(t *testing.T) { + runtimeCh := make(chan *RuntimeHandle, 1) errCh := make(chan error, 1) go func() { - errCh <- runtime.Close() - }() - if err := <-errCh; !errors.Is(err, ErrWrongThread) { - _ = runtime.Close() - t.Fatalf("Close() from another thread error = %v, want ErrWrongThread", err) - } - if err := runtime.Pump(0, -1); err != nil { - _ = runtime.Close() - t.Fatalf("Pump() after failed close: %v", err) - } - if err := runtime.Close(); err != nil { - t.Fatalf("Close() on owner thread after failed close: %v", err) - } -} - -func TestRuntimePumpWakesForNativeWorkAndForAWakeSource(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - mapHandle, err := runtime.NewMap() - if err != nil { - t.Fatalf("NewMap(): %v", err) - } - drainAllRuntimeEvents(t, runtime) - - // Native reports the malformed style from its own threads; the failure has - // to reach the parked owner thread. - if err := mapHandle.SetStyleURL("unsupported://style.json"); err != nil { - t.Fatalf("SetStyleURL(): %v", err) - } - loadingFailed := false - loadStarted := time.Now() - for i := 0; i < 20; i++ { - if err := runtime.Pump(10*time.Second, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - if time.Since(loadStarted) > 5*time.Second { - t.Fatal("parks sat out their timeouts while the style load was pending") - } - batch, err := runtime.DrainEvents(0) + runtime, err := NewRuntime() if err != nil { - t.Fatalf("DrainEvents(): %v", err) - } - for _, event := range batch.Events { - if event.Type == RuntimeEventMapLoadingFailed { - loadingFailed = true - } + errCh <- err + return } - if loadingFailed { - break - } - } - if !loadingFailed { - t.Fatal("the parked owner thread never saw the loading failure") - } - - // Nothing else ends the park below, so only the signal from the other - // goroutine can release it. - source, err := runtime.WakeSource() - if err != nil { - t.Fatalf("WakeSource(): %v", err) - } - drainAllRuntimeEvents(t, runtime) - signalErr := make(chan error, 1) - go func() { - time.Sleep(20 * time.Millisecond) - signalErr <- source.Signal() + runtimeCh <- runtime }() - parkStarted := time.Now() - if err := runtime.Pump(10*time.Second, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - if time.Since(parkStarted) > 5*time.Second { - t.Fatal("the parked owner thread timed out instead of taking the signal") - } - if err := <-signalErr; err != nil { - t.Fatalf("Signal(): %v", err) - } - // A wake source stays usable after its runtime closes. - if err := mapHandle.Close(); err != nil { - t.Fatalf("map Close(): %v", err) + var runtime *RuntimeHandle + select { + case err := <-errCh: + t.Fatalf("NewRuntime() on goroutine: %v", err) + case runtime = <-runtimeCh: } - if err := runtime.Close(); err != nil { - t.Fatalf("Close(): %v", err) - } - if err := source.Signal(); err != nil { - t.Fatalf("Signal() after runtime close: %v", err) - } - source.Close() - if err := source.Signal(); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("Signal() after source Close error = %v, want ErrInvalidArgument", err) - } -} - -func TestRuntimePumpConsumesOneLatchedSignal(t *testing.T) { - lockOSThreadForTest(t) - - runtime, err := NewRuntime() - if err != nil { - t.Fatalf("NewRuntime(): %v", err) - } - defer func() { - if err := runtime.Close(); err != nil { - t.Fatalf("Close(): %v", err) - } - }() - source, err := runtime.WakeSource() + m, err := awaitForTest(runtime.NewMapWithOptions(NewMapOptions(128, 128, 1))) if err != nil { - t.Fatalf("WakeSource(): %v", err) - } - defer source.Close() - drainAllRuntimeEvents(t, runtime) - - if err := source.Signal(); err != nil { - t.Fatalf("Signal(): %v", err) + t.Fatalf("NewMap() after goroutine migration: %v", err) } - signalledStarted := time.Now() - if err := runtime.Pump(10*time.Second, -1); err != nil { - t.Fatalf("Pump(): %v", err) - } - if time.Since(signalledStarted) > 5*time.Second { - t.Fatal("a pump waited even though the wake flag was set") - } - - // The pump above cleared the wake flag, so this one waits its full timeout. - idleStarted := time.Now() - if err := runtime.Pump(200*time.Millisecond, -1); err != nil { - t.Fatalf("Pump(): %v", err) + closed := make(chan error, 1) + go func() { closed <- m.Close() }() + if err := <-closed; err != nil { + t.Fatalf("Map Close() on another goroutine: %v", err) } - if time.Since(idleStarted) < 100*time.Millisecond { - t.Fatal("the first pump left the wake flag set") + go func() { closed <- closeRuntimeForTest(runtime) }() + if err := <-closed; err != nil { + t.Fatalf("Runtime Close() on another goroutine: %v", err) } } diff --git a/bindings/go/style.go b/bindings/go/style.go index 2568ac53b..a115e2d2e 100644 --- a/bindings/go/style.go +++ b/bindings/go/style.go @@ -7,8 +7,6 @@ package maplibre import "C" import ( - "fmt" - "runtime" "unsafe" "github.com/maplibre/maplibre-native-ffi/bindings/go/internal/callback" @@ -45,6 +43,32 @@ type StyleSourceInfo struct { RasterEncoding *StyleRasterDEMEncoding } +// StyleLayerInfo contains copied metadata for one style layer. +type StyleLayerInfo struct { + // Type is the style-spec layer type string. + Type string + // MinZoom is the lowest zoom at which the layer draws, math.Inf(-1) with no + // lower bound. + MinZoom float64 + // MaxZoom is the highest zoom at which the layer draws, math.Inf(1) with no + // upper bound. + MaxZoom float64 + Visibility StyleLayerVisibility + // HasSourceID reports whether the layer carries a source ID. Copy it with + // StartLayerSourceID. + HasSourceID bool + // SourceIDSize is the source ID byte length, 0 when HasSourceID is false. + SourceIDSize uint64 + SourceID *string + // HasSourceLayer reports whether the layer carries a source-layer ID. Copy + // it with StartLayerSourceLayer. + HasSourceLayer bool + // SourceLayerSize is the source-layer byte length, 0 when HasSourceLayer is + // false. + SourceLayerSize uint64 + SourceLayer *string +} + // StyleSourceTileJSON contains the retained TileJSON fields of an inline tile source. type StyleSourceTileJSON struct { TileURLs []string @@ -420,10 +444,9 @@ func (options *cStyleGeoJSONSourceOptions) free() { } // CustomGeometryTileCallback receives custom geometry tile requests. Native code -// may invoke it on worker threads, racing owner-thread map calls, so it must be -// thread-safe, must not call MapLibre map APIs, and should queue -// SetCustomGeometrySourceTileData or invalidation work back to the map owner -// thread. Panics are recovered and ignored. +// may invoke it concurrently on worker threads, so it must be thread-safe and +// must not call MapLibre map APIs directly. Queue tile-data and invalidation +// commands for later submission. Panics are recovered and ignored. type CustomGeometryTileCallback func(CanonicalTileID) // CustomGeometrySourceOptions configures a custom geometry source. CancelTile is @@ -441,10 +464,9 @@ type CustomGeometrySourceOptions struct { } // CustomMVTVectorTileCallback receives custom MVT vector tile requests. Native -// code may invoke it on worker threads, racing owner-thread map calls, so it -// must be thread-safe, must not call MapLibre map APIs, and should queue -// SetCustomMVTVectorSourceTileData, SetCustomMVTVectorSourceTileError, or -// invalidation work back to the map owner thread. Panics are recovered and +// code may invoke it concurrently on worker threads, so it must be thread-safe +// and must not call MapLibre map APIs directly. Queue tile-data, tile-error, +// and invalidation commands for later submission. Panics are recovered and // ignored. type CustomMVTVectorTileCallback func(CanonicalTileID) @@ -781,6 +803,14 @@ type StyleImageInfo struct { TextFitHeight *StyleImageTextFit } +// StyleImage is a complete copied runtime style image. +type StyleImage struct { + Info StyleImageInfo + Pixels []byte + StretchX []ImageStretch + StretchY []ImageStretch +} + type cPremultipliedRGBA8Image struct { raw C.mln_premultiplied_rgba8_image allocation unsafe.Pointer @@ -836,49 +866,6 @@ func styleImageInfoFromC(info C.mln_style_image_info) StyleImageInfo { // StyleImageStretches returns one runtime style image's stretchable intervals // and whether the image exists. -func (m *MapHandle) StyleImageStretches(imageID string) (stretchX, stretchY []ImageStretch, found bool, err error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, nil, false, err - } - defer release() - defer m.state.KeepAlive() - imageView := newCStringView(imageID) - defer imageView.free() - - var xCount, yCount C.size_t - var rawFound C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_copy_style_image_stretches( - C.mln_map(ptr), imageView.raw(), nil, 0, &xCount, nil, 0, &yCount, &rawFound, - )) - }); err != nil { - return nil, nil, false, err - } - if !bool(rawFound) { - return nil, nil, false, nil - } - - rawX := make([]C.mln_image_stretch, int(xCount)) - rawY := make([]C.mln_image_stretch, int(yCount)) - var xPointer, yPointer *C.mln_image_stretch - if len(rawX) > 0 { - xPointer = &rawX[0] - } - if len(rawY) > 0 { - yPointer = &rawY[0] - } - if err := checkNative(func() int32 { - return int32(C.mln_map_copy_style_image_stretches( - C.mln_map(ptr), imageView.raw(), - xPointer, C.size_t(len(rawX)), &xCount, - yPointer, C.size_t(len(rawY)), &yCount, &rawFound, - )) - }); err != nil { - return nil, nil, false, err - } - return stretchesFromC(rawX), stretchesFromC(rawY), true, nil -} func stretchesFromC(raw []C.mln_image_stretch) []ImageStretch { stretches := make([]ImageStretch, len(raw)) @@ -931,15 +918,28 @@ func styleSourceInfoFromC(info C.mln_style_source_info) StyleSourceInfo { return result } +func styleLayerInfoFromC(info C.mln_style_layer_info) StyleLayerInfo { + return StyleLayerInfo{ + Type: goStringView(info._type), + MinZoom: float64(info.min_zoom), + MaxZoom: float64(info.max_zoom), + Visibility: StyleLayerVisibility(info.visibility), + HasSourceID: info.fields&C.MLN_STYLE_LAYER_INFO_SOURCE_ID != 0, + SourceIDSize: uint64(info.source_id_size), + HasSourceLayer: info.fields&C.MLN_STYLE_LAYER_INFO_SOURCE_LAYER != 0, + SourceLayerSize: uint64(info.source_layer_size), + } +} + // AddGeoJSONSourceURL adds a GeoJSON source that loads from a URL. Later // SetGeoJSONSourceURL calls keep the options passed here, and later // SetGeoJSONSourceData calls require data prepared with matching options. -func (m *MapHandle) AddGeoJSONSourceURL(sourceID string, url string, options *StyleGeoJSONSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddGeoJSONSourceURL(sourceID string, url string, options *StyleGeoJSONSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -947,255 +947,241 @@ func (m *MapHandle) AddGeoJSONSourceURL(sourceID string, url string, options *St defer urlView.free() rawOptions, err := newCStyleGeoJSONSourceOptions(options) if err != nil { - return newBindingError(ErrInvalidArgument, err.Error()) + return nil, newBindingError(ErrInvalidArgument, err.Error()) } defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_geojson_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptions.ptr())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_geojson_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptions.ptr(), completion)) + }, completionCommand) } // SetGeoJSONSourceURL updates a GeoJSON source to load from a URL. -func (m *MapHandle) SetGeoJSONSourceURL(sourceID string, url string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetGeoJSONSourceURL(sourceID string, url string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() urlView := newCStringView(url) defer urlView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_geojson_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_geojson_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), completion)) + }, completionCommand) } // AddGeoJSONSourceData adds a GeoJSON source with prepared inline data. The // call borrows the handle and retains the prepared index, and the source // adopts the options the data was prepared with, fixed for the lifetime of the // source. -func (m *MapHandle) AddGeoJSONSourceData(sourceID string, data *GeoJSONSourceDataHandle) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddGeoJSONSourceData(sourceID string, data *GeoJSONSourceDataHandle) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() - dataPtr, dataRelease, err := data.ptr() + dataPtr, err := data.ptr() if err != nil { - return err + return nil, err } - defer dataRelease() + defer data.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_geojson_source_data(C.mln_map(ptr), sourceView.raw(), C.mln_geojson_source_data(dataPtr))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_geojson_source_data(C.mln_map(ptr), sourceView.raw(), C.mln_geojson_source_data(dataPtr), completion)) + }, completionCommand) } // SetGeoJSONSourceData updates a GeoJSON source with prepared inline data. The // call borrows the handle and retains the prepared index. The data must have // been prepared with options equal to the options the source was added with, // ClusterProperties excepted. -func (m *MapHandle) SetGeoJSONSourceData(sourceID string, data *GeoJSONSourceDataHandle) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetGeoJSONSourceData(sourceID string, data *GeoJSONSourceDataHandle) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() - dataPtr, dataRelease, err := data.ptr() + dataPtr, err := data.ptr() if err != nil { - return err + return nil, err } - defer dataRelease() + defer data.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_geojson_source_data(C.mln_map(ptr), sourceView.raw(), C.mln_geojson_source_data(dataPtr))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_geojson_source_data(C.mln_map(ptr), sourceView.raw(), C.mln_geojson_source_data(dataPtr), completion)) + }, completionCommand) } // SetGeoJSONSourceSynchronousTiling overrides a GeoJSON source's synchronous // tiling at runtime. While enabled is true, the source slices requested tiles // inline during the update pass, as if its options had set SynchronousTiling; // false restores the option the source was added with. -func (m *MapHandle) SetGeoJSONSourceSynchronousTiling(sourceID string, enabled bool) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetGeoJSONSourceSynchronousTiling(sourceID string, enabled bool) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_geojson_source_synchronous_tiling(C.mln_map(ptr), sourceView.raw(), C.bool(enabled))) - }) -} - -// AddCustomGeometrySource adds a custom geometry source to the current style. -// Callback state remains valid until source removal, style replacement, or map -// close, each of which frees it on the map owner thread. A failed add frees it -// before returning. -func (m *MapHandle) AddCustomGeometrySource(sourceID string, options CustomGeometrySourceOptions) error { - if options.FetchTile == nil { - return newBindingError(ErrInvalidArgument, "CustomGeometrySourceOptions.FetchTile is nil") - } - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - - return checkNative(func() int32 { - return callback.AddCustomGeometrySource(uint64(ptr), sourceID, options.toCallback()) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_geojson_source_synchronous_tiling(C.mln_map(ptr), sourceView.raw(), C.bool(enabled), completion)) + }, completionCommand) } // SetCustomGeometrySourceTileData sets custom geometry data for one tile. -func (m *MapHandle) SetCustomGeometrySourceTileData(sourceID string, tileID CanonicalTileID, data []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetCustomGeometrySourceTileData(sourceID string, tileID CanonicalTileID, data []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() rawData := newCBufferView(data) defer rawData.free() - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_set_custom_geometry_source_tile_data( C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID), rawData.raw(), + completion, )) - }) + }, completionCommand) } // InvalidateCustomGeometrySourceTile invalidates custom geometry data for one tile. -func (m *MapHandle) InvalidateCustomGeometrySourceTile(sourceID string, tileID CanonicalTileID) error { - ptr, release, err := m.ptr() +func (m *MapHandle) InvalidateCustomGeometrySourceTile(sourceID string, tileID CanonicalTileID) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_invalidate_custom_geometry_source_tile(C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_invalidate_custom_geometry_source_tile(C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID), completion)) + }, completionCommand) } // InvalidateCustomGeometrySourceRegion invalidates custom geometry data inside one geographic region. -func (m *MapHandle) InvalidateCustomGeometrySourceRegion(sourceID string, bounds LatLngBounds) error { - ptr, release, err := m.ptr() +func (m *MapHandle) InvalidateCustomGeometrySourceRegion(sourceID string, bounds LatLngBounds) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_invalidate_custom_geometry_source_region(C.mln_map(ptr), sourceView.raw(), cLatLngBounds(bounds))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_invalidate_custom_geometry_source_region(C.mln_map(ptr), sourceView.raw(), cLatLngBounds(bounds), completion)) + }, completionCommand) } // AddCustomMVTVectorSource adds a custom MVT vector source to the current style. // Callback state remains valid until source removal, style replacement, or map -// close, each of which frees it on the map owner thread. A failed add frees it -// before returning. -func (m *MapHandle) AddCustomMVTVectorSource(sourceID string, options CustomMVTVectorSourceOptions) error { +// close, each of which frees it through the C API's release callback. A failed +// add frees it before returning. +func (m *MapHandle) AddCustomMVTVectorSource(sourceID string, options CustomMVTVectorSourceOptions) (*Future[CommandCompletion], error) { + if sourceID == "" { + return nil, newBindingError(ErrInvalidArgument, "source ID is empty") + } if options.FetchTile == nil { - return newBindingError(ErrInvalidArgument, "CustomMVTVectorSourceOptions.FetchTile is nil") + return nil, newBindingError(ErrInvalidArgument, "CustomMVTVectorSourceOptions.FetchTile is nil") } - ptr, release, err := m.ptr() + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() defer m.state.KeepAlive() - - return checkNative(func() int32 { - return callback.AddCustomMVTVectorSource(uint64(ptr), sourceID, options.toCallback()) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.AddCustomMVTVectorSource( + uint64(ptr), sourceID, options.toCallback(), unsafe.Pointer(completion), + ) + }, completionCommand) } // SetCustomMVTVectorSourceTileData sets custom MVT vector data for one tile. -func (m *MapHandle) SetCustomMVTVectorSourceTileData(sourceID string, tileID CanonicalTileID, data []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetCustomMVTVectorSourceTileData(sourceID string, tileID CanonicalTileID, data []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() rawData := newCBufferView(data) defer rawData.free() - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_set_custom_mvt_vector_source_tile_data( C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID), rawData.raw(), + completion, )) - }) + }, completionCommand) } // SetCustomMVTVectorSourceTileError reports a custom MVT vector source error for one tile. -func (m *MapHandle) SetCustomMVTVectorSourceTileError(sourceID string, tileID CanonicalTileID, message string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetCustomMVTVectorSourceTileError(sourceID string, tileID CanonicalTileID, message string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() messageView := newCStringView(message) defer messageView.free() - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_set_custom_mvt_vector_source_tile_error( C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID), messageView.raw(), + completion, )) - }) + }, completionCommand) } // InvalidateCustomMVTVectorSourceTile invalidates custom MVT vector data for one tile. -func (m *MapHandle) InvalidateCustomMVTVectorSourceTile(sourceID string, tileID CanonicalTileID) error { - ptr, release, err := m.ptr() +func (m *MapHandle) InvalidateCustomMVTVectorSourceTile(sourceID string, tileID CanonicalTileID) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - return checkNative(func() int32 { - return int32(C.mln_map_invalidate_custom_mvt_vector_source_tile(C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_invalidate_custom_mvt_vector_source_tile(C.mln_map(ptr), sourceView.raw(), cCanonicalTileID(tileID), completion)) + }, completionCommand) } // SetStyleImage sets or replaces one runtime style image. -func (m *MapHandle) SetStyleImage(imageID string, image PremultipliedRGBA8Image, options StyleImageOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetStyleImage(imageID string, image PremultipliedRGBA8Image, options StyleImageOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() imageView := newCStringView(imageID) defer imageView.free() @@ -1203,129 +1189,42 @@ func (m *MapHandle) SetStyleImage(imageID string, image PremultipliedRGBA8Image, defer rawImage.free() rawOptions := newCStyleImageOptions(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_style_image(C.mln_map(ptr), imageView.raw(), &rawImage.raw, &rawOptions.raw)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_image(C.mln_map(ptr), imageView.raw(), &rawImage.raw, &rawOptions.raw, completion)) + }, completionCommand) } -// RemoveStyleImage removes one runtime style image and reports whether it existed. -func (m *MapHandle) RemoveStyleImage(imageID string) (bool, error) { - ptr, release, err := m.ptr() +// RemoveStyleImage submits a command that removes one runtime style image. The +// command commits when an image with the ID existed and was removed, and fails +// with ErrNotFound reported through its terminal event's Err payload field when +// none does. Check existence with StyleImageInfo's found flag. +func (m *MapHandle) RemoveStyleImage(imageID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return false, err - } - defer release() - defer m.state.KeepAlive() - imageView := newCStringView(imageID) - defer imageView.free() - var removed C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_remove_style_image(C.mln_map(ptr), imageView.raw(), &removed)) - }); err != nil { - return false, err + return nil, err } - return bool(removed), nil -} -// StyleImageExists reports whether one runtime style image exists. -func (m *MapHandle) StyleImageExists(imageID string) (bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return false, err - } - defer release() defer m.state.KeepAlive() imageView := newCStringView(imageID) defer imageView.free() - var exists C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_style_image_exists(C.mln_map(ptr), imageView.raw(), &exists)) - }); err != nil { - return false, err - } - return bool(exists), nil + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_remove_style_image(C.mln_map(ptr), imageView.raw(), completion)) + }, completionCommand) } // StyleImageInfo returns copied metadata for one runtime style image. -func (m *MapHandle) StyleImageInfo(imageID string) (StyleImageInfo, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return StyleImageInfo{}, false, err - } - defer release() - defer m.state.KeepAlive() - imageView := newCStringView(imageID) - defer imageView.free() - info := C.mln_style_image_info_default() - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_image_info(C.mln_map(ptr), imageView.raw(), &info, &found)) - }); err != nil { - return StyleImageInfo{}, false, err - } - return styleImageInfoFromC(info), bool(found), nil -} // StyleImagePremultipliedRGBA8 returns copied tightly packed premultiplied RGBA8 pixels. -func (m *MapHandle) StyleImagePremultipliedRGBA8(imageID string) ([]byte, bool, error) { - info, found, err := m.StyleImageInfo(imageID) - if err != nil || !found { - return nil, found, err - } - pixels := make([]byte, int(info.ByteLength)) - byteLength, found, err := m.StyleImagePremultipliedRGBA8Into(imageID, pixels) - if err != nil { - return nil, found, err - } - return pixels[:int(byteLength)], found, nil -} // StyleImagePremultipliedRGBA8Into copies tightly packed premultiplied RGBA8 pixels into buffer. -func (m *MapHandle) StyleImagePremultipliedRGBA8Into(imageID string, buffer []byte) (uint64, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return 0, false, err - } - defer release() - defer m.state.KeepAlive() - imageView := newCStringView(imageID) - defer imageView.free() - var rawBuffer *C.uint8_t - if len(buffer) > 0 { - rawBuffer = (*C.uint8_t)(unsafe.Pointer(&buffer[0])) - } - var byteLength C.size_t - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_copy_style_image_premultiplied_rgba8( - C.mln_map(ptr), - imageView.raw(), - rawBuffer, - C.size_t(len(buffer)), - &byteLength, - &found, - )) - }); err != nil { - runtime.KeepAlive(buffer) - return uint64(byteLength), bool(found), err - } - runtime.KeepAlive(buffer) - // An empty destination reaches native code as a size probe, which succeeds - // without copying, so report it as too small unless the image is empty. - if len(buffer) == 0 && byteLength > 0 { - return uint64(byteLength), bool(found), newBindingError(ErrInvalidArgument, - fmt.Sprintf("buffer length 0 is smaller than the required %d bytes", uint64(byteLength))) - } - return uint64(byteLength), bool(found), nil -} // AddImageSourceURL adds an image source that loads its image from a URL. -func (m *MapHandle) AddImageSourceURL(sourceID string, coordinates []LatLng, url string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddImageSourceURL(sourceID string, coordinates []LatLng, url string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1336,24 +1235,25 @@ func (m *MapHandle) AddImageSourceURL(sourceID string, coordinates []LatLng, url if len(rawCoordinates) > 0 { rawCoordinatesPtr = &rawCoordinates[0] } - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_add_image_source_url( C.mln_map(ptr), sourceView.raw(), rawCoordinatesPtr, C.size_t(len(rawCoordinates)), urlView.raw(), + completion, )) - }) + }, completionCommand) } // AddImageSourceImage adds an image source with inline image pixels. -func (m *MapHandle) AddImageSourceImage(sourceID string, coordinates []LatLng, image PremultipliedRGBA8Image) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddImageSourceImage(sourceID string, coordinates []LatLng, image PremultipliedRGBA8Image) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1364,58 +1264,59 @@ func (m *MapHandle) AddImageSourceImage(sourceID string, coordinates []LatLng, i } rawImage := newCPremultipliedRGBA8Image(image) defer rawImage.free() - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_add_image_source_image( C.mln_map(ptr), sourceView.raw(), rawCoordinatesPtr, C.size_t(len(rawCoordinates)), &rawImage.raw, + completion, )) - }) + }, completionCommand) } // SetImageSourceURL updates an image source to load its image from a URL. -func (m *MapHandle) SetImageSourceURL(sourceID string, url string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetImageSourceURL(sourceID string, url string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() urlView := newCStringView(url) defer urlView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_image_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_image_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), completion)) + }, completionCommand) } // SetImageSourceImage updates an image source with inline image pixels. -func (m *MapHandle) SetImageSourceImage(sourceID string, image PremultipliedRGBA8Image) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetImageSourceImage(sourceID string, image PremultipliedRGBA8Image) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() rawImage := newCPremultipliedRGBA8Image(image) defer rawImage.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_image_source_image(C.mln_map(ptr), sourceView.raw(), &rawImage.raw)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_image_source_image(C.mln_map(ptr), sourceView.raw(), &rawImage.raw, completion)) + }, completionCommand) } // SetImageSourceCoordinates updates image source coordinates. -func (m *MapHandle) SetImageSourceCoordinates(sourceID string, coordinates []LatLng) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetImageSourceCoordinates(sourceID string, coordinates []LatLng) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1424,51 +1325,26 @@ func (m *MapHandle) SetImageSourceCoordinates(sourceID string, coordinates []Lat if len(rawCoordinates) > 0 { rawCoordinatesPtr = &rawCoordinates[0] } - return checkNative(func() int32 { + return startCompletion(func(completion *C.mln_completion) int32 { return int32(C.mln_map_set_image_source_coordinates( C.mln_map(ptr), sourceView.raw(), rawCoordinatesPtr, C.size_t(len(rawCoordinates)), + completion, )) - }) + }, completionCommand) } // ImageSourceCoordinates returns copied image source coordinates. -func (m *MapHandle) ImageSourceCoordinates(sourceID string) ([]LatLng, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, false, err - } - defer release() - defer m.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - rawCoordinates := make([]C.mln_lat_lng, 4) - var count C.size_t - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_image_source_coordinates( - C.mln_map(ptr), - sourceView.raw(), - &rawCoordinates[0], - C.size_t(len(rawCoordinates)), - &count, - &found, - )) - }); err != nil { - return nil, false, err - } - return goLatLngSlice(rawCoordinates[:int(count)]), bool(found), nil -} // AddVectorSourceURL adds a vector source with a TileJSON URL. -func (m *MapHandle) AddVectorSourceURL(sourceID string, url string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddVectorSourceURL(sourceID string, url string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1476,18 +1352,18 @@ func (m *MapHandle) AddVectorSourceURL(sourceID string, url string, options *Sty defer urlView.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_vector_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_vector_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr, completion)) + }, completionCommand) } // AddVectorSourceTiles adds a vector source with inline tile URLs. -func (m *MapHandle) AddVectorSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddVectorSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1495,18 +1371,18 @@ func (m *MapHandle) AddVectorSourceTiles(sourceID string, tiles []string, option defer rawTiles.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_vector_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_vector_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr, completion)) + }, completionCommand) } // AddRasterSourceURL adds a raster source with a TileJSON URL. -func (m *MapHandle) AddRasterSourceURL(sourceID string, url string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddRasterSourceURL(sourceID string, url string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1514,18 +1390,18 @@ func (m *MapHandle) AddRasterSourceURL(sourceID string, url string, options *Sty defer urlView.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_raster_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_raster_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr, completion)) + }, completionCommand) } // AddRasterSourceTiles adds a raster source with inline tile URLs. -func (m *MapHandle) AddRasterSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddRasterSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1533,18 +1409,18 @@ func (m *MapHandle) AddRasterSourceTiles(sourceID string, tiles []string, option defer rawTiles.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_raster_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_raster_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr, completion)) + }, completionCommand) } // AddRasterDEMSourceURL adds a raster DEM source with a TileJSON URL. -func (m *MapHandle) AddRasterDEMSourceURL(sourceID string, url string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddRasterDEMSourceURL(sourceID string, url string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1552,18 +1428,18 @@ func (m *MapHandle) AddRasterDEMSourceURL(sourceID string, url string, options * defer urlView.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_raster_dem_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_raster_dem_source_url(C.mln_map(ptr), sourceView.raw(), urlView.raw(), rawOptionsPtr, completion)) + }, completionCommand) } // AddRasterDEMSourceTiles adds a raster DEM source with inline tile URLs. -func (m *MapHandle) AddRasterDEMSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddRasterDEMSourceTiles(sourceID string, tiles []string, options *StyleTileSourceOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() @@ -1571,208 +1447,53 @@ func (m *MapHandle) AddRasterDEMSourceTiles(sourceID string, tiles []string, opt defer rawTiles.free() rawOptions, rawOptionsPtr := cStyleTileSourceOptionsPointer(options) defer rawOptions.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_raster_dem_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_raster_dem_source_tiles(C.mln_map(ptr), sourceView.raw(), rawTiles.ptr(), rawTiles.count(), rawOptionsPtr, completion)) + }, completionCommand) } // AddStyleSourceJSON adds one style source from a style-spec source JSON object. -func (m *MapHandle) AddStyleSourceJSON(sourceID string, sourceJSON []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddStyleSourceJSON(sourceID string, sourceJSON []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() rawJSON := newCBufferView(sourceJSON) defer rawJSON.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_style_source_json(C.mln_map(ptr), sourceView.raw(), rawJSON.raw())) - }) -} - -// RemoveStyleSource removes one style source by ID and reports whether it was -// present. -func (m *MapHandle) RemoveStyleSource(sourceID string) (bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return false, err - } - defer release() - defer m.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - var removed C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_remove_style_source(C.mln_map(ptr), sourceView.raw(), &removed)) - }); err != nil { - return false, err - } - return bool(removed), nil + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_style_source_json(C.mln_map(ptr), sourceView.raw(), rawJSON.raw(), completion)) + }, completionCommand) } -// StyleSourceExists reports whether one style source ID exists. -func (m *MapHandle) StyleSourceExists(sourceID string) (bool, error) { - ptr, release, err := m.ptr() +// RemoveStyleSource submits a command that removes one style source by ID. The +// command commits when a source with the ID existed and was removed. It fails +// with ErrNotFound reported through its terminal event's Err payload field when +// none does, and with ErrInvalidState when a layer still uses the source. Check +// existence with StyleSourceInfo's found flag. +func (m *MapHandle) RemoveStyleSource(sourceID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return false, err - } - defer release() - defer m.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - var exists C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_style_source_exists(C.mln_map(ptr), sourceView.raw(), &exists)) - }); err != nil { - return false, err + return nil, err } - return bool(exists), nil -} -// StyleSourceType returns a source type and whether the source exists. -func (m *MapHandle) StyleSourceType(sourceID string) (StyleSourceType, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return StyleSourceTypeUnknown, false, err - } - defer release() defer m.state.KeepAlive() sourceView := newCStringView(sourceID) defer sourceView.free() - var sourceType C.uint32_t - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_source_type(C.mln_map(ptr), sourceView.raw(), &sourceType, &found)) - }); err != nil { - return StyleSourceTypeUnknown, false, err - } - return StyleSourceType(sourceType), bool(found), nil + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_remove_style_source(C.mln_map(ptr), sourceView.raw(), completion)) + }, completionCommand) } // StyleSourceInfo returns copied source metadata and whether the source exists. -func (m *MapHandle) StyleSourceInfo(sourceID string) (StyleSourceInfo, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return StyleSourceInfo{}, false, err - } - defer release() - defer m.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - info := C.mln_style_source_info{size: C.uint32_t(unsafe.Sizeof(C.mln_style_source_info{}))} - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_source_info(C.mln_map(ptr), sourceView.raw(), &info, &found)) - }); err != nil { - return StyleSourceInfo{}, false, err - } - if !bool(found) { - return StyleSourceInfo{}, false, nil - } - - result := styleSourceInfoFromC(info) - if result.HasAttribution { - attribution, copied, err := copyStyleSourceString(func(buffer *C.char, capacity C.size_t, size *C.size_t, valueFound *C.bool) int32 { - return int32(C.mln_map_copy_style_source_attribution( - C.mln_map(ptr), sourceView.raw(), buffer, capacity, size, valueFound, - )) - }) - if err != nil { - return StyleSourceInfo{}, false, err - } - if copied { - result.Attribution = &attribution - } - } - if info.fields&C.MLN_STYLE_SOURCE_INFO_URL != 0 { - url, copied, err := copyStyleSourceString(func(buffer *C.char, capacity C.size_t, size *C.size_t, valueFound *C.bool) int32 { - return int32(C.mln_map_copy_style_source_url( - C.mln_map(ptr), sourceView.raw(), buffer, capacity, size, valueFound, - )) - }) - if err != nil { - return StyleSourceInfo{}, false, err - } - if copied { - result.URL = &url - } - } - if result.TileJSON != nil { - var list C.mln_style_string_list - var listFound C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_source_tile_urls(C.mln_map(ptr), sourceView.raw(), &list, &listFound)) - }); err != nil { - return StyleSourceInfo{}, false, err - } - tileURLs, err := styleStringListStrings(list) - if err != nil { - return StyleSourceInfo{}, false, err - } - if bool(listFound) { - result.TileJSON.TileURLs = tileURLs - } - } - return result, true, nil -} - -func copyStyleSourceString(copy func(*C.char, C.size_t, *C.size_t, *C.bool) int32) (string, bool, error) { - var required C.size_t - var found C.bool - if err := checkNative(func() int32 { return copy(nil, 0, &required, &found) }); err != nil { - return "", false, err - } - if !bool(found) || required == 0 { - return "", bool(found), nil - } - - buffer := make([]byte, int(required)) - var size C.size_t - if err := checkNative(func() int32 { - return copy((*C.char)(unsafe.Pointer(&buffer[0])), C.size_t(len(buffer)), &size, &found) - }); err != nil { - return "", false, err - } - return string(buffer[:int(size)]), bool(found), nil -} // StyleSourceAttribution returns copied source attribution and whether the // source exists. -func (m *MapHandle) StyleSourceAttribution(sourceID string) (string, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return "", false, err - } - defer release() - defer m.state.KeepAlive() - sourceView := newCStringView(sourceID) - defer sourceView.free() - return copyStyleSourceString(func(buffer *C.char, capacity C.size_t, size *C.size_t, found *C.bool) int32 { - return int32(C.mln_map_copy_style_source_attribution( - C.mln_map(ptr), sourceView.raw(), buffer, capacity, size, found, - )) - }) -} // StyleSourceIDs returns copied source IDs in style order. -func (m *MapHandle) StyleSourceIDs() ([]string, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - var list C.mln_style_id_list - if err := checkNative(func() int32 { - return int32(C.mln_map_list_style_source_ids(C.mln_map(ptr), &list)) - }); err != nil { - return nil, err - } - return styleIDListStrings(list) -} func styleIDListStrings(list C.mln_style_id_list) ([]string, error) { defer C.mln_style_id_list_destroy(list) @@ -1812,12 +1533,12 @@ func styleStringListStrings(list C.mln_style_string_list) ([]string, error) { // AddHillshadeLayer adds a hillshade layer for a raster DEM source. Passing an // empty beforeLayerID appends the layer. -func (m *MapHandle) AddHillshadeLayer(layerID string, sourceID string, beforeLayerID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddHillshadeLayer(layerID string, sourceID string, beforeLayerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() @@ -1825,19 +1546,19 @@ func (m *MapHandle) AddHillshadeLayer(layerID string, sourceID string, beforeLay defer sourceView.free() beforeView := newCStringView(beforeLayerID) defer beforeView.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_hillshade_layer(C.mln_map(ptr), layerView.raw(), sourceView.raw(), beforeView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_hillshade_layer(C.mln_map(ptr), layerView.raw(), sourceView.raw(), beforeView.raw(), completion)) + }, completionCommand) } // AddColorReliefLayer adds a color-relief layer for a raster DEM source. // Passing an empty beforeLayerID appends the layer. -func (m *MapHandle) AddColorReliefLayer(layerID string, sourceID string, beforeLayerID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddColorReliefLayer(layerID string, sourceID string, beforeLayerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() @@ -1845,324 +1566,214 @@ func (m *MapHandle) AddColorReliefLayer(layerID string, sourceID string, beforeL defer sourceView.free() beforeView := newCStringView(beforeLayerID) defer beforeView.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_color_relief_layer(C.mln_map(ptr), layerView.raw(), sourceView.raw(), beforeView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_color_relief_layer(C.mln_map(ptr), layerView.raw(), sourceView.raw(), beforeView.raw(), completion)) + }, completionCommand) } // AddLocationIndicatorLayer adds a source-free location indicator layer. Passing // an empty beforeLayerID appends the layer. -func (m *MapHandle) AddLocationIndicatorLayer(layerID string, beforeLayerID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddLocationIndicatorLayer(layerID string, beforeLayerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() beforeView := newCStringView(beforeLayerID) defer beforeView.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_location_indicator_layer(C.mln_map(ptr), layerView.raw(), beforeView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_location_indicator_layer(C.mln_map(ptr), layerView.raw(), beforeView.raw(), completion)) + }, completionCommand) } // SetLocationIndicatorLocation sets a location indicator layer location. -func (m *MapHandle) SetLocationIndicatorLocation(layerID string, coordinate LatLng, altitude float64) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetLocationIndicatorLocation(layerID string, coordinate LatLng, altitude float64) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_location_indicator_location(C.mln_map(ptr), layerView.raw(), cLatLng(coordinate), C.double(altitude))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_location_indicator_location(C.mln_map(ptr), layerView.raw(), cLatLng(coordinate), C.double(altitude), completion)) + }, completionCommand) } // SetLocationIndicatorBearing sets a location indicator layer bearing in degrees. -func (m *MapHandle) SetLocationIndicatorBearing(layerID string, bearing float64) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetLocationIndicatorBearing(layerID string, bearing float64) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_location_indicator_bearing(C.mln_map(ptr), layerView.raw(), C.double(bearing))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_location_indicator_bearing(C.mln_map(ptr), layerView.raw(), C.double(bearing), completion)) + }, completionCommand) } -// SetLocationIndicatorAccuracyRadius sets a location indicator layer accuracy radius in meters. -func (m *MapHandle) SetLocationIndicatorAccuracyRadius(layerID string, radius float64) error { - ptr, release, err := m.ptr() +// SetLocationIndicatorAccuracyRadius sets a location indicator layer accuracy +// radius in meters. +func (m *MapHandle) SetLocationIndicatorAccuracyRadius(layerID string, radius float64) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_location_indicator_accuracy_radius(C.mln_map(ptr), layerView.raw(), C.double(radius))) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_location_indicator_accuracy_radius(C.mln_map(ptr), layerView.raw(), C.double(radius), completion)) + }, completionCommand) } // SetLocationIndicatorImageName sets one location indicator image-name property. -func (m *MapHandle) SetLocationIndicatorImageName(layerID string, imageKind LocationIndicatorImageKind, imageID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetLocationIndicatorImageName(layerID string, imageKind LocationIndicatorImageKind, imageID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() imageView := newCStringView(imageID) defer imageView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_location_indicator_image_name(C.mln_map(ptr), layerView.raw(), C.uint32_t(imageKind), imageView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_location_indicator_image_name(C.mln_map(ptr), layerView.raw(), C.uint32_t(imageKind), imageView.raw(), completion)) + }, completionCommand) } // AddStyleLayerJSON adds one style layer from a style-spec layer JSON object. // Passing an empty beforeLayerID appends the layer. -func (m *MapHandle) AddStyleLayerJSON(layerJSON []byte, beforeLayerID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) AddStyleLayerJSON(layerJSON []byte, beforeLayerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() beforeView := newCStringView(beforeLayerID) defer beforeView.free() rawJSON := newCBufferView(layerJSON) defer rawJSON.free() - return checkNative(func() int32 { - return int32(C.mln_map_add_style_layer_json(C.mln_map(ptr), rawJSON.raw(), beforeView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_add_style_layer_json(C.mln_map(ptr), rawJSON.raw(), beforeView.raw(), completion)) + }, completionCommand) } -// RemoveStyleLayer removes one style layer by ID and reports whether it was -// present. -func (m *MapHandle) RemoveStyleLayer(layerID string) (bool, error) { - ptr, release, err := m.ptr() +// RemoveStyleLayer submits a command that removes one style layer by ID. The +// command commits when a layer with the ID existed and was removed, and fails +// with ErrNotFound reported through its terminal event's Err payload field when +// none does. Check existence with StyleLayerInfo's found flag. +func (m *MapHandle) RemoveStyleLayer(layerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return false, err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var removed C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_remove_style_layer(C.mln_map(ptr), layerView.raw(), &removed)) - }); err != nil { - return false, err + return nil, err } - return bool(removed), nil -} -// StyleLayerExists reports whether one style layer ID exists. -func (m *MapHandle) StyleLayerExists(layerID string) (bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return false, err - } - defer release() defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() - var exists C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_style_layer_exists(C.mln_map(ptr), layerView.raw(), &exists)) - }); err != nil { - return false, err - } - return bool(exists), nil + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_remove_style_layer(C.mln_map(ptr), layerView.raw(), completion)) + }, completionCommand) } -// StyleLayerType returns a layer type string and whether the layer exists. -func (m *MapHandle) StyleLayerType(layerID string) (string, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return "", false, err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var layerType C.mln_buffer_view - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_layer_type(C.mln_map(ptr), layerView.raw(), &layerType, &found)) - }); err != nil { - return "", false, err - } - return goStringView(layerType), bool(found), nil -} +// StyleLayerInfo returns copied layer metadata and whether the layer exists. // StyleLayerIDs returns copied layer IDs in style order. -func (m *MapHandle) StyleLayerIDs() ([]string, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - var list C.mln_style_id_list - if err := checkNative(func() int32 { - return int32(C.mln_map_list_style_layer_ids(C.mln_map(ptr), &list)) - }); err != nil { - return nil, err - } - return styleIDListStrings(list) -} // MoveStyleLayer moves one style layer before another layer. Passing an empty // beforeLayerID moves layerID to the top of the style order. -func (m *MapHandle) MoveStyleLayer(layerID string, beforeLayerID string) error { - ptr, release, err := m.ptr() +func (m *MapHandle) MoveStyleLayer(layerID string, beforeLayerID string) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() beforeView := newCStringView(beforeLayerID) defer beforeView.free() - return checkNative(func() int32 { - return int32(C.mln_map_move_style_layer(C.mln_map(ptr), layerView.raw(), beforeView.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_move_style_layer(C.mln_map(ptr), layerView.raw(), beforeView.raw(), completion)) + }, completionCommand) } // StyleLayerJSON returns one copied style layer as a style-spec JSON object and // whether the layer exists. -func (m *MapHandle) StyleLayerJSON(layerID string) ([]byte, bool, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, false, err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var buffer C.mln_buffer - var found C.bool - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_layer_json(C.mln_map(ptr), layerView.raw(), &buffer, &found)) - }); err != nil { - return nil, false, err - } - if !bool(found) { - return nil, false, nil - } - value, err := goOwnedBuffer(buffer) - if err != nil { - return nil, false, err - } - return value, true, nil -} // SetStyleLightJSON sets the style light from a style-spec light JSON object. -func (m *MapHandle) SetStyleLightJSON(lightJSON []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetStyleLightJSON(lightJSON []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() rawJSON := newCBufferView(lightJSON) defer rawJSON.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_style_light_json(C.mln_map(ptr), rawJSON.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_light_json(C.mln_map(ptr), rawJSON.raw(), completion)) + }, completionCommand) } // SetStyleLightProperty sets one style light property. -func (m *MapHandle) SetStyleLightProperty(propertyName string, value []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetStyleLightProperty(propertyName string, value []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() propertyView := newCStringView(propertyName) defer propertyView.free() rawValue := newCBufferView(value) defer rawValue.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_style_light_property(C.mln_map(ptr), propertyView.raw(), rawValue.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_light_property(C.mln_map(ptr), propertyView.raw(), rawValue.raw(), completion)) + }, completionCommand) } // StyleLightProperty returns one copied style light property as a style-spec // JSON value. -func (m *MapHandle) StyleLightProperty(propertyName string) ([]byte, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - propertyView := newCStringView(propertyName) - defer propertyView.free() - var buffer C.mln_buffer - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_light_property(C.mln_map(ptr), propertyView.raw(), &buffer)) - }); err != nil { - return nil, err - } - return goOwnedBuffer(buffer) -} // SetStyleTransitionOptions replaces the style's global transition options // rather than merging into them, so absent duration and delay clear the // style-wide override. Loading a style replaces these options with the ones // that style declares, so apply an override after the style loads. -func (m *MapHandle) SetStyleTransitionOptions(options StyleTransitionOptions) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetStyleTransitionOptions(options StyleTransitionOptions) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() raw := newCStyleTransitionOptions(options) - return checkNative(func() int32 { - return int32(C.mln_map_set_style_transition_options(C.mln_map(ptr), &raw)) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_style_transition_options(C.mln_map(ptr), &raw, completion)) + }, completionCommand) } // StyleTransitionOptions returns the style's copied global transition options. -func (m *MapHandle) StyleTransitionOptions() (StyleTransitionOptions, error) { - ptr, release, err := m.ptr() - if err != nil { - return StyleTransitionOptions{}, err - } - defer release() - defer m.state.KeepAlive() - raw := C.mln_style_transition_options_default() - if err := checkNative(func() int32 { - return int32(C.mln_map_get_style_transition_options(C.mln_map(ptr), &raw)) - }); err != nil { - return StyleTransitionOptions{}, err - } - return styleTransitionOptionsFromC(raw), nil -} // SetLayerProperty sets one style layer property. -func (m *MapHandle) SetLayerProperty(layerID string, propertyName string, value []byte) error { - ptr, release, err := m.ptr() +func (m *MapHandle) SetLayerProperty(layerID string, propertyName string, value []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() @@ -2170,32 +1781,13 @@ func (m *MapHandle) SetLayerProperty(layerID string, propertyName string, value defer propertyView.free() rawValue := newCBufferView(value) defer rawValue.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_layer_property(C.mln_map(ptr), layerView.raw(), propertyView.raw(), rawValue.raw())) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_property(C.mln_map(ptr), layerView.raw(), propertyView.raw(), rawValue.raw(), completion)) + }, completionCommand) } // LayerProperty returns one copied style layer property as a style-spec JSON // value. -func (m *MapHandle) LayerProperty(layerID string, propertyName string) ([]byte, error) { - ptr, release, err := m.ptr() - if err != nil { - return nil, err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - propertyView := newCStringView(propertyName) - defer propertyView.free() - var buffer C.mln_buffer - if err := checkNative(func() int32 { - return int32(C.mln_map_get_layer_property(C.mln_map(ptr), layerView.raw(), propertyView.raw(), &buffer)) - }); err != nil { - return nil, err - } - return goOwnedBuffer(buffer) -} // StyleLayerVisibility reports whether a style layer draws. type StyleLayerVisibility uint32 @@ -2208,126 +1800,48 @@ const ( // SetLayerSourceLayer sets one layer's source-layer ID. Layer types that take no // source, such as background, are rejected. -func (m *MapHandle) SetLayerSourceLayer(layerID string, sourceLayer string) error { - return m.setLayerText(layerID, sourceLayer, func(rawMap C.mln_map, layer, text C.mln_buffer_view) int32 { - return int32(C.mln_map_set_layer_source_layer(rawMap, layer, text)) - }) -} // LayerSourceLayer returns one layer's source-layer ID, empty when the layer // carries none. -func (m *MapHandle) LayerSourceLayer(layerID string) (string, error) { - return m.copyLayerText(layerID, func(rawMap C.mln_map, layer C.mln_buffer_view, text *C.char, capacity C.size_t, size *C.size_t) int32 { - return int32(C.mln_map_copy_layer_source_layer(rawMap, layer, text, capacity, size)) - }) -} // SetLayerSourceID sets one layer's source ID. Layer types that take no source, // such as background, are rejected. The named source need not exist yet. -func (m *MapHandle) SetLayerSourceID(layerID string, sourceID string) error { - return m.setLayerText(layerID, sourceID, func(rawMap C.mln_map, layer, text C.mln_buffer_view) int32 { - return int32(C.mln_map_set_layer_source_id(rawMap, layer, text)) - }) -} // LayerSourceID returns one layer's source ID, empty when the layer carries // none. -func (m *MapHandle) LayerSourceID(layerID string) (string, error) { - return m.copyLayerText(layerID, func(rawMap C.mln_map, layer C.mln_buffer_view, text *C.char, capacity C.size_t, size *C.size_t) int32 { - return int32(C.mln_map_copy_layer_source_id(rawMap, layer, text, capacity, size)) - }) -} // SetLayerMinZoom sets the lowest zoom at which one layer draws. Pass -// math.Inf(-1) for no lower bound. -func (m *MapHandle) SetLayerMinZoom(layerID string, minZoom float64) error { - return m.setLayerZoom(layerID, minZoom, func(rawMap C.mln_map, layer C.mln_buffer_view, zoom C.double) int32 { - return int32(C.mln_map_set_layer_min_zoom(rawMap, layer, zoom)) - }) -} - -// LayerMinZoom returns the lowest zoom at which one layer draws. A layer with no -// lower bound reports math.Inf(-1). -func (m *MapHandle) LayerMinZoom(layerID string) (float64, error) { - return m.layerZoom(layerID, func(rawMap C.mln_map, layer C.mln_buffer_view, out *C.double) int32 { - return int32(C.mln_map_get_layer_min_zoom(rawMap, layer, out)) - }) -} +// math.Inf(-1) for no lower bound. Read the committed range with +// StyleLayerInfo. // SetLayerMaxZoom sets the highest zoom at which one layer draws. Pass -// math.Inf(1) for no upper bound. -func (m *MapHandle) SetLayerMaxZoom(layerID string, maxZoom float64) error { - return m.setLayerZoom(layerID, maxZoom, func(rawMap C.mln_map, layer C.mln_buffer_view, zoom C.double) int32 { - return int32(C.mln_map_set_layer_max_zoom(rawMap, layer, zoom)) - }) -} - -// LayerMaxZoom returns the highest zoom at which one layer draws. A layer with -// no upper bound reports math.Inf(1). -func (m *MapHandle) LayerMaxZoom(layerID string) (float64, error) { - return m.layerZoom(layerID, func(rawMap C.mln_map, layer C.mln_buffer_view, out *C.double) int32 { - return int32(C.mln_map_get_layer_max_zoom(rawMap, layer, out)) - }) -} - -// SetLayerVisibility sets whether one layer draws. -func (m *MapHandle) SetLayerVisibility(layerID string, visibility StyleLayerVisibility) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - return checkNative(func() int32 { - return int32(C.mln_map_set_layer_visibility(C.mln_map(ptr), layerView.raw(), C.uint32_t(visibility))) - }) -} +// math.Inf(1) for no upper bound. Read the committed range with +// StyleLayerInfo. -// LayerVisibility returns whether one layer draws. -func (m *MapHandle) LayerVisibility(layerID string) (StyleLayerVisibility, error) { - ptr, release, err := m.ptr() +// SetLayerVisibility sets whether one layer draws. Read the committed value +// with StyleLayerInfo. +func (m *MapHandle) SetLayerVisibility(layerID string, visibility StyleLayerVisibility) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return StyleLayerVisibilityVisible, err - } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var visibility C.uint32_t - if err := checkNative(func() int32 { - return int32(C.mln_map_get_layer_visibility(C.mln_map(ptr), layerView.raw(), &visibility)) - }); err != nil { - return StyleLayerVisibilityVisible, err + return nil, err } - return StyleLayerVisibility(visibility), nil -} -func (m *MapHandle) setLayerText(layerID string, text string, set func(C.mln_map, C.mln_buffer_view, C.mln_buffer_view) int32) error { - ptr, release, err := m.ptr() - if err != nil { - return err - } - defer release() defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() - textView := newCStringView(text) - defer textView.free() - return checkNative(func() int32 { - return set(C.mln_map(ptr), layerView.raw(), textView.raw()) - }) + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_visibility(C.mln_map(ptr), layerView.raw(), C.uint32_t(visibility), completion)) + }, completionCommand) } // copyMapText probes the required length, then copies. A null buffer with zero // capacity is a size probe the C API answers with OK. func (m *MapHandle) copyMapText(copy func(C.mln_map, *C.char, C.size_t, *C.size_t) int32) (string, error) { - ptr, release, err := m.ptr() + ptr, err := m.ptr() if err != nil { return "", err } - defer release() + defer m.state.KeepAlive() var required C.size_t @@ -2351,11 +1865,11 @@ func (m *MapHandle) copyMapText(copy func(C.mln_map, *C.char, C.size_t, *C.size_ } func (m *MapHandle) copyMapBytes(copy func(C.mln_map, *C.uint8_t, C.size_t, *C.size_t) int32) ([]byte, error) { - ptr, release, err := m.ptr() + ptr, err := m.ptr() if err != nil { return nil, err } - defer release() + defer m.state.KeepAlive() var required C.size_t @@ -2375,106 +1889,304 @@ func (m *MapHandle) copyMapBytes(copy func(C.mln_map, *C.uint8_t, C.size_t, *C.s return buffer[:int(size)], nil } -func (m *MapHandle) copyLayerText(layerID string, copy func(C.mln_map, C.mln_buffer_view, *C.char, C.size_t, *C.size_t) int32) (string, error) { - ptr, release, err := m.ptr() +// SetLayerFilter sets or clears one style layer filter. Passing nil clears the +// filter. +func (m *MapHandle) SetLayerFilter(layerID string, filter []byte) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { - return "", err + return nil, err } - defer release() + defer m.state.KeepAlive() layerView := newCStringView(layerID) defer layerView.free() + var rawFilter *C.mln_buffer_view + var filterView cBufferView + if filter != nil { + filterView = newCBufferView(filter) + defer filterView.free() + rawFilter = filterView.ptr() + } + return startCompletion(func(completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_filter(C.mln_map(ptr), layerView.raw(), rawFilter, completion)) + }, completionCommand) +} - var required C.size_t - if err := checkNative(func() int32 { - return copy(C.mln_map(ptr), layerView.raw(), nil, 0, &required) - }); err != nil { - return "", err +// LayerFilter returns one copied style layer filter as a style-spec JSON value. + +// StyleOptional is a copied style read that may not find its requested value. +type StyleOptional[T any] struct { + Value T + Found bool +} + +// StyleImageStretchResult contains both stretch axes for one style image. +type StyleImageStretchResult struct { + X []ImageStretch + Y []ImageStretch +} + +func copyStyleViews(raw *C.mln_buffer_view, count C.size_t) ([]string, error) { + if count == 0 { + return []string{}, nil } - if required == 0 { - return "", nil + if raw == nil { + return nil, newBindingError(ErrNative, "native style string array is invalid") + } + views := unsafe.Slice(raw, int(count)) + values := make([]string, len(views)) + for index := range views { + values[index] = goStringView(views[index]) } + return values, nil +} - buffer := make([]byte, int(required)) - var size C.size_t - if err := checkNative(func() int32 { - return copy(C.mln_map(ptr), layerView.raw(), (*C.char)(unsafe.Pointer(&buffer[0])), C.size_t(len(buffer)), &size) - }); err != nil { - return "", err +func completionOptionalBuffer(result *C.mln_completion_result) (StyleOptional[[]byte], error) { + if result.value_count == 0 { + return StyleOptional[[]byte]{}, nil } - return string(buffer[:int(size)]), nil + value, err := completionBuffer(result) + return StyleOptional[[]byte]{Value: value, Found: err == nil}, err } -func (m *MapHandle) setLayerZoom(layerID string, zoom float64, set func(C.mln_map, C.mln_buffer_view, C.double) int32) error { - ptr, release, err := m.ptr() +func completionStyleImage(result *C.mln_completion_result) (StyleOptional[StyleImage], error) { + if result.value_count == 0 { + return StyleOptional[StyleImage]{}, nil + } + raw, err := completionValue[C.mln_style_image_result](result) if err != nil { - return err + return StyleOptional[StyleImage]{}, err } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - return checkNative(func() int32 { - return set(C.mln_map(ptr), layerView.raw(), C.double(zoom)) - }) + pixels, ok := goByteSlice(raw.pixels.data, raw.pixels.size) + if !ok { + return StyleOptional[StyleImage]{}, newBindingError(ErrNative, "native style image pixels are invalid") + } + x := append([]C.mln_image_stretch(nil), unsafe.Slice(raw.stretch_x, int(raw.stretch_x_count))...) + y := append([]C.mln_image_stretch(nil), unsafe.Slice(raw.stretch_y, int(raw.stretch_y_count))...) + return StyleOptional[StyleImage]{ + Found: true, + Value: StyleImage{ + Info: styleImageInfoFromC(raw.info), Pixels: pixels, + StretchX: stretchesFromC(x), StretchY: stretchesFromC(y), + }, + }, nil } -func (m *MapHandle) layerZoom(layerID string, get func(C.mln_map, C.mln_buffer_view, *C.double) int32) (float64, error) { - ptr, release, err := m.ptr() +func completionStyleSource(result *C.mln_completion_result) (StyleOptional[StyleSourceInfo], error) { + if result.value_count == 0 { + return StyleOptional[StyleSourceInfo]{}, nil + } + raw, err := completionValue[C.mln_style_source_result](result) if err != nil { - return 0, err + return StyleOptional[StyleSourceInfo]{}, err } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var zoom C.double - if err := checkNative(func() int32 { - return get(C.mln_map(ptr), layerView.raw(), &zoom) - }); err != nil { - return 0, err + info := styleSourceInfoFromC(raw.info) + if bool(raw.info.has_attribution) { + value := goStringView(raw.attribution) + info.Attribution = &value + } + if raw.info.fields&C.MLN_STYLE_SOURCE_INFO_URL != 0 { + value := goStringView(raw.url) + info.URL = &value + } + if info.TileJSON != nil { + info.TileJSON.TileURLs, err = copyStyleViews(raw.tile_urls, raw.tile_url_count) + if err != nil { + return StyleOptional[StyleSourceInfo]{}, err + } } - return float64(zoom), nil + return StyleOptional[StyleSourceInfo]{Value: info, Found: true}, nil } -// SetLayerFilter sets or clears one style layer filter. Passing nil clears the -// filter. -func (m *MapHandle) SetLayerFilter(layerID string, filter []byte) error { - ptr, release, err := m.ptr() +func completionStyleLayer(result *C.mln_completion_result) (StyleOptional[StyleLayerInfo], error) { + if result.value_count == 0 { + return StyleOptional[StyleLayerInfo]{}, nil + } + raw, err := completionValue[C.mln_style_layer_result](result) if err != nil { - return err + return StyleOptional[StyleLayerInfo]{}, err } - defer release() - defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var rawFilter *C.mln_buffer_view - var filterView cBufferView - if filter != nil { - filterView = newCBufferView(filter) - defer filterView.free() - rawFilter = filterView.ptr() + info := styleLayerInfoFromC(raw.info) + if info.HasSourceID { + value := goStringView(raw.source_id) + info.SourceID = &value + } + if info.HasSourceLayer { + value := goStringView(raw.source_layer) + info.SourceLayer = &value + } + return StyleOptional[StyleLayerInfo]{Value: info, Found: true}, nil +} + +func completionStyleIDs(result *C.mln_completion_result) ([]string, error) { + return copyStyleViews((*C.mln_buffer_view)(result.value), C.size_t(result.value_count)) +} + +func (m *MapHandle) AddCustomGeometrySource(sourceID string, options CustomGeometrySourceOptions) (*Future[CommandCompletion], error) { + if sourceID == "" { + return nil, newBindingError(ErrInvalidArgument, "source ID is empty") + } + if options.FetchTile == nil { + return nil, newBindingError(ErrInvalidArgument, "CustomGeometrySourceOptions.FetchTile is nil") + } + ptr, err := m.ptr() + if err != nil { + return nil, err } - return checkNative(func() int32 { - return int32(C.mln_map_set_layer_filter(C.mln_map(ptr), layerView.raw(), rawFilter)) + defer m.state.KeepAlive() + return startCompletion(func(completion *C.mln_completion) int32 { + return callback.AddCustomGeometrySource( + uint64(ptr), sourceID, options.toCallback(), unsafe.Pointer(completion), + ) + }, completionCommand) +} + +func (m *MapHandle) StyleImage(imageID string) (*Future[StyleOptional[StyleImage]], error) { + view := newCStringView(imageID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_image_info(raw, view.raw(), completion)) + }, completionStyleImage) +} + +func (m *MapHandle) ImageSourceCoordinates(sourceID string) (*Future[StyleOptional[[]LatLng]], error) { + view := newCStringView(sourceID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_image_source_coordinates(raw, view.raw(), completion)) + }, func(result *C.mln_completion_result) (StyleOptional[[]LatLng], error) { + raw, err := completionSlice[C.mln_lat_lng](result) + if err != nil { + return StyleOptional[[]LatLng]{}, err + } + if len(raw) == 0 { + return StyleOptional[[]LatLng]{}, nil + } + return StyleOptional[[]LatLng]{Value: goLatLngSlice(raw), Found: true}, nil }) } -// LayerFilter returns one copied style layer filter as a style-spec JSON value. -func (m *MapHandle) LayerFilter(layerID string) ([]byte, error) { - ptr, release, err := m.ptr() +func (m *MapHandle) StyleSourceInfo(sourceID string) (*Future[StyleOptional[StyleSourceInfo]], error) { + view := newCStringView(sourceID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_source_info(raw, view.raw(), completion)) + }, completionStyleSource) +} + +func (m *MapHandle) StyleSourceIDs() (*Future[[]string], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_list_style_source_ids(raw, completion)) + }, completionStyleIDs) +} + +func (m *MapHandle) StyleLayerInfo(layerID string) (*Future[StyleOptional[StyleLayerInfo]], error) { + view := newCStringView(layerID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_layer_info(raw, view.raw(), completion)) + }, completionStyleLayer) +} + +func (m *MapHandle) StyleLayerIDs() (*Future[[]string], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_list_style_layer_ids(raw, completion)) + }, completionStyleIDs) +} + +func (m *MapHandle) StyleLayerJSON(layerID string) (*Future[StyleOptional[[]byte]], error) { + view := newCStringView(layerID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_layer_json(raw, view.raw(), completion)) + }, completionOptionalBuffer) +} + +func (m *MapHandle) StyleLightProperty(name string) (*Future[[]byte], error) { + view := newCStringView(name) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_light_property(raw, view.raw(), completion)) + }, completionNullableBuffer) +} + +func (m *MapHandle) StyleTransitionOptions() (*Future[StyleTransitionOptions], error) { + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_style_transition_options(raw, completion)) + }, func(result *C.mln_completion_result) (StyleTransitionOptions, error) { + raw, err := completionValue[C.mln_style_transition_options](result) + return styleTransitionOptionsFromC(raw), err + }) +} + +func (m *MapHandle) LayerProperty(layerID, name string) (*Future[[]byte], error) { + layer := newCStringView(layerID) + defer layer.free() + property := newCStringView(name) + defer property.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_layer_property(raw, layer.raw(), property.raw(), completion)) + }, completionNullableBuffer) +} + +func (m *MapHandle) LayerFilter(layerID string) (*Future[StyleOptional[[]byte]], error) { + view := newCStringView(layerID) + defer view.free() + return startMapCompletion(m, func(raw C.mln_map, completion *C.mln_completion) int32 { + return int32(C.mln_map_get_layer_filter(raw, view.raw(), completion)) + }, completionOptionalBuffer) +} + +func submitLayerTextCommand(m *MapHandle, layerID, text string, submit func(C.mln_map, C.mln_buffer_view, C.mln_buffer_view, *C.mln_completion) int32) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() if err != nil { return nil, err } - defer release() + defer m.state.KeepAlive() - layerView := newCStringView(layerID) - defer layerView.free() - var buffer C.mln_buffer - if err := checkNative(func() int32 { - return int32(C.mln_map_get_layer_filter(C.mln_map(ptr), layerView.raw(), &buffer)) - }); err != nil { + layer := newCStringView(layerID) + defer layer.free() + value := newCStringView(text) + defer value.free() + return startCompletion(func(completion *C.mln_completion) int32 { + return submit(C.mln_map(ptr), layer.raw(), value.raw(), completion) + }, completionCommand) +} + +func (m *MapHandle) SetLayerSourceLayer(layerID, sourceLayer string) (*Future[CommandCompletion], error) { + return submitLayerTextCommand(m, layerID, sourceLayer, func(raw C.mln_map, layer, value C.mln_buffer_view, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_source_layer(raw, layer, value, completion)) + }) +} + +func (m *MapHandle) SetLayerSourceID(layerID, sourceID string) (*Future[CommandCompletion], error) { + return submitLayerTextCommand(m, layerID, sourceID, func(raw C.mln_map, layer, value C.mln_buffer_view, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_source_id(raw, layer, value, completion)) + }) +} + +func submitLayerZoomCommand(m *MapHandle, layerID string, zoom float64, submit func(C.mln_map, C.mln_buffer_view, C.double, *C.mln_completion) int32) (*Future[CommandCompletion], error) { + ptr, err := m.ptr() + if err != nil { return nil, err } - return goOwnedBuffer(buffer) + + defer m.state.KeepAlive() + layer := newCStringView(layerID) + defer layer.free() + return startCompletion(func(completion *C.mln_completion) int32 { + return submit(C.mln_map(ptr), layer.raw(), C.double(zoom), completion) + }, completionCommand) +} + +func (m *MapHandle) SetLayerMinZoom(layerID string, zoom float64) (*Future[CommandCompletion], error) { + return submitLayerZoomCommand(m, layerID, zoom, func(raw C.mln_map, layer C.mln_buffer_view, value C.double, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_min_zoom(raw, layer, value, completion)) + }) +} + +func (m *MapHandle) SetLayerMaxZoom(layerID string, zoom float64) (*Future[CommandCompletion], error) { + return submitLayerZoomCommand(m, layerID, zoom, func(raw C.mln_map, layer C.mln_buffer_view, value C.double, completion *C.mln_completion) int32 { + return int32(C.mln_map_set_layer_max_zoom(raw, layer, value, completion)) + }) } diff --git a/bindings/go/style_images_test.go b/bindings/go/style_images_test.go index 3415a1463..1ecd8ce84 100644 --- a/bindings/go/style_images_test.go +++ b/bindings/go/style_images_test.go @@ -6,27 +6,25 @@ import ( ) func TestNinePatchStyleImageRoundTripsStretchContentAndTextFit(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } @@ -38,14 +36,15 @@ func TestNinePatchStyleImageRoundTripsStretchContentAndTextFit(t *testing.T) { Content: &ImageContent{Left: 0.5, Top: 0.5, Right: 1.5, Bottom: 1.5}, TextFitHeight: &textFit, } - if err := m.SetStyleImage("patch", image, options); err != nil { + if _, err := m.SetStyleImage("patch", image, options); err != nil { t.Fatalf("SetStyleImage(): %v", err) } - info, found, err := m.StyleImageInfo("patch") + styleImage, found, err := takeOptionalStyleOperationForTest(m.StyleImage("patch")) if err != nil || !found { - t.Fatalf("StyleImageInfo(patch) = (%+v, %v, %v)", info, found, err) + t.Fatalf("StyleImage(patch) = (%+v, %v, %v)", styleImage, found, err) } + info := styleImage.Info if info.StretchXCount != 1 || info.StretchYCount != 2 { t.Fatalf("stretch counts = (%d, %d), want (1, 2)", info.StretchXCount, info.StretchYCount) } @@ -60,10 +59,7 @@ func TestNinePatchStyleImageRoundTripsStretchContentAndTextFit(t *testing.T) { t.Fatalf("TextFitHeight = %v, want proportional", info.TextFitHeight) } - stretchX, stretchY, found, err := m.StyleImageStretches("patch") - if err != nil || !found { - t.Fatalf("StyleImageStretches(patch) = (%v, %v, %v, %v)", stretchX, stretchY, found, err) - } + stretchX, stretchY := styleImage.StretchX, styleImage.StretchY if len(stretchX) != 1 || stretchX[0] != (ImageStretch{From: 0, To: 1}) { t.Fatalf("stretchX = %v, want [{0 1}]", stretchX) } @@ -71,13 +67,13 @@ func TestNinePatchStyleImageRoundTripsStretchContentAndTextFit(t *testing.T) { t.Fatalf("stretchY = %v, want [{0 1} {1 2}]", stretchY) } - if _, _, found, err = m.StyleImageStretches("missing"); err != nil || found { - t.Fatalf("StyleImageStretches(missing) = (%v, %v), want (false, nil)", found, err) + if _, found, err = takeOptionalStyleOperationForTest(m.StyleImage("missing")); err != nil || found { + t.Fatalf("StyleImage(missing) = (%v, %v), want (false, nil)", found, err) } // A backwards interval is rejected by C. bad := StyleImageOptions{StretchX: []ImageStretch{{From: 2, To: 1}}} - if err := m.SetStyleImage("bad", image, bad); !errors.Is(err, ErrInvalidArgument) { + if _, err := m.SetStyleImage("bad", image, bad); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("SetStyleImage(backwards) error = %v; want ErrInvalidArgument", err) } } @@ -106,95 +102,78 @@ func TestStyleImageOptionsCloneKeepsStretchPresence(t *testing.T) { } func TestStyleImageCopiesPixelsAndMetadata(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } pixels := []byte{255, 0, 0, 255} pixelRatio := float32(2) sdf := true - if err := m.SetStyleImage("marker", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: pixels}, StyleImageOptions{PixelRatio: &pixelRatio, SDF: &sdf}); err != nil { + if _, err := m.SetStyleImage("marker", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: pixels}, StyleImageOptions{PixelRatio: &pixelRatio, SDF: &sdf}); err != nil { t.Fatalf("SetStyleImage(): %v", err) } pixels[0] = 0 - exists, err := m.StyleImageExists("marker") - if err != nil { - t.Fatalf("StyleImageExists(marker): %v", err) - } - if !exists { - t.Fatal("StyleImageExists(marker)=false, want true") - } - info, found, err := m.StyleImageInfo("marker") + styleImage, found, err := takeOptionalStyleOperationForTest(m.StyleImage("marker")) if err != nil { t.Fatalf("StyleImageInfo(marker): %v", err) } + info := styleImage.Info if !found || info.Width != 1 || info.Height != 1 || info.Stride != 4 || info.ByteLength != 4 || info.PixelRatio != pixelRatio || info.SDF != sdf { - t.Fatalf("StyleImageInfo(marker) = (%+v, %v), want copied 1x1 image metadata", info, found) - } - copied, found, err := m.StyleImagePremultipliedRGBA8("marker") - if err != nil { - t.Fatalf("StyleImagePremultipliedRGBA8(marker): %v", err) + t.Fatalf("StyleImage(marker) = (%+v, %v), want copied 1x1 image metadata", styleImage, found) } + copied := styleImage.Pixels if !found || len(copied) != 4 || copied[0] != 255 || copied[1] != 0 || copied[2] != 0 || copied[3] != 255 { t.Fatalf("StyleImagePremultipliedRGBA8(marker) = (%v, %v), want original copied pixels", copied, found) } - // An empty destination is a caller error here, not the C size probe. - if _, _, err := m.StyleImagePremultipliedRGBA8Into("marker", nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StyleImagePremultipliedRGBA8Into(empty buffer) error = %v, want ErrInvalidArgument", err) - } - removed, err := m.RemoveStyleImage("marker") - if err != nil { - t.Fatalf("RemoveStyleImage(marker): %v", err) - } - if !removed { - t.Fatal("RemoveStyleImage(marker)=false, want true") + completion, err := m.RemoveStyleImage("marker") + requireCommandCommitted(t, runtime, completion, err) + if _, found, err := takeOptionalStyleOperationForTest(m.StyleImage("marker")); err != nil || found { + t.Fatalf("StyleImageInfo(marker) after removal = (%v, %v), want (false, nil)", found, err) } - if err := m.SetStyleImage("bad-marker", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4}, StyleImageOptions{}); !errors.Is(err, ErrInvalidArgument) { + completion, err = m.RemoveStyleImage("marker") + requireCommandFailedWith(t, runtime, completion, err, ErrNotFound) + if _, err := m.SetStyleImage("bad-marker", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4}, StyleImageOptions{}); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("SetStyleImage(empty pixels) error = %v, want ErrInvalidArgument", err) } } func TestImageSourceCopiesPixelsAndCoordinates(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } coordinates := []LatLng{ @@ -204,12 +183,12 @@ func TestImageSourceCopiesPixelsAndCoordinates(t *testing.T) { {Latitude: 0, Longitude: 1}, } pixels := []byte{0, 255, 0, 255} - if err := m.AddImageSourceImage("image-source", coordinates, PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: pixels}); err != nil { + if _, err := m.AddImageSourceImage("image-source", coordinates, PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: pixels}); err != nil { t.Fatalf("AddImageSourceImage(): %v", err) } coordinates[0] = LatLng{Latitude: 9, Longitude: 9} pixels[1] = 0 - gotCoordinates, found, err := m.ImageSourceCoordinates("image-source") + gotCoordinates, found, err := takeOptionalStyleOperationForTest(m.ImageSourceCoordinates("image-source")) if err != nil { t.Fatalf("ImageSourceCoordinates(image-source): %v", err) } @@ -222,26 +201,26 @@ func TestImageSourceCopiesPixelsAndCoordinates(t *testing.T) { {Latitude: 1, Longitude: 3}, {Latitude: 1, Longitude: 2}, } - if err := m.SetImageSourceCoordinates("image-source", updatedCoordinates); err != nil { + if _, err := m.SetImageSourceCoordinates("image-source", updatedCoordinates); err != nil { t.Fatalf("SetImageSourceCoordinates(): %v", err) } - if err := m.SetImageSourceImage("image-source", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}); err != nil { + if _, err := m.SetImageSourceImage("image-source", PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}); err != nil { t.Fatalf("SetImageSourceImage(): %v", err) } - gotCoordinates, found, err = m.ImageSourceCoordinates("image-source") + gotCoordinates, found, err = takeOptionalStyleOperationForTest(m.ImageSourceCoordinates("image-source")) if err != nil { t.Fatalf("ImageSourceCoordinates(image-source after update): %v", err) } if !found || gotCoordinates[0] != updatedCoordinates[0] { t.Fatalf("ImageSourceCoordinates(image-source after update) = (%v, %v), want updated coordinates", gotCoordinates, found) } - if err := m.AddImageSourceURL("image-url-source", updatedCoordinates, "asset://fixtures/image.png"); err != nil { + if _, err := m.AddImageSourceURL("image-url-source", updatedCoordinates, "asset://fixtures/image.png"); err != nil { t.Fatalf("AddImageSourceURL(): %v", err) } - if err := m.SetImageSourceURL("image-url-source", "asset://fixtures/image-2.png"); err != nil { + if _, err := m.SetImageSourceURL("image-url-source", "asset://fixtures/image-2.png"); err != nil { t.Fatalf("SetImageSourceURL(): %v", err) } - if err := m.AddImageSourceImage("bad-image-source", updatedCoordinates[:3], PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 0, 0}}); !errors.Is(err, ErrInvalidArgument) { + if _, err := m.AddImageSourceImage("bad-image-source", updatedCoordinates[:3], PremultipliedRGBA8Image{Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 0, 0}}); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("AddImageSourceImage(3 coordinates) error = %v, want ErrInvalidArgument", err) } } diff --git a/bindings/go/style_layers_test.go b/bindings/go/style_layers_test.go index 5c355a265..6afbd092d 100644 --- a/bindings/go/style_layers_test.go +++ b/bindings/go/style_layers_test.go @@ -8,52 +8,50 @@ import ( ) func TestDedicatedStyleLayerHelpers(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } demOptions := StyleTileSourceOptions{}.WithTileSize(512).WithRasterEncoding(StyleRasterDEMEncodingMapbox) - if err := m.AddRasterDEMSourceTiles("dem", []string{"https://example.com/dem/{z}/{x}/{y}.png"}, &demOptions); err != nil { + if _, err := m.AddRasterDEMSourceTiles("dem", []string{"https://example.com/dem/{z}/{x}/{y}.png"}, &demOptions); err != nil { t.Fatalf("AddRasterDEMSourceTiles(): %v", err) } - if err := m.AddHillshadeLayer("hillshade", "dem", ""); err != nil { + if _, err := m.AddHillshadeLayer("hillshade", "dem", ""); err != nil { t.Fatalf("AddHillshadeLayer(): %v", err) } - if err := m.AddColorReliefLayer("relief", "dem", "hillshade"); err != nil { + if _, err := m.AddColorReliefLayer("relief", "dem", "hillshade"); err != nil { t.Fatalf("AddColorReliefLayer(): %v", err) } - if err := m.AddLocationIndicatorLayer("location", ""); err != nil { + if _, err := m.AddLocationIndicatorLayer("location", ""); err != nil { t.Fatalf("AddLocationIndicatorLayer(): %v", err) } - if err := m.SetLocationIndicatorLocation("location", LatLng{Latitude: 1, Longitude: 2}, 3); err != nil { + if _, err := m.SetLocationIndicatorLocation("location", LatLng{Latitude: 1, Longitude: 2}, 3); err != nil { t.Fatalf("SetLocationIndicatorLocation(): %v", err) } - if err := m.SetLocationIndicatorBearing("location", 45); err != nil { + if _, err := m.SetLocationIndicatorBearing("location", 45); err != nil { t.Fatalf("SetLocationIndicatorBearing(): %v", err) } - if err := m.SetLocationIndicatorAccuracyRadius("location", 12); err != nil { + if _, err := m.SetLocationIndicatorAccuracyRadius("location", 12); err != nil { t.Fatalf("SetLocationIndicatorAccuracyRadius(): %v", err) } - if err := m.SetLocationIndicatorImageName("location", LocationIndicatorImageKindTop, "marker"); err != nil { + if _, err := m.SetLocationIndicatorImageName("location", LocationIndicatorImageKindTop, "marker"); err != nil { t.Fatalf("SetLocationIndicatorImageName(): %v", err) } checks := map[string]string{ @@ -62,15 +60,15 @@ func TestDedicatedStyleLayerHelpers(t *testing.T) { "location": "location-indicator", } for id, wantType := range checks { - gotType, found, err := m.StyleLayerType(id) + info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo(id)) if err != nil { - t.Fatalf("StyleLayerType(%s): %v", id, err) + t.Fatalf("StyleLayerInfo(%s): %v", id, err) } - if !found || gotType != wantType { - t.Fatalf("StyleLayerType(%s) = (%q, %v), want %q true", id, gotType, found, wantType) + if !found || info.Type != wantType { + t.Fatalf("StyleLayerInfo(%s) type = (%q, %v), want %q true", id, info.Type, found, wantType) } } - ids, err := m.StyleLayerIDs() + ids, err := takeStyleOperationForTest(m.StyleLayerIDs()) if err != nil { t.Fatalf("StyleLayerIDs(): %v", err) } @@ -81,12 +79,10 @@ func TestDedicatedStyleLayerHelpers(t *testing.T) { if positions["relief"] >= positions["hillshade"] || positions["location"] <= positions["hillshade"] { t.Fatalf("StyleLayerIDs() = %v, want relief before hillshade and location after hillshade", ids) } - if err := m.SetLocationIndicatorImageName("location", LocationIndicatorImageKind(99), "bad"); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetLocationIndicatorImageName(invalid kind) error = %v, want ErrInvalidArgument", err) - } - if err := m.AddHillshadeLayer("bad-hillshade", "missing", ""); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AddHillshadeLayer(missing source) error = %v, want ErrInvalidArgument", err) - } + completion, err := m.SetLocationIndicatorImageName("location", LocationIndicatorImageKind(99), "bad") + requireStyleCommandFailed(t, runtime, completion, err) + completion, err = m.AddHillshadeLayer("bad-hillshade", "missing", "") + requireStyleCommandFailed(t, runtime, completion, err) } const layerAccessorStyleJSON = `{"version":8,"sources":{"geo":{"type":"geojson",` + @@ -94,133 +90,142 @@ const layerAccessorStyleJSON = `{"version":8,"sources":{"geo":{"type":"geojson", `{"id":"bg","type":"background"},{"id":"fill","type":"fill","source":"geo"}]}` func TestLayerBaseAccessorsRoundTrip(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(layerAccessorStyleJSON)); err != nil { + if _, err := m.SetStyleJSON([]byte(layerAccessorStyleJSON)); err != nil { t.Fatalf("SetStyleJSON(): %v", err) } - if got, err := m.LayerSourceLayer("fill"); err != nil || got != "" { - t.Fatalf("LayerSourceLayer(fill) = %q, %v; want \"\", nil", got, err) + info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")) + if err != nil || !found || info.SourceLayer != nil { + t.Fatalf("StyleLayerInfo(fill) source layer = %v, %v; want absent", info.SourceLayer, err) } - if err := m.SetLayerSourceLayer("fill", "roads"); err != nil { + if _, err := m.SetLayerSourceLayer("fill", "roads"); err != nil { t.Fatalf("SetLayerSourceLayer(): %v", err) } - if got, err := m.LayerSourceLayer("fill"); err != nil || got != "roads" { - t.Fatalf("LayerSourceLayer(fill) = %q, %v; want \"roads\", nil", got, err) - } - if got, err := m.LayerSourceID("fill"); err != nil || got != "geo" { - t.Fatalf("LayerSourceID(fill) = %q, %v; want \"geo\", nil", got, err) + info, found, err = takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")) + if err != nil || !found || info.SourceLayer == nil || *info.SourceLayer != "roads" || info.SourceID == nil || *info.SourceID != "geo" { + t.Fatalf("StyleLayerInfo(fill) sources = (%v, %v, %v)", info.SourceID, info.SourceLayer, err) } // A layer type that takes no source is rejected rather than silently ignored. - err = m.SetLayerSourceLayer("bg", "roads") - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetLayerSourceLayer(bg) error = %v; want ErrInvalidArgument", err) - } - if got, err := m.LayerSourceID("bg"); err != nil || got != "" { - t.Fatalf("LayerSourceID(bg) = %q, %v; want \"\", nil", got, err) + completion, err := m.SetLayerSourceLayer("bg", "roads") + requireStyleCommandFailed(t, runtime, completion, err) + background, found, queryErr := takeOptionalStyleOperationForTest(m.StyleLayerInfo("bg")) + if queryErr != nil || !found || background.SourceID != nil { + t.Fatalf("StyleLayerInfo(bg) source = %v, %v; want absent", background.SourceID, queryErr) } - // An unset zoom range crosses the boundary as infinities. - if got, err := m.LayerMinZoom("fill"); err != nil || !math.IsInf(got, -1) { - t.Fatalf("LayerMinZoom(fill) = %v, %v; want -Inf, nil", got, err) + // An unset zoom range crosses the boundary as infinities, and the layer + // info reports the source-ID and source-layer sizes that feed the copies. + info, found, err = takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")) + if err != nil || !found { + t.Fatalf("StyleLayerInfo(fill) = (%#v, %v, %v), want found", info, found, err) + } + if info.Type != "fill" { + t.Fatalf("StyleLayerInfo(fill) type = %q, want fill", info.Type) } - if got, err := m.LayerMaxZoom("fill"); err != nil || !math.IsInf(got, 1) { - t.Fatalf("LayerMaxZoom(fill) = %v, %v; want +Inf, nil", got, err) + if !math.IsInf(info.MinZoom, -1) || !math.IsInf(info.MaxZoom, 1) { + t.Fatalf("StyleLayerInfo(fill) zoom range = (%v, %v), want infinities", info.MinZoom, info.MaxZoom) } - if err := m.SetLayerMinZoom("fill", 4); err != nil { + if !info.HasSourceID || info.SourceIDSize != uint64(len("geo")) { + t.Fatalf("StyleLayerInfo(fill) source ID size = (%v, %d), want (true, %d)", info.HasSourceID, info.SourceIDSize, len("geo")) + } + if !info.HasSourceLayer || info.SourceLayerSize != uint64(len("roads")) { + t.Fatalf("StyleLayerInfo(fill) source layer size = (%v, %d), want (true, %d)", info.HasSourceLayer, info.SourceLayerSize, len("roads")) + } + if _, err := m.SetLayerMinZoom("fill", 4); err != nil { t.Fatalf("SetLayerMinZoom(): %v", err) } - if err := m.SetLayerMaxZoom("fill", 12.5); err != nil { + if _, err := m.SetLayerMaxZoom("fill", 12.5); err != nil { t.Fatalf("SetLayerMaxZoom(): %v", err) } - if got, err := m.LayerMinZoom("fill"); err != nil || got != 4 { - t.Fatalf("LayerMinZoom(fill) = %v, %v; want 4, nil", got, err) - } - if got, err := m.LayerMaxZoom("fill"); err != nil || got != 12.5 { - t.Fatalf("LayerMaxZoom(fill) = %v, %v; want 12.5, nil", got, err) + if info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")); err != nil || !found || info.MinZoom != 4 || info.MaxZoom != 12.5 { + t.Fatalf("StyleLayerInfo(fill) zoom range = (%v, %v, %v, %v); want 4 and 12.5", info.MinZoom, info.MaxZoom, found, err) } - if got, err := m.LayerVisibility("fill"); err != nil || got != StyleLayerVisibilityVisible { - t.Fatalf("LayerVisibility(fill) = %v, %v; want visible, nil", got, err) + if info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")); err != nil || !found || info.Visibility != StyleLayerVisibilityVisible { + t.Fatalf("StyleLayerInfo(fill) visibility = (%v, %v, %v); want visible", info.Visibility, found, err) } - if err := m.SetLayerVisibility("fill", StyleLayerVisibilityNone); err != nil { + if _, err := m.SetLayerVisibility("fill", StyleLayerVisibilityNone); err != nil { t.Fatalf("SetLayerVisibility(): %v", err) } - if got, err := m.LayerVisibility("fill"); err != nil || got != StyleLayerVisibilityNone { - t.Fatalf("LayerVisibility(fill) = %v, %v; want none, nil", got, err) + if info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")); err != nil || !found || info.Visibility != StyleLayerVisibilityNone { + t.Fatalf("StyleLayerInfo(fill) visibility = (%v, %v, %v); want none", info.Visibility, found, err) } // An unknown raw visibility passes through to C, which rejects it. - err = m.SetLayerVisibility("fill", StyleLayerVisibility(900)) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetLayerVisibility(900) error = %v; want ErrInvalidArgument", err) + completion, err = m.SetLayerVisibility("fill", StyleLayerVisibility(900)) + requireStyleCommandFailed(t, runtime, completion, err) + + // A background layer carries neither a source ID nor a source layer. + if info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("bg")); err != nil || !found || + info.HasSourceID || info.SourceIDSize != 0 || info.HasSourceLayer || info.SourceLayerSize != 0 { + t.Fatalf("StyleLayerInfo(bg) = (%#v, %v, %v), want found without source fields", info, found, err) } - if _, err := m.LayerMinZoom("missing"); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("LayerMinZoom(missing) error = %v; want ErrInvalidArgument", err) + // Removing an existing layer commits, and the info getter stops finding it. + completion, err = m.RemoveStyleLayer("fill") + requireCommandCommitted(t, runtime, completion, err) + if _, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("fill")); err != nil || found { + t.Fatalf("StyleLayerInfo(fill) after removal = (%v, %v), want (false, nil)", found, err) } } func TestStyleLayerJSONAndPropertySnapshots(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } - if err := m.AddStyleSourceJSON("points", []byte(`{"type":"geojson","data":{"type":"FeatureCollection","features":[]}}`)); err != nil { + if _, err := m.AddStyleSourceJSON("points", []byte(`{"type":"geojson","data":{"type":"FeatureCollection","features":[]}}`)); err != nil { t.Fatalf("AddStyleSourceJSON(points): %v", err) } layerJSON := []byte(`{"id":"points-layer","type":"circle","source":"points","paint":{"circle-radius":2}}`) - if err := m.AddStyleLayerJSON(layerJSON, ""); err != nil { + if _, err := m.AddStyleLayerJSON(layerJSON, ""); err != nil { t.Fatalf("AddStyleLayerJSON(): %v", err) } layerJSON[0] = 'x' - layerType, found, err := m.StyleLayerType("points-layer") + info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("points-layer")) if err != nil { - t.Fatalf("StyleLayerType(): %v", err) + t.Fatalf("StyleLayerInfo(): %v", err) } - if !found || layerType != "circle" { - t.Fatalf("StyleLayerType(points-layer) = (%q, %v), want circle true", layerType, found) + if !found || info.Type != "circle" { + t.Fatalf("StyleLayerInfo(points-layer) type = (%q, %v), want circle true", info.Type, found) } - copiedLayer, found, err := m.StyleLayerJSON("points-layer") + copiedLayer, found, err := takeOptionalStyleOperationForTest(m.StyleLayerJSON("points-layer")) if err != nil { t.Fatalf("StyleLayerJSON(): %v", err) } @@ -230,10 +235,10 @@ func TestStyleLayerJSONAndPropertySnapshots(t *testing.T) { if !bytes.Contains(copiedLayer, []byte(`"type":"circle"`)) { t.Fatalf("StyleLayerJSON(points-layer) = %s, want copied circle object", copiedLayer) } - if err := m.SetLayerProperty("points-layer", "circle-radius", []byte("5")); err != nil { + if _, err := m.SetLayerProperty("points-layer", "circle-radius", []byte("5")); err != nil { t.Fatalf("SetLayerProperty(circle-radius): %v", err) } - property, err := m.LayerProperty("points-layer", "circle-radius") + property, err := takeStyleOperationForTest(m.LayerProperty("points-layer", "circle-radius")) if err != nil { t.Fatalf("LayerProperty(circle-radius): %v", err) } @@ -241,101 +246,95 @@ func TestStyleLayerJSONAndPropertySnapshots(t *testing.T) { t.Fatalf("LayerProperty(circle-radius) = %s, want 5", property) } filter := []byte(`["==",["get","kind"],"unit"]`) - if err := m.SetLayerFilter("points-layer", filter); err != nil { + if _, err := m.SetLayerFilter("points-layer", filter); err != nil { t.Fatalf("SetLayerFilter(): %v", err) } - gotFilter, err := m.LayerFilter("points-layer") + gotFilter, found, err := takeOptionalStyleOperationForTest(m.LayerFilter("points-layer")) if err != nil { t.Fatalf("LayerFilter(): %v", err) } - if len(gotFilter) == 0 { + if !found || len(gotFilter) == 0 { t.Fatalf("LayerFilter() = nil, want copied filter") } - if err := m.SetLayerFilter("points-layer", nil); err != nil { + if _, err := m.SetLayerFilter("points-layer", nil); err != nil { t.Fatalf("SetLayerFilter(nil): %v", err) } - if err := m.SetLayerProperty("points-layer", "circle-radius", []byte("NaN")); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetLayerProperty(non-finite JSON double) error = %v, want ErrInvalidArgument", err) - } - if _, err := m.LayerProperty("missing", "circle-radius"); !errors.Is(err, ErrInvalidArgument) { + completion, err := m.SetLayerProperty("points-layer", "circle-radius", []byte("NaN")) + requireStyleCommandFailed(t, runtime, completion, err) + if _, err := takeStyleOperationForTest(m.LayerProperty("missing", "circle-radius")); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("LayerProperty(missing layer) error = %v, want ErrInvalidArgument", err) } } func TestStyleLightPropertyJSONSnapshots(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } - if err := m.SetStyleLightJSON([]byte(`{"anchor":"viewport","color":"#ffffff","intensity":0.5}`)); err != nil { + if _, err := m.SetStyleLightJSON([]byte(`{"anchor":"viewport","color":"#ffffff","intensity":0.5}`)); err != nil { t.Fatalf("SetStyleLightJSON(): %v", err) } - undefined, err := m.StyleLightProperty("does-not-exist") + undefined, err := takeStyleOperationForTest(m.StyleLightProperty("does-not-exist")) if err != nil { t.Fatalf("StyleLightProperty(does-not-exist): %v", err) } if undefined != nil { t.Fatalf("StyleLightProperty(does-not-exist) = %#v, want JSON null", undefined) } - if err := m.SetStyleLightProperty("intensity", []byte("0.75")); err != nil { + if _, err := m.SetStyleLightProperty("intensity", []byte("0.75")); err != nil { t.Fatalf("SetStyleLightProperty(intensity): %v", err) } - intensity, err := m.StyleLightProperty("intensity") + intensity, err := takeStyleOperationForTest(m.StyleLightProperty("intensity")) if err != nil { t.Fatalf("StyleLightProperty(intensity): %v", err) } if string(intensity) != "0.75" { t.Fatalf("StyleLightProperty(intensity) = %s, want 0.75", intensity) } - if err := m.SetStyleLightProperty("intensity", []byte("-Infinity")); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetStyleLightProperty(non-finite JSON double) error = %v, want ErrInvalidArgument", err) - } + completion, err := m.SetStyleLightProperty("intensity", []byte("-Infinity")) + requireStyleCommandFailed(t, runtime, completion, err) } func TestStyleLayerMetadataForMissingLayers(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } - ids, err := m.StyleLayerIDs() + ids, err := takeStyleOperationForTest(m.StyleLayerIDs()) if err != nil { t.Fatalf("StyleLayerIDs(): %v", err) } @@ -344,57 +343,42 @@ func TestStyleLayerMetadataForMissingLayers(t *testing.T) { t.Fatalf("StyleLayerIDs() unexpectedly contains missing layer: %v", ids) } } - exists, err := m.StyleLayerExists("missing") - if err != nil { - t.Fatalf("StyleLayerExists(): %v", err) - } - if exists { - t.Fatalf("StyleLayerExists(missing) = true, want false") - } - layerType, found, err := m.StyleLayerType("missing") + info, found, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("missing")) if err != nil { - t.Fatalf("StyleLayerType(): %v", err) + t.Fatalf("StyleLayerInfo(): %v", err) } - if found || layerType != "" { - t.Fatalf("StyleLayerType(missing) = (%q, %v), want empty false", layerType, found) + if found || info.Type != "" { + t.Fatalf("StyleLayerInfo(missing) = (%#v, %v), want empty false", info, found) } - removed, err := m.RemoveStyleLayer("missing") - if err != nil { - t.Fatalf("RemoveStyleLayer(): %v", err) - } - if removed { - t.Fatalf("RemoveStyleLayer(missing) = true, want false") - } - if err := m.MoveStyleLayer("missing", ""); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("MoveStyleLayer(missing) error = %v, want ErrInvalidArgument", err) - } - if _, err := m.StyleLayerExists(""); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StyleLayerExists(empty) error = %v, want ErrInvalidArgument", err) + completion, err := m.RemoveStyleLayer("missing") + requireCommandFailedWith(t, runtime, completion, err, ErrNotFound) + completion, err = m.MoveStyleLayer("missing", "") + requireStyleCommandFailed(t, runtime, completion, err) + if _, _, err := takeOptionalStyleOperationForTest(m.StyleLayerInfo("")); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("StyleLayerInfo(empty) error = %v, want ErrInvalidArgument", err) } } func TestStyleTransitionOptionsRoundTrip(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - defaults, err := m.StyleTransitionOptions() + defaults, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -407,10 +391,10 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { } // The style parser fills in its own 300ms duration for a style that declares no transition. - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } - parsed, err := m.StyleTransitionOptions() + parsed, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -422,10 +406,10 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { } const transitionStyle = `{"version":8,"transition":{"duration":750,"delay":100},"sources":{},"layers":[]}` - if err := m.SetStyleJSON([]byte(transitionStyle)); err != nil { + if _, err := m.SetStyleJSON([]byte(transitionStyle)); err != nil { t.Fatalf("SetStyleJSON(transition style): %v", err) } - declared, err := m.StyleTransitionOptions() + declared, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -444,10 +428,10 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { zero := 0.0 disabled := false options := StyleTransitionOptions{DurationMS: &zero, EnablePlacementTransitions: &disabled} - if err := m.SetStyleTransitionOptions(options); err != nil { + if _, err := m.SetStyleTransitionOptions(options); err != nil { t.Fatalf("SetStyleTransitionOptions(): %v", err) } - applied, err := m.StyleTransitionOptions() + applied, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -458,10 +442,10 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { // A literal that sets only a duration must leave the cross-fade alone. Go cannot default a // struct field to true, so an enable-shaped field would have disabled it here. durationOnly := 250.0 - if err := m.SetStyleTransitionOptions(StyleTransitionOptions{DurationMS: &durationOnly}); err != nil { + if _, err := m.SetStyleTransitionOptions(StyleTransitionOptions{DurationMS: &durationOnly}); err != nil { t.Fatalf("SetStyleTransitionOptions(): %v", err) } - kept, err := m.StyleTransitionOptions() + kept, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -469,10 +453,10 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { t.Fatal("a duration-only literal disabled the placement cross-fade") } - if err := m.SetStyleJSON([]byte(transitionStyle)); err != nil { + if _, err := m.SetStyleJSON([]byte(transitionStyle)); err != nil { t.Fatalf("SetStyleJSON(transition style): %v", err) } - reloaded, err := m.StyleTransitionOptions() + reloaded, err := takeStyleOperationForTest(m.StyleTransitionOptions()) if err != nil { t.Fatalf("StyleTransitionOptions(): %v", err) } @@ -481,8 +465,6 @@ func TestStyleTransitionOptionsRoundTrip(t *testing.T) { } negative := -1.0 - err = m.SetStyleTransitionOptions(StyleTransitionOptions{DelayMS: &negative}) - if !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetStyleTransitionOptions(negative delay) = %v, want ErrInvalidArgument", err) - } + completion, err := m.SetStyleTransitionOptions(StyleTransitionOptions{DelayMS: &negative}) + requireStyleCommandFailed(t, runtime, completion, err) } diff --git a/bindings/go/style_sources_test.go b/bindings/go/style_sources_test.go index 29ac17ba6..67cf01998 100644 --- a/bindings/go/style_sources_test.go +++ b/bindings/go/style_sources_test.go @@ -2,35 +2,92 @@ package maplibre import ( "errors" - stdruntime "runtime" "testing" ) -func TestStyleSourceMetadataForMissingSources(t *testing.T) { - lockOSThreadForTest(t) +func takeStyleOperationForTest[T any](future *Future[T], err error) (T, error) { + return awaitForTest(future, err) +} + +func takeOptionalStyleOperationForTest[T any](future *Future[StyleOptional[T]], err error) (T, bool, error) { + result, err := takeStyleOperationForTest(future, err) + return result.Value, result.Found, err +} + +func awaitCommandCompletionForTest(t *testing.T, _ *RuntimeHandle, future *Future[CommandCompletion], err error) CommandCompletion { + t.Helper() + result, err := awaitForTest(future, err) + if err != nil { + t.Fatalf("command completion: %v", err) + } + return result +} + +func requireStyleCommandFailed(t *testing.T, _ *RuntimeHandle, future *Future[CommandCompletion], err error) { + t.Helper() + completion, completionErr := awaitForTest(future, err) + if completionErr != nil { + t.Fatalf("command completion: %v", completionErr) + } + if completion.Disposition != CommandDispositionFailed { + t.Fatalf("command disposition = %v, want failed", completion.Disposition) + } +} + +// requireCommandCommitted waits for completion's terminal event and returns the +// map snapshot generation the commit published. +func requireCommandCommitted(t *testing.T, runtime *RuntimeHandle, future *Future[CommandCompletion], err error) uint64 { + t.Helper() + finished := awaitCommandCompletionForTest(t, runtime, future, err) + if finished.Disposition != CommandDispositionCommitted { + t.Fatalf("command disposition = %v, want committed", finished.Disposition) + } + if finished.Generation == 0 { + t.Fatal("command committed without publishing a generation") + } + return finished.Generation +} + +// requireCommandFailedWith waits for completion's terminal event and asserts it +// failed with the given binding error. +func requireCommandFailedWith(t *testing.T, _ *RuntimeHandle, future *Future[CommandCompletion], err, want error) { + t.Helper() + completion, completionErr := awaitForTest(future, err) + if completionErr != nil { + t.Fatalf("command completion: %v", completionErr) + } + if completion.Disposition != CommandDispositionFailed { + t.Fatalf("command disposition = %v, want failed", completion.Disposition) + } + got := kindForStatus(completion.RawStatus) + if !errors.Is(got, want) { + t.Fatalf("command terminal status = %v, want %v", got, want) + } +} +func TestStyleSourceMetadataForMissingSources(t *testing.T) { runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } - ids, err := m.StyleSourceIDs() + ids, err := takeStyleOperationForTest(m.StyleSourceIDs()) if err != nil { t.Fatalf("StyleSourceIDs(): %v", err) } @@ -39,93 +96,68 @@ func TestStyleSourceMetadataForMissingSources(t *testing.T) { t.Fatalf("StyleSourceIDs() unexpectedly contains missing source: %v", ids) } } - exists, err := m.StyleSourceExists("missing") - if err != nil { - t.Fatalf("StyleSourceExists(): %v", err) - } - if exists { - t.Fatalf("StyleSourceExists(missing) = true, want false") - } - sourceType, found, err := m.StyleSourceType("missing") - if err != nil { - t.Fatalf("StyleSourceType(): %v", err) - } - if found || sourceType != StyleSourceTypeUnknown { - t.Fatalf("StyleSourceType(missing) = (%v, %v), want (unknown, false)", sourceType, found) - } - _, found, err = m.StyleSourceInfo("missing") + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("missing")) if err != nil { t.Fatalf("StyleSourceInfo(): %v", err) } - if found { - t.Fatalf("StyleSourceInfo(missing) found = true, want false") - } - attribution, found, err := m.StyleSourceAttribution("missing") - if err != nil { - t.Fatalf("StyleSourceAttribution(): %v", err) - } - if found || attribution != "" { - t.Fatalf("StyleSourceAttribution(missing) = (%q, %v), want empty false", attribution, found) - } - removed, err := m.RemoveStyleSource("missing") - if err != nil { - t.Fatalf("RemoveStyleSource(): %v", err) + if found || info.Type != StyleSourceTypeUnknown { + t.Fatalf("StyleSourceInfo(missing) = (%#v, %v), want (unknown type, false)", info, found) } - if removed { - t.Fatalf("RemoveStyleSource(missing) = true, want false") + if info.Attribution != nil { + t.Fatalf("StyleSourceInfo(missing) attribution = %v, want absent", info.Attribution) } - if _, err := m.StyleSourceExists(""); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("StyleSourceExists(empty) error = %v, want ErrInvalidArgument", err) + completion, err := m.RemoveStyleSource("missing") + requireCommandFailedWith(t, runtime, completion, err, ErrNotFound) + if _, _, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("")); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("StyleSourceInfo(empty) error = %v, want ErrInvalidArgument", err) } } func TestStyleSourceURLAndTileBindings(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } geoJSONOptions := StyleGeoJSONSourceOptions{}. WithMinZoom(1). WithTolerance(0.5). WithBuffer(64) - if err := m.AddGeoJSONSourceURL("geojson-url", "asset://fixtures/points.geojson", &geoJSONOptions); err != nil { + if _, err := m.AddGeoJSONSourceURL("geojson-url", "asset://fixtures/points.geojson", &geoJSONOptions); err != nil { t.Fatalf("AddGeoJSONSourceURL(): %v", err) } - if err := m.SetGeoJSONSourceURL("geojson-url", "asset://fixtures/points-2.geojson"); err != nil { + if _, err := m.SetGeoJSONSourceURL("geojson-url", "asset://fixtures/points-2.geojson"); err != nil { t.Fatalf("SetGeoJSONSourceURL(): %v", err) } tileOptions := StyleTileSourceOptions{}. WithTileSize(256). WithAttribution("unit attribution") - if err := m.AddVectorSourceTiles("vector-tiles", []string{"https://example.com/vector/{z}/{x}/{y}.pbf"}, &tileOptions); err != nil { + if _, err := m.AddVectorSourceTiles("vector-tiles", []string{"https://example.com/vector/{z}/{x}/{y}.pbf"}, &tileOptions); err != nil { t.Fatalf("AddVectorSourceTiles(): %v", err) } - if err := m.AddRasterSourceURL("raster-url", "https://example.com/raster.json", &tileOptions); err != nil { + if _, err := m.AddRasterSourceURL("raster-url", "https://example.com/raster.json", &tileOptions); err != nil { t.Fatalf("AddRasterSourceURL(): %v", err) } demOptions := StyleTileSourceOptions{}. WithTileSize(512). WithRasterEncoding(StyleRasterDEMEncodingTerrarium) - if err := m.AddRasterDEMSourceTiles("dem-tiles", []string{"https://example.com/dem/{z}/{x}/{y}.png"}, &demOptions); err != nil { + if _, err := m.AddRasterDEMSourceTiles("dem-tiles", []string{"https://example.com/dem/{z}/{x}/{y}.png"}, &demOptions); err != nil { t.Fatalf("AddRasterDEMSourceTiles(): %v", err) } checks := map[string]StyleSourceType{ @@ -135,44 +167,40 @@ func TestStyleSourceURLAndTileBindings(t *testing.T) { "dem-tiles": StyleSourceTypeRasterDEM, } for id, wantType := range checks { - gotType, found, err := m.StyleSourceType(id) + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo(id)) if err != nil { - t.Fatalf("StyleSourceType(%s): %v", id, err) + t.Fatalf("StyleSourceInfo(%s): %v", id, err) } - if !found || gotType != wantType { - t.Fatalf("StyleSourceType(%s) = (%v, %v), want %v true", id, gotType, found, wantType) + if !found || info.Type != wantType { + t.Fatalf("StyleSourceInfo(%s) type = (%v, %v), want %v true", id, info.Type, found, wantType) } } - if err := m.AddVectorSourceTiles("bad-vector", nil, nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AddVectorSourceTiles(nil) error = %v, want ErrInvalidArgument", err) - } - if err := m.AddGeoJSONSourceURL("", "asset://fixtures/points.geojson", nil); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AddGeoJSONSourceURL(empty id) error = %v, want ErrInvalidArgument", err) - } + completion, err := m.AddVectorSourceTiles("bad-vector", nil, nil) + requireStyleCommandFailed(t, runtime, completion, err) + completion, err = m.AddGeoJSONSourceURL("", "asset://fixtures/points.geojson", nil) + requireStyleCommandFailed(t, runtime, completion, err) } func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } minZoom := 0.0 @@ -198,11 +226,11 @@ func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { "https://example.com/first/{z}/{x}/{y}.mlt", "https://example.com/second/{z}/{x}/{y}.mlt", } - if err := m.AddVectorSourceTiles("inline-vector", tileURLs, &options); err != nil { + if _, err := m.AddVectorSourceTiles("inline-vector", tileURLs, &options); err != nil { t.Fatalf("AddVectorSourceTiles(): %v", err) } - info, found, err := m.StyleSourceInfo("inline-vector") + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("inline-vector")) if err != nil { t.Fatalf("StyleSourceInfo(inline-vector): %v", err) } @@ -242,11 +270,19 @@ func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { t.Fatalf("StyleSourceInfo(inline-vector) raster encoding = %v, want absent", info.RasterEncoding) } - removed, err := m.RemoveStyleSource("inline-vector") - if err != nil || !removed { - t.Fatalf("RemoveStyleSource(inline-vector) = (%v, %v), want true and nil", removed, err) + layerJSON := []byte(`{"id":"inline-vector-layer","type":"line","source":"inline-vector","source-layer":"lines"}`) + layerID, err := m.AddStyleLayerJSON(layerJSON, "") + requireCommandCommitted(t, runtime, layerID, err) + blockedID, err := m.RemoveStyleSource("inline-vector") + requireCommandFailedWith(t, runtime, blockedID, err, ErrInvalidState) + removeLayerID, err := m.RemoveStyleLayer("inline-vector-layer") + requireCommandCommitted(t, runtime, removeLayerID, err) + removeID, err := m.RemoveStyleSource("inline-vector") + requireCommandCommitted(t, runtime, removeID, err) + if _, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("inline-vector")); err != nil || found { + t.Fatalf("StyleSourceInfo(inline-vector) after removal = (%v, %v), want (false, nil)", found, err) } - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON([]byte(replacement)): %v", err) } if *info.Attribution != attribution || info.TileJSON.TileURLs[1] != tileURLs[1] || *info.TileJSON.Bounds != bounds { @@ -254,10 +290,10 @@ func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { } url := "https://example.invalid/vector-tilejson.json" - if err := m.AddVectorSourceURL("url-vector", url, nil); err != nil { + if _, err := m.AddVectorSourceURL("url-vector", url, nil); err != nil { t.Fatalf("AddVectorSourceURL(): %v", err) } - urlInfo, found, err := m.StyleSourceInfo("url-vector") + urlInfo, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("url-vector")) if err != nil { t.Fatalf("StyleSourceInfo(url-vector): %v", err) } @@ -277,10 +313,10 @@ func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { t.Errorf("GeoJSONSourceDataHandle Close(): %v", err) } }() - if err := m.AddGeoJSONSourceData("inline-geojson", data); err != nil { + if _, err := m.AddGeoJSONSourceData("inline-geojson", data); err != nil { t.Fatalf("AddGeoJSONSourceData(): %v", err) } - geoJSONInfo, found, err := m.StyleSourceInfo("inline-geojson") + geoJSONInfo, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("inline-geojson")) if err != nil { t.Fatalf("StyleSourceInfo(inline-geojson): %v", err) } @@ -290,27 +326,25 @@ func TestStyleSourceInfoCopiesReconstructibleMetadata(t *testing.T) { } func TestGeoJSONSourceDataPrepareAndInstall(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } document := []byte(`{"type":"FeatureCollection","features":[{"type":"Feature","id":"feature-1","geometry":{"type":"LineString","coordinates":[[2,1],[4,3]]},"properties":{"name":"before","rank":7}}]}`) @@ -328,19 +362,19 @@ func TestGeoJSONSourceDataPrepareAndInstall(t *testing.T) { // The document is copied at preparation, so mutating it afterward does not // reach the prepared index. document[0] = 'x' - if err := m.AddGeoJSONSourceData("geojson-data", data); err != nil { + if _, err := m.AddGeoJSONSourceData("geojson-data", data); err != nil { t.Fatalf("AddGeoJSONSourceData(): %v", err) } - sourceType, found, err := m.StyleSourceType("geojson-data") + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("geojson-data")) if err != nil { - t.Fatalf("StyleSourceType(geojson-data): %v", err) + t.Fatalf("StyleSourceInfo(geojson-data): %v", err) } - if !found || sourceType != StyleSourceTypeGeoJSON { - t.Fatalf("StyleSourceType(geojson-data) = (%v, %v), want GeoJSON true", sourceType, found) + if !found || info.Type != StyleSourceTypeGeoJSON { + t.Fatalf("StyleSourceInfo(geojson-data) type = (%v, %v), want GeoJSON true", info.Type, found) } // One prepared handle installs on any number of sources. - if err := m.AddGeoJSONSourceData("geojson-data-2", data); err != nil { + if _, err := m.AddGeoJSONSourceData("geojson-data-2", data); err != nil { t.Fatalf("AddGeoJSONSourceData(reused handle): %v", err) } @@ -349,12 +383,10 @@ func TestGeoJSONSourceDataPrepareAndInstall(t *testing.T) { if err != nil { t.Fatalf("NewGeoJSONSourceData(update): %v", err) } - if err := m.SetGeoJSONSourceData("geojson-data", update); err != nil { - t.Fatalf("SetGeoJSONSourceData(): %v", err) - } - if err := m.SetGeoJSONSourceData("geojson-data-2", update); err != nil { - t.Fatalf("SetGeoJSONSourceData(reused handle): %v", err) - } + completion, err := m.SetGeoJSONSourceData("geojson-data", update) + requireCommandCommitted(t, runtime, completion, err) + completion, err = m.SetGeoJSONSourceData("geojson-data-2", update) + requireCommandCommitted(t, runtime, completion, err) if err := update.Close(); err != nil { t.Fatalf("update Close(): %v", err) } @@ -366,9 +398,8 @@ func TestGeoJSONSourceDataPrepareAndInstall(t *testing.T) { if err != nil { t.Fatalf("NewGeoJSONSourceData(mismatched): %v", err) } - if err := m.SetGeoJSONSourceData("geojson-data", mismatched); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetGeoJSONSourceData(mismatched options) error = %v, want ErrInvalidArgument", err) - } + completion, err = m.SetGeoJSONSourceData("geojson-data", mismatched) + requireCommandFailedWith(t, runtime, completion, err, ErrInvalidArgument) if err := mismatched.Close(); err != nil { t.Fatalf("mismatched Close(): %v", err) } @@ -382,22 +413,18 @@ func TestGeoJSONSourceDataPrepareAndInstall(t *testing.T) { if err := data.Close(); err != nil { t.Fatalf("second data Close(): %v", err) } - if err := m.AddGeoJSONSourceData("closed-handle", data); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AddGeoJSONSourceData(closed handle) error = %v, want ErrInvalidArgument", err) + if future, err := m.AddGeoJSONSourceData("closed-handle", data); future != nil || !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("AddGeoJSONSourceData(closed handle) = (%v, %v), want nil and ErrInvalidArgument", future, err) } - if err := m.SetGeoJSONSourceData("geojson-data", data); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetGeoJSONSourceData(closed handle) error = %v, want ErrInvalidArgument", err) + if future, err := m.SetGeoJSONSourceData("geojson-data", data); future != nil || !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("SetGeoJSONSourceData(closed handle) = (%v, %v), want nil and ErrInvalidArgument", future, err) } - sourceType, found, err = m.StyleSourceType("geojson-data") - if err != nil || !found || sourceType != StyleSourceTypeGeoJSON { - t.Fatalf("StyleSourceType(geojson-data) after handle close = (%v, %v, %v), want GeoJSON true nil", sourceType, found, err) + info, found, err = takeOptionalStyleOperationForTest(m.StyleSourceInfo("geojson-data")) + if err != nil || !found || info.Type != StyleSourceTypeGeoJSON { + t.Fatalf("StyleSourceInfo(geojson-data) after handle close = (%v, %v, %v), want GeoJSON true nil", info.Type, found, err) } - exists, err := m.StyleSourceExists("closed-handle") - if err != nil { - t.Fatalf("StyleSourceExists(closed-handle): %v", err) - } - if exists { - t.Fatalf("StyleSourceExists(closed-handle) = true, want false") + if _, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("closed-handle")); err != nil || found { + t.Fatalf("StyleSourceInfo(closed-handle) = (%v, %v), want (false, nil)", found, err) } } @@ -429,27 +456,25 @@ func TestGeoJSONSourceDataRejectsInvalidDocumentsAtPreparation(t *testing.T) { } func TestGeoJSONSourceClusterOptions(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } points := []byte(`{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"rank":1}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.001,0.001]},"properties":{"rank":2}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.002,0.002]},"properties":{"rank":3}}]}`) @@ -465,18 +490,18 @@ func TestGeoJSONSourceClusterOptions(t *testing.T) { t.Fatalf("NewGeoJSONSourceData(clustered): %v", err) } clusterProperties[0] = 'x' - if err := m.AddGeoJSONSourceData("cluster-source", data); err != nil { + if _, err := m.AddGeoJSONSourceData("cluster-source", data); err != nil { t.Fatalf("AddGeoJSONSourceData(clustered): %v", err) } if err := data.Close(); err != nil { t.Fatalf("data Close(): %v", err) } - sourceType, found, err := m.StyleSourceType("cluster-source") + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("cluster-source")) if err != nil { - t.Fatalf("StyleSourceType(cluster-source): %v", err) + t.Fatalf("StyleSourceInfo(cluster-source): %v", err) } - if !found || sourceType != StyleSourceTypeGeoJSON { - t.Fatalf("StyleSourceType(cluster-source) = (%v, %v), want GeoJSON true", sourceType, found) + if !found || info.Type != StyleSourceTypeGeoJSON { + t.Fatalf("StyleSourceInfo(cluster-source) type = (%v, %v), want GeoJSON true", info.Type, found) } // Different cluster aggregations would change cluster feature properties // under the source's layers, so the options match rejects them. @@ -485,9 +510,8 @@ func TestGeoJSONSourceClusterOptions(t *testing.T) { if err != nil { t.Fatalf("NewGeoJSONSourceData(updated cluster properties): %v", err) } - if err := m.SetGeoJSONSourceData("cluster-source", update); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetGeoJSONSourceData(updated cluster properties) error = %v, want ErrInvalidArgument", err) - } + completion, err := m.SetGeoJSONSourceData("cluster-source", update) + requireCommandFailedWith(t, runtime, completion, err, ErrInvalidArgument) if err := update.Close(); err != nil { t.Fatalf("update Close(): %v", err) } @@ -522,30 +546,28 @@ func TestGeoJSONSourceClusterOptions(t *testing.T) { } } -// Preparation touches no runtime or map, so a plain goroutine prepares data -// that installs on the map owner thread, and another goroutine releases it. +// Preparation touches no runtime or map, so one goroutine prepares data, +// another installs it on the map, and a third releases it. func TestGeoJSONSourceDataPreparesOnAnotherGoroutine(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } @@ -562,9 +584,8 @@ func TestGeoJSONSourceDataPreparesOnAnotherGoroutine(t *testing.T) { if result.err != nil { t.Fatalf("NewGeoJSONSourceData() on a goroutine: %v", result.err) } - if err := m.AddGeoJSONSourceData("worker-prepared", result.data); err != nil { - t.Fatalf("AddGeoJSONSourceData(worker prepared): %v", err) - } + completion, err := m.AddGeoJSONSourceData("worker-prepared", result.data) + requireCommandCommitted(t, runtime, completion, err) closed := make(chan error) go func() { closed <- result.data.Close() @@ -572,125 +593,103 @@ func TestGeoJSONSourceDataPreparesOnAnotherGoroutine(t *testing.T) { if err := <-closed; err != nil { t.Fatalf("Close() on a goroutine: %v", err) } - sourceType, found, err := m.StyleSourceType("worker-prepared") - if err != nil || !found || sourceType != StyleSourceTypeGeoJSON { - t.Fatalf("StyleSourceType(worker-prepared) = (%v, %v, %v), want GeoJSON true nil", sourceType, found, err) + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("worker-prepared")) + if err != nil || !found || info.Type != StyleSourceTypeGeoJSON { + t.Fatalf("StyleSourceInfo(worker-prepared) = (%v, %v, %v), want GeoJSON true nil", info.Type, found, err) } } func TestGeoJSONSourceSynchronousTilingOverride(t *testing.T) { - lockOSThreadForTest(t) - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } data, err := NewGeoJSONSourceData([]byte(`{"type":"FeatureCollection","features":[]}`), nil) if err != nil { t.Fatalf("NewGeoJSONSourceData(): %v", err) } - if err := m.AddGeoJSONSourceData("tracked", data); err != nil { + if _, err := m.AddGeoJSONSourceData("tracked", data); err != nil { t.Fatalf("AddGeoJSONSourceData(): %v", err) } - if err := m.SetGeoJSONSourceSynchronousTiling("tracked", true); err != nil { - t.Fatalf("SetGeoJSONSourceSynchronousTiling(true): %v", err) - } + completion, err := m.SetGeoJSONSourceSynchronousTiling("tracked", true) + requireCommandCommitted(t, runtime, completion, err) update, err := NewGeoJSONSourceData([]byte(`{"type":"Point","coordinates":[3,4]}`), nil) if err != nil { t.Fatalf("NewGeoJSONSourceData(update): %v", err) } - if err := m.SetGeoJSONSourceData("tracked", update); err != nil { - t.Fatalf("SetGeoJSONSourceData() under the override: %v", err) - } + completion, err = m.SetGeoJSONSourceData("tracked", update) + requireCommandCommitted(t, runtime, completion, err) if err := update.Close(); err != nil { t.Fatalf("update Close(): %v", err) } - if err := m.SetGeoJSONSourceSynchronousTiling("tracked", false); err != nil { - t.Fatalf("SetGeoJSONSourceSynchronousTiling(false): %v", err) - } + completion, err = m.SetGeoJSONSourceSynchronousTiling("tracked", false) + requireCommandCommitted(t, runtime, completion, err) if err := data.Close(); err != nil { t.Fatalf("data Close(): %v", err) } - if err := m.SetGeoJSONSourceSynchronousTiling("missing", true); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetGeoJSONSourceSynchronousTiling(missing source) error = %v, want ErrInvalidArgument", err) - } - if err := m.AddVectorSourceURL("vector", "https://example.invalid/tiles.json", nil); err != nil { + completion, err = m.SetGeoJSONSourceSynchronousTiling("missing", true) + requireCommandFailedWith(t, runtime, completion, err, ErrInvalidArgument) + if _, err := m.AddVectorSourceURL("vector", "https://example.invalid/tiles.json", nil); err != nil { t.Fatalf("AddVectorSourceURL(): %v", err) } - if err := m.SetGeoJSONSourceSynchronousTiling("vector", true); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("SetGeoJSONSourceSynchronousTiling(non-GeoJSON source) error = %v, want ErrInvalidArgument", err) - } + completion, err = m.SetGeoJSONSourceSynchronousTiling("vector", true) + requireCommandFailedWith(t, runtime, completion, err, ErrInvalidArgument) } func TestAddStyleSourceJSONCopiesGoBuffer(t *testing.T) { - stdruntime.LockOSThread() - defer stdruntime.UnlockOSThread() - runtime, err := NewRuntime() if err != nil { t.Fatalf("NewRuntime(): %v", err) } - m, err := runtime.NewMap() + m, err := awaitForTest(runtime.NewMap()) if err != nil { - _ = runtime.Close() + _ = closeRuntimeForTest(runtime) t.Fatalf("NewMap(): %v", err) } defer func() { if err := m.Close(); err != nil { t.Errorf("Map Close(): %v", err) } - if err := runtime.Close(); err != nil { + if err := closeRuntimeForTest(runtime); err != nil { t.Errorf("Runtime Close(): %v", err) } }() - if err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { + if _, err := m.SetStyleJSON([]byte(`{"version":8,"sources":{},"layers":[]}`)); err != nil { t.Fatalf("SetStyleJSON(empty style): %v", err) } source := []byte(`{"type":"geojson","data":{"type":"FeatureCollection","features":[]},"attribution":"unit-test"}`) - if err := m.AddStyleSourceJSON("go-json-source", source); err != nil { + if _, err := m.AddStyleSourceJSON("go-json-source", source); err != nil { t.Fatalf("AddStyleSourceJSON(): %v", err) } source[0] = 'x' - exists, err := m.StyleSourceExists("go-json-source") - if err != nil { - t.Fatalf("StyleSourceExists(): %v", err) - } - if !exists { - t.Fatalf("StyleSourceExists(go-json-source) = false, want true") - } - sourceType, found, err := m.StyleSourceType("go-json-source") - if err != nil { - t.Fatalf("StyleSourceType(): %v", err) - } - if !found || sourceType != StyleSourceTypeGeoJSON { - t.Fatalf("StyleSourceType(go-json-source) = (%v, %v), want GeoJSON true", sourceType, found) - } - info, found, err := m.StyleSourceInfo("go-json-source") + info, found, err := takeOptionalStyleOperationForTest(m.StyleSourceInfo("go-json-source")) if err != nil { t.Fatalf("StyleSourceInfo(): %v", err) } - if !found || info.IDSize != uint64(len("go-json-source")) { - t.Fatalf("StyleSourceInfo(go-json-source) = (%#v, %v), want copied ID size", info, found) + if !found || info.Type != StyleSourceTypeGeoJSON { + t.Fatalf("StyleSourceInfo(go-json-source) type = (%v, %v), want GeoJSON true", info.Type, found) } - if err := m.AddStyleSourceJSON("bad-json-source", []byte(`NaN`)); !errors.Is(err, ErrInvalidArgument) { - t.Fatalf("AddStyleSourceJSON(non-finite JSON double) error = %v, want ErrInvalidArgument", err) + if info.IDSize != uint64(len("go-json-source")) { + t.Fatalf("StyleSourceInfo(go-json-source) = %#v, want copied ID size", info) } + completion, err := m.AddStyleSourceJSON("bad-json-source", []byte(`NaN`)) + requireStyleCommandFailed(t, runtime, completion, err) } diff --git a/bindings/kotlin/build.gradle.kts b/bindings/kotlin/build.gradle.kts index d964624a0..253074b6d 100644 --- a/bindings/kotlin/build.gradle.kts +++ b/bindings/kotlin/build.gradle.kts @@ -149,6 +149,9 @@ kotlin { } sourceSets { + // Deferred is part of the public binding surface. + commonMain.dependencies { api(libs.coroutines) } + androidMain { dependencies { implementation(libs.javacpp) } } named("androidDeviceTest") { diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt index 2afc9bc49..54ab48d1f 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt @@ -1,99 +1,60 @@ package org.maplibre.nativeffi.internal.callback -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame import kotlin.test.assertTrue +import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.log.LogRecord class LogCallbackStateTest { @Test - fun callbackClosureWaitsForEnteredCallbacksAndSupportsClosureFromCallback() { - val entered = CountDownLatch(1) - val release = CountDownLatch(1) - val state = - install( - LogCallback { - entered.countDown() - release.await(5, TimeUnit.SECONDS) - true - } - ) - val invocation = thread { state.invoke(0, 0, 0, null) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - val closed = CountDownLatch(1) - val closer = thread { - state.close() - closed.countDown() - } - assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) - release.countDown() - invocation.join() - closer.join() + fun callbackContainsFailuresAndStopsAfterClose() { + val state = LogCallbackState.createForTesting(LogCallback { true }) + assertEquals(1, state.invoke(1, 0, 0, null)) + state.close() + assertEquals(0, state.invoke(1, 0, 0, null)) assertTrue(state.isClosedForTesting()) - lateinit var reentrant: LogCallbackState - reentrant = - install( - LogCallback { - reentrant.close() - true - } - ) - assertEquals(1, reentrant.invoke(0, 0, 0, null)) - assertTrue(reentrant.isClosedForTesting()) - LogCallbackState.clearForTesting() + val throwing = LogCallbackState.createForTesting(LogCallback { error("contained") }) + assertEquals(0, throwing.invoke(1, 0, 0, null)) + throwing.close() } @Test - fun replacementPreservesRawEnumsAndFailedReplacementKeepsPreviousState() { + fun nativeRegistrationReplacesAndClearsOwnedState() { + Maplibre.setLogCallback(LogCallback { true }) + Maplibre.setLogCallback(LogCallback { false }) + Maplibre.clearLogCallback() + Maplibre.clearLogCallback() + } + + @Test + fun callbackKeepsRawSeverityAndEventValues() { var copied: LogRecord? = null + val state = + LogCallbackState.createForTesting( + LogCallback { + copied = it + true + } + ) try { - val previous = - install( - LogCallback { - copied = it - false - } - ) - assertFailsWith { - LogCallbackState.setForTesting(LogCallback { true }) { error("registration failed") } - } - assertSame(previous, LogCallbackState.currentForTesting()) - assertFalse(previous.isClosedForTesting()) - previous.invoke(991, 992, 7, null) - assertEquals(991, copied?.severity?.nativeValue) - assertEquals(992, copied?.event?.nativeValue) - - LogCallbackState.setForTesting(LogCallback { true }) - assertTrue(previous.isClosedForTesting()) + assertEquals(1, state.invoke(991, 992, 7, null)) } finally { - LogCallbackState.clearForTesting() + state.close() } + assertEquals(991, copied?.severity?.nativeValue) + assertEquals(992, copied?.event?.nativeValue) + assertEquals(7, copied?.code) } @Test - fun elevenReplacementsKeepTheSharedThunkCallable() { + fun elevenRegistrationsKeepTheSharedThunkCallable() { try { - repeat(11) { index -> - LogCallbackState.setForTesting(LogCallback { it.code == index.toLong() }) - val state = assertNotNull(LogCallbackState.currentForTesting()) - assertEquals(1, state.invoke(0, 0, index.toLong(), null)) - } + repeat(11) { index -> Maplibre.setLogCallback(LogCallback { it.code == index.toLong() }) } } finally { - LogCallbackState.clearForTesting() + Maplibre.clearLogCallback() } } - - private fun install(callback: LogCallback): LogCallbackState { - LogCallbackState.setForTesting(callback) - return assertNotNull(LogCallbackState.currentForTesting()) - } } diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceCallbackStateTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceCallbackStateTest.kt index ee4797421..2284b44f9 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceCallbackStateTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceCallbackStateTest.kt @@ -19,88 +19,97 @@ import org.maplibre.nativeffi.resource.ResourceTransformCallback class ResourceCallbackStateTest { @Test - fun providerCopiesResourceRequestAndUnknownRawEnums() { - var copied: ResourceRequest? = null - ResourceProviderState( - ResourceProviderCallback { request, _ -> - copied = request - ResourceProviderDecision.PASS_THROUGH - } - ) - .use { state -> - withRequest { request -> - assertEquals(ResourceProviderDecision.PASS_THROUGH.nativeValue, state.invoke(request, 1)) - } - } - assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) - assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) - assertEquals(900, copied?.kind?.nativeValue) - assertEquals(901, copied?.loadingMethod?.nativeValue) - } - - @Test - fun providerClosureDuringAndConcurrentWithCallbacksRejectsLaterEntry() { - lateinit var reentrant: ResourceProviderState - reentrant = + fun providerCopiesResourceRequestAndUnknownRawEnums(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copied: ResourceRequest? = null ResourceProviderState( - ResourceProviderCallback { _, _ -> - reentrant.close() - ResourceProviderDecision.PASS_THROUGH + ResourceProviderCallback { request, _ -> + copied = request + ResourceProviderDecision.PASS_THROUGH + } + ) + .use { state -> + withRequest { request -> + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue, + state.invoke(request, 1), + ) + } } - ) - withRequest { request -> - assertEquals(ResourceProviderDecision.PASS_THROUGH.nativeValue, reentrant.invoke(request, 1)) - assertTrue(reentrant.isClosedForTesting()) - assertEquals(-1, reentrant.invoke(request, 1)) + assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) + assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) + assertEquals(900, copied?.kind?.nativeValue) + assertEquals(901, copied?.loadingMethod?.nativeValue) } - val entered = CountDownLatch(1) - val release = CountDownLatch(1) - val state = - ResourceProviderState( - ResourceProviderCallback { _, _ -> - entered.countDown() - release.await(5, TimeUnit.SECONDS) - ResourceProviderDecision.PASS_THROUGH + @Test + fun providerClosureDuringAndConcurrentWithCallbacksRejectsLaterEntry(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + lateinit var reentrant: ResourceProviderState + reentrant = + ResourceProviderState( + ResourceProviderCallback { _, _ -> + reentrant.close() + ResourceProviderDecision.PASS_THROUGH + } + ) + withRequest { request -> + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue, + reentrant.invoke(request, 1), + ) + assertTrue(reentrant.isClosedForTesting()) + assertEquals(-1, reentrant.invoke(request, 1)) + } + + val entered = CountDownLatch(1) + val release = CountDownLatch(1) + val state = + ResourceProviderState( + ResourceProviderCallback { _, _ -> + entered.countDown() + release.await(5, TimeUnit.SECONDS) + ResourceProviderDecision.PASS_THROUGH + } + ) + withRequest { request -> + val invocation = thread { state.invoke(request, 2) } + assertTrue(entered.await(5, TimeUnit.SECONDS)) + val closed = CountDownLatch(1) + val closer = thread { + state.close() + closed.countDown() } - ) - withRequest { request -> - val invocation = thread { state.invoke(request, 2) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - val closed = CountDownLatch(1) - val closer = thread { - state.close() - closed.countDown() + assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) + release.countDown() + invocation.join() + closer.join() + assertTrue(state.isClosedForTesting()) } - assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) - release.countDown() - invocation.join() - closer.join() - assertTrue(state.isClosedForTesting()) } - } @Test - fun resourceTransformCopiesUnknownKindsContainsFailuresAndClosesDuringCallback() { - var copiedKind: ResourceKind? = null - lateinit var state: ResourceTransformState - state = - ResourceTransformState( - ResourceTransformCallback { request -> - copiedKind = request.kind - state.close() - throw IllegalStateException("contained") + fun resourceTransformCopiesUnknownKindsContainsFailuresAndClosesDuringCallback(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copiedKind: ResourceKind? = null + lateinit var state: ResourceTransformState + state = + ResourceTransformState( + ResourceTransformCallback { request -> + copiedKind = request.kind + state.close() + throw IllegalStateException("contained") + } + ) + JavaCppSupport.cString("https://example.com/style.json").use { url -> + MaplibreNativeC.mln_resource_transform_response().use { response -> + assertEquals(MaplibreStatus.NATIVE_ERROR.nativeCode, state.invoke(991, url, response)) + assertEquals(991, copiedKind?.nativeValue) + assertTrue(state.isClosedForTesting()) + assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(991, url, response)) } - ) - JavaCppSupport.cString("https://example.com/style.json").use { url -> - MaplibreNativeC.mln_resource_transform_response().use { response -> - assertEquals(MaplibreStatus.NATIVE_ERROR.nativeCode, state.invoke(991, url, response)) - assertEquals(991, copiedKind?.nativeValue) - assertTrue(state.isClosedForTesting()) - assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(991, url, response)) } } - } @Test fun elevenLiveCallbackStatesKeepNonNullThunksAndDispatchByUserData() { diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/struct/JavaCppStructsTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/struct/JavaCppStructsTest.kt index 0983c1889..9793bcab0 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/struct/JavaCppStructsTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/internal/struct/JavaCppStructsTest.kt @@ -23,7 +23,6 @@ import org.maplibre.nativeffi.map.TileOptions import org.maplibre.nativeffi.map.ViewportMode import org.maplibre.nativeffi.map.ViewportOptions import org.maplibre.nativeffi.offline.OfflineRegionDefinition -import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.render.EglContextDescriptor import org.maplibre.nativeffi.render.MetalBorrowedTextureDescriptor @@ -40,231 +39,218 @@ import org.maplibre.nativeffi.style.StyleImageOptions class JavaCppStructsTest { @Test - fun stringViewsPreserveEmbeddedNulBytes() { - assertEquals("value\u0000suffix", JavaCppStructs.stringViewRoundTrip("value\u0000suffix")) - } + fun stringViewsPreserveEmbeddedNulBytes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertEquals("value\u0000suffix", JavaCppStructs.stringViewRoundTrip("value\u0000suffix")) + } @Test - fun optionDescriptorsPreservePresentZeroFields() { - val camera = - CameraOptions().apply { - center = LatLng(0.0, 0.0) - zoom = 0.0 - anchor = ScreenPoint(0.0, 0.0) - } - val (fields, copy) = JavaCppStructs.cameraOptionsRoundTrip(camera) - assertNotEquals(0, fields) - assertEquals(camera, copy) - - val animation = - JavaCppStructs.animationOptionsSnapshot( - AnimationOptions().apply { - durationMs = 0.0 - velocity = 0.0 - transitionId = -1 + fun optionDescriptorsPreservePresentZeroFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val camera = + CameraOptions().apply { + center = LatLng(0.0, 0.0) + zoom = 0.0 + anchor = ScreenPoint(0.0, 0.0) } - ) - assertNotEquals(0, animation.fields) - assertEquals(0.0, animation.durationMs) - assertEquals(0.0, animation.velocity) - assertEquals(-1L, animation.transitionId) + val (fields, copy) = JavaCppStructs.cameraOptionsRoundTrip(camera) + assertNotEquals(0, fields) + assertEquals(camera, copy) - val viewport = - ViewportOptions().apply { - northOrientation = NorthOrientation.LEFT - constrainMode = ConstrainMode.SCREEN - viewportMode = ViewportMode.FLIPPED_Y - } - assertEquals(viewport, JavaCppStructs.viewportOptionsRoundTrip(viewport)) - val tile = TileOptions().apply { lodMode = TileLodMode.DISTANCE } - assertEquals(tile, JavaCppStructs.tileOptionsRoundTrip(tile)) - val projection = - ProjectionModeOptions().apply { - axonometric = false - xSkew = 0.0 - ySkew = 0.0 - } - assertEquals(projection, JavaCppStructs.projectionModeOptionsRoundTrip(projection)) - } + val animation = + JavaCppStructs.animationOptionsSnapshot( + AnimationOptions().apply { + durationMs = 0.0 + velocity = 0.0 + transitionId = -1 + } + ) + assertNotEquals(0, animation.fields) + assertEquals(0.0, animation.durationMs) + assertEquals(0.0, animation.velocity) + assertEquals(-1L, animation.transitionId) - @Test - fun queryGeometryMaterializesEveryDiscriminator() { - val point = - JavaCppStructs.renderedQueryGeometryType(RenderedQueryGeometry.Point(ScreenPoint(1.0, 2.0))) - val box = - JavaCppStructs.renderedQueryGeometryType( - RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) - ) - val line = - JavaCppStructs.renderedQueryGeometryType( - RenderedQueryGeometry.LineString(listOf(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) - ) - assertEquals(3, setOf(point, box, line).size) - } + val viewport = + ViewportOptions().apply { + northOrientation = NorthOrientation.LEFT + constrainMode = ConstrainMode.SCREEN + viewportMode = ViewportMode.FLIPPED_Y + } + assertEquals(viewport, JavaCppStructs.viewportOptionsRoundTrip(viewport)) + val tile = TileOptions().apply { lodMode = TileLodMode.DISTANCE } + assertEquals(tile, JavaCppStructs.tileOptionsRoundTrip(tile)) + val projection = + ProjectionModeOptions().apply { + axonometric = false + xSkew = 0.0 + ySkew = 0.0 + } + assertEquals(projection, JavaCppStructs.projectionModeOptionsRoundTrip(projection)) + } @Test - fun featureStateSelectorEmbedsStringViewsByValue() { - val minimal = JavaCppStructs.featureStateSelectorSnapshot(FeatureStateSelector("point")) - assertEquals("point", minimal.sourceId) - assertEquals(0, minimal.fields) - assertEquals(null, minimal.sourceLayerId) - assertEquals(null, minimal.featureId) - assertEquals(null, minimal.stateKey) - - val full = - JavaCppStructs.featureStateSelectorSnapshot( - FeatureStateSelector("point").apply { - sourceLayerId = "layer" - featureId = "feature-1" - stateKey = "hover" - } - ) - assertEquals("point", full.sourceId) - assertEquals("layer", full.sourceLayerId) - assertEquals("feature-1", full.featureId) - assertEquals("hover", full.stateKey) - assertNotEquals(0, full.fields) - } + fun queryGeometryMaterializesEveryDiscriminator(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val point = + JavaCppStructs.renderedQueryGeometryType(RenderedQueryGeometry.Point(ScreenPoint(1.0, 2.0))) + val box = + JavaCppStructs.renderedQueryGeometryType( + RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) + ) + val line = + JavaCppStructs.renderedQueryGeometryType( + RenderedQueryGeometry.LineString(listOf(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) + ) + assertEquals(3, setOf(point, box, line).size) + } @Test - fun ownedBufferIsDestroyedAfterCopyFailure() { - assertEquals(1, JavaCppStructs.ownedBufferCleanupAfterCopyFailure()) - } + fun ownedBufferIsDestroyedAfterCopyFailure(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertEquals(1, JavaCppStructs.ownedBufferCleanupAfterCopyFailure()) + } @Test - fun offlineDefinitionsAndUnknownRuntimePayloadsCopyOwnedData() { - val definition = - OfflineRegionDefinition.TilePyramid( - "asset://style.json", - LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), - 1.0, - 5.0, - 2.0f, - true, - ) - assertEquals(definition, JavaCppStructs.offlineRegionDefinitionRoundTrip(definition)) - val info = JavaCppStructs.offlineRegionInfoSnapshot(7, definition, byteArrayOf(1, 2, 3)) - assertEquals(7, info.id) - assertEquals(definition, info.definition) - assertContentEquals(byteArrayOf(1, 2, 3), info.metadata) + fun offlineDefinitionsAndUnknownRuntimePayloadsCopyOwnedData(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val definition = + OfflineRegionDefinition.TilePyramid( + "asset://style.json", + LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), + 1.0, + 5.0, + 2.0f, + true, + ) + assertEquals(definition, JavaCppStructs.offlineRegionDefinitionRoundTrip(definition)) + val info = JavaCppStructs.offlineRegionInfoSnapshot(7, definition, byteArrayOf(1, 2, 3)) + assertEquals(7, info.id) + assertEquals(definition, info.definition) + assertContentEquals(byteArrayOf(1, 2, 3), info.metadata) - val payload = - JavaCppStructs.unknownRuntimePayload(999, byteArrayOf(1, 2, 3)) as RuntimeEventPayload.Unknown - assertEquals(999, payload.rawPayloadType) - assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) - assertEquals(1, JavaCppStructs.offlineRegionListCleanupAfterCopyFailure()) - } + val payload = + JavaCppStructs.unknownRuntimePayload(999, byteArrayOf(1, 2, 3)) + as RuntimeEventPayload.Unknown + assertEquals(999, payload.rawPayloadType) + assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) + assertEquals(1, JavaCppStructs.offlineRegionListCleanupAfterCopyFailure()) + } @Test - fun styleAndTextureImageReadersRejectOverflowingSizeTValues() { - val style = - JavaCppStructs.styleImageOptionsSnapshot( - StyleImageOptions().apply { - pixelRatio = 0.0f - sdf = false - } + fun styleAndTextureImageReadersRejectOverflowingSizeTValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val style = + JavaCppStructs.styleImageOptionsSnapshot( + StyleImageOptions().apply { + pixelRatio = 0.0f + sdf = false + } + ) + assertNotEquals(0, style.fields) + assertEquals(0.0f, style.pixelRatio) + assertEquals(false, style.sdf) + assertEquals( + TextureImageInfo(2, 3, 8, 24), + JavaCppStructs.textureImageInfoSnapshot(2, 3, 8, 24), ) - assertNotEquals(0, style.fields) - assertEquals(0.0f, style.pixelRatio) - assertEquals(false, style.sdf) - assertEquals( - TextureImageInfo(2, 3, 8, 24), - JavaCppStructs.textureImageInfoSnapshot(2, 3, 8, 24), - ) - assertFailsWith { - JavaCppStructs.textureImageInfoSnapshot(2, 3, 8, -1) + assertFailsWith { + JavaCppStructs.textureImageInfoSnapshot(2, 3, 8, -1) + } + assertFailsWith { JavaCppStructs.styleImageInfoSnapshot(-1) } + assertEquals(1, JavaCppStructs.styleStringListCleanupAfterCopyFailure()) + val source = JavaCppStructs.sourceInfoSnapshot(999, true) + assertEquals(999, source.type.nativeValue) + assertTrue(source.volatileSource) } - assertFailsWith { JavaCppStructs.styleImageInfoSnapshot(-1) } - assertEquals(1, JavaCppStructs.styleStringListCleanupAfterCopyFailure()) - val source = JavaCppStructs.sourceInfoSnapshot(999, true) - assertEquals(999, source.type.nativeValue) - assertTrue(source.volatileSource) - } @Test - fun backendDescriptorsPreserveExtentsPointersAndBackendFields() { - val extent = RenderTargetExtent(640, 480, 2.0) - val metal = - JavaCppStructs.metalSnapshot( - MetalBorrowedTextureDescriptor(extent, 65, 33, NativePointer.ofAddress(0x20)) - ) - assertEquals(640, metal.width) - assertEquals(0x20, metal.firstPointer) + fun backendDescriptorsPreserveExtentsPointersAndBackendFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val extent = RenderTargetExtent(640, 480, 2.0) + val metal = + JavaCppStructs.metalSnapshot( + MetalBorrowedTextureDescriptor(extent, 65, 33, NativePointer.ofAddress(0x20)) + ) + assertEquals(640, metal.width) + assertEquals(0x20, metal.firstPointer) - val context = - VulkanContextDescriptor( - NativePointer.ofAddress(0x10), - NativePointer.ofAddress(0x20), - NativePointer.ofAddress(0x30), - NativePointer.ofAddress(0x40), - 7, - NativePointer.ofAddress(0x41), - NativePointer.ofAddress(0x42), - ) - val vulkan = - JavaCppStructs.vulkanSnapshot( - VulkanBorrowedTextureDescriptor( - extent, - 65, - 33, - context, - NativePointer.ofAddress(0x50), - NativePointer.ofAddress(0x60), - 44, - 1, - ) - .apply { finalLayout = 2 } - ) - assertEquals(0x50, vulkan.firstPointer) - assertEquals(0x60, vulkan.secondPointer) - assertEquals(2, vulkan.extra) + val context = + VulkanContextDescriptor( + NativePointer.ofAddress(0x10), + NativePointer.ofAddress(0x20), + NativePointer.ofAddress(0x30), + NativePointer.ofAddress(0x40), + 7, + NativePointer.ofAddress(0x41), + NativePointer.ofAddress(0x42), + ) + val vulkan = + JavaCppStructs.vulkanSnapshot( + VulkanBorrowedTextureDescriptor( + extent, + 65, + 33, + context, + NativePointer.ofAddress(0x50), + NativePointer.ofAddress(0x60), + 44, + 1, + ) + .apply { finalLayout = 2 } + ) + assertEquals(0x50, vulkan.firstPointer) + assertEquals(0x60, vulkan.secondPointer) + assertEquals(2, vulkan.extra) - val egl = - EglContextDescriptor( - NativePointer.ofAddress(0x70), - NativePointer.ofAddress(0x71), - NativePointer.ofAddress(0x72), - NativePointer.ofAddress(0x73), - ) - val openGl = - JavaCppStructs.openGlSnapshot(OpenGLBorrowedTextureDescriptor(extent, 65, 33, egl, 17, 3553)) - assertEquals(17, openGl.firstPointer) - assertEquals(0x70, openGl.secondPointer) - assertEquals(3553, openGl.extra) - } + val egl = + EglContextDescriptor( + NativePointer.ofAddress(0x70), + NativePointer.ofAddress(0x71), + NativePointer.ofAddress(0x72), + NativePointer.ofAddress(0x73), + ) + val openGl = + JavaCppStructs.openGlSnapshot( + OpenGLBorrowedTextureDescriptor(extent, 65, 33, egl, 17, 3553) + ) + assertEquals(17, openGl.firstPointer) + assertEquals(0x70, openGl.secondPointer) + assertEquals(3553, openGl.extra) + } @Test - fun mapAndResourceDescriptorsCopyAllCreationFields() { - val map = - JavaCppStructs.mapOptionsSnapshot( - MapOptions().apply { - width = 320 - height = 240 - scaleFactor = 2.0 - mapMode = MapMode.STATIC - fastPforEnabled = true - } - ) - assertEquals(320, map.width) - assertEquals(240, map.height) - assertEquals(MapMode.STATIC.nativeValue, map.mapMode) - assertTrue(map.fastPforEnabled) + fun mapAndResourceDescriptorsCopyAllCreationFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val map = + JavaCppStructs.mapOptionsSnapshot( + MapOptions().apply { + width = 320 + height = 240 + scaleFactor = 2.0 + mapMode = MapMode.STATIC + fastPforEnabled = true + } + ) + assertEquals(320, map.width) + assertEquals(240, map.height) + assertEquals(MapMode.STATIC.nativeValue, map.mapMode) + assertTrue(map.fastPforEnabled) - val resource = - JavaCppStructs.resourceResponseSnapshot( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = byteArrayOf(1, 2, 3) - mustRevalidate = true - modifiedUnixMs = 10 - expiresUnixMs = 20 - retryAfterUnixMs = 30 - } - ) - assertEquals(ResourceResponseStatus.OK.nativeValue, resource.status) - assertContentEquals(byteArrayOf(1, 2, 3), resource.bytes) - assertTrue(resource.mustRevalidate) - assertTrue(resource.hasModified) - assertTrue(resource.hasExpires) - assertTrue(resource.hasRetryAfter) - } + val resource = + JavaCppStructs.resourceResponseSnapshot( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = byteArrayOf(1, 2, 3) + mustRevalidate = true + modifiedUnixMs = 10 + expiresUnixMs = 20 + retryAfterUnixMs = 30 + } + ) + assertEquals(ResourceResponseStatus.OK.nativeValue, resource.status) + assertContentEquals(byteArrayOf(1, 2, 3), resource.bytes) + assertTrue(resource.mustRevalidate) + assertTrue(resource.hasModified) + assertTrue(resource.hasExpires) + assertTrue(resource.hasRetryAfter) + } } diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt index c3bd69ba1..2d92d84ae 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt @@ -11,28 +11,29 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions class CustomGeometrySourceStateTest { @Test - fun callbacksCopyTileIdsContainFailuresAndStopAfterClosureDuringCallback() { - val received = mutableListOf() - lateinit var state: CustomGeometrySourceState - state = - CustomGeometrySourceState( - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) { - received += tileId - state.close() - throw IllegalStateException("contained") + fun callbacksCopyTileIdsContainFailuresAndStopAfterClosureDuringCallback(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val received = mutableListOf() + lateinit var state: CustomGeometrySourceState + state = + CustomGeometrySourceState( + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) { + received += tileId + state.close() + throw IllegalStateException("contained") + } } - } - ) - ) {} + ) + ) {} - state.fetchTileForTesting(CanonicalTileId(4, 5, 6)) - state.fetchTileForTesting(CanonicalTileId(7, 8, 9)) + state.fetchTileForTesting(CanonicalTileId(4, 5, 6)) + state.fetchTileForTesting(CanonicalTileId(7, 8, 9)) - assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) - assertTrue(state.isClosedForTesting()) - } + assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) + assertTrue(state.isClosedForTesting()) + } @Test fun trampolinesDispatchByUserDataPastTheJavaCppPool() { diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.android.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.android.kt index 029f0372b..61967c143 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.android.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.android.kt @@ -19,9 +19,9 @@ private class AndroidEglOwnedTextureSession( private val config: EGLConfig, private val surface: android.opengl.EGLSurface, private val context: android.opengl.EGLContext, - override val session: RenderSessionHandle, + override val attachment: RenderSessionAttachment, ) : OwnedTextureTestSession { - override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionHandle { + override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionAttachment { val descriptor = EglContextDescriptor( NativePointer.ofAddress(display.nativeHandle), @@ -32,32 +32,19 @@ private class AndroidEglOwnedTextureSession( return session .map() .attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) } - override fun acquireFrame(): OwnedTextureTestFrame { - val handle = session.acquireOpenGLOwnedTextureFrame() - val frame = handle.frame() - return object : OwnedTextureTestFrame { - override val width: Int - get() = frame.width() - - override val height: Int - get() = frame.height() - - override val isClosed: Boolean - get() = handle.isClosed - - override fun close() { - handle.close() - } - } + override fun frameSize(frame: AcquiredFrameHandle): OwnedTextureFrameSize { + val texture = frame.openGLTexture() + return OwnedTextureFrameSize(texture.width(), texture.height()) } override fun close() { try { - session.close() + session.abandonAndClose() } finally { releaseEgl(display, surface, context) } @@ -92,11 +79,12 @@ private fun createEglSession(map: MapHandle, width: Int, height: Int): OwnedText NativePointer.ofAddress(context.nativeHandle), NativePointer.NULL_POINTER, ) - val session = + val attachment = map.attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) - return AndroidEglOwnedTextureSession(display, config, surface, context, session) + return AndroidEglOwnedTextureSession(display, config, surface, context, attachment) } catch (error: Throwable) { releaseEgl(display, surface, context) throw error diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleAndroidTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleAndroidTest.kt index a55afe85e..6d2ca302c 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleAndroidTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleAndroidTest.kt @@ -16,73 +16,79 @@ import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC class ResourceRequestHandleAndroidTest { @Test - fun unreachableProviderOwnedHandleReleasesNativeRequest() { - val released = CountDownLatch(1) + fun unreachableProviderOwnedHandleReleasesNativeRequest(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val released = CountDownLatch(1) - registerUnreachableProviderOwnedHandle(released) + registerUnreachableProviderOwnedHandle(released) - assertTrue(awaitRelease(released), "expected unreachable request cleanup to release native") - } + assertTrue(awaitRelease(released), "expected unreachable request cleanup to release native") + } @Test - fun completionFailureIsTerminalAndCopiesDiagnosticBeforeReleaseCleanup() { - val releases = AtomicInteger(0) - val handle = - ResourceRequestHandle( - 1L, - completer = { _, _ -> MaplibreNativeC.mln_network_status_set(999_999) }, - releaser = { - releases.incrementAndGet() - MaplibreNativeC.mln_runtime_destroy(0L) - }, + fun completionFailureIsTerminalAndCopiesDiagnosticBeforeReleaseCleanup(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val releases = AtomicInteger(0) + val handle = + ResourceRequestHandle( + 1L, + completer = { _, _ -> MaplibreNativeC.mln_network_status_set(999_999) }, + releaser = { + releases.incrementAndGet() + MaplibreNativeC.mln_runtime_release(0L, null) + }, + ) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue, + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue, - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - val failure = - assertFailsWith { + val failure = + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } + assertTrue(failure.diagnostic.contains("network status")) + // The releaser's own failing call must not overwrite the copied diagnostic. + assertFalse(failure.diagnostic.contains("completion")) + assertEquals(1, releases.get()) + assertFailsWith { handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - assertTrue(failure.diagnostic.contains("network status")) - assertFalse(failure.diagnostic.contains("runtime")) - assertEquals(1, releases.get()) - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - } @Test - fun concurrentCloseDefersReleaseUntilCompletionAndCancellationUsesInjectedCheck() { - val entered = CountDownLatch(1) - val continueCompletion = CountDownLatch(1) - val releases = AtomicInteger(0) - val handle = - ResourceRequestHandle( - 1L, - completer = { _, _ -> - entered.countDown() - continueCompletion.await(5, TimeUnit.SECONDS) - MaplibreStatus.OK.nativeCode - }, - cancellationChecker = { true }, - releaser = { releases.incrementAndGet() }, + fun concurrentCloseDefersReleaseUntilCompletionAndCancellationUsesInjectedCheck(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val entered = CountDownLatch(1) + val continueCompletion = CountDownLatch(1) + val releases = AtomicInteger(0) + val handle = + ResourceRequestHandle( + 1L, + completer = { _, _ -> + entered.countDown() + continueCompletion.await(5, TimeUnit.SECONDS) + MaplibreStatus.OK.nativeCode + }, + cancellationChecker = { true }, + releaser = { releases.incrementAndGet() }, + ) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue, + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue, - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - assertTrue(handle.isCancelled()) - val completion = thread { handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - handle.close() - assertEquals(0, releases.get()) - continueCompletion.countDown() - completion.join() - assertEquals(1, releases.get()) - assertFailsWith { handle.isCancelled() } - } + assertTrue(handle.isCancelled()) + val completion = thread { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } + assertTrue(entered.await(5, TimeUnit.SECONDS)) + handle.close() + assertEquals(0, releases.get()) + continueCompletion.countDown() + completion.join() + assertEquals(1, releases.get()) + assertFailsWith { handle.isCancelled() } + } private fun registerUnreachableProviderOwnedHandle(released: CountDownLatch) { val handle = ResourceRequestHandle(1L, releaser = { released.countDown() }) diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/AndroidAssetStyleTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/AndroidAssetStyleTest.kt index 617073df9..93e7954a0 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/AndroidAssetStyleTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/AndroidAssetStyleTest.kt @@ -4,6 +4,7 @@ import androidx.test.platform.app.InstrumentationRegistry import java.io.File import java.util.concurrent.ConcurrentLinkedQueue import kotlin.test.Test +import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail import org.maplibre.nativeffi.Maplibre @@ -14,17 +15,17 @@ import org.maplibre.nativeffi.map.MapOptions class AndroidAssetStyleTest { @Test - fun styleLoadsFromAssetScheme() { + fun styleLoadsFromAssetScheme(): Unit = runSuspendTest { assertTrue(loadStyle("asset://style.json")) } @Test - fun styleLoadsFromAndroidAssetFileUri() { + fun styleLoadsFromAndroidAssetFileUri(): Unit = runSuspendTest { assertTrue(loadStyle("file:///android_asset/style.json")) } @Test - fun styleLoadsFromFilesystemFileUri() { + fun styleLoadsFromFilesystemFileUri(): Unit = runSuspendTest { val instrumentation = InstrumentationRegistry.getInstrumentation() val downloaded = File(instrumentation.targetContext.filesDir, "downloaded-style.json") instrumentation.context.assets.open("style.json").use { input -> @@ -34,7 +35,7 @@ class AndroidAssetStyleTest { } @Test - fun pmtilesAssetSourceReadsRangedMetadata() { + fun pmtilesAssetSourceReadsRangedMetadata(): Unit = runSuspendTest { val sourceErrors = ConcurrentLinkedQueue() Maplibre.setLogCallback( LogCallback { record -> @@ -55,14 +56,14 @@ class AndroidAssetStyleTest { }, ) .use { map -> - map.setStyleJson(PMTILES_STYLE.encodeToByteArray()) + map.setStyleJson(PMTILES_STYLE.encodeToByteArray()).await() assertTrue(waitForStyleLoaded(runtime, map)) - assertTrue(map.styleSourceExists("tiles")) + assertNotNull(map.styleSourceInfo("tiles").await()) // URL sources do not expose parsed TileJSON through styleSourceInfo. // A range miss returns the whole archive, which is not JSON, and // MapLibre logs a source load failure. repeat(2_000) { - runtime.pump(0) + runtime.barrier().await() val failed = runtime.drainEvents().events.filter { it.type == RuntimeEventType.MAP_LOADING_FAILED @@ -71,7 +72,6 @@ class AndroidAssetStyleTest { fail(failed.joinToString { it.message }) } sourceErrors.poll()?.let { fail(it) } - runtime.pump(1) waitForAsyncTestWork() } } @@ -81,7 +81,7 @@ class AndroidAssetStyleTest { } } - private fun loadStyle(styleUrl: String): Boolean { + private suspend fun loadStyle(styleUrl: String): Boolean { var loaded = false RuntimeHandle.create(RuntimeOptions()).use { runtime -> MapHandle.create( @@ -93,16 +93,16 @@ class AndroidAssetStyleTest { }, ) .use { map -> - map.setStyleUrl(styleUrl) + map.setStyleUrl(styleUrl).await() loaded = waitForStyleLoaded(runtime, map) } } return loaded } - private fun waitForStyleLoaded(runtime: RuntimeHandle, map: MapHandle): Boolean { + private suspend fun waitForStyleLoaded(runtime: RuntimeHandle, map: MapHandle): Boolean { repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() if ( runtime.drainEvents().events.any { it.type == RuntimeEventType.MAP_STYLE_LOADED && it.mapSource == map @@ -110,7 +110,6 @@ class AndroidAssetStyleTest { ) { return true } - runtime.pump(1) waitForAsyncTestWork() } return false diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventAndroidTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventAndroidTest.kt index 3af8a90b8..5541022e6 100644 --- a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventAndroidTest.kt +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventAndroidTest.kt @@ -10,52 +10,54 @@ import org.maplibre.nativeffi.map.MapOptions class RuntimeEventAndroidTest { @Test - fun unknownDomainsAndEventsFromReleasedMapsCopyWithoutResurrectingHandles() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - val mapId = map.nativeHandleId() - map.close() - try { - val orphan = - runtime.copyEventForTesting( - RuntimeEventType.MAP_STYLE_LOADED.nativeValue, - RuntimeEventSourceType.MAP.nativeValue, - mapId, - 0, - RuntimeEventPayload.None, - "", - ) - assertNull(orphan.mapSource) - assertNull(orphan.runtimeSource) - assertEquals(mapId, orphan.sourceId) + fun unknownDomainsAndEventsFromReleasedMapsCopyWithoutResurrectingHandles(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + val mapId = map.nativeHandleId() + map.close() + try { + val orphan = + runtime.copyEventForTesting( + RuntimeEventType.MAP_STYLE_LOADED.nativeValue, + RuntimeEventSourceType.MAP.nativeValue, + mapId, + 0, + RuntimeEventPayload.None, + "", + ) + assertNull(orphan.mapSource) + assertNull(orphan.runtimeSource) + assertEquals(mapId, orphan.sourceId) - val unknown = - runtime.copyEventForTesting( - 900, - 901, - 0x5AL, - 902, - JavaCppStructs.unknownRuntimePayload(903, byteArrayOf(1, 2, 3)), - "future event", - ) - assertEquals(900, unknown.type.nativeValue) - assertEquals(901, unknown.sourceType.nativeValue) - assertEquals(0x5AL, unknown.sourceId) - assertNull(unknown.mapSource) - assertNull(unknown.runtimeSource) - assertEquals("future event", unknown.message) - val payload = unknown.payload as RuntimeEventPayload.Unknown - assertEquals(903, payload.rawPayloadType) - assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) - } finally { - runtime.close() + val unknown = + runtime.copyEventForTesting( + 900, + 901, + 0x5AL, + 902, + JavaCppStructs.unknownRuntimePayload(903, byteArrayOf(1, 2, 3)), + "future event", + ) + assertEquals(900, unknown.type.nativeValue) + assertEquals(901, unknown.sourceType.nativeValue) + assertEquals(0x5AL, unknown.sourceId) + assertNull(unknown.mapSource) + assertNull(unknown.runtimeSource) + assertEquals("future event", unknown.message) + val payload = unknown.payload as RuntimeEventPayload.Unknown + assertEquals(903, payload.rawPayloadType) + assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) + } finally { + runtime.close() + } } - } } diff --git a/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorAndroidTest.kt b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorAndroidTest.kt new file mode 100644 index 000000000..65997918b --- /dev/null +++ b/bindings/kotlin/src/androidDeviceTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorAndroidTest.kt @@ -0,0 +1,28 @@ +package org.maplibre.nativeffi.runtime + +import java.util.concurrent.CompletableFuture +import kotlin.test.Test +import kotlin.test.assertTrue +import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.map.MapOptions + +class RuntimeExecutorAndroidTest { + @Test + fun runtimeAndMapProgressAcrossCoroutineResumptionAndHostThreads(): Unit = runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + val generation = CompletableFuture.supplyAsync { map.snapshot().generation }.get() + runtime.barrier().await() + assertTrue(map.queryCamera().await().generation >= generation) + map.close() + runtime.close() + } +} diff --git a/bindings/kotlin/src/androidMain/java/org/maplibre/nativeffi/internal/javacpp/MaplibreNativeCConfig.java b/bindings/kotlin/src/androidMain/java/org/maplibre/nativeffi/internal/javacpp/MaplibreNativeCConfig.java index 8b5045f4b..41cbc6c42 100644 --- a/bindings/kotlin/src/androidMain/java/org/maplibre/nativeffi/internal/javacpp/MaplibreNativeCConfig.java +++ b/bindings/kotlin/src/androidMain/java/org/maplibre/nativeffi/internal/javacpp/MaplibreNativeCConfig.java @@ -15,6 +15,8 @@ "maplibre_native_c/diagnostics.h", "maplibre_native_c/logging.h", "maplibre_native_c/android.h", + "maplibre_native_c/completion.h", + "maplibre_native_c/wake.h", "maplibre_native_c/runtime.h", "maplibre_native_c/camera.h", "maplibre_native_c/render_target.h", diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt new file mode 100644 index 000000000..4642faeb2 --- /dev/null +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt @@ -0,0 +1,111 @@ +package org.maplibre.nativeffi.internal.async + +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import org.bytedeco.javacpp.BytePointer +import org.bytedeco.javacpp.Pointer +import org.maplibre.nativeffi.error.MaplibreException +import org.maplibre.nativeffi.error.MaplibreStatus +import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport +import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC +import org.maplibre.nativeffi.internal.status.Status +import org.maplibre.nativeffi.runtime.CommandCompletion +import org.maplibre.nativeffi.runtime.CommandDisposition + +internal object CompletionBridge { + private class State( + val convert: (MaplibreNativeC.mln_completion_result) -> T, + val acceptErrorStatus: Boolean, + ) { + val deferred = CompletableDeferred() + val token = BytePointer(1L) + } + + private val states = ConcurrentHashMap>() + private val callback = + object : MaplibreNativeC.mln_completion_callback() { + override fun call(userData: Pointer?, result: MaplibreNativeC.mln_completion_result?) { + if (userData != null && result != null) complete(userData.address(), result) + } + } + private val release = + object : MaplibreNativeC.mln_completion_release() { + override fun call(userData: Pointer?) { + userData ?: return + states.remove(userData.address())?.token?.close() + } + } + + fun submit( + convert: (MaplibreNativeC.mln_completion_result) -> T, + call: (MaplibreNativeC.mln_completion) -> Int, + ): Deferred = submitInternal(convert, false, false, call) + + private fun submitInternal( + convert: (MaplibreNativeC.mln_completion_result) -> T, + rejectSynchronously: Boolean, + acceptErrorStatus: Boolean, + call: (MaplibreNativeC.mln_completion) -> Int, + ): Deferred { + val state = State(convert, acceptErrorStatus) + states[state.token.address()] = state + try { + MaplibreNativeC.mln_completion().use { completion -> + completion.size(completion.sizeof()) + completion.callback(callback) + completion.user_data(state.token) + completion.release_user_data(release) + Status.check(call(completion)) + } + } catch (failure: Throwable) { + if (states.remove(state.token.address(), state)) state.token.close() + if (rejectSynchronously) throw failure + state.deferred.completeExceptionally(failure) + } + return state.deferred + } + + fun unit(call: (MaplibreNativeC.mln_completion) -> Int): Deferred = submit({ _ -> }, call) + + fun unitChecked(call: (MaplibreNativeC.mln_completion) -> Int): Deferred = + submitInternal({ _ -> }, true, false, call) + + fun command(call: (MaplibreNativeC.mln_completion) -> Int): Deferred = + submitInternal( + { result -> + CommandCompletion( + CommandDisposition.fromNative(result.disposition()), + result.generation().toULong(), + MaplibreStatus.fromNative(result.status()), + diagnostic(result), + ) + }, + false, + true, + call, + ) + + @Suppress("UNCHECKED_CAST") + private fun complete(address: Long, result: MaplibreNativeC.mln_completion_result) { + val state = states[address] as? State ?: return + try { + val status = result.status() + if (status == MaplibreStatus.OK.nativeCode || state.acceptErrorStatus) { + state.deferred.complete(state.convert(result)) + } else { + val message = diagnostic(result) + state.deferred.completeExceptionally( + MaplibreException.forStatus(MaplibreStatus.fromNative(status), status, message) + ) + } + } catch (failure: Throwable) { + state.deferred.completeExceptionally(failure) + } + } + + private fun diagnostic(result: MaplibreNativeC.mln_completion_result): String { + val diagnostic = result.diagnostic() + return JavaCppSupport.byteArray(diagnostic.data(), diagnostic.size()).decodeToString() + } +} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt index 376a3f8b0..0be6a62ed 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt @@ -8,6 +8,7 @@ import org.bytedeco.javacpp.Pointer import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.resource.HttpHeaderTransformCallback import org.maplibre.nativeffi.resource.HttpHeaderTransformRequest import org.maplibre.nativeffi.resource.ResourceKind @@ -22,6 +23,7 @@ internal class HttpHeaderTransformState(private val callback: HttpHeaderTransfor transform.size(transform.sizeof()) transform.callback(NATIVE_CALLBACK) transform.user_data(JavaCppSupport.addressPointer(token)) + transform.release_user_data(NATIVE_RELEASE) STATES[token] = this } @@ -110,5 +112,15 @@ internal class HttpHeaderTransformState(private val callback: HttpHeaderTransfor STATES[userData?.address() ?: 0L]?.invoke(kind, url, response) ?: MaplibreStatus.INVALID_ARGUMENT.nativeCode } + + /** One process-wide release thunk; per-state thunks would exhaust the same pool. */ + private val NATIVE_RELEASE = + object : MaplibreNativeC.mln_runtime_callback_release() { + override fun call(userData: Pointer?) { + val state = STATES[userData?.address() ?: 0L] ?: return + HandleLeakCleaner.releaseNativeCallbackRoot(state) + state.close() + } + } } } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt index fe7e3c8cd..adb21b49f 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt @@ -1,13 +1,13 @@ package org.maplibre.nativeffi.internal.callback -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong import org.bytedeco.javacpp.BytePointer import org.bytedeco.javacpp.Pointer import org.maplibre.nativeffi.NativeAccess import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.log.LogEvent @@ -15,10 +15,13 @@ import org.maplibre.nativeffi.log.LogRecord import org.maplibre.nativeffi.log.LogSeverity /** Owns process-global Android JNI logging callback state. */ -@OptIn(ExperimentalAtomicApi::class) -internal class LogCallbackState private constructor(private val callback: LogCallback) : - AutoCloseable { - private val gate = CallbackGate("log callbacks") +internal class LogCallbackState(private val callback: LogCallback) : AutoCloseable { + private val token = TOKENS.getAndIncrement() + private val gate = CallbackGate("log callbacks") { STATES.remove(token) } + + init { + STATES[token] = this + } fun invoke(rawSeverity: Int, rawEvent: Int, code: Long, message: BytePointer?): Int { val lease = gate.enter() ?: return 0 @@ -38,81 +41,39 @@ internal class LogCallbackState private constructor(private val callback: LogCal } } - fun checkCanClose() = gate.checkCanClose() + override fun close() = gate.close() fun isClosedForTesting(): Boolean = gate.isClosedForTesting() - override fun close() = gate.close() - internal companion object { - private val updateLock = AtomicInt(0) - private val current = AtomicReference(null) - fun set(callback: LogCallback) { NativeAccess.ensureLoaded() - replace(callback) { - Status.check(MaplibreNativeC.mln_log_set_callback(NATIVE_CALLBACK, null)) - } - } - - internal fun setForTesting(callback: LogCallback, register: (LogCallbackState) -> Unit = {}) { - replace(callback, register) - } - - private fun replace(callback: LogCallback, register: (LogCallbackState) -> Unit) { val replacement = LogCallbackState(callback) - var previous: LogCallbackState? = null + HandleLeakCleaner.retainNativeCallbackRoot(replacement) try { - withUpdateLock { - current.load()?.checkCanClose() - // Publish before native install. The shared thunk dispatches through `current`, so - // installing first would deliver logs to the previous callback until exchange. - previous = current.exchange(replacement) - try { - register(replacement) - } catch (error: Throwable) { - current.store(previous) - previous = null - throw error - } - } + Status.check( + MaplibreNativeC.mln_log_set_callback( + NATIVE_CALLBACK, + JavaCppSupport.addressPointer(replacement.token), + NATIVE_RELEASE, + ) + ) } catch (error: Throwable) { - closeAndSuppress(error, replacement) + HandleLeakCleaner.releaseNativeCallbackRoot(replacement) + replacement.close() throw error } - closeQuietly(previous) } fun clear() { NativeAccess.ensureLoaded() - var previous: LogCallbackState? = null - withUpdateLock { - current.load()?.checkCanClose() - Status.check(MaplibreNativeC.mln_log_clear_callback()) - previous = current.exchange(null) - } - closeQuietly(previous) + Status.check(MaplibreNativeC.mln_log_clear_callback()) } - fun currentForTesting(): LogCallbackState? = current.load() + fun createForTesting(callback: LogCallback): LogCallbackState = LogCallbackState(callback) - internal fun clearForTesting() { - closeQuietly(current.exchange(null)) - } - - private fun closeQuietly(state: LogCallbackState?) { - try { - state?.close() - } catch (_: RuntimeException) {} - } - - private fun closeAndSuppress(error: Throwable, state: LogCallbackState?) { - try { - state?.close() - } catch (cleanup: Throwable) { - error.addSuppressed(cleanup) - } - } + private val TOKENS = AtomicLong(1) + private val STATES = ConcurrentHashMap() /** * One process-wide thunk. JavaCPP's FunctionPointer pool is ten slots per generated class, so @@ -126,18 +87,17 @@ internal class LogCallbackState private constructor(private val callback: LogCal event: Int, code: Long, message: BytePointer?, - ): Int = current.load()?.invoke(severity, event, code, message) ?: 0 + ): Int = STATES[userData?.address() ?: 0L]?.invoke(severity, event, code, message) ?: 0 } - private inline fun withUpdateLock(block: () -> R): R { - while (!updateLock.compareAndSet(0, 1)) { - // Spin briefly; log callback registration is process-global and infrequent. - } - try { - return block() - } finally { - updateLock.store(0) + /** One process-wide release thunk; per-state thunks would exhaust the same pool. */ + private val NATIVE_RELEASE = + object : MaplibreNativeC.mln_log_callback_release() { + override fun call(userData: Pointer?) { + val state = STATES[userData?.address() ?: 0L] ?: return + HandleLeakCleaner.releaseNativeCallbackRoot(state) + state.close() + } } - } } } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt index d1bea3ffb..646b0b780 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt @@ -6,6 +6,7 @@ import org.bytedeco.javacpp.BytePointer import org.bytedeco.javacpp.Pointer import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.resource.ResourceKind import org.maplibre.nativeffi.resource.ResourceLoadingMethod import org.maplibre.nativeffi.resource.ResourcePriority @@ -26,6 +27,7 @@ internal class ResourceProviderState(private val callback: ResourceProviderCallb provider.size(provider.sizeof()) provider.callback(NATIVE_CALLBACK) provider.user_data(JavaCppSupport.addressPointer(token)) + provider.release_user_data(NATIVE_RELEASE) STATES[token] = this } @@ -94,5 +96,15 @@ internal class ResourceProviderState(private val callback: ResourceProviderCallb handle: Long, ): Int = STATES[userData?.address() ?: 0L]?.invoke(request, handle) ?: UNKNOWN_DECISION } + + /** One process-wide release thunk; per-state thunks would exhaust the same pool. */ + private val NATIVE_RELEASE = + object : MaplibreNativeC.mln_runtime_callback_release() { + override fun call(userData: Pointer?) { + val state = STATES[userData?.address() ?: 0L] ?: return + HandleLeakCleaner.releaseNativeCallbackRoot(state) + state.close() + } + } } } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt index df40eff29..b3bb40c64 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt @@ -8,6 +8,7 @@ import org.bytedeco.javacpp.Pointer import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.resource.ResourceKind import org.maplibre.nativeffi.resource.ResourceTransformCallback import org.maplibre.nativeffi.resource.ResourceTransformRequest @@ -23,6 +24,7 @@ internal class ResourceTransformState(private val callback: ResourceTransformCal transform.size(transform.sizeof()) transform.callback(NATIVE_CALLBACK) transform.user_data(JavaCppSupport.addressPointer(token)) + transform.release_user_data(NATIVE_RELEASE) STATES[token] = this } @@ -101,5 +103,15 @@ internal class ResourceTransformState(private val callback: ResourceTransformCal STATES[userData?.address() ?: 0L]?.invoke(kind, url, response) ?: MaplibreStatus.INVALID_ARGUMENT.nativeCode } + + /** One process-wide release thunk; per-state thunks would exhaust the same pool. */ + private val NATIVE_RELEASE = + object : MaplibreNativeC.mln_runtime_callback_release() { + override fun call(userData: Pointer?) { + val state = STATES[userData?.address() ?: 0L] ?: return + HandleLeakCleaner.releaseNativeCallbackRoot(state) + state.close() + } + } } } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/javacpp/JavaCppStructs.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/javacpp/JavaCppStructs.kt index f6e97b610..6f89ff992 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/javacpp/JavaCppStructs.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/javacpp/JavaCppStructs.kt @@ -10,7 +10,6 @@ import org.maplibre.nativeffi.map.TileOptions import org.maplibre.nativeffi.map.ViewportOptions import org.maplibre.nativeffi.offline.OfflineRegionDefinition import org.maplibre.nativeffi.offline.OfflineRegionInfo -import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.render.JavaCppRenderStructs import org.maplibre.nativeffi.render.MetalBorrowedTextureDescriptor @@ -48,11 +47,6 @@ internal object JavaCppStructs { fun renderedQueryGeometryType(value: RenderedQueryGeometry): Int = JavaCppRenderStructs.renderedQueryGeometryType(value) - fun featureStateSelectorSnapshot( - value: FeatureStateSelector - ): JavaCppRenderStructs.FeatureStateSelectorSnapshot = - JavaCppRenderStructs.featureStateSelectorSnapshot(value) - fun ownedBufferCleanupAfterCopyFailure(): Int { var destroys = 0 try { diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt index d1804fda1..7d150e7e3 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt @@ -2,13 +2,12 @@ package org.maplibre.nativeffi.internal.lifecycle import java.util.concurrent.ConcurrentHashMap import org.maplibre.nativeffi.render.OwnedTextureFrameHandleCore -import org.maplibre.nativeffi.runtime.OfflineOperationLeakReport /** - * Reports owner-thread-affine native handles that become unreachable before explicit release. + * Reports native handles that become unreachable before explicit release. * - * Runtime, map, and render-session handles are bound to their owner thread, so this hook MUST NOT - * destroy them; explicit release on the owner thread stays the only path that frees native state. + * Cleanup remains explicit so failures are observable. Render sessions also require their graphics + * thread, which an unreachable-action thread cannot provide. * * Registered actions must capture leak-report state only. Capturing the wrapper would keep it * reachable and suppress every report. @@ -41,11 +40,6 @@ internal object HandleLeakCleaner { leakReportActions.register(handle, FrameLeakAction(frameCore)) } - /** Reports [leakReport] when an offline operation becomes unreachable. */ - fun registerOfflineOperation(handle: Any, leakReport: OfflineOperationLeakReport) { - leakReportActions.register(handle, OfflineOperationLeakAction(leakReport)) - } - private class LeakReportAction(private val leakReport: HandleStateCore.LeakReport) : Runnable { override fun run() { leakReport.report() @@ -57,11 +51,4 @@ internal object HandleLeakCleaner { frameCore.reportLeak() } } - - private class OfflineOperationLeakAction(private val leakReport: OfflineOperationLeakReport) : - Runnable { - override fun run() { - leakReport.report() - } - } } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt index 88bba37d5..c3c6c8bb6 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt @@ -2,7 +2,7 @@ package org.maplibre.nativeffi.map import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong -import org.bytedeco.javacpp.BoolPointer +import kotlinx.coroutines.Deferred import org.bytedeco.javacpp.BytePointer import org.bytedeco.javacpp.LongPointer import org.bytedeco.javacpp.Pointer @@ -11,8 +11,10 @@ import org.maplibre.nativeffi.NativeAccess import org.maplibre.nativeffi.camera.AnimationOptions import org.maplibre.nativeffi.camera.BoundOptions import org.maplibre.nativeffi.camera.BoundsConstraint -import org.maplibre.nativeffi.camera.CameraFitOptions +import org.maplibre.nativeffi.camera.CameraDelta import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraSnapshot +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.EdgeInsets import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.geo.CanonicalTileId @@ -21,12 +23,13 @@ import org.maplibre.nativeffi.geo.LatLngBounds import org.maplibre.nativeffi.geo.Quaternion import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.geo.Vec3 +import org.maplibre.nativeffi.internal.async.CompletionBridge +import org.maplibre.nativeffi.internal.async.mapDeferred import org.maplibre.nativeffi.internal.callback.CallbackGate import org.maplibre.nativeffi.internal.javacpp.ByteArrayViewScope import org.maplibre.nativeffi.internal.javacpp.GeoJsonSourceOptionsScope import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.javacpp.ownedBuffer import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.status.Status @@ -39,10 +42,13 @@ import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLOwnedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor import org.maplibre.nativeffi.render.PremultipliedRgba8Image +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor import org.maplibre.nativeffi.render.VulkanOwnedTextureDescriptor import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor +import org.maplibre.nativeffi.runtime.CommandCompletion import org.maplibre.nativeffi.runtime.RuntimeEventMask import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @@ -51,6 +57,7 @@ import org.maplibre.nativeffi.style.GeoJsonSourceDataHandle import org.maplibre.nativeffi.style.GeoJsonSourceOptions import org.maplibre.nativeffi.style.ImageContent import org.maplibre.nativeffi.style.ImageStretch +import org.maplibre.nativeffi.style.LayerInfo import org.maplibre.nativeffi.style.LocationIndicatorImageKind import org.maplibre.nativeffi.style.RasterDemEncoding import org.maplibre.nativeffi.style.SourceInfo @@ -68,9 +75,13 @@ import org.maplibre.nativeffi.style.VectorTileEncoding /** Owned Android JNI map handle. */ public actual class MapHandle -private constructor(private val runtime: RuntimeHandle, private val handleId: Long) : - AutoCloseable { - private val runtimeRetention = runtime.retainChild("MapHandle") +private constructor( + private val runtime: RuntimeHandle, + private val handleId: Long, + cachedEventMask: RuntimeEventMask, +) { + private var cachedEventMask = + RuntimeEventMask(cachedEventMask.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) private val core = HandleStateCore("MapHandle", handleId) init { @@ -87,33 +98,46 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo public actual fun runtime(): RuntimeHandle = runtime - public actual var eventMask: RuntimeEventMask - get() { - NativeAccess.ensureLoaded() - val outMask = LongArray(1) - Status.check(MaplibreNativeC.mln_map_get_event_mask(requireLiveHandle(), outMask)) - return RuntimeEventMask(outMask[0]) - } - set(value) { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_set_event_mask(requireLiveHandle(), value.nativeValue)) - } + public actual val eventMask: RuntimeEventMask + get() = cachedEventMask - public actual fun setStyleUrl(url: String) { + public actual fun setEventMask(value: RuntimeEventMask): Deferred { NativeAccess.ensureLoaded() - optionalCString(url).use { nativeUrl -> - Status.check(MaplibreNativeC.mln_map_set_style_url(requireLiveHandle(), nativeUrl)) - } + return command { completion -> + Status.check( + MaplibreNativeC.mln_map_set_event_mask(requireLiveHandle(), value.nativeValue, completion) + ) + } + .mapDeferred { + cachedEventMask = + RuntimeEventMask(value.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) + it + } } - public actual fun setStyleJson(json: ByteArray) { + public actual fun setStyleUrl(url: String): Deferred = command { completion -> NativeAccess.ensureLoaded() - ByteArrayViewScope(json).use { nativeJson -> - Status.check(MaplibreNativeC.mln_map_set_style_json(requireLiveHandle(), nativeJson.view)) + optionalCString(url).use { nativeUrl -> + Status.check( + MaplibreNativeC.mln_map_set_style_url(requireLiveHandle(), nativeUrl, completion) + ) } } - public actual fun setFeatureState(selector: FeatureStateSelector, value: ByteArray) { + public actual fun setStyleJson(json: ByteArray): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + ByteArrayViewScope(json).use { nativeJson -> + Status.check( + MaplibreNativeC.mln_map_set_style_json(requireLiveHandle(), nativeJson.view, completion) + ) + } + } + + public actual fun setFeatureState( + selector: FeatureStateSelector, + value: ByteArray, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() FeatureStateSelectorScope(selector).use { nativeSelector -> ByteArrayViewScope(value).use { nativeValue -> @@ -122,54 +146,55 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSelector.selector, nativeValue.view, + completion, ) ) } } } - public actual fun getFeatureState(selector: FeatureStateSelector): ByteArray { + public actual fun getFeatureState(selector: FeatureStateSelector): Deferred { NativeAccess.ensureLoaded() - FeatureStateSelectorScope(selector).use { nativeSelector -> - LongPointer(1).use { outState -> - outState.put(0, 0L) - Status.check( - MaplibreNativeC.mln_map_get_feature_state( - requireLiveHandle(), - nativeSelector.selector, - outState, - ) + return FeatureStateSelectorScope(selector).use { nativeSelector -> + CompletionBridge.submit(::requiredBuffer) { completion -> + MaplibreNativeC.mln_map_get_feature_state( + requireLiveHandle(), + nativeSelector.selector, + completion, ) - return ownedBuffer(outState.get()) } } } - public actual fun removeFeatureState(selector: FeatureStateSelector) { + public actual fun removeFeatureState( + selector: FeatureStateSelector + ): Deferred = command { completion -> NativeAccess.ensureLoaded() FeatureStateSelectorScope(selector).use { nativeSelector -> Status.check( - MaplibreNativeC.mln_map_remove_feature_state(requireLiveHandle(), nativeSelector.selector) + MaplibreNativeC.mln_map_remove_feature_state( + requireLiveHandle(), + nativeSelector.selector, + completion, + ) ) } } - public actual fun loadedStyleJson(): ByteArray { - NativeAccess.ensureLoaded() - return copyMapBytes(requireLiveHandle()) { mapId, text, capacity, outSize -> - MaplibreNativeC.mln_map_copy_loaded_style_json(mapId, text, capacity, outSize) + public actual fun loadedStyleJson(): Deferred = + CompletionBridge.submit(::requiredBuffer) { completion -> + MaplibreNativeC.mln_map_loaded_style_json(requireLiveHandle(), completion) } - } - public actual fun styleUrl(): String { - NativeAccess.ensureLoaded() - return copyMapBytes(requireLiveHandle()) { mapId, text, capacity, outSize -> - MaplibreNativeC.mln_map_copy_style_url(mapId, text, capacity, outSize) - } - .toString(java.nio.charset.StandardCharsets.UTF_8) - } + public actual fun styleUrl(): Deferred = + CompletionBridge.submit({ result -> requiredBuffer(result).decodeToString() }) { completion -> + MaplibreNativeC.mln_map_style_url(requireLiveHandle(), completion) + } - public actual fun addStyleSourceJson(sourceId: String, sourceJson: ByteArray) { + public actual fun addStyleSourceJson( + sourceId: String, + sourceJson: ByteArray, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> ByteArrayViewScope(sourceJson).use { nativeSourceJson -> @@ -178,129 +203,89 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeSourceJson.view, + completion, ) ) } } } - public actual fun removeStyleSource(sourceId: String): Boolean { - NativeAccess.ensureLoaded() - val outRemoved = booleanArrayOf(false) - StringViewScope(sourceId).use { nativeSourceId -> - Status.check( - MaplibreNativeC.mln_map_remove_style_source( - requireLiveHandle(), - nativeSourceId.view, - outRemoved, - ) - ) - } - return outRemoved[0] - } - - public actual fun styleSourceExists(sourceId: String): Boolean { - NativeAccess.ensureLoaded() - val outExists = booleanArrayOf(false) - StringViewScope(sourceId).use { nativeSourceId -> - Status.check( - MaplibreNativeC.mln_map_style_source_exists( - requireLiveHandle(), - nativeSourceId.view, - outExists, - ) - ) - } - return outExists[0] - } - - public actual fun styleSourceType(sourceId: String): SourceType? { - NativeAccess.ensureLoaded() - val outType = intArrayOf(0) - val outFound = booleanArrayOf(false) - StringViewScope(sourceId).use { nativeSourceId -> - Status.check( - MaplibreNativeC.mln_map_get_style_source_type( - requireLiveHandle(), - nativeSourceId.view, - outType, - outFound, + public actual fun removeStyleSource(sourceId: String): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(sourceId).use { nativeSourceId -> + Status.check( + MaplibreNativeC.mln_map_remove_style_source( + requireLiveHandle(), + nativeSourceId.view, + completion, + ) ) - ) + } } - return if (outFound[0]) SourceType.fromNative(outType[0]) else null - } - public actual fun styleSourceInfo(sourceId: String): SourceInfo? { - NativeAccess.ensureLoaded() - StringViewScope(sourceId).use { nativeSourceId -> - MaplibreNativeC.mln_style_source_info().use { outInfo -> - outInfo.size(outInfo.sizeof()) - val outFound = booleanArrayOf(false) - Status.check( + public actual fun styleSourceInfo(sourceId: String): Deferred { + NativeAccess.ensureLoaded() + return StringViewScope(sourceId).use { nativeSourceId -> + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else { + val raw = MaplibreNativeC.mln_style_source_result(result.value()) + val info = raw.info() + val fields = info.fields() + val tileUrls = + List(Math.toIntExact(raw.tile_url_count())) { index -> + stringView(raw.tile_urls().position(index.toLong())) + } + SourceInfo( + SourceType.fromNative(info.type()), + info.is_volatile(), + if (info.has_attribution()) stringView(raw.attribution()) else null, + if (fields and STYLE_SOURCE_INFO_URL != 0) stringView(raw.url()) else null, + if (fields and STYLE_SOURCE_INFO_TILEJSON != 0) + TileJson( + tileUrls, + info.min_zoom(), + info.max_zoom(), + TileScheme.fromNative(info.scheme()), + if (fields and STYLE_SOURCE_INFO_BOUNDS != 0) latLngBounds(info.bounds()) + else null, + ) + else null, + if (fields and STYLE_SOURCE_INFO_TILE_SIZE != 0) + Math.toIntExact(Integer.toUnsignedLong(info.tile_size())) + else null, + if (fields and STYLE_SOURCE_INFO_VECTOR_ENCODING != 0) + VectorTileEncoding.fromNative(info.vector_encoding()) + else null, + if (fields and STYLE_SOURCE_INFO_RASTER_ENCODING != 0) + RasterDemEncoding.fromNative(info.raster_encoding()) + else null, + ) + } + }, + { completion -> MaplibreNativeC.mln_map_get_style_source_info( requireLiveHandle(), nativeSourceId.view, - outInfo, - outFound, + completion, ) - ) - if (!outFound[0]) return null - val attribution = - if (outInfo.has_attribution()) - copyStyleSourceAttribution(requireLiveHandle(), nativeSourceId, outInfo) - else null - val fields = outInfo.fields() - val url = - if (fields and STYLE_SOURCE_INFO_URL != 0) - copyStyleSourceUrl(requireLiveHandle(), nativeSourceId, outInfo.url_size()) - else null - val tileJson = - if (fields and STYLE_SOURCE_INFO_TILEJSON != 0) - TileJson( - copyStyleSourceTileUrls(requireLiveHandle(), nativeSourceId), - outInfo.min_zoom(), - outInfo.max_zoom(), - TileScheme.fromNative(outInfo.scheme()), - if (fields and STYLE_SOURCE_INFO_BOUNDS != 0) latLngBounds(outInfo.bounds()) else null, - ) - else null - return SourceInfo( - SourceType.fromNative(outInfo.type()), - outInfo.is_volatile(), - attribution, - url, - tileJson, - if (fields and STYLE_SOURCE_INFO_TILE_SIZE != 0) - Math.toIntExact(Integer.toUnsignedLong(outInfo.tile_size())) - else null, - if (fields and STYLE_SOURCE_INFO_VECTOR_ENCODING != 0) - VectorTileEncoding.fromNative(outInfo.vector_encoding()) - else null, - if (fields and STYLE_SOURCE_INFO_RASTER_ENCODING != 0) - RasterDemEncoding.fromNative(outInfo.raster_encoding()) - else null, - ) - } + }, + ) } } - public actual fun styleSourceIds(): List { - NativeAccess.ensureLoaded() - LongPointer(1).use { outList -> - outList.put(0, 0L) - Status.check(MaplibreNativeC.mln_map_list_style_source_ids(requireLiveHandle(), outList)) - val list = outList.get() - require(list != 0L) { "mln_map_list_style_source_ids returned the null handle" } - return styleIdList(list) + public actual fun styleSourceIds(): Deferred> = + CompletionBridge.submit(::stringList) { completion -> + MaplibreNativeC.mln_map_list_style_source_ids(requireLiveHandle(), completion) } - } public actual fun addGeoJsonSourceUrl( sourceId: String, url: String, options: GeoJsonSourceOptions?, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> StringViewScope(url).use { nativeUrl -> @@ -311,6 +296,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeUrl.view, nativeOptions.options, + completion, ) ) } @@ -318,7 +304,10 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun addGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun addGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() data.withNativeHandle { nativeData -> StringViewScope(sourceId).use { nativeSourceId -> @@ -327,13 +316,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeData, + completion, ) ) } } } - public actual fun setGeoJsonSourceUrl(sourceId: String, url: String) { + public actual fun setGeoJsonSourceUrl( + sourceId: String, + url: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> StringViewScope(url).use { nativeUrl -> @@ -342,13 +335,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeUrl.view, + completion, ) ) } } } - public actual fun setGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun setGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() data.withNativeHandle { nativeData -> StringViewScope(sourceId).use { nativeSourceId -> @@ -357,13 +354,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeData, + completion, ) ) } } } - public actual fun setGeoJsonSourceSynchronousTiling(sourceId: String, enabled: Boolean) { + public actual fun setGeoJsonSourceSynchronousTiling( + sourceId: String, + enabled: Boolean, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> Status.check( @@ -371,6 +372,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, enabled, + completion, ) ) } @@ -379,7 +381,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo public actual fun addCustomGeometrySource( sourceId: String, options: CustomGeometrySourceOptions, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() // The release callback captures the registry rather than this map, so a map a // host leaks with a live source still reports as leaked. @@ -392,6 +394,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, sourceState.descriptor, + completion, ) ) } @@ -403,7 +406,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> CanonicalTileIdScope(tileId).use { nativeTileId -> @@ -414,6 +417,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeTileId.tileId, nativeData.view, + completion, ) ) } @@ -421,7 +425,10 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun invalidateCustomGeometrySourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomGeometrySourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> CanonicalTileIdScope(tileId).use { nativeTileId -> @@ -430,13 +437,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeTileId.tileId, + completion, ) ) } } } - public actual fun invalidateCustomGeometrySourceRegion(sourceId: String, bounds: LatLngBounds) { + public actual fun invalidateCustomGeometrySourceRegion( + sourceId: String, + bounds: LatLngBounds, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> Status.check( @@ -444,6 +455,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, latLngBounds(bounds), + completion, ) ) } @@ -452,7 +464,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo public actual fun addCustomMvtVectorSource( sourceId: String, options: CustomMvtVectorSourceOptions, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() // The release callback captures the registry rather than this map, so a map a // host leaks with a live source still reports as leaked. @@ -465,6 +477,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, sourceState.descriptor, + completion, ) ) } @@ -476,7 +489,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> CanonicalTileIdScope(tileId).use { nativeTileId -> @@ -487,6 +500,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeTileId.tileId, nativeData.view, + completion, ) ) } @@ -498,7 +512,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo sourceId: String, tileId: CanonicalTileId, message: String, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> CanonicalTileIdScope(tileId).use { nativeTileId -> @@ -509,6 +523,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeTileId.tileId, nativeMessage.view, + completion, ) ) } @@ -516,7 +531,10 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun invalidateCustomMvtVectorSourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomMvtVectorSourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> CanonicalTileIdScope(tileId).use { nativeTileId -> @@ -525,60 +543,100 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeTileId.tileId, + completion, ) ) } } } - public actual fun addVectorSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addVectorSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - addTileSourceUrl(MaplibreNativeC::mln_map_add_vector_source_url, sourceId, url, options) + addTileSourceUrl( + MaplibreNativeC::mln_map_add_vector_source_url, + sourceId, + url, + options, + completion, + ) } public actual fun addVectorSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - addTileSourceTiles(MaplibreNativeC::mln_map_add_vector_source_tiles, sourceId, tiles, options) + addTileSourceTiles( + MaplibreNativeC::mln_map_add_vector_source_tiles, + sourceId, + tiles, + options, + completion, + ) } - public actual fun addRasterSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addRasterSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - addTileSourceUrl(MaplibreNativeC::mln_map_add_raster_source_url, sourceId, url, options) + addTileSourceUrl( + MaplibreNativeC::mln_map_add_raster_source_url, + sourceId, + url, + options, + completion, + ) } public actual fun addRasterSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - addTileSourceTiles(MaplibreNativeC::mln_map_add_raster_source_tiles, sourceId, tiles, options) + addTileSourceTiles( + MaplibreNativeC::mln_map_add_raster_source_tiles, + sourceId, + tiles, + options, + completion, + ) } public actual fun addRasterDemSourceUrl( sourceId: String, url: String, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - addTileSourceUrl(MaplibreNativeC::mln_map_add_raster_dem_source_url, sourceId, url, options) + addTileSourceUrl( + MaplibreNativeC::mln_map_add_raster_dem_source_url, + sourceId, + url, + options, + completion, + ) } public actual fun addRasterDemSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() addTileSourceTiles( MaplibreNativeC::mln_map_add_raster_dem_source_tiles, sourceId, tiles, options, + completion, ) } @@ -586,7 +644,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo imageId: String, image: PremultipliedRgba8Image, options: StyleImageOptions, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(imageId).use { nativeImageId -> PremultipliedImageScope(image).use { nativeImage -> @@ -597,6 +655,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeImageId.view, nativeImage.image, nativeOptions.options, + completion, ) ) } @@ -604,83 +663,72 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun removeStyleImage(imageId: String): Boolean { - NativeAccess.ensureLoaded() - val outRemoved = booleanArrayOf(false) - StringViewScope(imageId).use { nativeImageId -> - Status.check( - MaplibreNativeC.mln_map_remove_style_image( - requireLiveHandle(), - nativeImageId.view, - outRemoved, - ) - ) - } - return outRemoved[0] - } - - public actual fun styleImageExists(imageId: String): Boolean { - NativeAccess.ensureLoaded() - val outExists = booleanArrayOf(false) - StringViewScope(imageId).use { nativeImageId -> - Status.check( - MaplibreNativeC.mln_map_style_image_exists( - requireLiveHandle(), - nativeImageId.view, - outExists, + public actual fun removeStyleImage(imageId: String): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(imageId).use { nativeImageId -> + Status.check( + MaplibreNativeC.mln_map_remove_style_image( + requireLiveHandle(), + nativeImageId.view, + completion, + ) ) - ) + } } - return outExists[0] - } - public actual fun styleImageInfo(imageId: String): StyleImageInfo? { - NativeAccess.ensureLoaded() + public actual fun styleImageInfo(imageId: String): Deferred = StringViewScope(imageId).use { nativeImageId -> - MaplibreNativeC.mln_style_image_info_default().use { outInfo -> - val outFound = booleanArrayOf(false) - Status.check( + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else styleImageInfo(MaplibreNativeC.mln_style_image_result(result.value()).info()) + }, + { completion -> MaplibreNativeC.mln_map_get_style_image_info( requireLiveHandle(), nativeImageId.view, - outInfo, - outFound, + completion, ) - ) - return if (outFound[0]) styleImageInfo(outInfo) else null - } + }, + ) } - } - public actual fun copyStyleImagePremultipliedRgba8(imageId: String): StyleImage? { - NativeAccess.ensureLoaded() - val info = styleImageInfo(imageId) ?: return null - val outPixels = ByteArray(Math.toIntExact(info.byteLength)) - val outFound = booleanArrayOf(false) + public actual fun copyStyleImagePremultipliedRgba8(imageId: String): Deferred = StringViewScope(imageId).use { nativeImageId -> - SizeTPointer(1).use { outByteLength -> - Status.check( - MaplibreNativeC.mln_map_copy_style_image_premultiplied_rgba8( + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else { + val raw = MaplibreNativeC.mln_style_image_result(result.value()) + val info = styleImageInfo(raw.info()) + StyleImage( + PremultipliedRgba8Image( + info.width, + info.height, + info.stride, + bufferView(raw.pixels()), + ), + info.pixelRatio, + info.sdf, + ) + } + }, + { completion -> + MaplibreNativeC.mln_map_get_style_image_info( requireLiveHandle(), nativeImageId.view, - outPixels, - outPixels.size.toLong(), - outByteLength, - outFound, + completion, ) - ) - } - } - return if (outFound[0]) { - StyleImage( - PremultipliedRgba8Image(info.width, info.height, info.stride, outPixels), - info.pixelRatio, - info.sdf, + }, ) - } else null - } + } - public actual fun addImageSourceUrl(sourceId: String, coordinates: List, url: String) { + public actual fun addImageSourceUrl( + sourceId: String, + coordinates: List, + url: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> LatLngArrayScope(coordinates).use { nativeCoordinates -> @@ -692,6 +740,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeCoordinates.coordinates, nativeCoordinates.count, nativeUrl.view, + completion, ) ) } @@ -703,7 +752,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo sourceId: String, coordinates: List, image: PremultipliedRgba8Image, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> LatLngArrayScope(coordinates).use { nativeCoordinates -> @@ -715,6 +764,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeCoordinates.coordinates, nativeCoordinates.count, nativeImage.image, + completion, ) ) } @@ -722,22 +772,27 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun setImageSourceUrl(sourceId: String, url: String) { - NativeAccess.ensureLoaded() - StringViewScope(sourceId).use { nativeSourceId -> - StringViewScope(url).use { nativeUrl -> - Status.check( - MaplibreNativeC.mln_map_set_image_source_url( - requireLiveHandle(), - nativeSourceId.view, - nativeUrl.view, + public actual fun setImageSourceUrl(sourceId: String, url: String): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(sourceId).use { nativeSourceId -> + StringViewScope(url).use { nativeUrl -> + Status.check( + MaplibreNativeC.mln_map_set_image_source_url( + requireLiveHandle(), + nativeSourceId.view, + nativeUrl.view, + completion, + ) ) - ) + } } } - } - public actual fun setImageSourceImage(sourceId: String, image: PremultipliedRgba8Image) { + public actual fun setImageSourceImage( + sourceId: String, + image: PremultipliedRgba8Image, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> PremultipliedImageScope(image).use { nativeImage -> @@ -746,13 +801,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeSourceId.view, nativeImage.image, + completion, ) ) } } } - public actual fun setImageSourceCoordinates(sourceId: String, coordinates: List) { + public actual fun setImageSourceCoordinates( + sourceId: String, + coordinates: List, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(sourceId).use { nativeSourceId -> LatLngArrayScope(coordinates).use { nativeCoordinates -> @@ -762,37 +821,39 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeCoordinates.coordinates, nativeCoordinates.count, + completion, ) ) } } } - public actual fun imageSourceCoordinates(sourceId: String): List? { - NativeAccess.ensureLoaded() - val outFound = booleanArrayOf(false) + public actual fun imageSourceCoordinates(sourceId: String): Deferred?> = StringViewScope(sourceId).use { nativeSourceId -> - LatLngArrayScope(IMAGE_SOURCE_COORDINATE_COUNT).use { outCoordinates -> - SizeTPointer(1).use { outCoordinateCount -> - Status.check( - MaplibreNativeC.mln_map_get_image_source_coordinates( - requireLiveHandle(), - nativeSourceId.view, - outCoordinates.coordinates, - outCoordinates.count, - outCoordinateCount, - outFound, - ) + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else { + val values = MaplibreNativeC.mln_lat_lng(result.value()) + List(Math.toIntExact(result.value_count())) { index -> + latLng(values.position(index.toLong())) + } + } + }, + { completion -> + MaplibreNativeC.mln_map_get_image_source_coordinates( + requireLiveHandle(), + nativeSourceId.view, + completion, ) - return if (outFound[0]) { - outCoordinates.toList(Math.toIntExact(outCoordinateCount.get())) - } else null - } - } + }, + ) } - } - public actual fun addStyleLayerJson(layerJson: ByteArray, beforeLayerId: String) { + public actual fun addStyleLayerJson( + layerJson: ByteArray, + beforeLayerId: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() ByteArrayViewScope(layerJson).use { nativeLayerJson -> StringViewScope(beforeLayerId).use { nativeBeforeLayerId -> @@ -801,13 +862,18 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerJson.view, nativeBeforeLayerId.view, + completion, ) ) } } } - public actual fun addHillshadeLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addHillshadeLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(sourceId).use { nativeSourceId -> @@ -818,6 +884,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeLayerId.view, nativeSourceId.view, nativeBeforeLayerId.view, + completion, ) ) } @@ -825,7 +892,11 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun addColorReliefLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addColorReliefLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(sourceId).use { nativeSourceId -> @@ -836,6 +907,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeLayerId.view, nativeSourceId.view, nativeBeforeLayerId.view, + completion, ) ) } @@ -843,7 +915,10 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun addLocationIndicatorLayer(layerId: String, beforeLayerId: String) { + public actual fun addLocationIndicatorLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(beforeLayerId).use { nativeBeforeLayerId -> @@ -852,6 +927,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, nativeBeforeLayerId.view, + completion, ) ) } @@ -862,7 +938,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo layerId: String, coordinate: LatLng, altitude: Double, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> latLng(coordinate).use { nativeCoordinate -> @@ -872,13 +948,17 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeLayerId.view, nativeCoordinate, altitude, + completion, ) ) } } } - public actual fun setLocationIndicatorBearing(layerId: String, bearing: Double) { + public actual fun setLocationIndicatorBearing( + layerId: String, + bearing: Double, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> Status.check( @@ -886,12 +966,16 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, bearing, + completion, ) ) } } - public actual fun setLocationIndicatorAccuracyRadius(layerId: String, radius: Double) { + public actual fun setLocationIndicatorAccuracyRadius( + layerId: String, + radius: Double, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> Status.check( @@ -899,6 +983,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, radius, + completion, ) ) } @@ -908,7 +993,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo layerId: String, imageKind: LocationIndicatorImageKind, imageId: String, - ) { + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(imageId).use { nativeImageId -> @@ -918,72 +1003,69 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeLayerId.view, imageKind.nativeValue, nativeImageId.view, + completion, ) ) } } } - public actual fun removeStyleLayer(layerId: String): Boolean { - NativeAccess.ensureLoaded() - val outRemoved = booleanArrayOf(false) - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_remove_style_layer( - requireLiveHandle(), - nativeLayerId.view, - outRemoved, - ) - ) - } - return outRemoved[0] - } - - public actual fun styleLayerExists(layerId: String): Boolean { - NativeAccess.ensureLoaded() - val outExists = booleanArrayOf(false) - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_style_layer_exists( - requireLiveHandle(), - nativeLayerId.view, - outExists, - ) - ) - } - return outExists[0] - } - - public actual fun styleLayerType(layerId: String): String? { - NativeAccess.ensureLoaded() - val outFound = booleanArrayOf(false) - MaplibreNativeC.mln_buffer_view().use { outType -> + public actual fun removeStyleLayer(layerId: String): Deferred = + command { completion -> + NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> Status.check( - MaplibreNativeC.mln_map_get_style_layer_type( + MaplibreNativeC.mln_map_remove_style_layer( requireLiveHandle(), nativeLayerId.view, - outType, - outFound, + completion, ) ) } - return if (outFound[0]) stringView(outType) else null } - } - public actual fun styleLayerIds(): List { - NativeAccess.ensureLoaded() - LongPointer(1).use { outList -> - outList.put(0, 0L) - Status.check(MaplibreNativeC.mln_map_list_style_layer_ids(requireLiveHandle(), outList)) - val list = outList.get() - require(list != 0L) { "mln_map_list_style_layer_ids returned the null handle" } - return styleIdList(list) + public actual fun styleLayerInfo(layerId: String): Deferred = + StringViewScope(layerId).use { nativeLayerId -> + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else { + val raw = MaplibreNativeC.mln_style_layer_result(result.value()) + val info = raw.info() + val fields = info.fields() + LayerInfo( + stringView(info.type()), + info.min_zoom(), + info.max_zoom(), + StyleLayerVisibility.fromNative(info.visibility()), + if (fields and MaplibreNativeC.MLN_STYLE_LAYER_INFO_SOURCE_ID != 0) + stringView(raw.source_id()) + else null, + if (fields and MaplibreNativeC.MLN_STYLE_LAYER_INFO_SOURCE_LAYER != 0) + stringView(raw.source_layer()) + else null, + ) + } + }, + { completion -> + MaplibreNativeC.mln_map_get_style_layer_info( + requireLiveHandle(), + nativeLayerId.view, + completion, + ) + }, + ) + } + + public actual fun styleLayerIds(): Deferred> = + CompletionBridge.submit(::stringList) { completion -> + MaplibreNativeC.mln_map_list_style_layer_ids(requireLiveHandle(), completion) } - } - public actual fun moveStyleLayer(layerId: String, beforeLayerId: String) { + public actual fun moveStyleLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(beforeLayerId).use { nativeBeforeLayerId -> @@ -992,42 +1074,45 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, nativeBeforeLayerId.view, + completion, ) ) } } } - public actual fun styleLayerJson(layerId: String): ByteArray? { - NativeAccess.ensureLoaded() + public actual fun styleLayerJson(layerId: String): Deferred = StringViewScope(layerId).use { nativeLayerId -> - LongPointer(1).use { outSnapshot -> - BoolPointer(1).use { outFound -> - outSnapshot.put(0, 0L) - Status.check( - MaplibreNativeC.mln_map_get_style_layer_json( - requireLiveHandle(), - nativeLayerId.view, - outSnapshot, - outFound, - ) + CompletionBridge.submit( + ::optionalBuffer, + { completion -> + MaplibreNativeC.mln_map_get_style_layer_json( + requireLiveHandle(), + nativeLayerId.view, + completion, ) - return if (outFound.get()) ownedBuffer(outSnapshot.get()) else null - } - } + }, + ) } - } - public actual fun setStyleLightJson(lightJson: ByteArray) { - NativeAccess.ensureLoaded() - ByteArrayViewScope(lightJson).use { nativeLightJson -> - Status.check( - MaplibreNativeC.mln_map_set_style_light_json(requireLiveHandle(), nativeLightJson.view) - ) + public actual fun setStyleLightJson(lightJson: ByteArray): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + ByteArrayViewScope(lightJson).use { nativeLightJson -> + Status.check( + MaplibreNativeC.mln_map_set_style_light_json( + requireLiveHandle(), + nativeLightJson.view, + completion, + ) + ) + } } - } - public actual fun setStyleLightProperty(propertyName: String, value: ByteArray) { + public actual fun setStyleLightProperty( + propertyName: String, + value: ByteArray, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(propertyName).use { nativePropertyName -> ByteArrayViewScope(value).use { nativeValue -> @@ -1036,52 +1121,57 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativePropertyName.view, nativeValue.view, + completion, ) ) } } } - public actual fun styleLightProperty(propertyName: String): ByteArray? { - NativeAccess.ensureLoaded() + public actual fun styleLightProperty(propertyName: String): Deferred = StringViewScope(propertyName).use { nativePropertyName -> - LongPointer(1).use { outSnapshot -> - outSnapshot.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::optionalBuffer, + { completion -> MaplibreNativeC.mln_map_get_style_light_property( requireLiveHandle(), nativePropertyName.view, - outSnapshot, + completion, ) - ) - return outSnapshot.get().takeIf { it != 0L }?.let(::ownedBuffer) - } + }, + ) } - } - public actual fun setStyleTransitionOptions(options: StyleTransitionOptions) { + public actual fun setStyleTransitionOptions( + options: StyleTransitionOptions + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StyleTransitionOptionsScope(options).use { nativeOptions -> Status.check( MaplibreNativeC.mln_map_set_style_transition_options( requireLiveHandle(), nativeOptions.options, + completion, ) ) } } - public actual fun styleTransitionOptions(): StyleTransitionOptions { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_style_transition_options_default().use { outOptions -> - Status.check( - MaplibreNativeC.mln_map_get_style_transition_options(requireLiveHandle(), outOptions) - ) - return styleTransitionOptions(outOptions) - } - } + public actual fun styleTransitionOptions(): Deferred = + CompletionBridge.submit( + { result -> + styleTransitionOptions(MaplibreNativeC.mln_style_transition_options(result.value())) + }, + { completion -> + MaplibreNativeC.mln_map_get_style_transition_options(requireLiveHandle(), completion) + }, + ) - public actual fun setLayerProperty(layerId: String, propertyName: String, value: ByteArray) { + public actual fun setLayerProperty( + layerId: String, + propertyName: String, + value: ByteArray, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(propertyName).use { nativePropertyName -> @@ -1092,6 +1182,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeLayerId.view, nativePropertyName.view, nativeValue.view, + completion, ) ) } @@ -1099,27 +1190,27 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - public actual fun layerProperty(layerId: String, propertyName: String): ByteArray? { - NativeAccess.ensureLoaded() + public actual fun layerProperty(layerId: String, propertyName: String): Deferred = StringViewScope(layerId).use { nativeLayerId -> StringViewScope(propertyName).use { nativePropertyName -> - LongPointer(1).use { outSnapshot -> - outSnapshot.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::optionalBuffer, + { completion -> MaplibreNativeC.mln_map_get_layer_property( requireLiveHandle(), nativeLayerId.view, nativePropertyName.view, - outSnapshot, + completion, ) - ) - return outSnapshot.get().takeIf { it != 0L }?.let(::ownedBuffer) - } + }, + ) } } - } - public actual fun setLayerFilter(layerId: String, filter: ByteArray) { + public actual fun setLayerFilter( + layerId: String, + filter: ByteArray, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> ByteArrayViewScope(filter).use { nativeFilter -> @@ -1128,90 +1219,69 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, nativeFilter.view, + completion, ) ) } } } - public actual fun clearLayerFilter(layerId: String) { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_set_layer_filter(requireLiveHandle(), nativeLayerId.view, null) - ) + public actual fun clearLayerFilter(layerId: String): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(layerId).use { nativeLayerId -> + Status.check( + MaplibreNativeC.mln_map_set_layer_filter( + requireLiveHandle(), + nativeLayerId.view, + null, + completion, + ) + ) + } } - } - public actual fun layerFilter(layerId: String): ByteArray? { - NativeAccess.ensureLoaded() + public actual fun layerFilter(layerId: String): Deferred = StringViewScope(layerId).use { nativeLayerId -> - LongPointer(1).use { outSnapshot -> - outSnapshot.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::optionalBuffer, + { completion -> MaplibreNativeC.mln_map_get_layer_filter( requireLiveHandle(), nativeLayerId.view, - outSnapshot, + completion, ) - ) - return outSnapshot.get().takeIf { it != 0L }?.let(::ownedBuffer) - } + }, + ) } - } public actual fun styleImageStretches( imageId: String - ): Pair, List>? { - NativeAccess.ensureLoaded() + ): Deferred, List>?> = StringViewScope(imageId).use { nativeImageId -> - SizeTPointer(1).use { outXCount -> - SizeTPointer(1).use { outYCount -> - val outFound = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_map_copy_style_image_stretches( - requireLiveHandle(), - nativeImageId.view, - null, - 0L, - outXCount, - null, - 0L, - outYCount, - outFound, - ) - ) - if (!outFound[0]) return null - - val xCount = Math.toIntExact(outXCount.get()) - val yCount = Math.toIntExact(outYCount.get()) - val rawX = if (xCount == 0) null else MaplibreNativeC.mln_image_stretch(xCount.toLong()) - val rawY = if (yCount == 0) null else MaplibreNativeC.mln_image_stretch(yCount.toLong()) - try { - Status.check( - MaplibreNativeC.mln_map_copy_style_image_stretches( - requireLiveHandle(), - nativeImageId.view, - rawX, - xCount.toLong(), - outXCount, - rawY, - yCount.toLong(), - outYCount, - outFound, - ) - ) - return readStretches(rawX, xCount) to readStretches(rawY, yCount) - } finally { - rawX?.close() - rawY?.close() + CompletionBridge.submit( + { result -> + if (result.value_count() == 0L) null + else { + val raw = MaplibreNativeC.mln_style_image_stretches_result(result.value()) + readStretches(raw.stretch_x(), Math.toIntExact(raw.stretch_x_count())) to + readStretches(raw.stretch_y(), Math.toIntExact(raw.stretch_y_count())) } - } - } + }, + { completion -> + MaplibreNativeC.mln_map_copy_style_image_stretches( + requireLiveHandle(), + nativeImageId.view, + completion, + ) + }, + ) } - } - public actual fun setLayerSourceLayer(layerId: String, sourceLayer: String) { + public actual fun setLayerSourceLayer( + layerId: String, + sourceLayer: String, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() StringViewScope(layerId).use { nativeLayerId -> StringViewScope(sourceLayer).use { nativeSourceLayer -> @@ -1220,618 +1290,360 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo requireLiveHandle(), nativeLayerId.view, nativeSourceLayer.view, + completion, ) ) } } - } - - public actual fun layerSourceLayer(layerId: String): String { - NativeAccess.ensureLoaded() - return copyLayerText(requireLiveHandle(), layerId) { mapId, view, text, capacity, outSize -> - MaplibreNativeC.mln_map_copy_layer_source_layer(mapId, view, text, capacity, outSize) - } - } - - public actual fun setLayerSourceId(layerId: String, sourceId: String) { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - StringViewScope(sourceId).use { nativeSourceId -> - Status.check( - MaplibreNativeC.mln_map_set_layer_source_id( - requireLiveHandle(), - nativeLayerId.view, - nativeSourceId.view, - ) - ) - } - } - } - - public actual fun layerSourceId(layerId: String): String { - NativeAccess.ensureLoaded() - return copyLayerText(requireLiveHandle(), layerId) { mapId, view, text, capacity, outSize -> - MaplibreNativeC.mln_map_copy_layer_source_id(mapId, view, text, capacity, outSize) - } - } - - public actual fun setLayerMinZoom(layerId: String, minZoom: Double) { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_set_layer_min_zoom(requireLiveHandle(), nativeLayerId.view, minZoom) - ) - } - } - - public actual fun layerMinZoom(layerId: String): Double { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - val outZoom = doubleArrayOf(0.0) - Status.check( - MaplibreNativeC.mln_map_get_layer_min_zoom(requireLiveHandle(), nativeLayerId.view, outZoom) - ) - return outZoom[0] - } - } - - public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double) { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_set_layer_max_zoom(requireLiveHandle(), nativeLayerId.view, maxZoom) - ) - } - } - - public actual fun layerMaxZoom(layerId: String): Double { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - val outZoom = doubleArrayOf(0.0) - Status.check( - MaplibreNativeC.mln_map_get_layer_max_zoom(requireLiveHandle(), nativeLayerId.view, outZoom) - ) - return outZoom[0] - } - } - - public actual fun setLayerVisibility(layerId: String, visibility: StyleLayerVisibility) { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - Status.check( - MaplibreNativeC.mln_map_set_layer_visibility( - requireLiveHandle(), - nativeLayerId.view, - visibility.nativeValue, - ) - ) - } - } - - public actual fun layerVisibility(layerId: String): StyleLayerVisibility { - NativeAccess.ensureLoaded() - StringViewScope(layerId).use { nativeLayerId -> - val outVisibility = intArrayOf(0) - Status.check( - MaplibreNativeC.mln_map_get_layer_visibility( - requireLiveHandle(), - nativeLayerId.view, - outVisibility, - ) - ) - return StyleLayerVisibility.fromNative(outVisibility[0]) - } - } - - public actual fun requestRepaint() { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_request_repaint(requireLiveHandle())) - } - - public actual fun requestStillImage() { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_request_still_image(requireLiveHandle())) - } - - public actual var debugOptions: Set - get() { - NativeAccess.ensureLoaded() - val outOptions = intArrayOf(0) - Status.check(MaplibreNativeC.mln_map_get_debug_options(requireLiveHandle(), outOptions)) - return debugOptions(outOptions[0]) - } - set(value) { - NativeAccess.ensureLoaded() - Status.check( - MaplibreNativeC.mln_map_set_debug_options(requireLiveHandle(), debugOptionMask(value)) - ) - } - - public actual var isRenderingStatsViewEnabled: Boolean - get() { - NativeAccess.ensureLoaded() - val outEnabled = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_map_get_rendering_stats_view_enabled(requireLiveHandle(), outEnabled) - ) - return outEnabled[0] - } - set(value) { - NativeAccess.ensureLoaded() - Status.check( - MaplibreNativeC.mln_map_set_rendering_stats_view_enabled(requireLiveHandle(), value) - ) - } - - public actual val isFullyLoaded: Boolean - get() { - NativeAccess.ensureLoaded() - val outLoaded = booleanArrayOf(false) - Status.check(MaplibreNativeC.mln_map_is_fully_loaded(requireLiveHandle(), outLoaded)) - return outLoaded[0] - } - - public actual fun dumpDebugLogs() { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_dump_debug_logs(requireLiveHandle())) - } - - public actual val size: MapSize - get() { - NativeAccess.ensureLoaded() - val outWidth = intArrayOf(0) - val outHeight = intArrayOf(0) - val outScaleFactor = doubleArrayOf(0.0) - Status.check( - MaplibreNativeC.mln_map_get_size(requireLiveHandle(), outWidth, outHeight, outScaleFactor) - ) - return MapSize(outWidth[0], outHeight[0], outScaleFactor[0]) - } - - public actual var viewportOptions: ViewportOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_map_viewport_options_default().use { outOptions -> - Status.check(MaplibreNativeC.mln_map_get_viewport_options(requireLiveHandle(), outOptions)) - return viewportOptions(outOptions) - } - } - set(value) { - NativeAccess.ensureLoaded() - ViewportOptionsScope(value).use { nativeOptions -> - Status.check( - MaplibreNativeC.mln_map_set_viewport_options(requireLiveHandle(), nativeOptions.options) - ) - } - } + } - public actual var tileOptions: TileOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_map_tile_options_default().use { outOptions -> - Status.check(MaplibreNativeC.mln_map_get_tile_options(requireLiveHandle(), outOptions)) - return tileOptions(outOptions) + public actual fun layerSourceLayer(layerId: String): Deferred = + StringViewScope(layerId).use { nativeLayerId -> + CompletionBridge.submit({ result -> requiredBuffer(result).decodeToString() }) { completion -> + MaplibreNativeC.mln_map_copy_layer_source_layer( + requireLiveHandle(), + nativeLayerId.view, + completion, + ) } } - set(value) { - NativeAccess.ensureLoaded() - TileOptionsScope(value).use { nativeOptions -> + + public actual fun setLayerSourceId( + layerId: String, + sourceId: String, + ): Deferred = command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(layerId).use { nativeLayerId -> + StringViewScope(sourceId).use { nativeSourceId -> Status.check( - MaplibreNativeC.mln_map_set_tile_options(requireLiveHandle(), nativeOptions.options) + MaplibreNativeC.mln_map_set_layer_source_id( + requireLiveHandle(), + nativeLayerId.view, + nativeSourceId.view, + completion, + ) ) } } + } - public actual val camera: CameraOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_camera_options_default().use { outCamera -> - Status.check(MaplibreNativeC.mln_map_get_camera(requireLiveHandle(), outCamera)) - return cameraOptions(outCamera) + public actual fun layerSourceId(layerId: String): Deferred = + StringViewScope(layerId).use { nativeLayerId -> + CompletionBridge.submit({ result -> requiredBuffer(result).decodeToString() }) { completion -> + MaplibreNativeC.mln_map_copy_layer_source_id( + requireLiveHandle(), + nativeLayerId.view, + completion, + ) } } - public actual fun jumpTo(camera: CameraOptions) { - NativeAccess.ensureLoaded() - CameraOptionsScope(camera).use { nativeCamera -> - Status.check(MaplibreNativeC.mln_map_jump_to(requireLiveHandle(), nativeCamera.options)) - } - } - - public actual fun easeTo(camera: CameraOptions, animation: AnimationOptions?) { - NativeAccess.ensureLoaded() - CameraOptionsScope(camera).use { nativeCamera -> - AnimationOptionsScope(animation).use { nativeAnimation -> + public actual fun setLayerMinZoom(layerId: String, minZoom: Double): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(layerId).use { nativeLayerId -> Status.check( - MaplibreNativeC.mln_map_ease_to( + MaplibreNativeC.mln_map_set_layer_min_zoom( requireLiveHandle(), - nativeCamera.options, - nativeAnimation.options, + nativeLayerId.view, + minZoom, + completion, ) ) } } - } - public actual fun flyTo(camera: CameraOptions, animation: AnimationOptions?) { - NativeAccess.ensureLoaded() - CameraOptionsScope(camera).use { nativeCamera -> - AnimationOptionsScope(animation).use { nativeAnimation -> + public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double): Deferred = + command { completion -> + NativeAccess.ensureLoaded() + StringViewScope(layerId).use { nativeLayerId -> Status.check( - MaplibreNativeC.mln_map_fly_to( + MaplibreNativeC.mln_map_set_layer_max_zoom( requireLiveHandle(), - nativeCamera.options, - nativeAnimation.options, + nativeLayerId.view, + maxZoom, + completion, ) ) } } - } - - public actual fun moveBy(deltaX: Double, deltaY: Double) { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_move_by(requireLiveHandle(), deltaX, deltaY)) - } - public actual fun moveByAnimated(deltaX: Double, deltaY: Double, animation: AnimationOptions?) { + public actual fun setLayerVisibility( + layerId: String, + visibility: StyleLayerVisibility, + ): Deferred = command { completion -> NativeAccess.ensureLoaded() - AnimationOptionsScope(animation).use { nativeAnimation -> + StringViewScope(layerId).use { nativeLayerId -> Status.check( - MaplibreNativeC.mln_map_move_by_animated( + MaplibreNativeC.mln_map_set_layer_visibility( requireLiveHandle(), - deltaX, - deltaY, - nativeAnimation.options, + nativeLayerId.view, + visibility.nativeValue, + completion, ) ) } } - public actual fun scaleBy(scale: Double, anchor: ScreenPoint?) { + public actual fun requestRepaint(): Deferred = command { completion -> NativeAccess.ensureLoaded() - ScreenPointScope(anchor).use { nativeAnchor -> - Status.check(MaplibreNativeC.mln_map_scale_by(requireLiveHandle(), scale, nativeAnchor.point)) - } + Status.check(MaplibreNativeC.mln_map_request_repaint(requireLiveHandle(), completion)) } - public actual fun scaleByAnimated( - scale: Double, - anchor: ScreenPoint?, - animation: AnimationOptions?, - ) { - NativeAccess.ensureLoaded() - ScreenPointScope(anchor).use { nativeAnchor -> - AnimationOptionsScope(animation).use { nativeAnimation -> - Status.check( - MaplibreNativeC.mln_map_scale_by_animated( - requireLiveHandle(), - scale, - nativeAnchor.point, - nativeAnimation.options, - ) - ) - } + public actual fun requestStillImage(): Deferred = + CompletionBridge.command { completion -> + MaplibreNativeC.mln_map_request_still_image(requireLiveHandle(), completion) } - } - - public actual fun rotateBy(first: ScreenPoint, second: ScreenPoint) { - NativeAccess.ensureLoaded() - Status.check( - MaplibreNativeC.mln_map_rotate_by( - requireLiveHandle(), - screenPoint(first), - screenPoint(second), - ) - ) - } - public actual fun rotateByAnimated( - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions?, - ) { + public actual fun snapshot(): MapSnapshot { NativeAccess.ensureLoaded() - AnimationOptionsScope(animation).use { nativeAnimation -> - Status.check( - MaplibreNativeC.mln_map_rotate_by_animated( - requireLiveHandle(), - screenPoint(first), - screenPoint(second), - nativeAnimation.options, - ) + MaplibreNativeC.mln_map_snapshot().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_map_snapshot_get(requireLiveHandle(), value)) + val extent = value.logical_extent() + return MapSnapshot( + value.generation(), + debugOptions(value.debug_options()), + cameraOptions(value.camera()), + MapSize(extent.width(), extent.height(), extent.scale_factor()), + projectionModeOptions(value.projection_mode()), + viewportOptions(value.viewport()), + value.fully_loaded(), + value.rendering_stats_view_enabled(), + value.repaint_demand(), + value.latest_render_update_generation(), + tileOptions(value.tile()), + boundOptions(value.bounds()), + freeCameraOptions(value.free_camera()), ) } } - public actual fun pitchBy(pitch: Double) { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_pitch_by(requireLiveHandle(), pitch)) - } - - public actual fun pitchByAnimated(pitch: Double, animation: AnimationOptions?) { - NativeAccess.ensureLoaded() - AnimationOptionsScope(animation).use { nativeAnimation -> + public actual fun setDebugOptions(options: Set): Deferred = + command { completion -> + NativeAccess.ensureLoaded() Status.check( - MaplibreNativeC.mln_map_pitch_by_animated( + MaplibreNativeC.mln_map_set_debug_options( requireLiveHandle(), - pitch, - nativeAnimation.options, + debugOptionMask(options), + completion, ) ) } - } - - public actual fun cancelTransitions() { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_cancel_transitions(requireLiveHandle())) - } - public actual var isGestureInProgress: Boolean - get() { + public actual fun setRenderingStatsViewEnabled(enabled: Boolean): Deferred = + command { completion -> NativeAccess.ensureLoaded() - val outInProgress = booleanArrayOf(false) Status.check( - MaplibreNativeC.mln_map_is_gesture_in_progress(requireLiveHandle(), outInProgress) + MaplibreNativeC.mln_map_set_rendering_stats_view_enabled( + requireLiveHandle(), + enabled, + completion, + ) ) - return outInProgress[0] - } - set(value) { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_map_set_gesture_in_progress(requireLiveHandle(), value)) } - public actual fun cameraForLatLngBounds( - bounds: LatLngBounds, - fitOptions: CameraFitOptions?, - ): CameraOptions { - NativeAccess.ensureLoaded() - CameraFitOptionsScope(fitOptions).use { nativeFitOptions -> - MaplibreNativeC.mln_camera_options_default().use { outCamera -> + public actual fun setViewportOptions(options: ViewportOptions): Deferred = + ViewportOptionsScope(options).use { nativeOptions -> + command { completion -> Status.check( - MaplibreNativeC.mln_map_camera_for_lat_lng_bounds( + MaplibreNativeC.mln_map_set_viewport_options( requireLiveHandle(), - latLngBounds(bounds), - nativeFitOptions.options, - outCamera, + nativeOptions.options, + completion, ) ) - return cameraOptions(outCamera) - } - } - } - - public actual fun cameraForLatLngs( - coordinates: List, - fitOptions: CameraFitOptions?, - ): CameraOptions { - NativeAccess.ensureLoaded() - LatLngArrayScope(coordinates).use { nativeCoordinates -> - CameraFitOptionsScope(fitOptions).use { nativeFitOptions -> - MaplibreNativeC.mln_camera_options_default().use { outCamera -> - Status.check( - MaplibreNativeC.mln_map_camera_for_lat_lngs( - requireLiveHandle(), - nativeCoordinates.coordinates, - nativeCoordinates.count, - nativeFitOptions.options, - outCamera, - ) - ) - return cameraOptions(outCamera) - } } } - } - public actual fun cameraForGeometry( - geometry: ByteArray, - fitOptions: CameraFitOptions?, - ): CameraOptions { - NativeAccess.ensureLoaded() - ByteArrayViewScope(geometry).use { nativeGeometry -> - CameraFitOptionsScope(fitOptions).use { nativeFitOptions -> - MaplibreNativeC.mln_camera_options_default().use { outCamera -> - Status.check( - MaplibreNativeC.mln_map_camera_for_geometry( - requireLiveHandle(), - nativeGeometry.view, - nativeFitOptions.options, - outCamera, - ) + public actual fun setTileOptions(options: TileOptions): Deferred = + TileOptionsScope(options).use { nativeOptions -> + command { completion -> + Status.check( + MaplibreNativeC.mln_map_set_tile_options( + requireLiveHandle(), + nativeOptions.options, + completion, ) - return cameraOptions(outCamera) - } - } - } - } - - public actual fun latLngBoundsForCamera(camera: CameraOptions): LatLngBounds = - latLngBoundsForCamera(MaplibreNativeC::mln_map_lat_lng_bounds_for_camera, camera) - - public actual fun latLngBoundsForCameraUnwrapped(camera: CameraOptions): LatLngBounds = - latLngBoundsForCamera(MaplibreNativeC::mln_map_lat_lng_bounds_for_camera_unwrapped, camera) - - public actual var bounds: BoundOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_bound_options_default().use { outOptions -> - Status.check(MaplibreNativeC.mln_map_get_bounds(requireLiveHandle(), outOptions)) - return boundOptions(outOptions) - } - } - set(value) { - NativeAccess.ensureLoaded() - BoundOptionsScope(value).use { nativeOptions -> - Status.check(MaplibreNativeC.mln_map_set_bounds(requireLiveHandle(), nativeOptions.options)) + ) } } - public actual var freeCameraOptions: FreeCameraOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_free_camera_options_default().use { outOptions -> + public actual fun setBounds(options: BoundOptions): Deferred = + BoundOptionsScope(options).use { nativeOptions -> + command { completion -> Status.check( - MaplibreNativeC.mln_map_get_free_camera_options(requireLiveHandle(), outOptions) + MaplibreNativeC.mln_map_set_bounds(requireLiveHandle(), nativeOptions.options, completion) ) - return freeCameraOptions(outOptions) } } - set(value) { - NativeAccess.ensureLoaded() - FreeCameraOptionsScope(value).use { nativeOptions -> + + public actual fun setFreeCameraOptions(options: FreeCameraOptions): Deferred = + FreeCameraOptionsScope(options).use { nativeOptions -> + command { completion -> Status.check( MaplibreNativeC.mln_map_set_free_camera_options( requireLiveHandle(), nativeOptions.options, + completion, ) ) } } - public actual var projectionMode: ProjectionModeOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_projection_mode_default().use { outMode -> - Status.check(MaplibreNativeC.mln_map_get_projection_mode(requireLiveHandle(), outMode)) - return projectionModeOptions(outMode) - } - } - set(value) { - NativeAccess.ensureLoaded() - ProjectionModeOptionsScope(value).use { nativeMode -> - Status.check( - MaplibreNativeC.mln_map_set_projection_mode(requireLiveHandle(), nativeMode.mode) - ) + public actual fun resize(size: MapSize): Deferred = + MaplibreNativeC.mln_logical_extent().use { extent -> + extent.width(size.width).height(size.height).scale_factor(size.scaleFactor) + command { completion -> + Status.check(MaplibreNativeC.mln_map_resize(requireLiveHandle(), extent, completion)) } } - public actual fun pixelForLatLng(coordinate: LatLng): ScreenPoint { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_screen_point().use { outPoint -> - Status.check( - MaplibreNativeC.mln_map_pixel_for_lat_lng(requireLiveHandle(), latLng(coordinate), outPoint) - ) - return screenPoint(outPoint) - } - } - - public actual fun latLngForPixel(point: ScreenPoint): LatLng { + public actual fun cameraSnapshot(): CameraSnapshot { NativeAccess.ensureLoaded() - MaplibreNativeC.mln_lat_lng().use { outCoordinate -> + MaplibreNativeC.mln_camera_options_default().use { outCamera -> + val outGeneration = longArrayOf(0L) Status.check( - MaplibreNativeC.mln_map_lat_lng_for_pixel( - requireLiveHandle(), - screenPoint(point), - outCoordinate, - ) + MaplibreNativeC.mln_map_camera_snapshot_get(requireLiveHandle(), outCamera, outGeneration) ) - return latLng(outCoordinate) + return CameraSnapshot(outGeneration[0], cameraOptions(outCamera)) } } - public actual fun pixelsForLatLngs(coordinates: List): List { - NativeAccess.ensureLoaded() - val coordinateSnapshot = coordinates.toList() - if (coordinateSnapshot.isEmpty()) { - Status.check(MaplibreNativeC.mln_map_pixels_for_lat_lngs(requireLiveHandle(), null, 0L, null)) - return emptyList() - } - LatLngArrayScope(coordinateSnapshot).use { nativeCoordinates -> - ScreenPointArrayScope(nativeCoordinates.count).use { outPoints -> - Status.check( - MaplibreNativeC.mln_map_pixels_for_lat_lngs( - requireLiveHandle(), - nativeCoordinates.coordinates, - nativeCoordinates.count, - outPoints.points, - ) - ) - return outPoints.toList(Math.toIntExact(nativeCoordinates.count)) + public actual fun updateCamera(update: CameraUpdate): Deferred = + CameraOptionsScope(update.camera).use { nativeCamera -> + AnimationOptionsScope(update.animation).use { nativeAnimation -> + MaplibreNativeC.mln_camera_update_default().use { nativeUpdate -> + nativeUpdate + .mode(update.mode.nativeValue) + .camera(nativeCamera.options) + .animation(nativeAnimation.options) + .gesture_phase(update.gesturePhase.nativeValue) + command { completion -> + Status.check( + MaplibreNativeC.mln_map_update_camera(requireLiveHandle(), nativeUpdate, completion) + ) + } + } } } - } - public actual fun latLngsForPixels(points: List): List { - NativeAccess.ensureLoaded() - val pointSnapshot = points.toList() - if (pointSnapshot.isEmpty()) { - Status.check(MaplibreNativeC.mln_map_lat_lngs_for_pixels(requireLiveHandle(), null, 0L, null)) - return emptyList() - } - ScreenPointArrayScope(pointSnapshot).use { nativePoints -> - LatLngArrayScope(nativePoints.count).use { outCoordinates -> - Status.check( - MaplibreNativeC.mln_map_lat_lngs_for_pixels( - requireLiveHandle(), - nativePoints.points, - nativePoints.count, - outCoordinates.coordinates, - ) - ) - return outCoordinates.toList(Math.toIntExact(nativePoints.count)) + public actual fun applyCameraDelta(delta: CameraDelta): Deferred = + AnimationOptionsScope(delta.animation).use { nativeAnimation -> + screenPoint(delta.offset).use { nativeOffset -> + val nativeAnchor = delta.anchor?.let(::screenPoint) + try { + MaplibreNativeC.mln_camera_delta_default().use { nativeDelta -> + nativeDelta + .kind(delta.kind.nativeValue) + .offset(nativeOffset) + .amount(delta.amount) + .has_anchor(nativeAnchor != null) + .animation(nativeAnimation.options) + nativeAnchor?.let(nativeDelta::anchor) + command { completion -> + Status.check( + MaplibreNativeC.mln_map_apply_camera_delta( + requireLiveHandle(), + nativeDelta, + completion, + ) + ) + } + } + } finally { + nativeAnchor?.close() + } } } - } + + public actual fun queryCamera(): Deferred = + CompletionBridge.submit( + { result -> + val raw = MaplibreNativeC.mln_camera_query_result(result.value()) + CameraSnapshot(raw.generation(), cameraOptions(raw.camera())) + }, + { completion -> MaplibreNativeC.mln_map_camera_query(requireLiveHandle(), completion) }, + ) public actual fun attachMetalOwnedTexture( - descriptor: MetalOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachMetalOwnedTexture(this, descriptor) + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalOwnedTexture(this, descriptor, options) public actual fun attachMetalBorrowedTexture( - descriptor: MetalBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachMetalBorrowedTexture(this, descriptor) + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalBorrowedTexture(this, descriptor, options) public actual fun attachVulkanOwnedTexture( - descriptor: VulkanOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachVulkanOwnedTexture(this, descriptor) + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanOwnedTexture(this, descriptor, options) public actual fun attachVulkanBorrowedTexture( - descriptor: VulkanBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachVulkanBorrowedTexture(this, descriptor) + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanBorrowedTexture(this, descriptor, options) public actual fun attachOpenGLOwnedTexture( - descriptor: OpenGLOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachOpenGLOwnedTexture(this, descriptor) + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLOwnedTexture(this, descriptor, options) public actual fun attachOpenGLBorrowedTexture( - descriptor: OpenGLBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachOpenGLBorrowedTexture(this, descriptor) - - public actual fun attachMetalSurface(descriptor: MetalSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachMetalSurface(this, descriptor) - - public actual fun attachVulkanSurface(descriptor: VulkanSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachVulkanSurface(this, descriptor) - - public actual fun attachOpenGLSurface(descriptor: OpenGLSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachOpenGLSurface(this, descriptor) - - public actual fun createProjection(): MapProjectionHandle { - NativeAccess.ensureLoaded() - LongPointer(1).use { outProjection -> - outProjection.put(0, 0L) - Status.check(MaplibreNativeC.mln_map_projection_create(requireLiveHandle(), outProjection)) - val address = outProjection.get() - require(address != 0L) { "mln_map_projection_create returned a null projection" } - return MapProjectionHandle(address) - } - } - - public actual override fun close() { - core.closeOnce( - destroy = { MaplibreNativeC.mln_map_destroy(handleId) }, - afterSuccess = { - runtime.unregisterMap(this) - runtimeRetention.close() + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLBorrowedTexture(this, descriptor, options) + + public actual fun attachMetalSurface( + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachMetalSurface(this, descriptor, options) + + public actual fun attachVulkanSurface( + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachVulkanSurface(this, descriptor, options) + + public actual fun attachOpenGLSurface( + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachOpenGLSurface(this, descriptor, options) + + public actual fun createProjection(): Deferred = + CompletionBridge.submit( + { result -> + val address = LongPointer(result.value()).get() + require(address != 0L) { "mln_map_projection_create returned a null projection" } + MapProjectionHandle(address) }, + { completion -> MaplibreNativeC.mln_map_projection_create(requireLiveHandle(), completion) }, ) + + public actual fun close(): Deferred { + if (!core.beginClose()) return kotlinx.coroutines.CompletableDeferred(Unit) + val completed = + try { + CompletionBridge.unitChecked { completion -> + MaplibreNativeC.mln_map_release(handleId, completion) + } + } catch (error: Throwable) { + core.abortClose() + throw error + } + core.completeClose { runtime.unregisterMap(this) } + return completed } internal fun nativeHandleId(): Long = handleId - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - core.retainChild(childTypeName) - private fun addTileSourceUrl( function: ( @@ -1839,10 +1651,12 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo MaplibreNativeC.mln_buffer_view, MaplibreNativeC.mln_buffer_view, MaplibreNativeC.mln_style_tile_source_options, + MaplibreNativeC.mln_completion, ) -> Int, sourceId: String, url: String, options: TileSourceOptions?, + completion: MaplibreNativeC.mln_completion, ) { StringViewScope(sourceId).use { nativeSourceId -> StringViewScope(url).use { nativeUrl -> @@ -1853,6 +1667,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeSourceId.view, nativeUrl.view, nativeOptions.options, + completion, ) ) } @@ -1868,10 +1683,12 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo MaplibreNativeC.mln_buffer_view?, Long, MaplibreNativeC.mln_style_tile_source_options, + MaplibreNativeC.mln_completion, ) -> Int, sourceId: String, tiles: List, options: TileSourceOptions?, + completion: MaplibreNativeC.mln_completion, ) { val tileSnapshot = tiles.toList() StringViewScope(sourceId).use { nativeSourceId -> @@ -1884,6 +1701,7 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo nativeTiles.views, nativeTiles.count, nativeOptions.options, + completion, ) ) } @@ -1891,32 +1709,24 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } - private fun latLngBoundsForCamera( - function: (Long, MaplibreNativeC.mln_camera_options, MaplibreNativeC.mln_lat_lng_bounds) -> Int, - camera: CameraOptions, - ): LatLngBounds { - NativeAccess.ensureLoaded() - CameraOptionsScope(camera).use { nativeCamera -> - MaplibreNativeC.mln_lat_lng_bounds().use { outBounds -> - Status.check(function(requireLiveHandle(), nativeCamera.options, outBounds)) - return latLngBounds(outBounds) - } - } - } - public actual companion object { - public actual fun create(runtime: RuntimeHandle, options: MapOptions): MapHandle { + public actual fun create(runtime: RuntimeHandle, options: MapOptions): Deferred { NativeAccess.ensureLoaded() - MapOptionsScope(options).use { nativeOptions -> - LongPointer(1).use { outMap -> - outMap.put(0, 0L) - Status.check( - MaplibreNativeC.mln_map_create(runtime.nativeHandleId(), nativeOptions.options, outMap) - ) - val address = outMap.get() - require(address != 0L) { "mln_map_create returned a null map" } - return MapHandle(runtime, address).also { runtime.registerMap(it) } - } + return MapOptionsScope(options).use { nativeOptions -> + CompletionBridge.submit( + { result -> + val address = LongPointer(result.value()).get() + require(address != 0L) { "mln_map_create returned a null map" } + MapHandle(runtime, address, options.eventMask).also(runtime::registerMap) + }, + { completion -> + MaplibreNativeC.mln_map_create( + runtime.nativeHandleId(), + nativeOptions.options, + completion, + ) + }, + ) } } } @@ -1927,6 +1737,13 @@ private constructor(private val runtime: RuntimeHandle, private val handleId: Lo } } +private inline fun command( + crossinline call: (MaplibreNativeC.mln_completion) -> Unit +): Deferred = CompletionBridge.command { completion -> + call(completion) + MaplibreNativeC.MLN_STATUS_OK +} + private fun latLng(value: LatLng): MaplibreNativeC.mln_lat_lng = MaplibreNativeC.mln_lat_lng().latitude(value.latitude).longitude(value.longitude) @@ -1960,6 +1777,23 @@ private fun stringView(view: MaplibreNativeC.mln_buffer_view): String { return String(bytes, java.nio.charset.StandardCharsets.UTF_8) } +private fun bufferView(view: MaplibreNativeC.mln_buffer_view): ByteArray = + JavaCppSupport.byteArray(view.data(), view.size()) + +private fun optionalBuffer(result: MaplibreNativeC.mln_completion_result): ByteArray? = + if (result.value_count() == 0L) null + else bufferView(MaplibreNativeC.mln_buffer_view(result.value())) + +private fun requiredBuffer(result: MaplibreNativeC.mln_completion_result): ByteArray = + requireNotNull(optionalBuffer(result)) { "native completion omitted its byte result" } + +private fun stringList(result: MaplibreNativeC.mln_completion_result): List { + val count = Math.toIntExact(result.value_count()) + if (count == 0) return emptyList() + val views = MaplibreNativeC.mln_buffer_view(result.value()) + return List(count) { index -> stringView(views.position(index.toLong())) } +} + private fun styleIdList(list: Long): List = try { SizeTPointer(1).use { outCount -> @@ -2003,116 +1837,6 @@ private fun styleStringList( destroyer(list) } -/** - * Probes the required length, then copies. A null buffer with zero capacity is a size probe the C - * API answers with OK. - */ -private inline fun copyMapBytes( - mapId: Long, - copy: (Long, BytePointer?, Long, SizeTPointer) -> Int, -): ByteArray { - SizeTPointer(1).use { outSize -> - Status.check(copy(mapId, null, 0L, outSize)) - val required = Math.toIntExact(outSize.get()) - if (required == 0) return byteArrayOf() - BytePointer(required.toLong()).use { buffer -> - SizeTPointer(1).use { outCopied -> - Status.check(copy(mapId, buffer, required.toLong(), outCopied)) - val bytes = ByteArray(Math.toIntExact(outCopied.get())) - buffer.get(bytes, 0, bytes.size) - return bytes - } - } - } -} - -private inline fun copyLayerText( - mapId: Long, - layerId: String, - copy: (Long, MaplibreNativeC.mln_buffer_view, BytePointer?, Long, SizeTPointer) -> Int, -): String { - StringViewScope(layerId).use { nativeLayerId -> - SizeTPointer(1).use { outSize -> - Status.check(copy(mapId, nativeLayerId.view, null, 0L, outSize)) - val required = Math.toIntExact(outSize.get()) - if (required == 0) return "" - BytePointer(required.toLong()).use { buffer -> - SizeTPointer(1).use { outCopied -> - Status.check(copy(mapId, nativeLayerId.view, buffer, required.toLong(), outCopied)) - val bytes = ByteArray(Math.toIntExact(outCopied.get())) - buffer.get(bytes, 0, bytes.size) - return String(bytes, java.nio.charset.StandardCharsets.UTF_8) - } - } - } - } -} - -private fun copyStyleSourceAttribution( - mapId: Long, - sourceId: StringViewScope, - info: MaplibreNativeC.mln_style_source_info, -): String { - val attributionSize = Math.toIntExact(info.attribution_size()) - if (attributionSize == 0) return "" - BytePointer(attributionSize.toLong()).use { outAttribution -> - SizeTPointer(1).use { outSize -> - val outFound = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_map_copy_style_source_attribution( - mapId, - sourceId.view, - outAttribution, - attributionSize.toLong(), - outSize, - outFound, - ) - ) - if (!outFound[0]) return "" - val bytes = ByteArray(Math.toIntExact(outSize.get())) - outAttribution.get(bytes, 0, bytes.size) - return String(bytes, java.nio.charset.StandardCharsets.UTF_8) - } - } -} - -private fun copyStyleSourceUrl(mapId: Long, sourceId: StringViewScope, urlSize: Long): String { - if (urlSize == 0L) return "" - BytePointer(urlSize).use { outUrl -> - SizeTPointer(1).use { outSize -> - val outFound = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_map_copy_style_source_url( - mapId, - sourceId.view, - outUrl, - urlSize, - outSize, - outFound, - ) - ) - check(outFound[0]) { "style source disappeared while its metadata was copied" } - val bytes = ByteArray(Math.toIntExact(outSize.get())) - outUrl.get(bytes, 0, bytes.size) - return String(bytes, java.nio.charset.StandardCharsets.UTF_8) - } - } -} - -private fun copyStyleSourceTileUrls(mapId: Long, sourceId: StringViewScope): List { - LongPointer(1).use { outList -> - outList.put(0, 0L) - val outFound = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_map_get_style_source_tile_urls(mapId, sourceId.view, outList, outFound) - ) - check(outFound[0]) { "style source disappeared while its metadata was copied" } - val list = outList.get() - require(list != 0L) { "mln_map_get_style_source_tile_urls returned the null handle" } - return styleStringList(list) - } -} - private const val STYLE_SOURCE_INFO_URL: Int = 1 shl 0 private const val STYLE_SOURCE_INFO_TILEJSON: Int = 1 shl 1 private const val STYLE_SOURCE_INFO_BOUNDS: Int = 1 shl 2 @@ -2369,45 +2093,6 @@ private class LatLngArrayScope : AutoCloseable { } } -private class ScreenPointArrayScope : AutoCloseable { - val points: MaplibreNativeC.mln_screen_point - val count: Long - - constructor(values: List) { - val pointSnapshot = values.toList() - points = MaplibreNativeC.mln_screen_point(pointSnapshot.size.toLong()) - count = pointSnapshot.size.toLong() - pointSnapshot.forEachIndexed { index, point -> - points.position(index.toLong()).x(point.x).y(point.y) - } - points.position(0) - } - - constructor(count: Long) { - points = MaplibreNativeC.mln_screen_point(count) - this.count = count - } - - fun toList(count: Int): List = - List(count) { index -> - val point = points.position(index.toLong()) - ScreenPoint(point.x(), point.y()) - } - .also { points.position(0) } - - override fun close() { - points.close() - } -} - -private class ScreenPointScope(value: ScreenPoint?) : AutoCloseable { - val point: MaplibreNativeC.mln_screen_point? = value?.let(::screenPoint) - - override fun close() { - point?.close() - } -} - private class CameraOptionsScope(value: CameraOptions) : AutoCloseable { val options: MaplibreNativeC.mln_camera_options = MaplibreNativeC.mln_camera_options_default() @@ -2494,35 +2179,6 @@ private class AnimationOptionsScope(value: AnimationOptions?) : AutoCloseable { } } -private class CameraFitOptionsScope(value: CameraFitOptions?) : AutoCloseable { - val options: MaplibreNativeC.mln_camera_fit_options? = value?.let { - MaplibreNativeC.mln_camera_fit_options_default() - } - - init { - if (value != null && options != null) { - var fields = 0 - value.padding?.let { - fields = fields or MaplibreNativeC.MLN_CAMERA_FIT_OPTION_PADDING - writeEdgeInsets(options.padding(), it) - } - value.bearing?.let { - fields = fields or MaplibreNativeC.MLN_CAMERA_FIT_OPTION_BEARING - options.bearing(it) - } - value.pitch?.let { - fields = fields or MaplibreNativeC.MLN_CAMERA_FIT_OPTION_PITCH - options.pitch(it) - } - options.fields(fields) - } - } - - override fun close() { - options?.close() - } -} - private class BoundOptionsScope(value: BoundOptions) : AutoCloseable { val options: MaplibreNativeC.mln_bound_options = MaplibreNativeC.mln_bound_options_default() @@ -2596,8 +2252,8 @@ private class CanonicalTileIdScope(value: CanonicalTileId) : AutoCloseable { /** * Owns map/style-scoped custom geometry source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ internal class CustomGeometrySourceState( private val options: CustomGeometrySourceOptions, @@ -2736,8 +2392,8 @@ internal class CustomGeometrySourceState( /** * Owns map/style-scoped custom MVT vector source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ internal class CustomMvtVectorSourceState( private val options: CustomMvtVectorSourceOptions, @@ -3146,9 +2802,9 @@ private class MapOptionsScope(value: MapOptions) : AutoCloseable { val options: MaplibreNativeC.mln_map_options = MaplibreNativeC.mln_map_options_default() init { - value.width?.let { options.width(it) } - value.height?.let { options.height(it) } - value.scaleFactor?.let { options.scale_factor(it) } + value.width?.let { options.initial_extent().width(it) } + value.height?.let { options.initial_extent().height(it) } + value.scaleFactor?.let { options.initial_extent().scale_factor(it) } value.mapMode?.let { Status.requireArgument(it.isKnown) { "Unknown map mode cannot be used as input: ${it.nativeValue}" @@ -3256,10 +2912,11 @@ internal object JavaCppMapStructs { fun mapOptionsSnapshot(value: MapOptions): MapOptionsSnapshot = MapOptionsScope(value).use { + val extent = it.options.initial_extent() MapOptionsSnapshot( - it.options.width(), - it.options.height(), - it.options.scale_factor(), + extent.width(), + extent.height(), + extent.scale_factor(), it.options.map_mode(), it.options.fast_pfor_enabled(), ) diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt index 21f33b2c4..34154d624 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt @@ -11,25 +11,29 @@ import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.status.Status -/** Any-thread Android JNI standalone projection snapshot. */ -public actual class MapProjectionHandle internal constructor(private val handleId: Long) : - AutoCloseable { +/** + * Owned Android JNI standalone projection snapshot. + * + * Every call is synchronous, runs on the calling thread, is internally serialized, and may be made + * from any thread. A projection copies the map's transform state at creation and never observes map + * changes made after that and remains usable after its source map and runtime close. + */ +public actual class MapProjectionHandle internal constructor(private val handleId: Long) { private val core = HandleStateCore("MapProjectionHandle", handleId) init { HandleLeakCleaner.register(this, core.leakReport) } - public actual val camera: CameraOptions - get() { - NativeAccess.ensureLoaded() - MaplibreNativeC.mln_camera_options_default().use { outCamera -> - withLiveHandle { handle -> - Status.check(MaplibreNativeC.mln_map_projection_get_camera(handle, outCamera)) - } - return projectionCameraOptions(outCamera) + public actual fun camera(): CameraOptions { + NativeAccess.ensureLoaded() + MaplibreNativeC.mln_camera_options_default().use { outCamera -> + return withLiveHandle { handle -> + Status.check(MaplibreNativeC.mln_map_projection_get_camera(handle, outCamera)) + projectionCameraOptions(outCamera) } } + } public actual fun setCamera(camera: CameraOptions) { NativeAccess.ensureLoaded() @@ -122,8 +126,15 @@ public actual class MapProjectionHandle internal constructor(private val handleI public actual val isClosed: Boolean get() = core.isReleased() - public actual override fun close() { - core.closeOnce(destroy = { MaplibreNativeC.mln_map_projection_destroy(handleId) }) + public actual fun close() { + if (!core.beginClose()) return + try { + Status.check(MaplibreNativeC.mln_map_projection_close(handleId)) + } catch (error: Throwable) { + core.abortClose() + throw error + } + core.completeClose() } private fun withLiveHandle(block: (Long) -> T): T = core.withLive { block(handleId) } diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt deleted file mode 100644 index 70f780cbd..000000000 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner - -/** Explicit handle for a Metal session-owned texture frame. */ -public actual class MetalOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val nativeFrame: MaplibreNativeC.mln_metal_owned_texture_frame, - private val scope: FrameScope, - private val frameValue: MetalOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("MetalOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): MetalOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseMetalFrame(nativeFrame) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeFrame.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt deleted file mode 100644 index 4dbed190c..000000000 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner - -/** Explicit handle for an OpenGL session-owned texture frame. */ -public actual class OpenGLOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val nativeFrame: MaplibreNativeC.mln_opengl_owned_texture_frame, - private val scope: FrameScope, - private val frameValue: OpenGLOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("OpenGLOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): OpenGLOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseOpenGLFrame(nativeFrame) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeFrame.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt index a1164c02c..943bd03b9 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt @@ -1,6 +1,8 @@ package org.maplibre.nativeffi.render import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Deferred import org.bytedeco.javacpp.BytePointer import org.bytedeco.javacpp.LongPointer import org.bytedeco.javacpp.Pointer @@ -10,10 +12,10 @@ import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.ScreenBox import org.maplibre.nativeffi.geo.ScreenPoint +import org.maplibre.nativeffi.internal.async.CompletionBridge import org.maplibre.nativeffi.internal.javacpp.ByteArrayViewScope import org.maplibre.nativeffi.internal.javacpp.JavaCppSupport import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.javacpp.ownedBuffer import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.status.Status @@ -24,186 +26,260 @@ import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.query.SourceFeatureQueryOptions -/** Owned Android JNI render session handle. Close it on the thread that attached it. */ +/** Owned Android JNI render session handle. */ public actual class RenderSessionHandle private constructor(private val map: MapHandle, private val handleId: Long) : AutoCloseable { - private val mapRetention = map.retainChild("RenderSessionHandle") private val core = HandleStateCore("RenderSessionHandle", handleId, map) + private val acquiredFrameScopes = ConcurrentHashMap.newKeySet() init { HandleLeakCleaner.register(this, core.leakReport) } - private val activeFrame = ActiveFrameState() - public actual val isClosed: Boolean get() = core.isReleased() public actual fun map(): MapHandle = map - public actual fun resize(width: Int, height: Int, scaleFactor: Double) { + public actual fun capabilities(): RenderSessionCapabilities { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("resize") - Status.requireArgument(width >= 0) { "width must be non-negative" } - Status.requireArgument(height >= 0) { "height must be non-negative" } - Status.check( - MaplibreNativeC.mln_render_session_resize(requireLiveHandle(), width, height, scaleFactor) - ) + MaplibreNativeC.mln_render_session_capabilities().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_render_session_get_capabilities(requireLiveHandle(), value)) + val flags = value.flags() + return RenderSessionCapabilities( + RenderDriver.fromNative(value.driver()), + value.texture_ring_depth(), + flags and MaplibreNativeC.MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION != 0, + flags and MaplibreNativeC.MLN_RENDER_SESSION_CAPABILITY_READBACK != 0, + flags and MaplibreNativeC.MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC != 0, + flags and MaplibreNativeC.MLN_RENDER_SESSION_CAPABILITY_PRESENTATION != 0, + ) + } } - public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor) { + public actual fun snapshot(): RenderSessionSnapshot { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_metal_surface_set_target( - requireLiveHandle(), - metalSurfaceDescriptor(descriptor), + MaplibreNativeC.mln_render_session_snapshot().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_render_session_get_snapshot(requireLiveHandle(), value)) + val nativeExtent = value.extent() + return RenderSessionSnapshot( + RenderSessionState.fromNative(value.state()), + RenderDriver.fromNative(value.driver()), + RenderResult.fromNative(value.latest_result()), + RenderTargetExtent( + nativeExtent.width(), + nativeExtent.height(), + nativeExtent.scale_factor(), + ), + value.generation().toULong(), + value.map_update_generation().toULong(), + value.rendered_update_generation().toULong(), + value.extent_generation().toULong(), + value.frame_generation().toULong(), + value.latest_demand_token().toULong(), + value.pending_demand_count(), + value.acquired_frame_count(), + value.target_ready(), + value.pending_changes(), ) - ) + } } - public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor) { + public actual fun requestFrame(demand: FrameDemand) { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_vulkan_surface_set_target( - requireLiveHandle(), - vulkanSurfaceDescriptor(descriptor), - ) - ) + MaplibreNativeC.mln_frame_demand_default().use { value -> + var flags = 0 + if (demand.ifNeeded) flags = flags or MaplibreNativeC.MLN_FRAME_DEMAND_IF_NEEDED + if (demand.present) flags = flags or MaplibreNativeC.MLN_FRAME_DEMAND_PRESENT + value + .flags(flags) + .token(demand.token.toLong()) + .coalescing_boundary(demand.coalescingBoundary.toLong()) + .timeout_ns(demand.timeoutNanoseconds.toLong()) + Status.check(MaplibreNativeC.mln_render_session_request_frame(requireLiveHandle(), value)) + } } - public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor) { + public actual fun drainFrameResults(): List { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_opengl_surface_set_target( - requireLiveHandle(), - openglSurfaceDescriptor(descriptor), + LongPointer(1).use { outBatch -> + outBatch.put(0, 0L) + Status.check( + MaplibreNativeC.mln_render_session_drain_frame_results(requireLiveHandle(), outBatch) ) - ) + val batch = outBatch.get() + try { + org.bytedeco.javacpp.SizeTPointer(1).use { outCount -> + Status.check(MaplibreNativeC.mln_render_frame_batch_count(batch, outCount)) + return List(Math.toIntExact(outCount.get())) { index -> + MaplibreNativeC.mln_render_frame_result().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_render_frame_batch_get(batch, index.toLong(), value)) + frameResult(value) + } + } + } + } finally { + if (batch != 0L) MaplibreNativeC.mln_render_frame_batch_release(batch) + } + } } - public actual fun setMetalBorrowedTextureTarget(descriptor: MetalBorrowedTextureDescriptor) { + public actual fun serviceDriverWork(maxWork: Int): Int { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_metal_borrowed_texture_set_target( - requireLiveHandle(), - metalBorrowedTextureDescriptor(descriptor), + Status.requireArgument(maxWork >= 0) { "maxWork must be non-negative" } + org.bytedeco.javacpp.SizeTPointer(1).use { outServiced -> + Status.check( + MaplibreNativeC.mln_render_session_service_driver_work( + requireLiveHandle(), + maxWork.toLong(), + outServiced, + ) ) - ) + return Math.toIntExact(outServiced.get()) + } } - public actual fun setVulkanBorrowedTextureTarget(descriptor: VulkanBorrowedTextureDescriptor) { + public actual fun acquireFrame(): AcquiredFrameHandle? { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_vulkan_borrowed_texture_set_target( + LongPointer(1).use { outFrame -> + outFrame.put(0, 0L) + val status = MaplibreNativeC.mln_render_session_acquire_frame(requireLiveHandle(), outFrame) + if (status == MaplibreStatus.NOT_READY.nativeCode) return null + Status.check(status) + val scope = FrameScope() + acquiredFrameScopes.add(scope) + return AcquiredFrameHandle(this, outFrame.get(), scope) + } + } + + public actual fun resize(extent: RenderTargetExtent): Deferred = + MaplibreNativeC.mln_render_target_extent().use { nativeExtent -> + nativeExtent.size(nativeExtent.sizeof()) + setExtent(nativeExtent, extent) + unit { completion -> + MaplibreNativeC.mln_render_session_resize(requireLiveHandle(), nativeExtent, completion) + } + } + + public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor): Deferred = + unit { completion -> + MaplibreNativeC.mln_metal_surface_set_target( requireLiveHandle(), - vulkanBorrowedTextureDescriptor(descriptor), + metalSurfaceDescriptor(descriptor), + completion, ) - ) - } + } - public actual fun setOpenGLBorrowedTextureTarget(descriptor: OpenGLBorrowedTextureDescriptor) { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - Status.check( - MaplibreNativeC.mln_opengl_borrowed_texture_set_target( + public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor): Deferred = + unit { completion -> + MaplibreNativeC.mln_vulkan_surface_set_target( requireLiveHandle(), - openglBorrowedTextureDescriptor(descriptor), + vulkanSurfaceDescriptor(descriptor), + completion, ) - ) - } + } - public actual fun renderUpdate(): RenderUpdate { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("render") - val outResult = intArrayOf(0) - val outNeedsRepaint = booleanArrayOf(false) - Status.check( - MaplibreNativeC.mln_render_session_render_update( + public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor): Deferred = + unit { completion -> + MaplibreNativeC.mln_opengl_surface_set_target( requireLiveHandle(), - outResult, - outNeedsRepaint, + openglSurfaceDescriptor(descriptor), + completion, ) + } + + public actual fun setMetalBorrowedTextureTarget( + descriptor: MetalBorrowedTextureDescriptor + ): Deferred = unit { completion -> + MaplibreNativeC.mln_metal_borrowed_texture_set_target( + requireLiveHandle(), + metalBorrowedTextureDescriptor(descriptor), + completion, ) - return RenderUpdate(RenderResult.fromNative(outResult[0]), outNeedsRepaint[0]) } - public actual fun detach() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("detach") - Status.check(MaplibreNativeC.mln_render_session_detach(requireLiveHandle())) - mapRetention.close() + public actual fun setVulkanBorrowedTextureTarget( + descriptor: VulkanBorrowedTextureDescriptor + ): Deferred = unit { completion -> + MaplibreNativeC.mln_vulkan_borrowed_texture_set_target( + requireLiveHandle(), + vulkanBorrowedTextureDescriptor(descriptor), + completion, + ) } - public actual fun reduceMemoryUse() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("reduce memory use") - Status.check(MaplibreNativeC.mln_render_session_reduce_memory_use(requireLiveHandle())) + public actual fun setOpenGLBorrowedTextureTarget( + descriptor: OpenGLBorrowedTextureDescriptor + ): Deferred = unit { completion -> + MaplibreNativeC.mln_opengl_borrowed_texture_set_target( + requireLiveHandle(), + openglBorrowedTextureDescriptor(descriptor), + completion, + ) } - public actual fun clearData() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("clear data") - Status.check(MaplibreNativeC.mln_render_session_clear_data(requireLiveHandle())) + public actual fun reduceMemoryUse(): Deferred = unit { + MaplibreNativeC.mln_render_session_reduce_memory_use(requireLiveHandle(), it) } - public actual fun dumpDebugLogs() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("dump debug logs") - Status.check(MaplibreNativeC.mln_render_session_dump_debug_logs(requireLiveHandle())) + public actual fun clearData(): Deferred = unit { + MaplibreNativeC.mln_render_session_clear_data(requireLiveHandle(), it) + } + + public actual fun dumpDebugLogs(): Deferred = unit { + MaplibreNativeC.mln_render_session_dump_debug_logs(requireLiveHandle(), it) + } + + public actual fun barrier(): Deferred = unit { + MaplibreNativeC.mln_render_session_barrier(requireLiveHandle(), it) + } + + public actual fun detach(): Deferred = unit { + MaplibreNativeC.mln_render_session_detach(requireLiveHandle(), it) } public actual fun queryRenderedFeatures( geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions?, - ): List { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query rendered features") + ): Deferred> = RenderedQueryGeometryScope(geometry).use { nativeGeometry -> RenderedFeatureQueryOptionsScope(options).use { nativeOptions -> - LongPointer(1).use { outResult -> - outResult.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::queriedFeatures, + { completion -> MaplibreNativeC.mln_render_session_query_rendered_features( requireLiveHandle(), nativeGeometry.geometry, nativeOptions.options, - outResult, + completion, ) - ) - return queriedFeatureList(outResult.get()) - } + }, + ) } } - } public actual fun querySourceFeatures( sourceId: String, options: SourceFeatureQueryOptions?, - ): List { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query source features") + ): Deferred> = StringViewScope(sourceId).use { nativeSourceId -> SourceFeatureQueryOptionsScope(options).use { nativeOptions -> - LongPointer(1).use { outResult -> - outResult.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::queriedFeatures, + { completion -> MaplibreNativeC.mln_render_session_query_source_features( requireLiveHandle(), nativeSourceId.view, nativeOptions.options, - outResult, + completion, ) - ) - return queriedFeatureList(outResult.get()) - } + }, + ) } } - } public actual fun queryFeatureExtension( sourceId: String, @@ -211,17 +287,15 @@ private constructor(private val map: MapHandle, private val handleId: Long) : Au extension: String, extensionField: String, arguments: ByteArray?, - ): ByteArray { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query feature extension") + ): Deferred = StringViewScope(sourceId).use { nativeSourceId -> ByteArrayViewScope(feature).use { nativeFeature -> StringViewScope(extension).use { nativeExtension -> StringViewScope(extensionField).use { nativeExtensionField -> ByteArrayViewScope(arguments ?: byteArrayOf()).use { nativeArguments -> - LongPointer(1).use { outResult -> - outResult.put(0, 0L) - Status.check( + CompletionBridge.submit( + ::requiredBuffer, + { completion -> MaplibreNativeC.mln_render_session_query_feature_extensions( requireLiveHandle(), nativeSourceId.view, @@ -229,135 +303,50 @@ private constructor(private val map: MapHandle, private val handleId: Long) : Au nativeExtension.view, nativeExtensionField.view, if (arguments == null) null else nativeArguments.view, - outResult, + completion, ) - ) - return ownedBuffer(outResult.get()) - } + }, + ) } } } } } - } - - public actual fun textureImageInfo(): TextureImageInfo { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("read texture data") - val outInfo = MaplibreNativeC.mln_texture_image_info_default() - val status = - MaplibreNativeC.mln_texture_read_premultiplied_rgba8( - requireLiveHandle(), - null as BytePointer?, - 0L, - outInfo, - ) - val info = textureImageInfo(outInfo) - if ( - status == MaplibreStatus.OK.nativeCode || - (status == MaplibreStatus.INVALID_ARGUMENT.nativeCode && info.byteLength > 0) - ) { - return info - } - Status.check(status) - error("unreachable") - } - public actual fun readPremultipliedRgba8(buffer: NativeBuffer): TextureImageInfo { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("read texture data") - val outInfo = MaplibreNativeC.mln_texture_image_info_default() - Status.check( - MaplibreNativeC.mln_texture_read_premultiplied_rgba8( - requireLiveHandle(), - buffer.borrowBuffer(), - buffer.byteLength(), - outInfo, - ) + public actual fun readPremultipliedRgba8(): Deferred = + CompletionBridge.submit( + { result -> + val raw = MaplibreNativeC.mln_texture_readback_result(result.value()) + TextureReadback( + JavaCppSupport.byteArray(raw.data().data(), raw.data().size()), + textureImageInfo(raw.info()), + ) + }, + { completion -> + MaplibreNativeC.mln_texture_read_premultiplied_rgba8(requireLiveHandle(), completion) + }, ) - val info = textureImageInfo(outInfo) - // Native reads an empty destination as a size probe rather than a copy, so the - // capacity is rechecked here. - buffer.ensureCapacity(info.byteLength) - return info - } - public actual fun acquireMetalOwnedTextureFrame(): MetalOwnedTextureFrameHandle { + public actual fun abandon(): RenderAbandonResult { NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - val nativeFrame = MaplibreNativeC.mln_metal_owned_texture_frame() - nativeFrame.size(nativeFrame.sizeof()) - try { - Status.check( - MaplibreNativeC.mln_metal_owned_texture_acquire_frame(requireLiveHandle(), nativeFrame) + MaplibreNativeC.mln_render_abandon_result().use { result -> + result.size(result.sizeof()) + Status.check(MaplibreNativeC.mln_render_session_abandon(requireLiveHandle(), result)) + acquiredFrameScopes.forEach(FrameScope::close) + return RenderAbandonResult( + RenderAbandonDisposition.fromNative(result.disposition()), + result.quarantined_resource_count(), ) - val scope = FrameScope() - return MetalOwnedTextureFrameHandle( - this, - nativeFrame, - scope, - metalOwnedTextureFrame(nativeFrame, scope), - ) - } catch (error: Throwable) { - activeFrame.endBorrow() - nativeFrame.close() - throw error } } - public actual fun acquireVulkanOwnedTextureFrame(): VulkanOwnedTextureFrameHandle { - NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - val nativeFrame = MaplibreNativeC.mln_vulkan_owned_texture_frame() - nativeFrame.size(nativeFrame.sizeof()) - try { - Status.check( - MaplibreNativeC.mln_vulkan_owned_texture_acquire_frame(requireLiveHandle(), nativeFrame) - ) - val scope = FrameScope() - return VulkanOwnedTextureFrameHandle( - this, - nativeFrame, - scope, - vulkanOwnedTextureFrame(nativeFrame, scope), - ) - } catch (error: Throwable) { - activeFrame.endBorrow() - nativeFrame.close() - throw error - } - } - - public actual fun acquireOpenGLOwnedTextureFrame(): OpenGLOwnedTextureFrameHandle { + public actual override fun close() { NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - val nativeFrame = MaplibreNativeC.mln_opengl_owned_texture_frame() - nativeFrame.size(nativeFrame.sizeof()) - try { - Status.check( - MaplibreNativeC.mln_opengl_owned_texture_acquire_frame(requireLiveHandle(), nativeFrame) - ) - val scope = FrameScope() - return OpenGLOwnedTextureFrameHandle( - this, - nativeFrame, - scope, - openglOwnedTextureFrame(nativeFrame, scope), - ) - } catch (error: Throwable) { - activeFrame.endBorrow() - nativeFrame.close() - throw error - } + core.closeOnce(destroy = { MaplibreNativeC.mln_render_session_destroy(handleId) }) } - public actual override fun close() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("destroy") - core.closeOnce( - destroy = { MaplibreNativeC.mln_render_session_destroy(handleId) }, - afterSuccess = { mapRetention.close() }, - ) + internal fun frameReleased(scope: FrameScope) { + acquiredFrameScopes.remove(scope) } private fun requireLiveHandle(): Long { @@ -365,142 +354,278 @@ private constructor(private val map: MapHandle, private val handleId: Long) : Au return handleId } - internal fun releaseMetalFrame(frame: MaplibreNativeC.mln_metal_owned_texture_frame) { - Status.check(MaplibreNativeC.mln_metal_owned_texture_release_frame(requireLiveHandle(), frame)) - } + private fun unit(start: (MaplibreNativeC.mln_completion) -> Int): Deferred = + CompletionBridge.unit(start) - internal fun releaseVulkanFrame(frame: MaplibreNativeC.mln_vulkan_owned_texture_frame) { - Status.check(MaplibreNativeC.mln_vulkan_owned_texture_release_frame(requireLiveHandle(), frame)) + private fun requiredBuffer(result: MaplibreNativeC.mln_completion_result): ByteArray { + require(result.value_count() == 1L) { "native completion omitted its byte result" } + val view = MaplibreNativeC.mln_buffer_view(result.value()) + return JavaCppSupport.byteArray(view.data(), view.size()) } - internal fun releaseOpenGLFrame(frame: MaplibreNativeC.mln_opengl_owned_texture_frame) { - Status.check(MaplibreNativeC.mln_opengl_owned_texture_release_frame(requireLiveHandle(), frame)) + private fun queriedFeatures(result: MaplibreNativeC.mln_completion_result): List { + val count = Math.toIntExact(result.value_count()) + if (count == 0) return emptyList() + val values = MaplibreNativeC.mln_queried_feature(result.value()) + return List(count) { index -> queriedFeature(values.position(index.toLong())) } } - internal fun finishFrameBorrow() { - activeFrame.endBorrow() - } + internal companion object { + private fun attach( + map: MapHandle, + options: RenderSessionAttachOptions, + call: + ( + MaplibreNativeC.mln_render_session_attach_options, + LongPointer, + MaplibreNativeC.mln_completion, + ) -> Int, + ): RenderSessionAttachment { + NativeAccess.ensureLoaded() + Status.requireArgument(options.requestedTextureRingDepth >= 0) { + "requestedTextureRingDepth must be non-negative" + } + MaplibreNativeC.mln_render_session_attach_options_default().use { nativeOptions -> + nativeOptions + .driver(options.driver.nativeValue) + .requested_texture_ring_depth(options.requestedTextureRingDepth) + LongPointer(1).use { outSession -> + outSession.put(0, 0L) + val completed = CompletionBridge.unitChecked { completion -> + call(nativeOptions, outSession, completion) + } + val sessionId = outSession.get() + require(sessionId != 0L) { "render session attach returned a null session" } + val session = RenderSessionHandle(map, sessionId) + return try { + RenderSessionAttachment(session, retainSessionUntilComplete(session, completed)) + } catch (error: Throwable) { + runCatching { session.abandon() } + runCatching { session.close() } + throw error + } + } + } + } - public companion object { internal fun attachMetalOwnedTexture( map: MapHandle, descriptor: MetalOwnedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_metal_owned_texture_attach( map.nativeHandleId(), metalOwnedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachMetalBorrowedTexture( map: MapHandle, descriptor: MetalBorrowedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_metal_borrowed_texture_attach( map.nativeHandleId(), metalBorrowedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachVulkanOwnedTexture( map: MapHandle, descriptor: VulkanOwnedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_vulkan_owned_texture_attach( map.nativeHandleId(), vulkanOwnedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachVulkanBorrowedTexture( map: MapHandle, descriptor: VulkanBorrowedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_vulkan_borrowed_texture_attach( map.nativeHandleId(), vulkanBorrowedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachOpenGLOwnedTexture( map: MapHandle, descriptor: OpenGLOwnedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_opengl_owned_texture_attach( map.nativeHandleId(), openglOwnedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachOpenGLBorrowedTexture( map: MapHandle, descriptor: OpenGLBorrowedTextureDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_opengl_borrowed_texture_attach( map.nativeHandleId(), openglBorrowedTextureDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachMetalSurface( map: MapHandle, descriptor: MetalSurfaceDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_metal_surface_attach( map.nativeHandleId(), metalSurfaceDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachVulkanSurface( map: MapHandle, descriptor: VulkanSurfaceDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_vulkan_surface_attach( map.nativeHandleId(), vulkanSurfaceDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } internal fun attachOpenGLSurface( map: MapHandle, descriptor: OpenGLSurfaceDescriptor, - ): RenderSessionHandle = - attach(map) { outSession -> + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attach(map, options) { nativeOptions, outSession, completion -> MaplibreNativeC.mln_opengl_surface_attach( map.nativeHandleId(), openglSurfaceDescriptor(descriptor), + nativeOptions, outSession, + completion, ) } + } +} - private fun attach(map: MapHandle, block: (LongPointer) -> Int): RenderSessionHandle { - NativeAccess.ensureLoaded() - LongPointer(1).use { outSession -> - outSession.put(0, 0L) - Status.check(block(outSession)) - val session = outSession.get() - require(session != 0L) { "render session attach returned a null session" } - return RenderSessionHandle(map, session) +public actual class AcquiredFrameHandle +internal constructor( + private val session: RenderSessionHandle, + private var frameId: Long, + private val scope: FrameScope, +) { + private val released = java.util.concurrent.atomic.AtomicBoolean(false) + + public actual val isReleased: Boolean + get() = released.get() + + public actual fun result(): RenderFrameResult = withFrame { frame -> + MaplibreNativeC.mln_render_frame_result().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_acquired_frame_get_result(frame, value)) + frameResult(value) + } + } + + public actual fun producerSync(): GpuSync = withFrame { frame -> + MaplibreNativeC.mln_gpu_sync_default().use { value -> + Status.check(MaplibreNativeC.mln_acquired_frame_get_producer_sync(frame, value)) + GpuSync( + GpuSyncKind.fromNative(value.kind()), + address(value.`object`()).toULong(), + value.value().toULong(), + ) + } + } + + public actual fun metalTexture(): MetalOwnedTextureFrame = withFrame { frame -> + MaplibreNativeC.mln_metal_owned_texture_frame().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_acquired_frame_get_metal_texture(frame, value)) + metalOwnedTextureFrame(value, scope) + } + } + + public actual fun vulkanTexture(): VulkanOwnedTextureFrame = withFrame { frame -> + MaplibreNativeC.mln_vulkan_owned_texture_frame().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_acquired_frame_get_vulkan_texture(frame, value)) + vulkanOwnedTextureFrame(value, scope) + } + } + + public actual fun openGLTexture(): OpenGLOwnedTextureFrame = withFrame { frame -> + MaplibreNativeC.mln_opengl_owned_texture_frame().use { value -> + value.size(value.sizeof()) + Status.check(MaplibreNativeC.mln_acquired_frame_get_opengl_texture(frame, value)) + openglOwnedTextureFrame(value, scope) + } + } + + public actual fun release(consumerCompletion: GpuSync) { + check(released.compareAndSet(false, true)) { "AcquiredFrameHandle is already released" } + MaplibreNativeC.mln_gpu_sync_default().use { nativeSync -> + nativeSync + .kind(consumerCompletion.kind.nativeValue) + .`object`(pointerOrNull(consumerCompletion.objectHandle)) + .value(consumerCompletion.value.toLong()) + LongPointer(1).use { frame -> + frame.put(0, frameId) + try { + Status.check(MaplibreNativeC.mln_acquired_frame_release(frame, nativeSync)) + frameId = 0L + scope.close() + session.frameReleased(scope) + } catch (error: Throwable) { + released.set(false) + throw error + } } } } + + private inline fun withFrame(block: (Long) -> T): T { + scope.ensureActive() + check(!isReleased) { "AcquiredFrameHandle is already released" } + return block(frameId) + } } private fun metalOwnedTextureDescriptor( @@ -753,6 +878,9 @@ private fun queriedFeature(value: MaplibreNativeC.mln_queried_feature): QueriedF private fun pointerOrNull(pointer: NativePointer): Pointer? = if (pointer.isNull) null else AddressPointer(pointer.address) +private fun pointerOrNull(address: ULong): Pointer? = + if (address == 0uL) null else AddressPointer(address.toLong()) + private fun latLng(value: MaplibreNativeC.mln_lat_lng): LatLng = LatLng(value.latitude(), value.longitude()) @@ -943,37 +1071,21 @@ private class AddressPointer(address: Long) : Pointer(null as Pointer?) { } } +private fun frameResult(value: MaplibreNativeC.mln_render_frame_result): RenderFrameResult = + RenderFrameResult( + RenderResult.fromNative(value.disposition()), + value.token().toULong(), + value.map_update_generation().toULong(), + value.extent_generation().toULong(), + value.frame_generation().toULong(), + value.needs_repaint(), + ) + /** Direct test seam for the JavaCPP render and query adapter. */ internal object JavaCppRenderStructs { fun renderedQueryGeometryType(value: RenderedQueryGeometry): Int = RenderedQueryGeometryScope(value).use { it.geometry.type() } - fun featureStateSelectorSnapshot(value: FeatureStateSelector): FeatureStateSelectorSnapshot = - FeatureStateSelectorScope(value).use { - val fields = it.selector.fields() - FeatureStateSelectorSnapshot( - fields, - stringView(it.selector.source_id()), - if ((fields and MaplibreNativeC.MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID) != 0) - stringView(it.selector.source_layer_id()) - else null, - if ((fields and MaplibreNativeC.MLN_FEATURE_STATE_SELECTOR_FEATURE_ID) != 0) - stringView(it.selector.feature_id()) - else null, - if ((fields and MaplibreNativeC.MLN_FEATURE_STATE_SELECTOR_STATE_KEY) != 0) - stringView(it.selector.state_key()) - else null, - ) - } - - data class FeatureStateSelectorSnapshot( - val fields: Int, - val sourceId: String, - val sourceLayerId: String?, - val featureId: String?, - val stateKey: String?, - ) - fun textureImageInfoSnapshot( width: Int, height: Int, diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt deleted file mode 100644 index ce375e40a..000000000 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner - -/** Explicit handle for a Vulkan session-owned texture frame. */ -public actual class VulkanOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val nativeFrame: MaplibreNativeC.mln_vulkan_owned_texture_frame, - private val scope: FrameScope, - private val frameValue: VulkanOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("VulkanOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): VulkanOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseVulkanFrame(nativeFrame) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeFrame.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt index a6bff947a..30d1ad757 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt @@ -127,9 +127,8 @@ internal constructor( /** * Releases the native request once the wrapper becomes unreachable. * - * Request handles carry no owner-thread affinity, so the cleanup thread may reclaim one. This - * holds the ownership state alone; holding the wrapper would keep it reachable and suppress every - * reclaim. + * Request handles carry no thread affinity, so the cleanup thread may reclaim one. This holds the + * ownership state alone; holding the wrapper would keep it reachable and suppress every reclaim. */ private class CloseWhenUnreachableAction(private val core: ResourceRequestHandleCore) : Runnable { override fun run() { diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt deleted file mode 100644 index b8438a5c4..000000000 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner - -/** Owner-thread offline database operation backed by the Android JNI bridge. */ -public actual class OfflineOperationHandle -internal constructor( - private val runtime: RuntimeHandle, - id: Long, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, -) : AutoCloseable { - private val runtimeRetention = runtime.retainChild("OfflineOperationHandle") - private val core = - OfflineOperationHandleCore(runtime, id, kind, resultKind, runtimeRetention::close) - - init { - HandleLeakCleaner.registerOfflineOperation(this, core.leakReport) - } - - public actual val id: Long - get() = core.id - - public actual val kind: OfflineOperationKind - get() = core.kind - - public actual val resultKind: OfflineOperationResultKind - get() = core.resultKind - - public actual val isClosed: Boolean - get() = core.isClosed - - internal fun requireLive(expectedRuntime: RuntimeHandle): Long = core.requireLive(expectedRuntime) - - internal fun requireLive( - expectedRuntime: RuntimeHandle, - expectedKind: OfflineOperationKind, - expectedResultKind: OfflineOperationResultKind, - ): Long = core.requireLive(expectedRuntime, expectedKind, expectedResultKind) - - internal fun markConsumed() { - core.markConsumed() - } - - public actual override fun close() { - if (!isClosed) runtime.discardOfflineOperation(this) - } -} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt index cdbafcc6f..90b1bee4e 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt @@ -5,16 +5,17 @@ import java.nio.Buffer import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.charset.StandardCharsets -import org.bytedeco.javacpp.BoolPointer +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import org.bytedeco.javacpp.BytePointer import org.bytedeco.javacpp.LongPointer import org.bytedeco.javacpp.Pointer import org.bytedeco.javacpp.SizeTPointer import org.maplibre.nativeffi.NativeAccess -import org.maplibre.nativeffi.error.InvalidStateException import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds import org.maplibre.nativeffi.geo.TileId +import org.maplibre.nativeffi.internal.async.CompletionBridge import org.maplibre.nativeffi.internal.callback.HttpHeaderTransformState import org.maplibre.nativeffi.internal.callback.ResourceProviderState import org.maplibre.nativeffi.internal.callback.ResourceTransformState @@ -38,460 +39,264 @@ import org.maplibre.nativeffi.resource.ResourceProviderCallback import org.maplibre.nativeffi.resource.ResourceTransformCallback /** Owned runtime handle backed by the Android JNI bridge. */ -public actual class RuntimeHandle private constructor(private val handleId: Long) : AutoCloseable { +public actual class RuntimeHandle private constructor(private val handleId: Long) { private val core = HandleStateCore("RuntimeHandle", handleId) + private val liveMaps = mutableMapOf>() + + /** Teardown report of the close that consumed this handle. */ + @Volatile private var tornDown: Deferred? = null init { HandleLeakCleaner.register(this, core.leakReport) } - private var resourceProviderState: ResourceProviderState? = null - private var resourceTransformState: ResourceTransformState? = null - private var httpHeaderTransformState: HttpHeaderTransformState? = null - private val liveMaps = mutableMapOf>() - // One batch struct per handle, reused by every drain and freed by close(). - private val batch = - MaplibreNativeC.mln_runtime_event_batch().also { - Pointer.memset(it, 0, EventLayout.BATCH_SIZE.toLong()) - } - private var eventBuffer: ByteBuffer? = null - private var eventBufferBase = 0L - private var messageBuffer: ByteBuffer? = null - private var messageBufferBase = 0L - public actual val isClosed: Boolean get() = core.isReleased() - public actual fun pump(timeoutMillis: Long, budgetMillis: Long) { - NativeAccess.ensureLoaded() - Status.check(MaplibreNativeC.mln_runtime_pump(requireLiveHandle(), timeoutMillis, budgetMillis)) - } - - public actual fun acquireWakeSource(): WakeSource { - NativeAccess.ensureLoaded() - val runtime = requireLiveHandle() - LongPointer(1).use { outSource -> - outSource.put(0, 0L) - Status.check(MaplibreNativeC.mln_runtime_wake_source_acquire(runtime, outSource)) - val sourceId = outSource.get() - require(sourceId != 0L) { "mln_runtime_wake_source_acquire returned a null wake source" } - return WakeSource(sourceId) - } + public actual fun barrier(): Deferred = CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_barrier(requireLiveHandle(), completion) } - public actual fun startAmbientCacheOperation( - operation: AmbientCacheOperation - ): OfflineOperationHandle { - NativeAccess.ensureLoaded() - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_run_ambient_cache_operation_start( + public actual fun runAmbientCacheOperation(operation: AmbientCacheOperation): Deferred = + CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_run_ambient_cache_operation( requireLiveHandle(), operation.nativeValue, - outOperationId, + completion, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.AMBIENT_CACHE, - OfflineOperationResultKind.NONE, - ) - } + } - public actual fun startSetMaximumAmbientCacheSize(size: Long): OfflineOperationHandle { - NativeAccess.ensureLoaded() + public actual fun setMaximumAmbientCacheSize(size: Long): Deferred { Status.requireArgument(size >= 0) { "size must be non-negative" } - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_set_maximum_ambient_cache_size_start( + return CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_set_maximum_ambient_cache_size( requireLiveHandle(), size, - outOperationId, + completion, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.SET_MAXIMUM_AMBIENT_CACHE_SIZE, - OfflineOperationResultKind.NONE, - ) + } } - public actual fun startCreateOfflineRegion( + public actual fun createOfflineRegion( definition: OfflineRegionDefinition, metadata: ByteArray, - ): OfflineOperationHandle { - val outOperationId = longArrayOf(0L) + ): Deferred = OfflineRegionDefinitionScope(definition).use { nativeDefinition -> - Status.check( - MaplibreNativeC.mln_runtime_offline_region_create_start( - requireLiveHandle(), - nativeDefinition.definition, - metadata, - metadata.size.toLong(), - outOperationId, + bytePointer(metadata).use { nativeMetadata -> + CompletionBridge.submit( + { result -> requireNotNull(offlineRegionInfo(result)) }, + { completion -> + MaplibreNativeC.mln_runtime_offline_region_create( + requireLiveHandle(), + nativeDefinition.definition, + nativeMetadata, + metadata.size.toLong(), + completion, + ) + }, ) - ) + } } - return offlineOperation( - outOperationId[0], - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ) - } - public actual fun startOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - startOfflineLongOperation(id, MaplibreNativeC::mln_runtime_offline_region_get_start), - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, + public actual fun offlineRegion(id: Long): Deferred = + CompletionBridge.submit( + ::offlineRegionInfo, + { completion -> + MaplibreNativeC.mln_runtime_offline_region_get(requireLiveHandle(), id, completion) + }, ) - public actual fun startOfflineRegions(): OfflineOperationHandle> = - offlineOperation( - startOfflineOperation(MaplibreNativeC::mln_runtime_offline_regions_list_start), - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, + public actual fun offlineRegions(): Deferred> = + CompletionBridge.submit( + ::offlineRegionList, + { completion -> + MaplibreNativeC.mln_runtime_offline_regions_list(requireLiveHandle(), completion) + }, ) - public actual fun startMergeOfflineRegionsDatabase( - path: String - ): OfflineOperationHandle> { + public actual fun mergeOfflineRegionsDatabase(path: String): Deferred> { JavaCppSupport.requireValidCString(path) - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_offline_regions_merge_database_start( - requireLiveHandle(), - path, - outOperationId, + return JavaCppSupport.cString(path).use { nativePath -> + CompletionBridge.submit( + ::offlineRegionList, + { completion -> + MaplibreNativeC.mln_runtime_offline_regions_merge_database( + requireLiveHandle(), + nativePath, + completion, + ) + }, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) + } } - public actual fun startUpdateOfflineRegionMetadata( + public actual fun updateOfflineRegionMetadata( id: Long, metadata: ByteArray, - ): OfflineOperationHandle { - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_offline_region_update_metadata_start( - requireLiveHandle(), - id, - metadata, - metadata.size.toLong(), - outOperationId, + ): Deferred = + bytePointer(metadata).use { nativeMetadata -> + CompletionBridge.submit( + { result -> requireNotNull(offlineRegionInfo(result)) }, + { completion -> + MaplibreNativeC.mln_runtime_offline_region_update_metadata( + requireLiveHandle(), + id, + nativeMetadata, + metadata.size.toLong(), + completion, + ) + }, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, - ) - } + } - public actual fun startOfflineRegionStatus( - id: Long - ): OfflineOperationHandle = - offlineOperation( - startOfflineLongOperation(id, MaplibreNativeC::mln_runtime_offline_region_get_status_start), - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, + public actual fun offlineRegionStatus(id: Long): Deferred = + CompletionBridge.submit( + { result -> offlineRegionStatus(MaplibreNativeC.mln_offline_region_status(result.value())) }, + { completion -> + MaplibreNativeC.mln_runtime_offline_region_get_status(requireLiveHandle(), id, completion) + }, ) - public actual fun startSetOfflineRegionObserved( - id: Long, - observed: Boolean, - ): OfflineOperationHandle { - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_offline_region_set_observed_start( + public actual fun setOfflineRegionObserved(id: Long, observed: Boolean): Deferred = + CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_offline_region_set_observed( requireLiveHandle(), id, observed, - outOperationId, + completion, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.REGION_SET_OBSERVED, - OfflineOperationResultKind.NONE, - ) - } + } - public actual fun startSetOfflineRegionDownloadState( + public actual fun setOfflineRegionDownloadState( id: Long, downloadState: OfflineRegionDownloadState, - ): OfflineOperationHandle { + ): Deferred { Status.requireArgument(downloadState.isKnown) { "Unknown offline region download state cannot be used as input: ${downloadState.nativeValue}" } - val outOperationId = longArrayOf(0L) - Status.check( - MaplibreNativeC.mln_runtime_offline_region_set_download_state_start( + return CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_offline_region_set_download_state( requireLiveHandle(), id, downloadState.nativeValue, - outOperationId, + completion, ) - ) - return offlineOperation( - outOperationId[0], - OfflineOperationKind.REGION_SET_DOWNLOAD_STATE, - OfflineOperationResultKind.NONE, - ) + } } - public actual fun startInvalidateOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - startOfflineLongOperation(id, MaplibreNativeC::mln_runtime_offline_region_invalidate_start), - OfflineOperationKind.REGION_INVALIDATE, - OfflineOperationResultKind.NONE, - ) - - public actual fun startDeleteOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - startOfflineLongOperation(id, MaplibreNativeC::mln_runtime_offline_region_delete_start), - OfflineOperationKind.REGION_DELETE, - OfflineOperationResultKind.NONE, - ) + public actual fun invalidateOfflineRegion(id: Long): Deferred = + CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_offline_region_invalidate(requireLiveHandle(), id, completion) + } - public actual fun takeCreateOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ) - val region = - takeOfflineRegionSnapshot( - operationId, - MaplibreNativeC::mln_runtime_offline_region_create_take_result, - ) - operation.markConsumed() - return region - } - - public actual fun takeOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo? { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) - LongPointer(1).use { outSnapshot -> - outSnapshot.put(0, 0L) - BoolPointer(1).use { outFound -> - Status.check( - MaplibreNativeC.mln_runtime_offline_region_get_take_result( - requireLiveHandle(), - operationId, - outSnapshot, - outFound, - ) - ) - operation.markConsumed() - return if (outFound.get()) offlineRegionSnapshot(outSnapshot) else null - } + public actual fun deleteOfflineRegion(id: Long): Deferred = + CompletionBridge.unit { completion -> + MaplibreNativeC.mln_runtime_offline_region_delete(requireLiveHandle(), id, completion) } - } - public actual fun takeOfflineRegionsResult( - operation: OfflineOperationHandle> - ): List { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, - ) - val regions = - takeOfflineRegionList( - operationId, - MaplibreNativeC::mln_runtime_offline_regions_list_take_result, - ) - operation.markConsumed() - return regions + private fun offlineRegionInfo(result: MaplibreNativeC.mln_completion_result): OfflineRegionInfo? { + if (result.value_count() == 0L) return null + return offlineRegionInfo(MaplibreNativeC.mln_offline_region_info(result.value())) } - public actual fun takeMergeOfflineRegionsDatabaseResult( - operation: OfflineOperationHandle> + private fun offlineRegionList( + result: MaplibreNativeC.mln_completion_result ): List { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) - val regions = - takeOfflineRegionList( - operationId, - MaplibreNativeC::mln_runtime_offline_regions_merge_database_take_result, - ) - operation.markConsumed() - return regions - } - - public actual fun takeUpdateOfflineRegionMetadataResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, - ) - val region = - takeOfflineRegionSnapshot( - operationId, - MaplibreNativeC::mln_runtime_offline_region_update_metadata_take_result, - ) - operation.markConsumed() - return region - } - - public actual fun takeOfflineRegionStatusResult( - operation: OfflineOperationHandle - ): OfflineRegionStatus { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, - ) - MaplibreNativeC.mln_offline_region_status().use { status -> - status.size(status.sizeof()) - Status.check( - MaplibreNativeC.mln_runtime_offline_region_get_status_take_result( - requireLiveHandle(), - operationId, - status, - ) - ) - operation.markConsumed() - return offlineRegionStatus(status) - } + val count = Math.toIntExact(result.value_count()) + if (count == 0) return emptyList() + val values = MaplibreNativeC.mln_offline_region_info(result.value()) + return List(count) { index -> offlineRegionInfo(values.position(index.toLong())) } } - public actual fun setResourceProvider(callback: ResourceProviderCallback) { + public actual fun setResourceProvider( + callback: ResourceProviderCallback + ): Deferred { val replacement = ResourceProviderState(callback) - val previous: ResourceProviderState? - try { - resourceProviderState?.checkCanClose() - Status.check( - MaplibreNativeC.mln_runtime_set_resource_provider( - requireLiveHandle(), - replacement.descriptor(), - ) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return command { completion -> + MaplibreNativeC.mln_runtime_set_resource_provider( + requireLiveHandle(), + replacement.descriptor(), + completion, ) - previous = resourceProviderState - resourceProviderState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) - } catch (error: Throwable) { - closeAndSuppress(error, replacement) - throw error } - releaseCallbackRoot(previous) } - public actual fun clearResourceProvider() { - resourceProviderState?.checkCanClose() - Status.check(MaplibreNativeC.mln_runtime_clear_resource_provider(requireLiveHandle())) - val previous = resourceProviderState - resourceProviderState = null - // The install path retained this as a strong leak-cleaner root, so closing - // alone would keep it and everything its callback captured reachable. - releaseCallbackRoot(previous) + public actual fun clearResourceProvider(): Deferred = command { completion -> + MaplibreNativeC.mln_runtime_clear_resource_provider(requireLiveHandle(), completion) } - public actual fun setResourceTransform(callback: ResourceTransformCallback) { + public actual fun setResourceTransform( + callback: ResourceTransformCallback + ): Deferred { val replacement = ResourceTransformState(callback) - val previous: ResourceTransformState? - try { - resourceTransformState?.checkCanClose() - Status.check( - MaplibreNativeC.mln_runtime_set_resource_transform( - requireLiveHandle(), - replacement.descriptor(), - ) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return command { completion -> + MaplibreNativeC.mln_runtime_set_resource_transform( + requireLiveHandle(), + replacement.descriptor(), + completion, ) - previous = resourceTransformState - resourceTransformState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) - } catch (error: Throwable) { - closeAndSuppress(error, replacement) - throw error } - releaseCallbackRoot(previous) } - public actual fun clearResourceTransform() { - resourceTransformState?.checkCanClose() - Status.check(MaplibreNativeC.mln_runtime_clear_resource_transform(requireLiveHandle())) - val previous = resourceTransformState - resourceTransformState = null - releaseCallbackRoot(previous) + public actual fun clearResourceTransform(): Deferred = command { completion -> + MaplibreNativeC.mln_runtime_clear_resource_transform(requireLiveHandle(), completion) } - public actual fun setHttpHeaderTransform(callback: HttpHeaderTransformCallback) { + public actual fun setHttpHeaderTransform( + callback: HttpHeaderTransformCallback + ): Deferred { val replacement = HttpHeaderTransformState(callback) - try { - httpHeaderTransformState?.checkCanClose() - Status.check( - MaplibreNativeC.mln_runtime_set_http_header_transform( - requireLiveHandle(), - replacement.descriptor(), - ) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return command { completion -> + MaplibreNativeC.mln_runtime_set_http_header_transform( + requireLiveHandle(), + replacement.descriptor(), + completion, ) - val previous = httpHeaderTransformState - httpHeaderTransformState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) - releaseCallbackRoot(previous) - } catch (error: Throwable) { - closeAndSuppress(error, replacement) - throw error } } - public actual fun clearHttpHeaderTransform() { - httpHeaderTransformState?.checkCanClose() - Status.check(MaplibreNativeC.mln_runtime_clear_http_header_transform(requireLiveHandle())) - val previous = httpHeaderTransformState - httpHeaderTransformState = null - releaseCallbackRoot(previous) - } + public actual fun clearHttpHeaderTransform(): Deferred = + command { completion -> + MaplibreNativeC.mln_runtime_clear_http_header_transform(requireLiveHandle(), completion) + } - public actual fun drainEvents(maxEvents: Int): RuntimeEventBatch { + public actual fun drainEvents(): RuntimeEventBatch { NativeAccess.ensureLoaded() - Status.requireArgument(maxEvents >= 0) { "maxEvents must be non-negative" } val runtime = requireLiveHandle() - batch.size(EventLayout.BATCH_SIZE) - Status.check(MaplibreNativeC.mln_runtime_drain_events(runtime, maxEvents.toLong(), batch)) - val remainingCount = batch.remaining_count() - val eventCount = Math.toIntExact(batch.event_count()) - if (eventCount == 0) { - return RuntimeEventBatch(emptyList(), remainingCount) - } - // The stride the batch reports can exceed the probed record, so index by it. - val eventSize = batch.event_size() - require(eventSize >= EventLayout.EVENT_SIZE) { - "Loaded native library reports a ${eventSize}-byte runtime event, " + - "smaller than this binding's ${EventLayout.EVENT_SIZE}-byte record" - } - val events = eventBytes(batch.events(), eventCount.toLong() * eventSize) - val messages = messageBytes(batch.messages(), batch.messages_size()) - val copied = - List(eventCount) { index -> - copiedEvent(events, index * eventSize, eventSize, messages).toRuntimeEvent() + LongPointer(1).use { outBatch -> + outBatch.put(0, 0L) + Status.check(MaplibreNativeC.mln_runtime_drain_events(runtime, outBatch)) + val batch = outBatch.get() + require(batch != 0L) { "mln_runtime_drain_events returned a null event batch" } + try { + MaplibreNativeC.mln_runtime_event_batch_view().use { view -> + view.size(view.sizeof()) + Status.check(MaplibreNativeC.mln_event_batch_get(batch, view)) + val eventCount = Math.toIntExact(view.event_count()) + if (eventCount == 0) return RuntimeEventBatch(emptyList()) + val eventSize = view.event_size() + require(eventSize >= EventLayout.EVENT_SIZE) { + "Loaded native library reports a ${eventSize}-byte runtime event, " + + "smaller than this binding's ${EventLayout.EVENT_SIZE}-byte record" + } + val events = directBuffer(view.events(), eventCount.toLong() * eventSize) + val messages = + if (view.messages() == null || view.messages().isNull || view.messages_size() == 0L) + null + else directBuffer(view.messages(), view.messages_size()) + return RuntimeEventBatch( + List(eventCount) { index -> + copiedEvent(events, index * eventSize, eventSize, messages).toRuntimeEvent() + } + ) + } + } finally { + MaplibreNativeC.mln_event_batch_release(batch) } - return RuntimeEventBatch(copied, remainingCount) + } } public actual var eventMask: RuntimeEventMask @@ -508,76 +313,37 @@ public actual class RuntimeHandle private constructor(private val handleId: Long ) } - public actual override fun close() { - resourceProviderState?.checkCanClose() - resourceTransformState?.checkCanClose() - httpHeaderTransformState?.checkCanClose() - core.closeOnce( - destroy = { MaplibreNativeC.mln_runtime_destroy(handleId) }, - afterSuccess = { - batch.close() - eventBuffer = null - messageBuffer = null - releaseCallbackRoot(resourceProviderState) - resourceProviderState = null - releaseCallbackRoot(resourceTransformState) - resourceTransformState = null - releaseCallbackRoot(httpHeaderTransformState) - httpHeaderTransformState = null - liveMaps.clear() - }, - ) + public actual fun close(): Deferred { + if (!core.beginClose()) return tornDown ?: CompletableDeferred(Unit) + val completed = + try { + CompletionBridge.unitChecked { completion -> + MaplibreNativeC.mln_runtime_release(handleId, completion) + } + } catch (error: Throwable) { + core.abortClose() + throw error + } + tornDown = completed + core.completeClose { liveMaps.clear() } + return completed } public actual companion object { public actual fun create(options: RuntimeOptions): RuntimeHandle { NativeAccess.ensureLoaded() RuntimeOptionsScope(options).use { nativeOptions -> - val outRuntime = LongPointer(1) - outRuntime.put(0, 0L) - Status.check(MaplibreNativeC.mln_runtime_create(nativeOptions.options, outRuntime)) - val runtime = outRuntime.get() - require(runtime != 0L) { "mln_runtime_create returned a null runtime" } - return RuntimeHandle(runtime) + LongPointer(1).use { outRuntime -> + outRuntime.put(0, 0L) + Status.check(MaplibreNativeC.mln_runtime_create(nativeOptions.options, outRuntime)) + val runtime = outRuntime.get() + require(runtime != 0L) { "mln_runtime_create returned a null runtime" } + return RuntimeHandle(runtime) + } } } } - private fun offlineOperation( - operationId: Long, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, - ): OfflineOperationHandle = OfflineOperationHandle(this, operationId, kind, resultKind) - - private fun startOfflineOperation(start: (Long, LongArray) -> Int): Long { - val outOperationId = longArrayOf(0L) - Status.check(start(requireLiveHandle(), outOperationId)) - return outOperationId[0] - } - - private fun startOfflineLongOperation(value: Long, start: (Long, Long, LongArray) -> Int): Long { - val outOperationId = longArrayOf(0L) - Status.check(start(requireLiveHandle(), value, outOperationId)) - return outOperationId[0] - } - - internal fun discardOfflineOperation(operation: OfflineOperationHandle<*>) { - if (operation.isClosed) return - val operationId = operation.requireLive(this) - val runtimeId = - try { - requireLiveHandle() - } catch (error: InvalidStateException) { - operation.markConsumed() - throw error - } - Status.check(MaplibreNativeC.mln_runtime_offline_operation_discard(runtimeId, operationId)) - operation.markConsumed() - } - - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - core.retainChild(childTypeName) - internal fun nativeHandleId(): Long = requireLiveHandle() internal fun registerMap(map: MapHandle) { @@ -604,37 +370,6 @@ public actual class RuntimeHandle private constructor(private val handleId: Long return handleId } - /** - * Maps a batch pointer to a reused direct buffer, re-basing only when the runtime's storage moved - * or grew past the mapped window. - */ - private fun eventBytes(events: Pointer, byteCount: Long): ByteBuffer { - val cached = eventBuffer - if (cached != null && eventBufferBase == events.address() && cached.capacity() >= byteCount) { - return cached - } - val mapped = directBuffer(events, byteCount) - eventBuffer = mapped - eventBufferBase = events.address() - return mapped - } - - private fun messageBytes(messages: Pointer?, byteCount: Long): ByteBuffer? { - if (messages == null || messages.isNull || byteCount == 0L) { - return null - } - val cached = messageBuffer - if ( - cached != null && messageBufferBase == messages.address() && cached.capacity() >= byteCount - ) { - return cached - } - val mapped = directBuffer(messages, byteCount) - messageBuffer = mapped - messageBufferBase = messages.address() - return mapped - } - /** Reads one event record at [base] into binding-owned values. */ private fun copiedEvent( events: ByteBuffer, @@ -652,7 +387,7 @@ public actual class RuntimeHandle private constructor(private val handleId: Long message = message( messages, - events.getInt(base + EventLayout.MESSAGE_OFFSET), + events.getLong(base + EventLayout.MESSAGE_OFFSET), events.getInt(base + EventLayout.MESSAGE_SIZE), ), ) @@ -682,28 +417,12 @@ public actual class RuntimeHandle private constructor(private val handleId: Long } return map } - - private fun takeOfflineRegionSnapshot( - operationId: Long, - take: (Long, Long, LongPointer) -> Int, - ): OfflineRegionInfo = - LongPointer(1).use { outSnapshot -> - outSnapshot.put(0, 0L) - Status.check(take(requireLiveHandle(), operationId, outSnapshot)) - offlineRegionSnapshot(outSnapshot) - } - - private fun takeOfflineRegionList( - operationId: Long, - take: (Long, Long, LongPointer) -> Int, - ): List = - LongPointer(1).use { outList -> - outList.put(0, 0L) - Status.check(take(requireLiveHandle(), operationId, outList)) - offlineRegionList(outList) - } } +private inline fun command( + crossinline call: (MaplibreNativeC.mln_completion) -> Int +): Deferred = CompletionBridge.command { call(it) } + private fun releaseCallbackRoot(root: AutoCloseable?) { HandleLeakCleaner.releaseNativeCallbackRoot(root) closeQuietly(root) @@ -735,6 +454,9 @@ private fun byteArray(pointer: Pointer?, byteCount: Long): ByteArray { return bytes } +private fun bytePointer(bytes: ByteArray): BytePointer = + BytePointer(bytes.size.toLong()).also { pointer -> pointer.put(bytes, 0, bytes.size) } + /** One event's fields, copied out of the drained batch before the drain returns. */ private class CopiedEvent( val type: Int, @@ -748,12 +470,12 @@ private class CopiedEvent( private fun directBuffer(pointer: Pointer, byteCount: Long): ByteBuffer = BytePointer(pointer).capacity(byteCount).asByteBuffer().order(ByteOrder.nativeOrder()) -private fun message(messages: ByteBuffer?, offset: Int, size: Int): String { +private fun message(messages: ByteBuffer?, offset: Long, size: Int): String { if (messages == null || size == 0) { return "" } val bytes = ByteArray(size) - readAt(messages, offset, bytes) + readAt(messages, Math.toIntExact(offset), bytes) return String(bytes, StandardCharsets.UTF_8) } @@ -795,8 +517,6 @@ private fun runtimeEventPayload( events.getLong(base + EventLayout.TILE_COUNT_LIMIT_REGION_ID), events.getLong(base + EventLayout.TILE_COUNT_LIMIT_LIMIT), ) - MaplibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED -> - offlineOperationCompletedPayload(events, base) MaplibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED -> RuntimeEventPayload.CameraTransitionFinished(events.getLong(base + EventLayout.TRANSITION_ID)) else -> unknownPayload(payloadType, events, base, eventSize) @@ -864,18 +584,6 @@ private fun offlineRegionStatusPayload( ), ) -private fun offlineOperationCompletedPayload( - events: ByteBuffer, - base: Int, -): RuntimeEventPayload.OfflineOperationCompleted = - RuntimeEventPayload.OfflineOperationCompleted( - events.getLong(base + EventLayout.OPERATION_ID), - OfflineOperationKind.fromNative(events.getInt(base + EventLayout.OPERATION_KIND)), - OfflineOperationResultKind.fromNative(events.getInt(base + EventLayout.OPERATION_RESULT_KIND)), - events.getInt(base + EventLayout.OPERATION_RESULT_STATUS), - events.get(base + EventLayout.OPERATION_FOUND) != 0.toByte(), - ) - private fun offlineRegionStatus( status: MaplibreNativeC.mln_offline_region_status ): OfflineRegionStatus = @@ -1020,14 +728,13 @@ private object EventLayout { BytePointer(records).capacity(2L * stride).asByteBuffer().order(ByteOrder.nativeOrder()) val EVENT_SIZE: Int = Math.toIntExact(stride) - val BATCH_SIZE: Int = MaplibreNativeC.mln_runtime_event_batch().use { it.sizeof() } val TYPE: Int = probeInt { it.type(INT_SENTINEL) } val SOURCE_TYPE: Int = probeInt { it.source_type(INT_SENTINEL) } val SOURCE: Int = probeLong { it.source(LONG_SENTINEL) } val CODE: Int = probeInt { it.code(INT_SENTINEL) } val PAYLOAD_TYPE: Int = probeInt { it.payload_type(INT_SENTINEL) } - val MESSAGE_OFFSET: Int = probeInt { it.message_offset(INT_SENTINEL) } + val MESSAGE_OFFSET: Int = probeLong { it.message_offset(LONG_SENTINEL) } val MESSAGE_SIZE: Int = probeInt { it.message_size(INT_SENTINEL) } val RENDER_FRAME_MODE: Int = probeInt { it.payload().render_frame().mode(INT_SENTINEL) } @@ -1116,20 +823,6 @@ private object EventLayout { it.payload().offline_region_tile_count_limit()._limit(LONG_SENTINEL) } - val OPERATION_ID: Int = probeLong { - it.payload().offline_operation_completed().operation_id(LONG_SENTINEL) - } - val OPERATION_KIND: Int = probeInt { - it.payload().offline_operation_completed().operation_kind(INT_SENTINEL) - } - val OPERATION_RESULT_KIND: Int = probeInt { - it.payload().offline_operation_completed().result_kind(INT_SENTINEL) - } - val OPERATION_RESULT_STATUS: Int = probeInt { - it.payload().offline_operation_completed().result_status(INT_SENTINEL) - } - val OPERATION_FOUND: Int = probeFlag { it.payload().offline_operation_completed().found(true) } - val TRANSITION_ID: Int = probeLong { it.payload().camera_transition_finished().transition_id(LONG_SENTINEL) } @@ -1147,7 +840,6 @@ private object EventLayout { REGION_STATUS_REGION_ID, RESPONSE_ERROR_REGION_ID, TILE_COUNT_LIMIT_REGION_ID, - OPERATION_ID, TRANSITION_ID, ) diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt deleted file mode 100644 index ebaaa699d..000000000 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import org.maplibre.nativeffi.NativeAccess -import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.internal.javacpp.MaplibreNativeC -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner -import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore -import org.maplibre.nativeffi.internal.status.Status - -/** Owned wake source backed by the Android JNI bridge. */ -public actual class WakeSource internal constructor(private val sourceId: Long) : AutoCloseable { - private val core = HandleStateCore("WakeSource", sourceId) - - init { - HandleLeakCleaner.register(this, core.leakReport) - } - - public actual val isClosed: Boolean - get() = core.isReleased() - - public actual fun signal() { - NativeAccess.ensureLoaded() - core.withLive { Status.check(MaplibreNativeC.mln_wake_source_signal(sourceId)) } - } - - public actual override fun close() { - core.closeOnce( - destroy = { - MaplibreNativeC.mln_wake_source_destroy(sourceId) - MaplibreStatus.OK.nativeCode - } - ) - } -} diff --git a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt index 4fb8dd5af..d140e7875 100644 --- a/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt +++ b/bindings/kotlin/src/androidMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt @@ -31,7 +31,7 @@ public actual class GeoJsonSourceDataHandle internal constructor(private val han ) } - /** Runs [block] with the live native handle and release held off for the borrow window. */ + /** Runs [block] after checking that this wrapper still owns the native handle. */ internal fun withNativeHandle(block: (Long) -> R): R = core.withLive { block(handleId) } public actual companion object { diff --git a/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/MetalRenderTargetTest.kt b/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/MetalRenderTargetTest.kt index 456677d43..38cd9fb5a 100644 --- a/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/MetalRenderTargetTest.kt +++ b/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/MetalRenderTargetTest.kt @@ -2,7 +2,7 @@ package org.maplibre.nativeffi.render import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertSame import kotlin.test.assertTrue import kotlinx.cinterop.BetaInteropApi @@ -13,14 +13,16 @@ import kotlinx.cinterop.objcPtr import kotlinx.cinterop.toLong import kotlinx.cinterop.useContents import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.Maplibre -import org.maplibre.nativeffi.error.UnsupportedFeatureException -import org.maplibre.nativeffi.log.LogCallback +import org.maplibre.nativeffi.error.MaplibreException +import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapMode import org.maplibre.nativeffi.map.MapOptions -import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.map.MapSize import org.maplibre.nativeffi.runtime.RuntimeHandle +import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.runtime.runSuspendTest import platform.CoreGraphics.CGSizeMake import platform.Metal.MTLCreateSystemDefaultDevice import platform.Metal.MTLDeviceProtocol @@ -35,94 +37,86 @@ import platform.QuartzCore.CAMetalLayer @OptIn(BetaInteropApi::class, ExperimentalForeignApi::class) class MetalRenderTargetTest { - // BND-162, BND-171: borrowed texture and surface attach paths preserve caller-owned backend - // handles. + // BND-162, BND-171, BND-175, BND-176: borrowed-texture and surface attach paths preserve + // caller-owned backend handles, accept a replacement target, and reject each other's kind. + // + // These sessions run on the core-worker driver, so a test demands frames and polls the + // frame-result queue instead of servicing driver work. @Test - fun metalBorrowedTextureAndSurfaceAttachThroughPublicBinding() { - if (!metalSupportedOrInapplicable()) return + fun metalBorrowedTextureAndSurfaceAttachThroughPublicBinding(): Unit = runSuspendTest { + if (!metalSupportedOrInapplicable()) return@runSuspendTest val device = MTLCreateSystemDefaultDevice() ?: error("MTLCreateSystemDefaultDevice returned nil") - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) + val runtime = RuntimeHandle.create(RuntimeOptions()) try { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + val borrowedTexture = createMetalTexture(device, 32, 16) + val borrowedMap = createMap(runtime) try { - val borrowedTexture = createMetalTexture(device, 32, 16) - val borrowedMap = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, + val borrowedTextureAddress = borrowedTexture.address() + val borrowedDescriptor = + MetalBorrowedTextureDescriptor( + extent = RenderTargetExtent(32, 16, 1.0), + physicalWidth = 32, + physicalHeight = 16, + texture = NativePointer.ofAddress(borrowedTextureAddress), ) + val attachment = borrowedMap.attachMetalBorrowedTexture(borrowedDescriptor) + val session = attachment.session try { - val borrowedTextureAddress = borrowedTexture.address() - val borrowedDescriptor = - MetalBorrowedTextureDescriptor( - extent = RenderTargetExtent(32, 16, 1.0), - physicalWidth = 32, - physicalHeight = 16, - texture = NativePointer.ofAddress(borrowedTextureAddress), - ) - val session = borrowedMap.attachMetalBorrowedTexture(borrowedDescriptor) - try { - assertSame(borrowedMap, session.map()) - borrowedMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue( - waitForMapEvent(runtime, borrowedMap, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) - ) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - assertFailsWith { session.acquireMetalOwnedTextureFrame() } - assertFailsWith { session.textureImageInfo() } - } finally { - session.close() - } - assertEquals(borrowedTextureAddress, borrowedDescriptor.texture.address) + attachment.completed.await() + assertSame(borrowedMap, session.map()) + borrowedMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) + + // A borrowed target has no texture ring and no readback path. + assertFalse(session.capabilities().frameAcquisition) + assertFalse(session.capabilities().readback) + assertUnsupported { session.readPremultipliedRgba8() } } finally { - borrowedMap.close() + session.abandonAndClose() + runtime.barrier().await() } + // The session never retained the texture the host attached with. + assertEquals(borrowedTextureAddress, borrowedDescriptor.texture.address) + } finally { + borrowedMap.close() + runtime.barrier().await() + } - val layer = createMetalLayer(device, 32, 16) - val surfaceMap = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, + val layer = createMetalLayer(device, 32, 16) + val surfaceMap = createMap(runtime) + try { + val attachment = + surfaceMap.attachMetalSurface( + MetalSurfaceDescriptor( + extent = RenderTargetExtent(32, 16, 1.0), + context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), + layer = NativePointer.ofAddress(layer.address()), + ) ) + val session = attachment.session try { - val session = - surfaceMap.attachMetalSurface( - MetalSurfaceDescriptor( - extent = RenderTargetExtent(32, 16, 1.0), - context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), - layer = NativePointer.ofAddress(layer.address()), - ) - ) - try { - assertSame(surfaceMap, session.map()) - surfaceMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue( - waitForMapEvent(runtime, surfaceMap, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) - ) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - assertFailsWith { session.acquireMetalOwnedTextureFrame() } - assertFailsWith { session.textureImageInfo() } - } finally { - session.close() - } + attachment.completed.await() + assertSame(surfaceMap, session.map()) + surfaceMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) + + assertFalse(session.capabilities().frameAcquisition) + assertFalse(session.capabilities().readback) + assertUnsupported { session.readPremultipliedRgba8() } } finally { - surfaceMap.close() + session.abandonAndClose() + runtime.barrier().await() } } finally { - runtime.close() + surfaceMap.close() + runtime.barrier().await() } } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + runtime.close().await() } } @@ -130,24 +124,17 @@ class MetalRenderTargetTest { // session writes drawableSize from the extent's physical size. @Test - fun metalSurfaceAttachAcceptsNullDeviceAndWritesDrawableSize() { - if (!metalSupportedOrInapplicable()) return - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + fun metalSurfaceAttachAcceptsNullDeviceAndWritesDrawableSize(): Unit = runSuspendTest { + if (!metalSupportedOrInapplicable()) return@runSuspendTest + val runtime = RuntimeHandle.create(RuntimeOptions()) try { - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) + val map = createMap(runtime) try { val layer = CAMetalLayer() layer.drawableSize = CGSizeMake(1.0, 1.0) val extent = RenderTargetExtent(32, 16, 2.0) val physical = extent.physicalSize() - val session = + val attachment = map.attachMetalSurface( MetalSurfaceDescriptor( extent = extent, @@ -155,167 +142,155 @@ class MetalRenderTargetTest { layer = NativePointer.ofAddress(layer.address()), ) ) + val session = attachment.session try { + attachment.completed.await() layer.drawableSize.useContents { assertEquals(physical.width.toDouble(), width) assertEquals(physical.height.toDouble(), height) } - map.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE)) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) + map.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) } finally { - session.close() + session.abandonAndClose() + runtime.barrier().await() } } finally { map.close() + runtime.barrier().await() } } finally { - runtime.close() + runtime.close().await() } } - // BND-175: replacing a host-owned target keeps the session and hands it the - // replacement's extent, which the map catches up to on its next pump. + // Replacing a host-owned target keeps the session and hands it the replacement's extent, + // which the map catches up to on a later frame. @Test - fun metalSetTargetReplacesBorrowedTextureAndSurfaceTargets() { - if (!metalSupportedOrInapplicable()) return + fun metalSetTargetReplacesBorrowedTextureAndSurfaceTargets(): Unit = runSuspendTest { + if (!metalSupportedOrInapplicable()) return@runSuspendTest val device = MTLCreateSystemDefaultDevice() ?: error("MTLCreateSystemDefaultDevice returned nil") - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) + val runtime = RuntimeHandle.create(RuntimeOptions()) try { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + val borrowedTexture = createMetalTexture(device, 32, 16) + val borrowedMap = createMap(runtime) try { - val borrowedTexture = createMetalTexture(device, 32, 16) - val borrowedMap = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, + val borrowedTextureAddress = borrowedTexture.address() + val attachment = + borrowedMap.attachMetalBorrowedTexture( + MetalBorrowedTextureDescriptor( + extent = RenderTargetExtent(32, 16, 1.0), + physicalWidth = 32, + physicalHeight = 16, + texture = NativePointer.ofAddress(borrowedTextureAddress), + ) ) + val session = attachment.session try { - val borrowedTextureAddress = borrowedTexture.address() - val session = - borrowedMap.attachMetalBorrowedTexture( - MetalBorrowedTextureDescriptor( - extent = RenderTargetExtent(32, 16, 1.0), - physicalWidth = 32, - physicalHeight = 16, - texture = NativePointer.ofAddress(borrowedTextureAddress), - ) - ) - try { - borrowedMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue( - waitForMapEvent(runtime, borrowedMap, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) - ) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) + attachment.completed.await() + borrowedMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) - // A caller-owned texture is sized by its owner, so the host - // reallocates and hands the replacement over instead of resizing. - assertFailsWith { session.resize(16, 8, 1.0) } - val replacementTexture = createMetalTexture(device, 16, 8) - val replacement = - MetalBorrowedTextureDescriptor( - extent = RenderTargetExtent(16, 8, 1.0), - physicalWidth = 16, - physicalHeight = 8, - texture = NativePointer.ofAddress(replacementTexture.address()), - ) - // Freshly allocated, so anything non-zero later came from this - // session rendering into it after the handoff. - assertTrue( - readMetalTextureRgba(device, replacementTexture, 16, 8).all { it == 0.toByte() }, - "a freshly allocated replacement should start blank", - ) - session.setMetalBorrowedTextureTarget(replacement) - assertSame(borrowedMap, session.map()) - assertEquals(RenderResult.SIZE_PENDING, session.renderUpdate().result) - runtime.pump(0) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - // The session paints the texture it was handed, not the one it had. - assertTrue( - readMetalTextureRgba(device, replacementTexture, 16, 8).any { it != 0.toByte() }, - "the replacement texture should have been rendered into", + // A caller-owned texture is sized by its owner, so the host reallocates it and hands the + // replacement over. The map remains the independent logical-size authority. + assertUnsupported { session.resize(RenderTargetExtent(16, 8, 1.0)) } + val replacementTexture = createMetalTexture(device, 16, 8) + val replacement = + MetalBorrowedTextureDescriptor( + extent = RenderTargetExtent(16, 8, 1.0), + physicalWidth = 16, + physicalHeight = 8, + texture = NativePointer.ofAddress(replacementTexture.address()), ) - assertEquals(replacementTexture.address(), replacement.texture.address) - // The session never retained the outgoing texture, so the host - // still owns the one it attached with. - assertEquals(borrowedTextureAddress, borrowedTexture.address()) - } finally { - session.close() - } + // Freshly allocated, so anything non-zero later came from this + // session rendering into it after the handoff. + assertTrue( + readMetalTextureRgba(device, replacementTexture, 16, 8).all { it == 0.toByte() }, + "a freshly allocated replacement should start blank", + ) + session.setMetalBorrowedTextureTarget(replacement).await() + borrowedMap.resize(MapSize(16, 8, 1.0)).await() + runtime.barrier().await() + assertSame(borrowedMap, session.map()) + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) + // The session paints the texture it was handed, not the one it had. + assertTrue( + readMetalTextureRgba(device, replacementTexture, 16, 8).any { it != 0.toByte() }, + "the replacement texture should have been rendered into", + ) + assertEquals(replacementTexture.address(), replacement.texture.address) + // The session never retained the outgoing texture, so the host + // still owns the one it attached with. + assertEquals(borrowedTextureAddress, borrowedTexture.address()) } finally { - borrowedMap.close() + session.abandonAndClose() + runtime.barrier().await() } + } finally { + borrowedMap.close() + runtime.barrier().await() + } - val layer = createMetalLayer(device, 32, 16) - val replacementLayer = createMetalLayer(device, 16, 8) - val surfaceMap = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, + val layer = createMetalLayer(device, 32, 16) + val replacementLayer = createMetalLayer(device, 16, 8) + val surfaceMap = createMap(runtime) + try { + val attachment = + surfaceMap.attachMetalSurface( + MetalSurfaceDescriptor( + extent = RenderTargetExtent(32, 16, 1.0), + context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), + layer = NativePointer.ofAddress(layer.address()), + ) ) + val session = attachment.session try { - val session = - surfaceMap.attachMetalSurface( - MetalSurfaceDescriptor( - extent = RenderTargetExtent(32, 16, 1.0), - context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), - layer = NativePointer.ofAddress(layer.address()), - ) - ) - try { - surfaceMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue( - waitForMapEvent(runtime, surfaceMap, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) - ) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) + attachment.completed.await() + surfaceMap.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) - // A recreated host surface replaces the presentation target while - // the session and its renderer go on living. - session.setMetalSurfaceTarget( + // A recreated host surface replaces the presentation target while + // the session and its renderer go on living. + session + .setMetalSurfaceTarget( MetalSurfaceDescriptor( extent = RenderTargetExtent(16, 8, 1.0), context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), layer = NativePointer.ofAddress(replacementLayer.address()), ) ) - assertSame(surfaceMap, session.map()) - assertEquals(RenderResult.SIZE_PENDING, session.renderUpdate().result) - runtime.pump(0) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - } finally { - session.close() - } + .await() + surfaceMap.resize(MapSize(16, 8, 1.0)).await() + runtime.barrier().await() + assertSame(surfaceMap, session.map()) + assertEquals(RenderResult.RENDERED, session.awaitRenderedFrame().disposition) } finally { - surfaceMap.close() + session.abandonAndClose() + runtime.barrier().await() } } finally { - runtime.close() + surfaceMap.close() + runtime.barrier().await() } } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + runtime.close().await() } } - // BND-176: a borrowed texture session and a surface session reject each - // other's set-target pairing. + // A borrowed texture session and a surface session reject each other's set-target pairing. @Test - fun metalSetTargetReportsUnsupportedForSwappedBorrowedAndSurfaceTargets() { - if (!metalSupportedOrInapplicable()) return + fun metalSetTargetReportsUnsupportedForSwappedBorrowedAndSurfaceTargets(): Unit = runSuspendTest { + if (!metalSupportedOrInapplicable()) return@runSuspendTest val device = MTLCreateSystemDefaultDevice() ?: error("MTLCreateSystemDefaultDevice returned nil") val metalContext = MetalContextDescriptor(NativePointer.ofAddress(device.address())) - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + val runtime = RuntimeHandle.create(RuntimeOptions()) try { val texture = createMetalTexture(device, 32, 16) val textureTarget = @@ -333,51 +308,60 @@ class MetalRenderTargetTest { layer = NativePointer.ofAddress(layer.address()), ) - val borrowedMap = createStaticMap(runtime) + val borrowedMap = createMap(runtime) try { - val session = borrowedMap.attachMetalBorrowedTexture(textureTarget) + val attachment = borrowedMap.attachMetalBorrowedTexture(textureTarget) try { - assertFailsWith { - session.setMetalSurfaceTarget(surfaceTarget) - } + attachment.completed.await() + assertUnsupported { attachment.session.setMetalSurfaceTarget(surfaceTarget) } } finally { - session.close() + attachment.session.abandonAndClose() + runtime.barrier().await() } } finally { borrowedMap.close() + runtime.barrier().await() } - val surfaceMap = createStaticMap(runtime) + val surfaceMap = createMap(runtime) try { - val session = surfaceMap.attachMetalSurface(surfaceTarget) + val attachment = surfaceMap.attachMetalSurface(surfaceTarget) try { - assertFailsWith { - session.setMetalBorrowedTextureTarget(textureTarget) - } + attachment.completed.await() + assertUnsupported { attachment.session.setMetalBorrowedTextureTarget(textureTarget) } } finally { - session.close() + attachment.session.abandonAndClose() + runtime.barrier().await() } } finally { surfaceMap.close() + runtime.barrier().await() } } finally { - runtime.close() + runtime.close().await() } } + /** A target-kind mismatch fails either at submission or at completion. */ + private suspend fun assertUnsupported(submit: () -> Deferred<*>) { + val failure = runCatching { submit().await() }.exceptionOrNull() + assertTrue(failure is MaplibreException, "expected an unsupported failure: $failure") + assertEquals(MaplibreStatus.UNSUPPORTED, failure.status, failure.diagnostic) + } + private fun metalSupportedOrInapplicable(): Boolean { return RenderBackend.METAL in Maplibre.supportedRenderBackends() } - private fun createStaticMap(runtime: RuntimeHandle): MapHandle = + private suspend fun createMap(runtime: RuntimeHandle): MapHandle = MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) + runtime, + MapOptions().apply { + width = 32 + height = 16 + }, + ) + .await() private fun createMetalTexture(device: MTLDeviceProtocol, width: Int, height: Int): ObjCObject { val descriptor = diff --git a/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.apple.kt b/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.apple.kt index 47ece1ac6..fee073ea4 100644 --- a/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.apple.kt +++ b/bindings/kotlin/src/appleTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.apple.kt @@ -14,52 +14,40 @@ internal actual object OwnedTextureTestSupport { if (RenderBackend.METAL !in Maplibre.supportedRenderBackends()) return null val device = MTLCreateSystemDefaultDevice() ?: error("MTLCreateSystemDefaultDevice returned nil") - val session = + val attachment = map.attachMetalOwnedTexture( MetalOwnedTextureDescriptor( extent = RenderTargetExtent(width, height, 1.0), context = MetalContextDescriptor(NativePointer.ofAddress(device.address())), - ) + ), + OWNED_TEXTURE_ATTACH_OPTIONS, ) - return AppleOwnedTextureSession(device.address(), session) + return AppleOwnedTextureSession(device.address(), attachment) } } private class AppleOwnedTextureSession( private val deviceAddress: Long, - override val session: RenderSessionHandle, + override val attachment: RenderSessionAttachment, ) : OwnedTextureTestSession { - override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionHandle = + override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionAttachment = session .map() .attachMetalOwnedTexture( MetalOwnedTextureDescriptor( extent = RenderTargetExtent(width, height, 1.0), context = MetalContextDescriptor(NativePointer.ofAddress(deviceAddress)), - ) + ), + OWNED_TEXTURE_ATTACH_OPTIONS, ) - override fun acquireFrame(): OwnedTextureTestFrame { - val handle = session.acquireMetalOwnedTextureFrame() - val frame = handle.frame() - return object : OwnedTextureTestFrame { - override val width: Int - get() = frame.width() - - override val height: Int - get() = frame.height() - - override val isClosed: Boolean - get() = handle.isClosed - - override fun close() { - handle.close() - } - } + override fun frameSize(frame: AcquiredFrameHandle): OwnedTextureFrameSize { + val texture = frame.metalTexture() + return OwnedTextureFrameSize(texture.width(), texture.height()) } override fun close() { - session.close() + session.abandonAndClose() } } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraOptions.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraOptions.kt index da43ddc2e..ae8aeb932 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraOptions.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraOptions.kt @@ -17,8 +17,8 @@ public class CameraOptions { public var padding: EdgeInsets? = null /** - * The screen point that stays fixed while a camera command applies. Input-only: it always reads - * back as null from [org.maplibre.nativeffi.map.MapHandle.camera]. + * The screen point that stays fixed while a camera command applies. Input-only: snapshots omit + * this field. */ public var anchor: ScreenPoint? = null diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraUpdate.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraUpdate.kt new file mode 100644 index 000000000..6e20662cf --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/camera/CameraUpdate.kt @@ -0,0 +1,47 @@ +package org.maplibre.nativeffi.camera + +import org.maplibre.nativeffi.geo.ScreenPoint + +/** Transition behavior for one atomic camera update. */ +public enum class CameraUpdateMode(internal val nativeValue: Int) { + JUMP(0), + EASE(1), + FLY(2), +} + +/** Gesture boundary carried with one camera update. */ +public enum class GesturePhase(internal val nativeValue: Int) { + NONE(0), + BEGIN(1), + UPDATE(2), + END(3), + CANCEL(4), +} + +/** One atomic camera command copied by the native runtime. */ +public data class CameraUpdate( + public val mode: CameraUpdateMode = CameraUpdateMode.JUMP, + public val camera: CameraOptions = CameraOptions(), + public val animation: AnimationOptions = AnimationOptions(), + public val gesturePhase: GesturePhase = GesturePhase.NONE, +) + +/** Relative camera operation kind. */ +public enum class CameraDeltaKind(internal val nativeValue: Int) { + MOVE(0), + SCALE(1), + BEARING(2), + PITCH(3), +} + +/** One relative camera operation. */ +public data class CameraDelta( + public val kind: CameraDeltaKind = CameraDeltaKind.MOVE, + public val offset: ScreenPoint = ScreenPoint(0.0, 0.0), + public val amount: Double = 0.0, + public val anchor: ScreenPoint? = null, + public val animation: AnimationOptions = AnimationOptions(), +) + +/** Camera state and the map generation that published it. */ +public data class CameraSnapshot(public val generation: Long, public val camera: CameraOptions) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreException.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreException.kt index e76cc1b1c..0cd7adc61 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreException.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreException.kt @@ -38,5 +38,10 @@ private fun statusLabel(status: MaplibreStatus): String = MaplibreStatus.WRONG_THREAD -> "WRONG_THREAD" MaplibreStatus.UNSUPPORTED -> "UNSUPPORTED" MaplibreStatus.NATIVE_ERROR -> "NATIVE_ERROR" + MaplibreStatus.CANCELLED -> "CANCELLED" + MaplibreStatus.BUSY -> "BUSY" + MaplibreStatus.TARGET_LOST -> "TARGET_LOST" + MaplibreStatus.NOT_READY -> "NOT_READY" + MaplibreStatus.NOT_FOUND -> "NOT_FOUND" else -> "MaplibreStatus(${status.nativeCode})" } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreStatus.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreStatus.kt index 9ddcd514a..2e3a8ed3e 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreStatus.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/MaplibreStatus.kt @@ -17,6 +17,11 @@ public value class MaplibreStatus(public val nativeCode: Int) { public val WRONG_THREAD: MaplibreStatus = MaplibreStatus(-3) public val UNSUPPORTED: MaplibreStatus = MaplibreStatus(-4) public val NATIVE_ERROR: MaplibreStatus = MaplibreStatus(-5) + public val CANCELLED: MaplibreStatus = MaplibreStatus(-6) + public val BUSY: MaplibreStatus = MaplibreStatus(-7) + public val TARGET_LOST: MaplibreStatus = MaplibreStatus(-8) + public val NOT_READY: MaplibreStatus = MaplibreStatus(-9) + public val NOT_FOUND: MaplibreStatus = MaplibreStatus(-10) /** Returns the Kotlin status category for a C ABI status value. */ internal fun fromNative(nativeCode: Int): MaplibreStatus = MaplibreStatus(nativeCode) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/WrongThreadException.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/WrongThreadException.kt index 2256c26fb..b22c3e31a 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/WrongThreadException.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/error/WrongThreadException.kt @@ -1,6 +1,6 @@ package org.maplibre.nativeffi.error -/** Error for owner-thread-affine native handles called from the wrong thread. */ +/** Error for graphics-thread-affine native handles called from the wrong thread. */ public class WrongThreadException internal constructor(nativeStatusCode: Int, diagnostic: String = "") : MaplibreException(MaplibreStatus.WRONG_THREAD, nativeStatusCode, diagnostic) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/async/Deferred.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/async/Deferred.kt new file mode 100644 index 000000000..568ff5674 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/async/Deferred.kt @@ -0,0 +1,13 @@ +package org.maplibre.nativeffi.internal.async + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +/** Transforms an eager deferred value without imposing a caller scope. */ +internal fun Deferred.mapDeferred(transform: (T) -> R): Deferred = + CoroutineScope(Dispatchers.Unconfined).async(start = CoroutineStart.UNDISPATCHED) { + transform(await()) + } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistry.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistry.kt deleted file mode 100644 index ed00cfe14..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistry.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.maplibre.nativeffi.internal.callback - -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import org.maplibre.nativeffi.internal.status.Status - -/** Process-global callback replacement registry shared by platform log bridges. */ -@OptIn(ExperimentalAtomicApi::class) -internal class LogCallbackRegistry { - private val updateLock = AtomicInt(0) - private val installed = AtomicInt(0) - private val current = AtomicReference(null) - - fun set(replacement: T, install: () -> Int) { - var previous: T? = null - try { - withUpdateLock { - if (installed.load() == 0) { - Status.check(install()) - installed.store(1) - } - previous = current.exchange(replacement) - } - } catch (error: Throwable) { - replacement.close() - throw error - } - previous?.close() - } - - fun clear(clearNative: () -> Int) { - var previous: T? = null - withUpdateLock { - if (installed.load() != 0) { - Status.check(clearNative()) - installed.store(0) - } - previous = current.exchange(null) - } - previous?.close() - } - - fun current(): T? = current.load() - - private inline fun withUpdateLock(block: () -> R): R { - while (!updateLock.compareAndSet(0, 1)) { - // Spin briefly; log callback registration is process-global and infrequent. - } - try { - return block() - } finally { - updateLock.store(0) - } - } -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCore.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCore.kt index a576104cf..5ee7b6c29 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCore.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCore.kt @@ -1,7 +1,6 @@ package org.maplibre.nativeffi.internal.lifecycle import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi import org.maplibre.nativeffi.internal.status.Status @@ -15,8 +14,6 @@ internal class HandleStateCore( @Suppress("unused") private val parents: Array = parents val leakReport: LeakReport = LeakReport(typeName, handleId) private val releaseState = AtomicInt(STATE_LIVE) - private val liveChildren = AtomicReference>(emptyList()) - private val activeUses = AtomicInt(0) fun requireLive() { when (releaseState.load()) { @@ -26,25 +23,10 @@ internal class HandleStateCore( } } - /** - * Runs [block] with release held off until it returns, for handles the host may use and release - * from different threads. Calling [closeOnce] from inside [block] deadlocks. - */ + /** Runs [block] after checking that this wrapper still owns its native handle. */ fun withLive(block: () -> T): T { - addActiveUse(1) - try { - requireLive() - return block() - } finally { - addActiveUse(-1) - } - } - - private fun addActiveUse(delta: Int) { - while (true) { - val current = activeUses.load() - if (activeUses.compareAndSet(current, current + delta)) return - } + requireLive() + return block() } fun isReleased(): Boolean = releaseState.load() == STATE_CLOSED @@ -53,80 +35,42 @@ internal class HandleStateCore( fun handleId(): Long = handleId /** - * Retains this handle on behalf of a live child wrapper. [childTypeName] appears in the error a - * blocked parent release throws. + * Acquires the exclusive close lease before an asynchronous native close starts. + * + * Returns false when the handle is already closed. The caller must pair a true result with + * [completeClose] or [abortClose]. */ - fun retainChild(childTypeName: String): ChildRetention { - while (true) { - requireLive() - val children = liveChildren.load() - if (!liveChildren.compareAndSet(children, children + childTypeName)) { - continue - } - try { - requireLive() - return ChildRetention(this, childTypeName) - } catch (error: Throwable) { - releaseChild(childTypeName) - throw error - } - } - } - - fun closeOnce(destroy: () -> Int, afterSuccess: () -> Unit = {}) { + fun beginClose(): Boolean { if (!releaseState.compareAndSet(STATE_LIVE, STATE_RELEASING)) { when (releaseState.load()) { - STATE_CLOSED -> return + STATE_CLOSED -> return false STATE_RELEASING -> throw Status.invalidState("$typeName is currently releasing") else -> throw Status.released(typeName) } } - val children = liveChildren.load() - if (children.isNotEmpty()) { - releaseState.store(STATE_LIVE) - throw Status.liveChildren(typeName, children) - } - // Uses that already passed their liveness check still hold the handle; wait them out. - while (activeUses.load() != 0) { - yieldWhileClosing() - } - try { - Status.check(destroy()) - } catch (error: Throwable) { - releaseState.store(STATE_LIVE) - throw error - } + return true + } + + fun completeClose(afterSuccess: () -> Unit = {}) { + check(releaseState.load() == STATE_RELEASING) leakReport.markReleased() releaseState.store(STATE_CLOSED) afterSuccess() } - private fun releaseChild(childTypeName: String) { - while (true) { - val children = liveChildren.load() - val index = children.indexOf(childTypeName) - if (index < 0) { - return - } - val remaining = children.toMutableList().apply { removeAt(index) } - if (liveChildren.compareAndSet(children, remaining)) { - return - } - } + fun abortClose() { + check(releaseState.compareAndSet(STATE_RELEASING, STATE_LIVE)) } - /** One child wrapper's retention of its parent handle. Releasing more than once is a no-op. */ - internal class ChildRetention( - private val owner: HandleStateCore, - private val childTypeName: String, - ) { - private val released = AtomicInt(0) - - fun close() { - if (released.compareAndSet(0, 1)) { - owner.releaseChild(childTypeName) - } + fun closeOnce(destroy: () -> Int, afterSuccess: () -> Unit = {}) { + if (!beginClose()) return + try { + Status.check(destroy()) + } catch (error: Throwable) { + abortClose() + throw error } + completeClose(afterSuccess) } @OptIn(ExperimentalAtomicApi::class) @@ -143,10 +87,7 @@ internal class HandleStateCore( fun report() { if (released.load() == 0) { - writeLine( - "Leaked $typeName native handle 0x${handleId.toString(16)}; " + - "close handles explicitly." - ) + writeLine("Leaked $typeName native handle 0x${handleId.toString(16)}; close it explicitly.") } } } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandles.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandles.kt index 926cd87f6..5c0d78d3a 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandles.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandles.kt @@ -34,8 +34,6 @@ internal sealed interface NativeHandle { @JvmInline internal value class NativeQueriedFeatureList(override val raw: Long) : NativeHandle -@JvmInline internal value class NativeWakeSource(override val raw: Long) : NativeHandle - @JvmInline internal value class NativeResourceRequest(override val raw: Long) : NativeHandle @JvmInline internal value class NativeGeoJsonSourceData(override val raw: Long) : NativeHandle diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt index 8b0e27ecd..15a578ed2 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt @@ -1,14 +1,14 @@ package org.maplibre.nativeffi.map -import org.maplibre.nativeffi.camera.AnimationOptions +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.camera.BoundOptions -import org.maplibre.nativeffi.camera.CameraFitOptions -import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraSnapshot +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds -import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.render.MetalBorrowedTextureDescriptor import org.maplibre.nativeffi.render.MetalOwnedTextureDescriptor @@ -17,10 +17,13 @@ import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLOwnedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor import org.maplibre.nativeffi.render.PremultipliedRgba8Image -import org.maplibre.nativeffi.render.RenderSessionHandle +import org.maplibre.nativeffi.render.RenderDriver +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor import org.maplibre.nativeffi.render.VulkanOwnedTextureDescriptor import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor +import org.maplibre.nativeffi.runtime.CommandCompletion import org.maplibre.nativeffi.runtime.RuntimeEventMask import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @@ -28,9 +31,9 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions import org.maplibre.nativeffi.style.GeoJsonSourceDataHandle import org.maplibre.nativeffi.style.GeoJsonSourceOptions import org.maplibre.nativeffi.style.ImageStretch +import org.maplibre.nativeffi.style.LayerInfo import org.maplibre.nativeffi.style.LocationIndicatorImageKind import org.maplibre.nativeffi.style.SourceInfo -import org.maplibre.nativeffi.style.SourceType import org.maplibre.nativeffi.style.StyleImage import org.maplibre.nativeffi.style.StyleImageInfo import org.maplibre.nativeffi.style.StyleImageOptions @@ -39,7 +42,7 @@ import org.maplibre.nativeffi.style.StyleTransitionOptions import org.maplibre.nativeffi.style.TileSourceOptions /** Owned map handle. Platform actuals own the native map carrier. */ -public expect class MapHandle : AutoCloseable { +public expect class MapHandle { public val isClosed: Boolean public fun runtime(): RuntimeHandle @@ -52,7 +55,10 @@ public expect class MapHandle : AutoCloseable { * reads this mask, changes one bit, and writes it back. Narrowing gates later events and keeps * queued ones. */ - public var eventMask: RuntimeEventMask + public val eventMask: RuntimeEventMask + + /** Changes the map-originated event types queued after this command commits. */ + public fun setEventMask(value: RuntimeEventMask): Deferred /** * Starts loading the style at [url]. The call returns once the request is queued; a load failure @@ -60,7 +66,7 @@ public expect class MapHandle : AutoCloseable { * * @see org.maplibre.nativeffi.runtime.RuntimeHandle.drainEvents */ - public fun setStyleUrl(url: String) + public fun setStyleUrl(url: String): Deferred /** * Loads [json] as the map style. @@ -70,436 +76,487 @@ public expect class MapHandle : AutoCloseable { * * @see org.maplibre.nativeffi.runtime.RuntimeHandle.drainEvents */ - public fun setStyleJson(json: ByteArray) + public fun setStyleJson(json: ByteArray): Deferred - public fun setFeatureState(selector: FeatureStateSelector, value: ByteArray) + /** + * Submits one copied per-feature-state command. [value] must hold one UTF-8 JSON object. Feature + * state belongs to this map; render sessions push it into the renderer on the next render update. + */ + public fun setFeatureState( + selector: FeatureStateSelector, + value: ByteArray, + ): Deferred - public fun getFeatureState(selector: FeatureStateSelector): ByteArray + /** + * Reads per-feature state behind commands accepted before this call. The bytes hold one JSON + * object; missing feature state reads as an empty object. The read copies this map's store, not + * the last rendered frame, so it does not require a render session or a loaded source. + */ + public fun getFeatureState(selector: FeatureStateSelector): Deferred - public fun removeFeatureState(selector: FeatureStateSelector) + /** + * Removes per-feature state. [FeatureStateSelector.featureId] and [FeatureStateSelector.stateKey] + * narrow the removal: passing both removes one state key from one feature, passing only a feature + * ID removes all state for that feature, and passing neither removes all feature state for the + * source and source-layer. + */ + public fun removeFeatureState(selector: FeatureStateSelector): Deferred /** * Returns the style document this map's style was last parsed from, byte-for-byte, or an empty * byte array when no document has been parsed. Runtime mutations do not change it. */ - public fun loadedStyleJson(): ByteArray + public fun loadedStyleJson(): Deferred /** * Returns the URL this map's style was last requested from, recorded when the request is made * rather than when it completes, or an empty string when no URL is available. */ - public fun styleUrl(): String + public fun styleUrl(): Deferred - public fun addStyleSourceJson(sourceId: String, sourceJson: ByteArray) - - public fun removeStyleSource(sourceId: String): Boolean - - public fun styleSourceExists(sourceId: String): Boolean + public fun addStyleSourceJson( + sourceId: String, + sourceJson: ByteArray, + ): Deferred - public fun styleSourceType(sourceId: String): SourceType? + /** + * Removes one style source. The returned deferred fails with + * [org.maplibre.nativeffi.error.MaplibreStatus.NOT_FOUND] when no source has [sourceId] and + * [org.maplibre.nativeffi.error.MaplibreStatus.INVALID_STATE] when a layer still uses the source. + */ + public fun removeStyleSource(sourceId: String): Deferred - public fun styleSourceInfo(sourceId: String): SourceInfo? + /** Returns one source's copied metadata, or null when no source carries [sourceId]. */ + public fun styleSourceInfo(sourceId: String): Deferred - public fun styleSourceIds(): List + public fun styleSourceIds(): Deferred> - public fun addGeoJsonSourceUrl(sourceId: String, url: String, options: GeoJsonSourceOptions?) + public fun addGeoJsonSourceUrl( + sourceId: String, + url: String, + options: GeoJsonSourceOptions?, + ): Deferred /** * Adds a GeoJSON source with prepared inline data. The call borrows [data], and the source adopts * the options the data was prepared with, fixed for the lifetime of the source. */ - public fun addGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) + public fun addGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred - public fun setGeoJsonSourceUrl(sourceId: String, url: String) + public fun setGeoJsonSourceUrl(sourceId: String, url: String): Deferred /** * Updates one GeoJSON source with prepared inline data. The call borrows [data]. The data must * have been prepared with options equal to the options the source was added with, * `clusterProperties` excepted; a mismatch is rejected. */ - public fun setGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) + public fun setGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred /** * Overrides one GeoJSON source's synchronous tiling at runtime. While [enabled] is true, the * source slices requested tiles inline during the update pass, as if the source's options had set * [GeoJsonSourceOptions.synchronousTiling]; false restores the option the source was added with. */ - public fun setGeoJsonSourceSynchronousTiling(sourceId: String, enabled: Boolean) + public fun setGeoJsonSourceSynchronousTiling( + sourceId: String, + enabled: Boolean, + ): Deferred /** * Adds a custom geometry source that calls [options] back for tile data. * * The source belongs to this map's current style. Its callback state lives until the source * leaves the style, which happens when [removeStyleSource] removes it, when a style load replaces - * the style that held it, or when this map closes. Native reports that moment on the map owner - * thread, and the binding closes the source's callbacks there, waiting for any in-flight tile + * the style that held it, or when this map closes. Native reports that moment on the runtime + * worker, and the binding closes the source's callbacks there, waiting for any in-flight tile * callback to return. */ - public fun addCustomGeometrySource(sourceId: String, options: CustomGeometrySourceOptions) + public fun addCustomGeometrySource( + sourceId: String, + options: CustomGeometrySourceOptions, + ): Deferred public fun setCustomGeometrySourceTileData( sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) + ): Deferred - public fun invalidateCustomGeometrySourceTile(sourceId: String, tileId: CanonicalTileId) + public fun invalidateCustomGeometrySourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred - public fun invalidateCustomGeometrySourceRegion(sourceId: String, bounds: LatLngBounds) + public fun invalidateCustomGeometrySourceRegion( + sourceId: String, + bounds: LatLngBounds, + ): Deferred /** * Adds a custom MVT vector source that calls [options] back for encoded tile bytes. * * The source belongs to this map's current style. Its callback state lives until the source * leaves the style, which happens when [removeStyleSource] removes it, when a style load replaces - * the style that held it, or when this map closes. Native reports that moment on the map owner - * thread, and the binding closes the source's callbacks there, waiting for any in-flight tile + * the style that held it, or when this map closes. Native reports that moment on the runtime + * worker, and the binding closes the source's callbacks there, waiting for any in-flight tile * callback to return. */ - public fun addCustomMvtVectorSource(sourceId: String, options: CustomMvtVectorSourceOptions) + public fun addCustomMvtVectorSource( + sourceId: String, + options: CustomMvtVectorSourceOptions, + ): Deferred public fun setCustomMvtVectorSourceTileData( sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) + ): Deferred public fun setCustomMvtVectorSourceTileError( sourceId: String, tileId: CanonicalTileId, message: String, - ) + ): Deferred - public fun invalidateCustomMvtVectorSourceTile(sourceId: String, tileId: CanonicalTileId) + public fun invalidateCustomMvtVectorSourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred - public fun addVectorSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) + public fun addVectorSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred public fun addVectorSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) + ): Deferred - public fun addRasterSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) + public fun addRasterSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred public fun addRasterSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) + ): Deferred - public fun addRasterDemSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) + public fun addRasterDemSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred public fun addRasterDemSourceTiles( sourceId: String, tiles: List, options: TileSourceOptions?, - ) + ): Deferred public fun setStyleImage( imageId: String, image: PremultipliedRgba8Image, options: StyleImageOptions, - ) - - public fun removeStyleImage(imageId: String): Boolean + ): Deferred - public fun styleImageExists(imageId: String): Boolean + /** + * Removes one runtime style image. The returned deferred fails with + * [org.maplibre.nativeffi.error.MaplibreStatus.NOT_FOUND] when no image has [imageId]. + */ + public fun removeStyleImage(imageId: String): Deferred - public fun styleImageInfo(imageId: String): StyleImageInfo? + /** Returns one image's copied metadata, or null when no image carries [imageId]. */ + public fun styleImageInfo(imageId: String): Deferred /** * Returns one runtime style image's stretchable intervals, or null when no image carries * [imageId]. The pair holds the horizontal intervals first. */ - public fun styleImageStretches(imageId: String): Pair, List>? + public fun styleImageStretches( + imageId: String + ): Deferred, List>?> - public fun copyStyleImagePremultipliedRgba8(imageId: String): StyleImage? + public fun copyStyleImagePremultipliedRgba8(imageId: String): Deferred - public fun addImageSourceUrl(sourceId: String, coordinates: List, url: String) + public fun addImageSourceUrl( + sourceId: String, + coordinates: List, + url: String, + ): Deferred public fun addImageSourceImage( sourceId: String, coordinates: List, image: PremultipliedRgba8Image, - ) + ): Deferred - public fun setImageSourceUrl(sourceId: String, url: String) + public fun setImageSourceUrl(sourceId: String, url: String): Deferred - public fun setImageSourceImage(sourceId: String, image: PremultipliedRgba8Image) + public fun setImageSourceImage( + sourceId: String, + image: PremultipliedRgba8Image, + ): Deferred - public fun setImageSourceCoordinates(sourceId: String, coordinates: List) + public fun setImageSourceCoordinates( + sourceId: String, + coordinates: List, + ): Deferred - public fun imageSourceCoordinates(sourceId: String): List? + public fun imageSourceCoordinates(sourceId: String): Deferred?> - public fun addStyleLayerJson(layerJson: ByteArray, beforeLayerId: String) + public fun addStyleLayerJson( + layerJson: ByteArray, + beforeLayerId: String, + ): Deferred - public fun addHillshadeLayer(layerId: String, sourceId: String, beforeLayerId: String) + public fun addHillshadeLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred - public fun addColorReliefLayer(layerId: String, sourceId: String, beforeLayerId: String) + public fun addColorReliefLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred - public fun addLocationIndicatorLayer(layerId: String, beforeLayerId: String) + public fun addLocationIndicatorLayer( + layerId: String, + beforeLayerId: String, + ): Deferred - public fun setLocationIndicatorLocation(layerId: String, coordinate: LatLng, altitude: Double) + public fun setLocationIndicatorLocation( + layerId: String, + coordinate: LatLng, + altitude: Double, + ): Deferred - public fun setLocationIndicatorBearing(layerId: String, bearing: Double) + public fun setLocationIndicatorBearing( + layerId: String, + bearing: Double, + ): Deferred - public fun setLocationIndicatorAccuracyRadius(layerId: String, radius: Double) + public fun setLocationIndicatorAccuracyRadius( + layerId: String, + radius: Double, + ): Deferred public fun setLocationIndicatorImageName( layerId: String, imageKind: LocationIndicatorImageKind, imageId: String, - ) - - public fun removeStyleLayer(layerId: String): Boolean + ): Deferred - public fun styleLayerExists(layerId: String): Boolean + /** + * Removes one style layer. The returned deferred fails with + * [org.maplibre.nativeffi.error.MaplibreStatus.NOT_FOUND] when no layer has [layerId]. + */ + public fun removeStyleLayer(layerId: String): Deferred - public fun styleLayerType(layerId: String): String? + /** Returns one layer's copied metadata, or null when no layer carries [layerId]. */ + public fun styleLayerInfo(layerId: String): Deferred - public fun styleLayerIds(): List + public fun styleLayerIds(): Deferred> - public fun moveStyleLayer(layerId: String, beforeLayerId: String) + public fun moveStyleLayer(layerId: String, beforeLayerId: String): Deferred - public fun styleLayerJson(layerId: String): ByteArray? + public fun styleLayerJson(layerId: String): Deferred - public fun setStyleLightJson(lightJson: ByteArray) + public fun setStyleLightJson(lightJson: ByteArray): Deferred - public fun setStyleLightProperty(propertyName: String, value: ByteArray) + public fun setStyleLightProperty( + propertyName: String, + value: ByteArray, + ): Deferred - public fun styleLightProperty(propertyName: String): ByteArray? + public fun styleLightProperty(propertyName: String): Deferred /** * Sets the style's global transition options, replacing rather than merging. Loading a style * replaces these options with the ones that style declares, so apply an override after the load. */ - public fun setStyleTransitionOptions(options: StyleTransitionOptions) + public fun setStyleTransitionOptions(options: StyleTransitionOptions): Deferred - public fun styleTransitionOptions(): StyleTransitionOptions + public fun styleTransitionOptions(): Deferred - public fun setLayerProperty(layerId: String, propertyName: String, value: ByteArray) + public fun setLayerProperty( + layerId: String, + propertyName: String, + value: ByteArray, + ): Deferred - public fun layerProperty(layerId: String, propertyName: String): ByteArray? + public fun layerProperty(layerId: String, propertyName: String): Deferred - public fun setLayerFilter(layerId: String, filter: ByteArray) + public fun setLayerFilter(layerId: String, filter: ByteArray): Deferred - public fun clearLayerFilter(layerId: String) + public fun clearLayerFilter(layerId: String): Deferred - public fun layerFilter(layerId: String): ByteArray? + public fun layerFilter(layerId: String): Deferred /** Sets one layer's source-layer ID. Layer types that take no source are rejected. */ - public fun setLayerSourceLayer(layerId: String, sourceLayer: String) + public fun setLayerSourceLayer(layerId: String, sourceLayer: String): Deferred /** Returns one layer's source-layer ID, empty when the layer carries none. */ - public fun layerSourceLayer(layerId: String): String + public fun layerSourceLayer(layerId: String): Deferred /** * Sets one layer's source ID. Layer types that take no source are rejected. The named source need * not exist yet. */ - public fun setLayerSourceId(layerId: String, sourceId: String) + public fun setLayerSourceId(layerId: String, sourceId: String): Deferred /** Returns one layer's source ID, empty when the layer carries none. */ - public fun layerSourceId(layerId: String): String + public fun layerSourceId(layerId: String): Deferred /** Sets the lowest zoom at which one layer draws. Pass negative infinity for no lower bound. */ - public fun setLayerMinZoom(layerId: String, minZoom: Double) - - /** - * Returns the lowest zoom at which one layer draws. A layer with no lower bound reports negative - * infinity. - */ - public fun layerMinZoom(layerId: String): Double + public fun setLayerMinZoom(layerId: String, minZoom: Double): Deferred /** Sets the highest zoom at which one layer draws. Pass positive infinity for no upper bound. */ - public fun setLayerMaxZoom(layerId: String, maxZoom: Double) - - /** - * Returns the highest zoom at which one layer draws. A layer with no upper bound reports positive - * infinity. - */ - public fun layerMaxZoom(layerId: String): Double - - public fun setLayerVisibility(layerId: String, visibility: StyleLayerVisibility) - - public fun layerVisibility(layerId: String): StyleLayerVisibility - - public fun requestRepaint() - - public fun requestStillImage() - - public var debugOptions: Set - - public var isRenderingStatsViewEnabled: Boolean - - public val isFullyLoaded: Boolean - - public fun dumpDebugLogs() - - /** - * The map's logical viewport size in UI pixels and its pixel ratio. The scale factor is fixed for - * the lifetime of the map and is independent of any render target's scale factor. - */ - public val size: MapSize - - public var viewportOptions: ViewportOptions - - public var tileOptions: TileOptions - - /** The current camera. [CameraOptions.anchor] is input-only and always reads back as null. */ - public val camera: CameraOptions - - /** Applies [camera] immediately with no transition. */ - public fun jumpTo(camera: CameraOptions) - - /** - * Transitions to [camera] along an eased path. A null [animation] uses a zero duration, so the - * camera applies instantly. - */ - public fun easeTo(camera: CameraOptions, animation: AnimationOptions?) + public fun setLayerMaxZoom(layerId: String, maxZoom: Double): Deferred - /** - * Transitions to [camera] along a curved flight path. A null [animation] derives a duration from - * a default velocity of 1.2 screenfuls per second, so the camera animates. - */ - public fun flyTo(camera: CameraOptions, animation: AnimationOptions?) + public fun setLayerVisibility( + layerId: String, + visibility: StyleLayerVisibility, + ): Deferred - public fun moveBy(deltaX: Double, deltaY: Double) + /** Submits a repaint command. */ + public fun requestRepaint(): Deferred - /** - * Pans the camera by a screen-space delta. A null [animation] uses a zero duration, so the move - * applies instantly. - */ - public fun moveByAnimated(deltaX: Double, deltaY: Double, animation: AnimationOptions?) + /** Suspends until one noncoalescing still-image request completes. */ + public fun requestStillImage(): Deferred - public fun scaleBy(scale: Double, anchor: ScreenPoint?) + /** Copies the latest immutable state generation published by the map worker. */ + public fun snapshot(): MapSnapshot /** - * Scales the camera around an optional screen anchor. A null [animation] uses a zero duration, so - * the scale applies instantly. + * Submits a debug-overlay command. The committed mask is visible through [snapshot] as + * [MapSnapshot.debugOptions]. */ - public fun scaleByAnimated(scale: Double, anchor: ScreenPoint?, animation: AnimationOptions?) - - public fun rotateBy(first: ScreenPoint, second: ScreenPoint) + public fun setDebugOptions(options: Set): Deferred /** - * Rotates the camera by the angle between two screen points. A null [animation] uses a zero - * duration, so the rotation applies instantly. + * Submits a rendering-stats visibility command. The committed value is visible through [snapshot] + * as [MapSnapshot.renderingStatsViewEnabled]. */ - public fun rotateByAnimated(first: ScreenPoint, second: ScreenPoint, animation: AnimationOptions?) - - public fun pitchBy(pitch: Double) + public fun setRenderingStatsViewEnabled(enabled: Boolean): Deferred /** - * Pitches the camera by a delta in degrees. A null [animation] uses a zero duration, so the pitch - * applies instantly. + * Submits a copied viewport-options command. The committed options are visible through [snapshot] + * as [MapSnapshot.viewportOptions]. */ - public fun pitchByAnimated(pitch: Double, animation: AnimationOptions?) - - public fun cancelTransitions() + public fun setViewportOptions(options: ViewportOptions): Deferred /** - * Whether a host-driven gesture is in progress. The flag stays set until the host clears it, so - * pair every `true` with a `false`. + * Submits a copied tile-options command. The committed options are visible through [snapshot] as + * [MapSnapshot.tileOptions]. */ - public var isGestureInProgress: Boolean - - public fun cameraForLatLngBounds( - bounds: LatLngBounds, - fitOptions: CameraFitOptions?, - ): CameraOptions - - public fun cameraForLatLngs( - coordinates: List, - fitOptions: CameraFitOptions?, - ): CameraOptions - - public fun cameraForGeometry(geometry: ByteArray, fitOptions: CameraFitOptions?): CameraOptions + public fun setTileOptions(options: TileOptions): Deferred /** - * Computes geographic bounds for a camera from two viewport corners. - * - * The box is the hull of the top-left and bottom-right screen corners for that camera in the - * current viewport. When bearing and pitch are zero, the box equals the visible area. Those - * corners are the northwest and southeast of the viewport. Longitudes stay in -180 to 180. + * Submits a copied camera-constraint command. The committed constraints are visible through + * [snapshot] as [MapSnapshot.bounds]. */ - public fun latLngBoundsForCamera(camera: CameraOptions): LatLngBounds + public fun setBounds(options: BoundOptions): Deferred /** - * Computes geographic bounds for a camera from the four viewport corners. - * - * The axis-aligned hull of all four screen corners and the center encompasses the projected - * viewport. Longitudes unwrap onto the shortest path through the center. A viewport that crosses - * the antimeridian reports values outside -180 to 180. + * Submits a copied free-camera command. The committed options are visible through [snapshot] as + * [MapSnapshot.freeCameraOptions]. */ - public fun latLngBoundsForCameraUnwrapped(camera: CameraOptions): LatLngBounds - - public var bounds: BoundOptions + public fun setFreeCameraOptions(options: FreeCameraOptions): Deferred - public var freeCameraOptions: FreeCameraOptions + /** Submits the map's logical extent. */ + public fun resize(size: MapSize): Deferred - public var projectionMode: ProjectionModeOptions + /** Copies the latest camera generation published by the map worker. */ + public fun cameraSnapshot(): CameraSnapshot - public fun pixelForLatLng(coordinate: LatLng): ScreenPoint + /** Submits one atomic camera update. */ + public fun updateCamera(update: CameraUpdate): Deferred - public fun latLngForPixel(point: ScreenPoint): LatLng + /** Submits one relative camera operation. */ + public fun applyCameraDelta(delta: CameraDelta): Deferred - public fun pixelsForLatLngs(coordinates: List): List - - public fun latLngsForPixels(points: List): List + /** Suspends for an ordered camera observation behind commands accepted before this call. */ + public fun queryCamera(): Deferred /** - * Attaches a render target to this map, returning the map's one live render session. - * - * The calling thread becomes the session's owner thread for the session's lifetime, and it need - * not be this map's owner thread. For a graphics API with a thread-current context, call this on - * the thread where that context is current. Every session call, including close, throws - * `WrongThreadException` from any other thread. - * - * Close the session before closing the map. The map applies its logical size on its own owner - * thread, so map size and rendering lag until that thread pumps the runtime once after attaching. + * Starts attaching a render target and returns its immediately usable session plus completion + * operation. A caller-graphics-thread session can service attachment work while the operation is + * pending. */ - public fun attachMetalOwnedTexture(descriptor: MetalOwnedTextureDescriptor): RenderSessionHandle + public fun attachMetalOwnedTexture( + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment public fun attachMetalBorrowedTexture( - descriptor: MetalBorrowedTextureDescriptor - ): RenderSessionHandle + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment - public fun attachVulkanOwnedTexture(descriptor: VulkanOwnedTextureDescriptor): RenderSessionHandle + public fun attachVulkanOwnedTexture( + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment public fun attachVulkanBorrowedTexture( - descriptor: VulkanBorrowedTextureDescriptor - ): RenderSessionHandle + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment - public fun attachOpenGLOwnedTexture(descriptor: OpenGLOwnedTextureDescriptor): RenderSessionHandle + public fun attachOpenGLOwnedTexture( + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD), + ): RenderSessionAttachment public fun attachOpenGLBorrowedTexture( - descriptor: OpenGLBorrowedTextureDescriptor - ): RenderSessionHandle + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD), + ): RenderSessionAttachment /** - * Attaches a Metal `CAMetalLayer` as this map's one live render session. + * Attaches a Metal `CAMetalLayer` render target. * * A null `context.device` is accepted. The session then uses `MTLCreateSystemDefaultDevice()`. */ - public fun attachMetalSurface(descriptor: MetalSurfaceDescriptor): RenderSessionHandle + public fun attachMetalSurface( + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment - public fun attachVulkanSurface(descriptor: VulkanSurfaceDescriptor): RenderSessionHandle + public fun attachVulkanSurface( + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions = RenderSessionAttachOptions(), + ): RenderSessionAttachment - public fun attachOpenGLSurface(descriptor: OpenGLSurfaceDescriptor): RenderSessionHandle + public fun attachOpenGLSurface( + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD), + ): RenderSessionAttachment - public fun createProjection(): MapProjectionHandle + public fun createProjection(): Deferred - /** - * Releases the native map on its owner thread. - * - * Closing discards this map's queued runtime events. Closing succeeds only once every child - * wrapper is released; a render session releases its retention when it is detached or closed. - */ - override fun close() + /** Reports native map retirement. Queued events keep this map's source ID. */ + public fun close(): Deferred public companion object { - public fun create(runtime: RuntimeHandle, options: MapOptions): MapHandle + /** Creates a map without blocking the caller's coroutine. */ + public fun create(runtime: RuntimeHandle, options: MapOptions): Deferred } } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt index a8c82bec2..eb4404312 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt @@ -5,21 +5,34 @@ import org.maplibre.nativeffi.camera.EdgeInsets import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.ScreenPoint -/** Any-thread standalone projection snapshot created from a map. */ -public expect class MapProjectionHandle : AutoCloseable { - public val camera: CameraOptions - +/** + * Owned standalone projection snapshot created from a map. + * + * Every call is synchronous, runs on the calling thread, is internally serialized, and may be made + * from any thread. A projection copies the map's transform state at creation and never observes map + * changes made after that and remains usable after its source map and runtime close. + */ +public expect class MapProjectionHandle { + /** Copies the projection camera, observing every earlier projection setter. */ + public fun camera(): CameraOptions + + /** Applies a camera update; only fields present on [camera] affect the projection. */ public fun setCamera(camera: CameraOptions) + /** Applies a camera fit for [coordinates]. */ public fun setVisibleCoordinates(coordinates: List, padding: EdgeInsets) + /** Applies a camera fit for GeoJSON Geometry bytes. */ public fun setVisibleGeometry(geometry: ByteArray, padding: EdgeInsets) + /** Converts a geographic coordinate to a logical-pixel screen point. */ public fun pixelForLatLng(coordinate: LatLng): ScreenPoint + /** Converts a logical-pixel screen point to a geographic coordinate. */ public fun latLngForPixel(point: ScreenPoint): LatLng public val isClosed: Boolean - override fun close() + /** Closes the projection after waiting for projection calls already running on other threads. */ + public fun close() } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapSnapshot.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapSnapshot.kt new file mode 100644 index 000000000..827bc9233 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/map/MapSnapshot.kt @@ -0,0 +1,28 @@ +package org.maplibre.nativeffi.map + +import org.maplibre.nativeffi.camera.BoundOptions +import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.FreeCameraOptions + +/** + * One immutable state generation published by the map worker. + * + * Every committed map command publishes a new generation in its completion, so a snapshot whose + * [generation] is at or past that value observes the commit. + */ +public data class MapSnapshot( + public val generation: Long, + public val debugOptions: Set, + public val camera: CameraOptions, + public val size: MapSize, + public val projectionMode: ProjectionModeOptions, + public val viewportOptions: ViewportOptions, + /** True once every requested style and tile resource finished loading. */ + public val isFullyLoaded: Boolean, + public val renderingStatsViewEnabled: Boolean, + public val repaintDemand: Boolean, + public val latestRenderUpdateGeneration: Long, + public val tileOptions: TileOptions, + public val bounds: BoundOptions, + public val freeCameraOptions: FreeCameraOptions, +) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt deleted file mode 100644 index 23f065f87..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.maplibre.nativeffi.render - -/** Explicit handle for a Metal session-owned texture frame. */ -public expect class MetalOwnedTextureFrameHandle : AutoCloseable { - public fun frame(): MetalOwnedTextureFrame - - public val isClosed: Boolean - - override fun close() -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextDescriptor.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextDescriptor.kt index b7ca11872..d0d254b90 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextDescriptor.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextDescriptor.kt @@ -3,8 +3,9 @@ package org.maplibre.nativeffi.render /** OpenGL platform context provider data for OpenGL render targets. */ public sealed interface OpenGLContextDescriptor { /** - * Whether the render session shares its thread with host graphics work. WGL and EGL surface - * targets support both. A texture target hands its texture to the host, so it is shared only. + * Whether the render session shares its driver thread and graphics objects with the host. A + * private EGL owned texture target uses dedicated ownership and grants readback without frame + * acquisition. */ public var ownership: OpenGLContextOwnership } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextOwnership.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextOwnership.kt index 2a665c652..a5031a758 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextOwnership.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLContextOwnership.kt @@ -3,15 +3,15 @@ package org.maplibre.nativeffi.render import kotlin.jvm.JvmInline /** - * How a render session's OpenGL context relates to the thread that attached it. + * How a render session's OpenGL context relates to its driver thread and host graphics state. * * A shared session leaves the thread as it found it: every render makes the session context current * and restores whatever was current before. The session context joins the share group named by the * context descriptor, so a host may hand the session a texture and sample it from its own context. * - * A dedicated session owns the thread. It makes its context current once and keeps it current - * between renders, and it joins no share group. Use this when a thread exists to drive one render - * session and runs no other graphics work, such as an Android host that renders into a SurfaceView. + * A dedicated session owns its driver thread's context. It keeps the context current between + * renders and joins no share group. The driver may be a native core worker or a dedicated host + * thread, such as an Android host that renders into a SurfaceView. * * This is an open domain: a value may have no named constant here, so a `when` over this type needs * an `else` branch. Unknown values keep their raw [nativeValue]. diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt deleted file mode 100644 index 50660cc11..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.maplibre.nativeffi.render - -/** Explicit handle for an OpenGL session-owned texture frame. */ -public expect class OpenGLOwnedTextureFrameHandle : AutoCloseable { - public fun frame(): OpenGLOwnedTextureFrame - - public val isClosed: Boolean - - override fun close() -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCore.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCore.kt index 3fb4b91e9..6d5a465fb 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCore.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCore.kt @@ -27,7 +27,7 @@ internal class OwnedTextureFrameHandleCore(private val typeName: String) { fun reportLeak(writeLine: (String) -> Unit = { message -> println(message) }) { if (!isClosed()) { writeLine( - "Leaked $typeName; close frame handles explicitly on the render session owner thread." + "Leaked $typeName; close frame handles explicitly on the render session graphics thread." ) } } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderDriverContract.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderDriverContract.kt new file mode 100644 index 000000000..e72013835 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderDriverContract.kt @@ -0,0 +1,146 @@ +package org.maplibre.nativeffi.render + +import kotlin.jvm.JvmInline +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.internal.status.Status + +/** Execution placement for a render session. Unknown native values remain representable. */ +@JvmInline +public value class RenderDriver public constructor(public val nativeValue: Int) { + public companion object { + public val CORE_WORKER: RenderDriver = RenderDriver(1) + public val CALLER_GRAPHICS_THREAD: RenderDriver = RenderDriver(2) + + internal fun fromNative(value: Int): RenderDriver = RenderDriver(value) + } +} + +/** Attachment policy for a render session. */ +public data class RenderSessionAttachOptions( + public val driver: RenderDriver = RenderDriver.CORE_WORKER, + public val requestedTextureRingDepth: Int = 0, +) { + init { + Status.requireArgument(requestedTextureRingDepth in 0..3) { + "requestedTextureRingDepth must be between zero and three" + } + } +} + +/** A session and the deferred result of its asynchronous attachment. */ +public data class RenderSessionAttachment( + public val session: RenderSessionHandle, + public val completed: Deferred, +) + +internal fun retainSessionUntilComplete( + session: RenderSessionHandle, + source: Deferred, +): Deferred { + val retained = CompletableDeferred() + source.invokeOnCompletion { failure -> + if (failure == null) retained.complete(Unit) else retained.completeExceptionally(failure) + session.hashCode() + } + return retained +} + +public data class RenderSessionCapabilities( + public val driver: RenderDriver, + public val textureRingDepth: Int, + public val frameAcquisition: Boolean, + public val readback: Boolean, + public val consumerSync: Boolean, + public val presentation: Boolean, +) + +@JvmInline +public value class RenderSessionState public constructor(public val nativeValue: Int) { + public companion object { + public val ATTACHING: RenderSessionState = RenderSessionState(1) + public val ATTACHED: RenderSessionState = RenderSessionState(2) + public val DETACHING: RenderSessionState = RenderSessionState(3) + public val DETACHED: RenderSessionState = RenderSessionState(4) + public val TARGET_LOST: RenderSessionState = RenderSessionState(5) + public val ABANDONED: RenderSessionState = RenderSessionState(6) + + internal fun fromNative(value: Int): RenderSessionState = RenderSessionState(value) + } +} + +public data class FrameDemand( + public val token: ULong = 0u, + public val coalescingBoundary: ULong = 0u, + public val timeoutNanoseconds: ULong = 0u, + public val ifNeeded: Boolean = true, + public val present: Boolean = false, +) + +public data class RenderFrameResult( + public val disposition: RenderResult, + public val token: ULong, + public val mapUpdateGeneration: ULong, + public val extentGeneration: ULong, + public val frameGeneration: ULong, + /** + * Whether the map asked for another frame while it rendered this one, as during an ongoing camera + * transition. Set only when [disposition] is [RenderResult.RENDERED], and false for every other + * outcome, so a host can re-arm its frame loop without the runtime event round trip. + */ + public val needsRepaint: Boolean, +) + +public data class RenderSessionSnapshot( + public val state: RenderSessionState, + public val driver: RenderDriver, + public val latestResult: RenderResult, + public val extent: RenderTargetExtent, + public val generation: ULong, + public val mapUpdateGeneration: ULong, + public val renderedUpdateGeneration: ULong, + public val extentGeneration: ULong, + public val frameGeneration: ULong, + public val latestDemandToken: ULong, + public val pendingDemandCount: Int, + public val acquiredFrameCount: Int, + public val targetReady: Boolean, + public val pendingChanges: Boolean, +) + +@JvmInline +public value class GpuSyncKind public constructor(public val nativeValue: Int) { + public companion object { + public val CPU_COMPLETE: GpuSyncKind = GpuSyncKind(0) + public val METAL_SHARED_EVENT: GpuSyncKind = GpuSyncKind(1) + public val VULKAN_TIMELINE_SEMAPHORE: GpuSyncKind = GpuSyncKind(2) + public val OPENGL_FENCE: GpuSyncKind = GpuSyncKind(3) + public val WEBGPU_TOKEN: GpuSyncKind = GpuSyncKind(4) + + internal fun fromNative(value: Int): GpuSyncKind = GpuSyncKind(value) + } +} + +/** Native synchronization payload. The object value follows the backend's C ownership rules. */ +public data class GpuSync( + public val kind: GpuSyncKind = GpuSyncKind.CPU_COMPLETE, + public val objectHandle: ULong = 0u, + public val value: ULong = 0u, +) + +@JvmInline +public value class RenderAbandonDisposition public constructor(public val nativeValue: Int) { + public companion object { + public val CLEAN: RenderAbandonDisposition = RenderAbandonDisposition(0) + public val QUARANTINED: RenderAbandonDisposition = RenderAbandonDisposition(1) + + internal fun fromNative(value: Int): RenderAbandonDisposition = RenderAbandonDisposition(value) + } +} + +public data class RenderAbandonResult( + public val disposition: RenderAbandonDisposition, + public val quarantinedResourceCount: Int, +) + +public data class TextureReadback(public val bytes: ByteArray, public val info: TextureImageInfo) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderResult.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderResult.kt index 3ef7b59b2..5bb2e5420 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderResult.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderResult.kt @@ -3,7 +3,7 @@ package org.maplibre.nativeffi.render import kotlin.jvm.JvmInline /** - * Outcome of a successful render-update call. + * Terminal disposition of an accepted frame demand. * * This is an open domain: a value may have no named constant here, so a `when` over this type needs * an `else` branch. Unknown values keep their raw [nativeValue]. @@ -29,6 +29,12 @@ public value class RenderResult(public val nativeValue: Int) { */ public val TARGET_NOT_READY: RenderResult = RenderResult(3) + /** A newer demand in the same coalescing boundary replaced this demand. */ + public val SUPERSEDED: RenderResult = RenderResult(4) + + /** The demand's timeout elapsed before rendering began. */ + public val DEADLINE_MISSED: RenderResult = RenderResult(5) + internal fun fromNative(nativeValue: UInt): RenderResult = fromNative(nativeValue.toInt()) internal fun fromNative(nativeValue: Int): RenderResult = RenderResult(nativeValue) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt index a954aa5b4..a6350473e 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt @@ -1,147 +1,107 @@ package org.maplibre.nativeffi.render +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.query.QueriedFeature import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.query.SourceFeatureQueryOptions -/** Owned render session handle. Platform actuals own the native render session carrier. */ +/** Owned render session control handle. Driver methods remain graphics-thread-affine. */ public expect class RenderSessionHandle : AutoCloseable { public val isClosed: Boolean public fun map(): MapHandle - /** - * Resizes this attached render session. - * - * Surface and owned-texture sessions resize in place. Borrowed texture targets are sized by their - * owner and throw `UnsupportedFeatureException`; hand over a new texture with the backend's - * set-target method, such as [setMetalBorrowedTextureTarget]. - * - * The session keeps its renderer across a resize, along with the tile pyramid, glyph and image - * atlases, and symbol placement. A scale factor change retires the renderer instead, because - * shaders are compiled for one pixel ratio. Map-owned feature state survives either way. - */ - public fun resize(width: Int, height: Int, scaleFactor: Double) + public fun capabilities(): RenderSessionCapabilities - /** - * Presents this attached surface session through a new surface, keeping this session's renderer - * and the state it holds. - * - * [descriptor] names the graphics context this session attached with, and its extent applies as - * [resize] applies one, including a scale factor change starting a new renderer. A descriptor - * whose `context.device` is neither null nor this session's device throws - * `InvalidArgumentException` and leaves this session rendering into the surface it has. - */ - public fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor) + public fun snapshot(): RenderSessionSnapshot - /** - * Presents this attached surface session through a new surface. - * - * See [setMetalSurfaceTarget] for what replacing a surface preserves. The outgoing `VkSurfaceKHR` - * must still be valid, since this session holds a swapchain built from it. A host that must - * release its surface first closes this session and attaches again instead. - * - * The replacement must report the color format and surface-transform support this session - * compiled its render pass and shaders for; one that does not throws - * `UnsupportedFeatureException`. - */ - public fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor) + public fun requestFrame(demand: FrameDemand = FrameDemand()) - /** - * Presents this attached surface session through a new surface. - * - * See [setMetalSurfaceTarget] for what replacing a surface preserves. The new surface is made - * current on the next render, so a host may hand over a replacement for one it has already - * destroyed, and an unusable surface is reported by the next [renderUpdate] rather than here. - */ - public fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor) + public fun drainFrameResults(): List - /** - * Renders this attached texture session into a new caller-owned texture, keeping this session's - * renderer and the state it holds. A scale factor change starts a new renderer, as in [resize]. - * - * The replacement must belong to the device this session attached with and carry the pixel format - * it attached with; another device throws `InvalidArgumentException` and another pixel format - * throws `UnsupportedFeatureException`, both leaving this session rendering into the texture it - * has. The caller owns the replacement and keeps it valid until the next replacement, [detach], - * or [close]. This session never retains or reads the outgoing texture. - */ - public fun setMetalBorrowedTextureTarget(descriptor: MetalBorrowedTextureDescriptor) + /** Services typed native work for a caller-graphics-thread driver. */ + public fun serviceDriverWork(maxWork: Int = 0): Int - /** - * Renders this attached texture session into a new caller-owned image. - * - * See [setMetalBorrowedTextureTarget] for what replacing a target preserves. The replacement - * carries the format and both layouts this session attached with, since its render pass was built - * around them. - */ - public fun setVulkanBorrowedTextureTarget(descriptor: VulkanBorrowedTextureDescriptor) + public fun acquireFrame(): AcquiredFrameHandle? - /** - * Renders this attached texture session into a new caller-owned texture. - * - * See [setMetalBorrowedTextureTarget] for what replacing a target preserves. The replacement - * belongs to the context this session attached with, or one in its share group, and that context - * must be current on this thread. - */ - public fun setOpenGLBorrowedTextureTarget(descriptor: OpenGLBorrowedTextureDescriptor) + public fun resize(extent: RenderTargetExtent): Deferred - /** - * Renders the latest available map render update into this session's render target. - * - * The map retains its latest update, so repeated calls re-render it and report - * [RenderResult.RENDERED] again. Every other result names the wake to wait for: - * [RenderResult.NO_UPDATE] and [RenderResult.SIZE_PENDING] resolve on a render-update-available - * event, and [RenderResult.TARGET_NOT_READY] resolves when the host changes the render target. - * - * [RenderUpdate.needsRepaint] reports whether the map asked for another frame while rendering - * this one, so a host can re-arm its frame loop before it drains events. - */ - public fun renderUpdate(): RenderUpdate + public fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor): Deferred + + public fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor): Deferred + + public fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor): Deferred - public fun detach() + public fun setMetalBorrowedTextureTarget( + descriptor: MetalBorrowedTextureDescriptor + ): Deferred - public fun reduceMemoryUse() + public fun setVulkanBorrowedTextureTarget( + descriptor: VulkanBorrowedTextureDescriptor + ): Deferred - public fun clearData() + public fun setOpenGLBorrowedTextureTarget( + descriptor: OpenGLBorrowedTextureDescriptor + ): Deferred - public fun dumpDebugLogs() + public fun reduceMemoryUse(): Deferred + + public fun clearData(): Deferred + + public fun dumpDebugLogs(): Deferred + + public fun barrier(): Deferred + + public fun detach(): Deferred public fun queryRenderedFeatures( geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions?, - ): List + ): Deferred> public fun querySourceFeatures( sourceId: String, options: SourceFeatureQueryOptions?, - ): List + ): Deferred> - /** - * Queries a feature extension from the latest render session state. - * - * [feature] contains one UTF-8 GeoJSON Feature. [arguments], when present, contains a UTF-8 JSON - * object. The result is either a JSON value or a GeoJSON FeatureCollection. - */ public fun queryFeatureExtension( sourceId: String, feature: ByteArray, extension: String, extensionField: String, arguments: ByteArray?, - ): ByteArray + ): Deferred - public fun textureImageInfo(): TextureImageInfo + public fun readPremultipliedRgba8(): Deferred - public fun readPremultipliedRgba8(buffer: NativeBuffer): TextureImageInfo + /** + * Irreversibly abandons a lost target without graphics calls. + * + * The call waits for the map's in-flight tile work before it returns, after which no library + * thread touches the target or its device, so the host may destroy them immediately. Calling it + * from a MapLibre callback is unsupported. + */ + public fun abandon(): RenderAbandonResult - public fun acquireMetalOwnedTextureFrame(): MetalOwnedTextureFrameHandle + /** Destroys detached or abandoned CPU-side session state. */ + override fun close() +} - public fun acquireVulkanOwnedTextureFrame(): VulkanOwnedTextureFrameHandle +/** An acquired texture-ring slot. Accessors become invalid after release or abandonment. */ +public expect class AcquiredFrameHandle { + public val isReleased: Boolean - public fun acquireOpenGLOwnedTextureFrame(): OpenGLOwnedTextureFrameHandle + public fun result(): RenderFrameResult - override fun close() + public fun producerSync(): GpuSync + + public fun metalTexture(): MetalOwnedTextureFrame + + public fun vulkanTexture(): VulkanOwnedTextureFrame + + public fun openGLTexture(): OpenGLOwnedTextureFrame + + public fun release(consumerCompletion: GpuSync = GpuSync()) } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderUpdate.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderUpdate.kt deleted file mode 100644 index 270badc8b..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/RenderUpdate.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.maplibre.nativeffi.render - -/** - * Outcome of a successful render-update call. - * - * [needsRepaint] is true only when [result] is [RenderResult.RENDERED] and the map asked for - * another frame while it rendered this one, as during an ongoing camera transition. It carries the - * same signal as the map-render-frame-finished event's needs-repaint field, without the event round - * trip. - */ -public data class RenderUpdate(public val result: RenderResult, public val needsRepaint: Boolean) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt deleted file mode 100644 index 10d4c0b92..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.maplibre.nativeffi.render - -/** Explicit handle for a Vulkan session-owned texture frame. */ -public expect class VulkanOwnedTextureFrameHandle : AutoCloseable { - public fun frame(): VulkanOwnedTextureFrame - - public val isClosed: Boolean - - override fun close() -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandCompletion.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandCompletion.kt new file mode 100644 index 000000000..2a32b1ee2 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandCompletion.kt @@ -0,0 +1,11 @@ +package org.maplibre.nativeffi.runtime + +import org.maplibre.nativeffi.error.MaplibreStatus + +/** Terminal metadata for one accepted ordered command. */ +public data class CommandCompletion( + public val disposition: CommandDisposition, + public val generation: ULong, + public val status: MaplibreStatus, + public val diagnostic: String, +) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandDisposition.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandDisposition.kt new file mode 100644 index 000000000..8066a7398 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/CommandDisposition.kt @@ -0,0 +1,19 @@ +package org.maplibre.nativeffi.runtime + +import kotlin.jvm.JvmInline + +/** Terminal disposition reported for an accepted runtime command. */ +@JvmInline +public value class CommandDisposition(public val nativeValue: Int) { + public companion object { + public val COMMITTED: CommandDisposition = CommandDisposition(0) + public val SUPERSEDED: CommandDisposition = CommandDisposition(1) + public val FAILED: CommandDisposition = CommandDisposition(2) + public val CANCELLED: CommandDisposition = CommandDisposition(3) + + internal fun fromNative(nativeValue: UInt): CommandDisposition = + CommandDisposition(nativeValue.toInt()) + + internal fun fromNative(nativeValue: Int): CommandDisposition = CommandDisposition(nativeValue) + } +} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt deleted file mode 100644 index 0206fc0d7..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.maplibre.nativeffi.runtime - -/** Owner-thread offline database operation that must be taken or discarded. */ -public expect class OfflineOperationHandle : AutoCloseable { - /** Native `uint64_t` operation id preserved as a [Long] bit pattern. */ - public val id: Long - - public val kind: OfflineOperationKind - - public val resultKind: OfflineOperationResultKind - - public val isClosed: Boolean - - override fun close() -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCore.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCore.kt deleted file mode 100644 index 4b4cbe7a1..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCore.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import org.maplibre.nativeffi.error.InvalidStateException -import org.maplibre.nativeffi.error.MaplibreStatus - -/** Platform-neutral state for one owner-thread offline operation. */ -@OptIn(ExperimentalAtomicApi::class) -internal class OfflineOperationHandleCore( - private val runtimeIdentity: Any, - val id: Long, - val kind: OfflineOperationKind, - val resultKind: OfflineOperationResultKind, - private val releaseRuntimeRetention: () -> Unit, -) { - val leakReport = OfflineOperationLeakReport(id, kind, resultKind) - private val closed = AtomicInt(0) - - init { - require(id != 0L) { "offline operation id must not be zero" } - } - - val isClosed: Boolean - get() = closed.load() != 0 - - fun requireLive(expectedRuntime: Any): Long { - if (isClosed) { - throw InvalidStateException( - MaplibreStatus.INVALID_STATE.nativeCode, - "OfflineOperationHandle is already closed", - ) - } - if (runtimeIdentity !== expectedRuntime) { - throw InvalidStateException( - MaplibreStatus.INVALID_STATE.nativeCode, - "OfflineOperationHandle belongs to a different RuntimeHandle", - ) - } - return id - } - - fun requireLive( - expectedRuntime: Any, - expectedKind: OfflineOperationKind, - expectedResultKind: OfflineOperationResultKind, - ): Long { - val operationId = requireLive(expectedRuntime) - if (kind != expectedKind || resultKind != expectedResultKind) { - throw InvalidStateException( - MaplibreStatus.INVALID_STATE.nativeCode, - "OfflineOperationHandle has kind $kind/$resultKind, " + - "expected $expectedKind/$expectedResultKind", - ) - } - return operationId - } - - /** Closes this operation, returning true for the call that closed it. */ - fun markConsumed(): Boolean { - if (!closed.compareAndSet(0, 1)) return false - leakReport.markClosed() - releaseRuntimeRetention() - return true - } -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationKind.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationKind.kt deleted file mode 100644 index 1be40edae..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationKind.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.jvm.JvmInline - -/** - * Offline operation kind reported by completion events. - * - * This is an open domain: a value may have no named constant here, so a `when` over this type needs - * an `else` branch. Unknown values keep their raw [nativeValue]. - */ -@JvmInline -public value class OfflineOperationKind(public val nativeValue: Int) { - public companion object { - public val AMBIENT_CACHE: OfflineOperationKind = OfflineOperationKind(1) - public val REGION_CREATE: OfflineOperationKind = OfflineOperationKind(2) - public val REGION_GET: OfflineOperationKind = OfflineOperationKind(3) - public val REGIONS_LIST: OfflineOperationKind = OfflineOperationKind(4) - public val REGIONS_MERGE_DATABASE: OfflineOperationKind = OfflineOperationKind(5) - public val REGION_UPDATE_METADATA: OfflineOperationKind = OfflineOperationKind(6) - public val REGION_GET_STATUS: OfflineOperationKind = OfflineOperationKind(7) - public val REGION_SET_OBSERVED: OfflineOperationKind = OfflineOperationKind(8) - public val REGION_SET_DOWNLOAD_STATE: OfflineOperationKind = OfflineOperationKind(9) - public val REGION_INVALIDATE: OfflineOperationKind = OfflineOperationKind(10) - public val REGION_DELETE: OfflineOperationKind = OfflineOperationKind(11) - public val SET_MAXIMUM_AMBIENT_CACHE_SIZE: OfflineOperationKind = OfflineOperationKind(12) - - internal fun fromNative(nativeValue: UInt): OfflineOperationKind = - fromNative(nativeValue.toInt()) - - internal fun fromNative(nativeValue: Int): OfflineOperationKind = - OfflineOperationKind(nativeValue) - } -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationLeakReport.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationLeakReport.kt deleted file mode 100644 index 196cfa11f..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationLeakReport.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.ExperimentalAtomicApi - -/** Report-only cleanup state for an owner-thread offline operation. */ -@OptIn(ExperimentalAtomicApi::class) -internal class OfflineOperationLeakReport( - private val id: Long, - private val kind: OfflineOperationKind, - private val resultKind: OfflineOperationResultKind, - private val writeLine: (String) -> Unit = { message -> println(message) }, -) { - private val closed = AtomicInt(0) - - fun markClosed() { - closed.store(1) - } - - fun report() { - if (closed.load() == 0) { - writeLine( - "Leaked OfflineOperationHandle id=$id kind=$kind resultKind=$resultKind; " + - "take or discard operations explicitly on the runtime owner thread." - ) - } - } -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationResultKind.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationResultKind.kt deleted file mode 100644 index 265a6a239..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationResultKind.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.jvm.JvmInline - -/** - * Offline operation result kind reported by completion events. - * - * This is an open domain: a value may have no named constant here, so a `when` over this type needs - * an `else` branch. Unknown values keep their raw [nativeValue]. - */ -@JvmInline -public value class OfflineOperationResultKind(public val nativeValue: Int) { - public companion object { - public val NONE: OfflineOperationResultKind = OfflineOperationResultKind(0) - public val REGION: OfflineOperationResultKind = OfflineOperationResultKind(1) - public val OPTIONAL_REGION: OfflineOperationResultKind = OfflineOperationResultKind(2) - public val REGION_LIST: OfflineOperationResultKind = OfflineOperationResultKind(3) - public val REGION_STATUS: OfflineOperationResultKind = OfflineOperationResultKind(4) - - internal fun fromNative(nativeValue: UInt): OfflineOperationResultKind = - fromNative(nativeValue.toInt()) - - internal fun fromNative(nativeValue: Int): OfflineOperationResultKind = - OfflineOperationResultKind(nativeValue) - } -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEvent.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEvent.kt index bf4d1b8f9..d3d8a472d 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEvent.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEvent.kt @@ -16,7 +16,7 @@ import org.maplibre.nativeffi.map.MapHandle * @property mapSource the map that raised the event, resolved through a weak reference to the * public wrapper and null once the host drops its own reference to that [MapHandle]. Read * [sourceType] to tell a map-originated event from a runtime-originated one. - * @property code the native status code carried by the event. + * @property code the secondary event detail carried by the event. * @property payload the typed payload, preserved as raw bytes for unknown domains. * @property message the copied diagnostic message. */ @@ -27,12 +27,11 @@ public data class RuntimeEvent( public val runtimeSource: RuntimeHandle?, public val mapSource: MapHandle?, /** - * Secondary event detail whose meaning [type] selects, 0 for types that carry no detail: + * Secondary event detail whose meaning [type] selects; types not listed here carry 0: * - [RuntimeEventType.MAP_CAMERA_WILL_CHANGE] and [RuntimeEventType.MAP_CAMERA_DID_CHANGE] carry * a [CameraChangeMode], read as `CameraChangeMode(event.code)`. * - [RuntimeEventType.MAP_LOADING_FAILED] carries the ordinal of an unnamed internal map load * error kind. - * - [RuntimeEventType.OFFLINE_OPERATION_COMPLETED] carries a native status value. */ public val code: Int, public val payload: RuntimeEventPayload, diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventBatch.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventBatch.kt index 083438f12..affd7d039 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventBatch.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventBatch.kt @@ -8,10 +8,5 @@ package org.maplibre.nativeffi.runtime * is closed. * * @property events the drained events in queue order. - * @property remainingCount events still queued after this batch, as an unsigned 64-bit count. A - * nonzero value means another drain reports more events, and an unbounded drain always reports 0. */ -public data class RuntimeEventBatch( - public val events: List, - public val remainingCount: Long, -) +public data class RuntimeEventBatch(public val events: List) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventMask.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventMask.kt index 9a34c02df..f63ff1997 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventMask.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventMask.kt @@ -7,8 +7,8 @@ import kotlin.jvm.JvmInline * * One bit selects one [RuntimeEventType]: the bit for a type is 1 shifted left by the type's * [RuntimeEventType.nativeValue], so [of] derives the bit for a type this version does not name. An - * event type that a mask leaves out is never built and never queued, so it neither reaches a batch - * nor wakes a parked [RuntimeHandle.pump]. + * event type that a mask leaves out is never built or queued, so it neither reaches a batch nor + * invokes the runtime's event wake. * * [org.maplibre.nativeffi.map.MapHandle.eventMask] reads the [ALL_MAP_EVENTS] bits and * [RuntimeHandle.eventMask] reads the [ALL_RUNTIME_EVENTS] bits, so both accept [ALL]. A bit @@ -71,8 +71,6 @@ public value class RuntimeEventMask(public val nativeValue: Long) { of(RuntimeEventType.OFFLINE_REGION_RESPONSE_ERROR) public val OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED: RuntimeEventMask = of(RuntimeEventType.OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED) - public val OFFLINE_OPERATION_COMPLETED: RuntimeEventMask = - of(RuntimeEventType.OFFLINE_OPERATION_COMPLETED) /** Selects every map-originated event type this version defines. */ public val ALL_MAP_EVENTS: RuntimeEventMask = @@ -100,8 +98,7 @@ public value class RuntimeEventMask(public val nativeValue: Long) { public val ALL_RUNTIME_EVENTS: RuntimeEventMask = OFFLINE_REGION_STATUS_CHANGED + OFFLINE_REGION_RESPONSE_ERROR + - OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED + - OFFLINE_OPERATION_COMPLETED + OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED /** Selects every event type this version defines. */ public val ALL: RuntimeEventMask = ALL_MAP_EVENTS + ALL_RUNTIME_EVENTS diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventPayload.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventPayload.kt index ae461d00b..f38aee0c5 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventPayload.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventPayload.kt @@ -37,15 +37,6 @@ public sealed interface RuntimeEventPayload { public data class OfflineRegionTileCountLimit(public val regionId: Long, public val limit: Long) : RuntimeEventPayload - public data class OfflineOperationCompleted( - /** Native `uint64_t` operation id preserved as a [Long] bit pattern. */ - public val operationId: Long, - public val operationKind: OfflineOperationKind, - public val resultKind: OfflineOperationResultKind, - public val resultStatus: Int, - public val found: Boolean, - ) : RuntimeEventPayload - /** * Reports that a camera transition released the camera. * diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventType.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventType.kt index ab435b266..104522e3f 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventType.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventType.kt @@ -50,14 +50,8 @@ public value class RuntimeEventType(public val nativeValue: Int) { public val OFFLINE_REGION_RESPONSE_ERROR: RuntimeEventType = RuntimeEventType(20) public val OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED: RuntimeEventType = RuntimeEventType(21) - /** - * [RuntimeEvent.code] carries the operation result as a native status value, the same value - * [RuntimeEventPayload.OfflineOperationCompleted.resultStatus] reports. - */ - public val OFFLINE_OPERATION_COMPLETED: RuntimeEventType = RuntimeEventType(22) - /** [RuntimeEvent.payload] carries [RuntimeEventPayload.CameraTransitionFinished]. */ - public val MAP_CAMERA_TRANSITION_FINISHED: RuntimeEventType = RuntimeEventType(23) + public val MAP_CAMERA_TRANSITION_FINISHED: RuntimeEventType = RuntimeEventType(22) internal fun fromNative(nativeValue: UInt): RuntimeEventType = fromNative(nativeValue.toInt()) diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt index 31a9687cd..02ffff6aa 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt @@ -1,5 +1,6 @@ package org.maplibre.nativeffi.runtime +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.offline.OfflineRegionDefinition import org.maplibre.nativeffi.offline.OfflineRegionDownloadState import org.maplibre.nativeffi.offline.OfflineRegionInfo @@ -9,126 +10,67 @@ import org.maplibre.nativeffi.resource.ResourceProviderCallback import org.maplibre.nativeffi.resource.ResourceTransformCallback /** Owned runtime handle. Platform actuals own the native runtime carrier. */ -public expect class RuntimeHandle : AutoCloseable { +public expect class RuntimeHandle { public val isClosed: Boolean - /** - * Advances this runtime: parks the owner thread when [timeoutMillis] allows it, then drains the - * owner-thread task queues. Drain the queued runtime events with [drainEvents] afterwards. - * - * [timeoutMillis] bounds the park. Zero drains and returns, a positive value parks for up to that - * many milliseconds, and a negative value parks until a wake arrives. A parking call returns as - * soon as the runtime's wake flag is set, and returns without parking while unread runtime events - * are queued. Timers and ready I/O set the flag only when they queue owner-thread work, so pass a - * bounded timeout to cap how long a call waits. - * - * [budgetMillis] bounds the drain. A negative value drains without a bound, and zero or a - * positive value stops the drain at the first task boundary after that many milliseconds. The - * first queued task always runs, so a bounded pump always makes progress, and tasks left behind - * set the wake flag so the next pump returns without parking and continues them. - * - * A non-zero timeout blocks the calling thread and ignores interruption. Call it outside any lock - * that a thread signalling a [WakeSource] takes. - */ - public fun pump(timeoutMillis: Long, budgetMillis: Long = -1) + /** Completes after every command accepted before this call reaches a terminal disposition. */ + public fun barrier(): Deferred - /** - * Acquires a [WakeSource] that releases this runtime's parked owner thread. The returned source - * is usable from any thread, and the caller closes it. - */ - public fun acquireWakeSource(): WakeSource - - public fun startAmbientCacheOperation( - operation: AmbientCacheOperation - ): OfflineOperationHandle + public fun runAmbientCacheOperation(operation: AmbientCacheOperation): Deferred /** * Starts a change to this runtime's maximum ambient cache size. Lowering it evicts cached ambient * resources; offline regions are unaffected. */ - public fun startSetMaximumAmbientCacheSize(size: Long): OfflineOperationHandle + public fun setMaximumAmbientCacheSize(size: Long): Deferred - public fun startCreateOfflineRegion( + public fun createOfflineRegion( definition: OfflineRegionDefinition, metadata: ByteArray, - ): OfflineOperationHandle + ): Deferred - public fun startOfflineRegion(id: Long): OfflineOperationHandle + public fun offlineRegion(id: Long): Deferred - public fun startOfflineRegions(): OfflineOperationHandle> + public fun offlineRegions(): Deferred> - public fun startMergeOfflineRegionsDatabase( - path: String - ): OfflineOperationHandle> + public fun mergeOfflineRegionsDatabase(path: String): Deferred> - public fun startUpdateOfflineRegionMetadata( - id: Long, - metadata: ByteArray, - ): OfflineOperationHandle + public fun updateOfflineRegionMetadata(id: Long, metadata: ByteArray): Deferred - public fun startOfflineRegionStatus(id: Long): OfflineOperationHandle + public fun offlineRegionStatus(id: Long): Deferred - public fun startSetOfflineRegionObserved( - id: Long, - observed: Boolean, - ): OfflineOperationHandle + public fun setOfflineRegionObserved(id: Long, observed: Boolean): Deferred - public fun startSetOfflineRegionDownloadState( + public fun setOfflineRegionDownloadState( id: Long, downloadState: OfflineRegionDownloadState, - ): OfflineOperationHandle + ): Deferred - public fun startInvalidateOfflineRegion(id: Long): OfflineOperationHandle + public fun invalidateOfflineRegion(id: Long): Deferred - public fun startDeleteOfflineRegion(id: Long): OfflineOperationHandle + public fun deleteOfflineRegion(id: Long): Deferred - public fun takeCreateOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo + public fun setResourceProvider(callback: ResourceProviderCallback): Deferred - public fun takeOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo? + public fun clearResourceProvider(): Deferred - public fun takeOfflineRegionsResult( - operation: OfflineOperationHandle> - ): List + public fun setResourceTransform(callback: ResourceTransformCallback): Deferred - public fun takeMergeOfflineRegionsDatabaseResult( - operation: OfflineOperationHandle> - ): List + public fun clearResourceTransform(): Deferred - public fun takeUpdateOfflineRegionMetadataResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo + public fun setHttpHeaderTransform( + callback: HttpHeaderTransformCallback + ): Deferred - public fun takeOfflineRegionStatusResult( - operation: OfflineOperationHandle - ): OfflineRegionStatus - - public fun setResourceProvider(callback: ResourceProviderCallback) - - public fun clearResourceProvider() - - public fun setResourceTransform(callback: ResourceTransformCallback) - - public fun clearResourceTransform() - - public fun setHttpHeaderTransform(callback: HttpHeaderTransformCallback) - - public fun clearHttpHeaderTransform() + public fun clearHttpHeaderTransform(): Deferred /** * Drains this runtime's queued events into one batch, in queue order. * - * [maxEvents] bounds the drain. Zero drains every queued event; a positive value drains at most - * that many events and reports the rest in [RuntimeEventBatch.remainingCount]. A negative value - * throws [org.maplibre.nativeffi.error.InvalidArgumentException]. - * - * The two subscription masks decide which events reach the queue. Draining is a queue operation - * that runs no owner-thread work, so call [pump] first and drain what the pump produced. + * The two subscription masks decide which events reach the queue. Draining copies events that the + * runtime worker has already published. */ - public fun drainEvents(maxEvents: Int = 0): RuntimeEventBatch + public fun drainEvents(): RuntimeEventBatch /** * Runtime-originated event types that this runtime queues, [RuntimeEventMask.ALL] until a host @@ -140,9 +82,19 @@ public expect class RuntimeHandle : AutoCloseable { */ public var eventMask: RuntimeEventMask - override fun close() + /** + * Releases this runtime and its callback state, and reports the end of native teardown. + * + * The call consumes the handle before it returns, and it throws when this runtime still has a + * live or pending child. The returned [Deferred] completes after every earlier accepted + * submission, including released maps' teardown, has finished and the runtime's threads and + * resources are gone, so a host that awaits it may exit the process without racing native + * teardown. Closing an already closed runtime returns that same [Deferred]. + */ + public fun close(): Deferred public companion object { + /** Creates a runtime. */ public fun create(options: RuntimeOptions): RuntimeHandle } } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/SuspendUse.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/SuspendUse.kt new file mode 100644 index 000000000..15fc4324d --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/SuspendUse.kt @@ -0,0 +1,21 @@ +package org.maplibre.nativeffi.runtime + +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import org.maplibre.nativeffi.map.MapHandle + +/** Runs [block] and suspends until this runtime's native teardown has finished. */ +public suspend inline fun RuntimeHandle.use(block: suspend (RuntimeHandle) -> T): T = + try { + block(this) + } finally { + withContext(NonCancellable) { close().await() } + } + +/** Runs [block] and suspends until this map has closed. */ +public suspend inline fun MapHandle.use(block: suspend (MapHandle) -> T): T = + try { + block(this) + } finally { + close() + } diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt deleted file mode 100644 index 1b43f3560..000000000 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.maplibre.nativeffi.runtime - -/** - * Releases a runtime owner thread parked in [RuntimeHandle.pump]. A wake source is usable from any - * thread and stays usable after its runtime closes. - */ -public expect class WakeSource : AutoCloseable { - public val isClosed: Boolean - - /** - * Sets the runtime's wake flag and releases the parked owner thread. A signal raised while the - * owner thread runs makes the next [RuntimeHandle.pump] return without parking. Signalling after - * the runtime closes succeeds and does nothing. - */ - public fun signal() - - override fun close() -} diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt index cdb1cf317..0d0821d3f 100644 --- a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt @@ -10,11 +10,11 @@ package org.maplibre.nativeffi.style * [org.maplibre.nativeffi.map.MapHandle.setGeoJsonSourceData] rejects data whose baked-in options * differ from the source's. * - * Creation touches no runtime or map and is callable from any thread, so a host prepares data on a - * worker thread or coroutine dispatcher and installs it on the map owner thread. The prepared data - * is immutable and safe to share across threads. Install calls borrow the handle, so one prepared - * value may be installed on any number of sources and closed at any time afterward; closing never - * invalidates a source the data was installed on. + * Creation touches no runtime or map and is callable from any thread, so a host can prepare data on + * a worker thread or coroutine dispatcher while map work continues. The prepared data is immutable + * and safe to share across threads. Install calls borrow the handle, so one prepared value may be + * installed on any number of sources and closed at any time afterward; closing never invalidates a + * source the data was installed on. */ public expect class GeoJsonSourceDataHandle : AutoCloseable { public val isClosed: Boolean diff --git a/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/LayerInfo.kt b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/LayerInfo.kt new file mode 100644 index 000000000..a75c615a4 --- /dev/null +++ b/bindings/kotlin/src/commonMain/kotlin/org/maplibre/nativeffi/style/LayerInfo.kt @@ -0,0 +1,19 @@ +package org.maplibre.nativeffi.style + +/** + * Copied metadata for one style layer. + * + * [sourceId] and [sourceLayer] are present when the layer carries them. The value remains valid + * after its layer or map closes. + */ +public data class LayerInfo( + /** Style-spec layer type string, for example `"fill"` or `"background"`. */ + public val type: String, + /** Lowest zoom at which the layer draws; negative infinity with no lower bound. */ + public val minZoom: Double, + /** Highest zoom at which the layer draws; positive infinity with no upper bound. */ + public val maxZoom: Double, + public val visibility: StyleLayerVisibility, + public val sourceId: String?, + public val sourceLayer: String?, +) diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/BackgroundThread.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/BackgroundThread.kt index db3362012..1f89f5e86 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/BackgroundThread.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/BackgroundThread.kt @@ -1,17 +1,7 @@ package org.maplibre.nativeffi -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi - /** Runs [block] on a second native thread and waits for it to finish. */ internal expect fun runOnBackgroundThread(block: () -> Unit) /** Parks the current thread for [millis] milliseconds. */ internal expect fun sleepMillis(millis: Int) - -@OptIn(ExperimentalAtomicApi::class) -internal fun failureFromBackgroundThread(block: () -> Unit): Throwable { - val result = AtomicReference(null) - runOnBackgroundThread { result.store(runCatching(block).exceptionOrNull()) } - return result.load() ?: error("background-thread operation succeeded") -} diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/MaplibreConformanceTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/MaplibreConformanceTest.kt index f0bfac81a..bcb4c6128 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/MaplibreConformanceTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/MaplibreConformanceTest.kt @@ -9,28 +9,30 @@ import org.maplibre.nativeffi.runtime.NetworkStatus class MaplibreConformanceTest { @Test - fun globalOperationsReachTheNativeLibrary() { - Maplibre.loadNativeLibrary() - assertEquals(Maplibre.EXPECTED_C_ABI_VERSION, Maplibre.cVersion()) - Maplibre.supportedRenderBackends() - Maplibre.supportedOpenGLContextProviders() + fun globalOperationsReachTheNativeLibrary(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + Maplibre.loadNativeLibrary() + assertEquals(Maplibre.EXPECTED_C_ABI_VERSION, Maplibre.cVersion()) + Maplibre.supportedRenderBackends() + Maplibre.supportedOpenGLContextProviders() - val original = Maplibre.networkStatus - try { - Maplibre.setNetworkStatus(NetworkStatus.OFFLINE) - assertEquals(NetworkStatus.OFFLINE, Maplibre.networkStatus) - Maplibre.setNetworkStatus(NetworkStatus.ONLINE) - assertEquals(NetworkStatus.ONLINE, Maplibre.networkStatus) - } finally { - Maplibre.setNetworkStatus(original) - } + val original = Maplibre.networkStatus + try { + Maplibre.setNetworkStatus(NetworkStatus.OFFLINE) + assertEquals(NetworkStatus.OFFLINE, Maplibre.networkStatus) + Maplibre.setNetworkStatus(NetworkStatus.ONLINE) + assertEquals(NetworkStatus.ONLINE, Maplibre.networkStatus) + } finally { + Maplibre.setNetworkStatus(original) + } - val meters = Maplibre.projectedMetersForLatLng(LatLng(0.0, 0.0)) - assertEquals(LatLng(0.0, 0.0), Maplibre.latLngForProjectedMeters(meters)) - } + val meters = Maplibre.projectedMetersForLatLng(LatLng(0.0, 0.0)) + assertEquals(LatLng(0.0, 0.0), Maplibre.latLngForProjectedMeters(meters)) + } @Test - fun networkStatusRejectsAnUnknownInputBeforeNativeCall() { - assertFailsWith { Maplibre.setNetworkStatus(NetworkStatus(999)) } - } + fun networkStatusRejectsAnUnknownInputBeforeNativeCall(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertFailsWith { Maplibre.setNetworkStatus(NetworkStatus(999)) } + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/OptionsValueSemanticsTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/OptionsValueSemanticsTest.kt index c0c8827ac..827f8174b 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/OptionsValueSemanticsTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/OptionsValueSemanticsTest.kt @@ -68,462 +68,488 @@ class OptionsValueSemanticsTest { } @Test - fun cameraOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - CameraOptions().apply { - center = LatLng(1.0, 2.0) - centerAltitude = 3.0 - padding = EdgeInsets(4.0, 5.0, 6.0, 7.0) - anchor = ScreenPoint(8.0, 9.0) - zoom = 10.0 - bearing = 11.0 - pitch = 12.0 - roll = 13.0 - fieldOfView = 14.0 - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { center = LatLng(90.0, 90.0) }, - { centerAltitude = 300.0 }, - { padding = EdgeInsets.ZERO }, - { anchor = ScreenPoint(80.0, 90.0) }, - { zoom = 100.0 }, - { bearing = 110.0 }, - { pitch = 120.0 }, - { roll = 130.0 }, - { fieldOfView = 140.0 }, - ), - ) - } + fun cameraOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + CameraOptions().apply { + center = LatLng(1.0, 2.0) + centerAltitude = 3.0 + padding = EdgeInsets(4.0, 5.0, 6.0, 7.0) + anchor = ScreenPoint(8.0, 9.0) + zoom = 10.0 + bearing = 11.0 + pitch = 12.0 + roll = 13.0 + fieldOfView = 14.0 + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { center = LatLng(90.0, 90.0) }, + { centerAltitude = 300.0 }, + { padding = EdgeInsets.ZERO }, + { anchor = ScreenPoint(80.0, 90.0) }, + { zoom = 100.0 }, + { bearing = 110.0 }, + { pitch = 120.0 }, + { roll = 130.0 }, + { fieldOfView = 140.0 }, + ), + ) + } @Test - fun animationOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - AnimationOptions().apply { - durationMs = 1.0 - velocity = 2.0 - minZoom = 3.0 - easing = UnitBezier(0.1, 0.2, 0.3, 0.4) - transitionId = 4L - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { durationMs = 10.0 }, - { velocity = 20.0 }, - { minZoom = 30.0 }, - { easing = UnitBezier(0.9, 0.8, 0.7, 0.6) }, - { transitionId = 40L }, - ), - ) - } + fun animationOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + AnimationOptions().apply { + durationMs = 1.0 + velocity = 2.0 + minZoom = 3.0 + easing = UnitBezier(0.1, 0.2, 0.3, 0.4) + transitionId = 4L + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { durationMs = 10.0 }, + { velocity = 20.0 }, + { minZoom = 30.0 }, + { easing = UnitBezier(0.9, 0.8, 0.7, 0.6) }, + { transitionId = 40L }, + ), + ) + } @Test - fun boundOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - BoundOptions().apply { - bounds = BoundsConstraint.Bounded(LatLngBounds(LatLng(0.0, 0.0), LatLng(1.0, 1.0))) - minZoom = 2.0 - maxZoom = 3.0 - minPitch = 4.0 - maxPitch = 5.0 - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { bounds = BoundsConstraint.Bounded(LatLngBounds(LatLng(-1.0, -1.0), LatLng(2.0, 2.0))) }, - { bounds = BoundsConstraint.Unbounded }, - { minZoom = 20.0 }, - { maxZoom = 30.0 }, - { minPitch = 40.0 }, - { maxPitch = 50.0 }, - ), - ) - } + fun boundOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + BoundOptions().apply { + bounds = BoundsConstraint.Bounded(LatLngBounds(LatLng(0.0, 0.0), LatLng(1.0, 1.0))) + minZoom = 2.0 + maxZoom = 3.0 + minPitch = 4.0 + maxPitch = 5.0 + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { + bounds = BoundsConstraint.Bounded(LatLngBounds(LatLng(-1.0, -1.0), LatLng(2.0, 2.0))) + }, + { bounds = BoundsConstraint.Unbounded }, + { minZoom = 20.0 }, + { maxZoom = 30.0 }, + { minPitch = 40.0 }, + { maxPitch = 50.0 }, + ), + ) + } @Test - fun cameraFitOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - CameraFitOptions().apply { - padding = EdgeInsets(1.0, 2.0, 3.0, 4.0) - bearing = 5.0 - pitch = 6.0 - } - }, - copyOf = { it.copy() }, - mutators = listOf({ padding = EdgeInsets.ZERO }, { bearing = 50.0 }, { pitch = 60.0 }), - ) - } + fun cameraFitOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + CameraFitOptions().apply { + padding = EdgeInsets(1.0, 2.0, 3.0, 4.0) + bearing = 5.0 + pitch = 6.0 + } + }, + copyOf = { it.copy() }, + mutators = listOf({ padding = EdgeInsets.ZERO }, { bearing = 50.0 }, { pitch = 60.0 }), + ) + } @Test - fun freeCameraOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - FreeCameraOptions().apply { - position = Vec3(1.0, 2.0, 3.0) - orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - } - }, - copyOf = { it.copy() }, - mutators = - listOf({ position = Vec3(9.0, 9.0, 9.0) }, { orientation = Quaternion(1.0, 0.0, 0.0, 0.0) }), - ) - } + fun freeCameraOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + FreeCameraOptions().apply { + position = Vec3(1.0, 2.0, 3.0) + orientation = Quaternion(0.0, 0.0, 0.0, 1.0) + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { position = Vec3(9.0, 9.0, 9.0) }, + { orientation = Quaternion(1.0, 0.0, 0.0, 0.0) }, + ), + ) + } @Test - fun viewportOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - ViewportOptions().apply { - northOrientation = NorthOrientation(0) - constrainMode = ConstrainMode(0) - viewportMode = ViewportMode(0) - frustumOffset = EdgeInsets(1.0, 2.0, 3.0, 4.0) - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { northOrientation = NorthOrientation(2) }, - { constrainMode = ConstrainMode(1) }, - { viewportMode = ViewportMode(1) }, - { frustumOffset = EdgeInsets.ZERO }, - ), - ) - } + fun viewportOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + ViewportOptions().apply { + northOrientation = NorthOrientation(0) + constrainMode = ConstrainMode(0) + viewportMode = ViewportMode(0) + frustumOffset = EdgeInsets(1.0, 2.0, 3.0, 4.0) + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { northOrientation = NorthOrientation(2) }, + { constrainMode = ConstrainMode(1) }, + { viewportMode = ViewportMode(1) }, + { frustumOffset = EdgeInsets.ZERO }, + ), + ) + } @Test - fun tileOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - TileOptions().apply { - prefetchZoomDelta = 1 - lodMinRadius = 2.0 - lodScale = 3.0 - lodPitchThreshold = 4.0 - lodZoomShift = 5.0 - lodMode = TileLodMode(0) - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { prefetchZoomDelta = 7 }, - { lodMinRadius = 20.0 }, - { lodScale = 30.0 }, - { lodPitchThreshold = 40.0 }, - { lodZoomShift = 50.0 }, - { lodMode = TileLodMode(1) }, - ), - ) - } + fun tileOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + TileOptions().apply { + prefetchZoomDelta = 1 + lodMinRadius = 2.0 + lodScale = 3.0 + lodPitchThreshold = 4.0 + lodZoomShift = 5.0 + lodMode = TileLodMode(0) + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { prefetchZoomDelta = 7 }, + { lodMinRadius = 20.0 }, + { lodScale = 30.0 }, + { lodPitchThreshold = 40.0 }, + { lodZoomShift = 50.0 }, + { lodMode = TileLodMode(1) }, + ), + ) + } @Test - fun projectionModeOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - ProjectionModeOptions().apply { - axonometric = true - xSkew = 1.0 - ySkew = 2.0 - } - }, - copyOf = { it.copy() }, - mutators = listOf({ axonometric = false }, { xSkew = 10.0 }, { ySkew = 20.0 }), - ) - } + fun projectionModeOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + ProjectionModeOptions().apply { + axonometric = true + xSkew = 1.0 + ySkew = 2.0 + } + }, + copyOf = { it.copy() }, + mutators = listOf({ axonometric = false }, { xSkew = 10.0 }, { ySkew = 20.0 }), + ) + } @Test - fun mapOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - MapOptions().apply { - width = 100 - height = 200 - scaleFactor = 2.0 - mapMode = MapMode(0) - fastPforEnabled = false - eventMask = RuntimeEventMask.ALL - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { width = 300 }, - { height = 400 }, - { scaleFactor = 3.0 }, - { mapMode = MapMode(1) }, - { fastPforEnabled = true }, - { eventMask = RuntimeEventMask.ALL_MAP_EVENTS }, - ), - ) - } + fun mapOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + MapOptions().apply { + width = 100 + height = 200 + scaleFactor = 2.0 + mapMode = MapMode(0) + fastPforEnabled = false + eventMask = RuntimeEventMask.ALL + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { width = 300 }, + { height = 400 }, + { scaleFactor = 3.0 }, + { mapMode = MapMode(1) }, + { fastPforEnabled = true }, + { eventMask = RuntimeEventMask.ALL_MAP_EVENTS }, + ), + ) + } @Test - fun runtimeOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - RuntimeOptions().apply { - assetPath = "assets" - cachePath = "cache" - eventMask = RuntimeEventMask.ALL - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { assetPath = "other-assets" }, - { cachePath = "other-cache" }, - { eventMask = RuntimeEventMask.NONE }, - ), - ) - } + fun runtimeOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + RuntimeOptions().apply { + assetPath = "assets" + cachePath = "cache" + eventMask = RuntimeEventMask.ALL + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { assetPath = "other-assets" }, + { cachePath = "other-cache" }, + { eventMask = RuntimeEventMask.NONE }, + ), + ) + } @Test - fun tileSourceOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - TileSourceOptions().apply { - minZoom = 1.0 - maxZoom = 2.0 - attribution = "attribution" - scheme = TileScheme.XYZ - bounds = LatLngBounds(LatLng(0.0, 0.0), LatLng(1.0, 1.0)) - tileSize = 256 - vectorEncoding = VectorTileEncoding.MVT - rasterDemEncoding = RasterDemEncoding.MAPBOX - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { minZoom = 10.0 }, - { maxZoom = 20.0 }, - { attribution = "other" }, - { scheme = TileScheme.TMS }, - { bounds = LatLngBounds(LatLng(-1.0, -1.0), LatLng(2.0, 2.0)) }, - { tileSize = 512 }, - { vectorEncoding = VectorTileEncoding.MLT }, - { rasterDemEncoding = RasterDemEncoding.TERRARIUM }, - ), - ) - } + fun tileSourceOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + TileSourceOptions().apply { + minZoom = 1.0 + maxZoom = 2.0 + attribution = "attribution" + scheme = TileScheme.XYZ + bounds = LatLngBounds(LatLng(0.0, 0.0), LatLng(1.0, 1.0)) + tileSize = 256 + vectorEncoding = VectorTileEncoding.MVT + rasterDemEncoding = RasterDemEncoding.MAPBOX + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { minZoom = 10.0 }, + { maxZoom = 20.0 }, + { attribution = "other" }, + { scheme = TileScheme.TMS }, + { bounds = LatLngBounds(LatLng(-1.0, -1.0), LatLng(2.0, 2.0)) }, + { tileSize = 512 }, + { vectorEncoding = VectorTileEncoding.MLT }, + { rasterDemEncoding = RasterDemEncoding.TERRARIUM }, + ), + ) + } @Test - fun geoJsonSourceOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - GeoJsonSourceOptions().apply { - minZoom = 1.0 - maxZoom = 2.0 - tolerance = 0.5 - clusterMaxZoom = 14.0 - clusterProperties = clusterProperties("sum") - tileSize = 256 - buffer = 64 - clusterRadius = 40 - clusterMinPoints = 3 - lineMetrics = true - cluster = true - synchronousTiling = true - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { minZoom = 10.0 }, - { maxZoom = 20.0 }, - { tolerance = 0.25 }, - { clusterMaxZoom = 12.0 }, - { clusterProperties = clusterProperties("max") }, - { tileSize = 512 }, - { buffer = 128 }, - { clusterRadius = 50 }, - { clusterMinPoints = 2 }, - { lineMetrics = false }, - { cluster = false }, - { synchronousTiling = false }, - ), - ) - } + fun geoJsonSourceOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + GeoJsonSourceOptions().apply { + minZoom = 1.0 + maxZoom = 2.0 + tolerance = 0.5 + clusterMaxZoom = 14.0 + clusterProperties = clusterProperties("sum") + tileSize = 256 + buffer = 64 + clusterRadius = 40 + clusterMinPoints = 3 + lineMetrics = true + cluster = true + synchronousTiling = true + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { minZoom = 10.0 }, + { maxZoom = 20.0 }, + { tolerance = 0.25 }, + { clusterMaxZoom = 12.0 }, + { clusterProperties = clusterProperties("max") }, + { tileSize = 512 }, + { buffer = 128 }, + { clusterRadius = 50 }, + { clusterMinPoints = 2 }, + { lineMetrics = false }, + { cluster = false }, + { synchronousTiling = false }, + ), + ) + } /** Builds a `clusterProperties` object whose aggregation operator is [operator]. */ private fun clusterProperties(operator: String): ByteArray = "{\"weight\":[\"$operator\",[\"get\",\"weight\"]]}".encodeToByteArray() @Test - fun styleImageOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - StyleImageOptions().apply { - pixelRatio = 2.0f - sdf = true - } - }, - copyOf = { it.copy() }, - mutators = listOf({ pixelRatio = 3.0f }, { sdf = false }), - ) - } + fun styleImageOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + StyleImageOptions().apply { + pixelRatio = 2.0f + sdf = true + } + }, + copyOf = { it.copy() }, + mutators = listOf({ pixelRatio = 3.0f }, { sdf = false }), + ) + } @Test - fun styleTransitionOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - StyleTransitionOptions().apply { - durationMs = 300.0 - delayMs = 0.0 - enablePlacementTransitions = false - } - }, - copyOf = { it.copy() }, - mutators = - listOf( - { durationMs = 500.0 }, - // A present zero stays distinguishable from an absent field. - { delayMs = null }, - // A present false stays distinguishable from an absent field. - { enablePlacementTransitions = null }, - ), - ) - } + fun styleTransitionOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + StyleTransitionOptions().apply { + durationMs = 300.0 + delayMs = 0.0 + enablePlacementTransitions = false + } + }, + copyOf = { it.copy() }, + mutators = + listOf( + { durationMs = 500.0 }, + // A present zero stays distinguishable from an absent field. + { delayMs = null }, + // A present false stays distinguishable from an absent field. + { enablePlacementTransitions = null }, + ), + ) + } @Test - fun renderedFeatureQueryOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - RenderedFeatureQueryOptions().apply { - layerIds = listOf("a", "b") - filter = "true".encodeToByteArray() - } - }, - copyOf = { it.copy() }, - mutators = listOf({ layerIds = listOf("a") }, { filter = "\"filter\"".encodeToByteArray() }), - ) - } + fun renderedFeatureQueryOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + RenderedFeatureQueryOptions().apply { + layerIds = listOf("a", "b") + filter = "true".encodeToByteArray() + } + }, + copyOf = { it.copy() }, + mutators = listOf({ layerIds = listOf("a") }, { filter = "\"filter\"".encodeToByteArray() }), + ) + } @Test - fun sourceFeatureQueryOptionsComparesByFieldValue() { - assertValueSemantics( - baseline = { - SourceFeatureQueryOptions().apply { - sourceLayerIds = listOf("a", "b") - filter = "true".encodeToByteArray() - } - }, - copyOf = { it.copy() }, - mutators = - listOf({ sourceLayerIds = listOf("a") }, { filter = "\"filter\"".encodeToByteArray() }), - ) - } + fun sourceFeatureQueryOptionsComparesByFieldValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertValueSemantics( + baseline = { + SourceFeatureQueryOptions().apply { + sourceLayerIds = listOf("a", "b") + filter = "true".encodeToByteArray() + } + }, + copyOf = { it.copy() }, + mutators = + listOf({ sourceLayerIds = listOf("a") }, { filter = "\"filter\"".encodeToByteArray() }), + ) + } @Test - fun absentLayerIdsDifferFromEmptyLayerIds() { - // The native field mask distinguishes an absent layer filter from an empty one. - val absent = RenderedFeatureQueryOptions() - val empty = RenderedFeatureQueryOptions().apply { layerIds = emptyList() } - - assertNotEquals(absent, empty) - } + fun absentLayerIdsDifferFromEmptyLayerIds(): Unit = + org.maplibre.nativeffi.runtime + .runSuspendTest { // The native field mask distinguishes an absent layer filter from an empty + // one. + val absent = RenderedFeatureQueryOptions() + val empty = RenderedFeatureQueryOptions().apply { layerIds = emptyList() } + + assertNotEquals(absent, empty) + } @Test - fun absentJsonBytesDifferFromEmptyJsonBytes() { - assertNotEquals( - RenderedFeatureQueryOptions(), - RenderedFeatureQueryOptions().apply { filter = byteArrayOf() }, - ) - assertNotEquals( - SourceFeatureQueryOptions(), - SourceFeatureQueryOptions().apply { filter = byteArrayOf() }, - ) - assertNotEquals( - GeoJsonSourceOptions(), - GeoJsonSourceOptions().apply { clusterProperties = byteArrayOf() }, - ) - } + fun absentJsonBytesDifferFromEmptyJsonBytes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertNotEquals( + RenderedFeatureQueryOptions(), + RenderedFeatureQueryOptions().apply { filter = byteArrayOf() }, + ) + assertNotEquals( + SourceFeatureQueryOptions(), + SourceFeatureQueryOptions().apply { filter = byteArrayOf() }, + ) + assertNotEquals( + GeoJsonSourceOptions(), + GeoJsonSourceOptions().apply { clusterProperties = byteArrayOf() }, + ) + } @Test - fun copyBlockAppliesToTheCopyOnly() { - val original = CameraOptions().apply { zoom = 1.0 } - val derived = original.copy { zoom = 2.0 } + fun copyBlockAppliesToTheCopyOnly(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val original = CameraOptions().apply { zoom = 1.0 } + val derived = original.copy { zoom = 2.0 } - assertEquals(1.0, original.zoom) - assertEquals(2.0, derived.zoom) - } + assertEquals(1.0, original.zoom) + assertEquals(2.0, derived.zoom) + } @Test - fun queryOptionsSnapshotCallerOwnedLayerIds() { - // BND-069. - val layerIds = mutableListOf("a") - val options = RenderedFeatureQueryOptions().apply { this.layerIds = layerIds } - val copy = options.copy() + fun queryOptionsSnapshotCallerOwnedLayerIds(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { // BND-069. + val layerIds = mutableListOf("a") + val options = RenderedFeatureQueryOptions().apply { this.layerIds = layerIds } + val copy = options.copy() - layerIds.add("b") + layerIds.add("b") - assertEquals(listOf("a"), options.layerIds) - assertEquals(listOf("a"), copy.layerIds) + assertEquals(listOf("a"), options.layerIds) + assertEquals(listOf("a"), copy.layerIds) - val sourceLayerIds = mutableListOf("a") - val sourceOptions = SourceFeatureQueryOptions().apply { this.sourceLayerIds = sourceLayerIds } + val sourceLayerIds = mutableListOf("a") + val sourceOptions = SourceFeatureQueryOptions().apply { this.sourceLayerIds = sourceLayerIds } - sourceLayerIds.add("b") + sourceLayerIds.add("b") - assertEquals(listOf("a"), sourceOptions.sourceLayerIds) - } + assertEquals(listOf("a"), sourceOptions.sourceLayerIds) + } @Test - fun byteTransitDescriptorsSnapshotCallerOwnedArrays() { - val filter = "true".encodeToByteArray() - val query = RenderedFeatureQueryOptions().apply { this.filter = filter } - filter[0] = 'f'.code.toByte() - assertContentEquals("true".encodeToByteArray(), query.filter) - - val exposedFilter = query.filter ?: error("missing filter") - exposedFilter[0] = 'f'.code.toByte() - assertContentEquals("true".encodeToByteArray(), query.filter) - - val sourceFilter = "false".encodeToByteArray() - val sourceQuery = SourceFeatureQueryOptions().apply { this.filter = sourceFilter } - sourceFilter[0] = 't'.code.toByte() - assertContentEquals("false".encodeToByteArray(), sourceQuery.filter) - - val properties = "{}".encodeToByteArray() - val source = GeoJsonSourceOptions().apply { clusterProperties = properties } - properties[0] = '['.code.toByte() - assertContentEquals("{}".encodeToByteArray(), source.clusterProperties) - - val geometry = "{\"type\":\"Point\",\"coordinates\":[0,0]}".encodeToByteArray() - val region = OfflineRegionDefinition.GeometryRegion("style", geometry, 0.0, 1.0, 1f, false) - geometry[0] = '['.code.toByte() - assertContentEquals( - "{\"type\":\"Point\",\"coordinates\":[0,0]}".encodeToByteArray(), - region.geometry, - ) - } + fun byteTransitDescriptorsSnapshotCallerOwnedArrays(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val filter = "true".encodeToByteArray() + val query = RenderedFeatureQueryOptions().apply { this.filter = filter } + filter[0] = 'f'.code.toByte() + assertContentEquals("true".encodeToByteArray(), query.filter) + + val exposedFilter = query.filter ?: error("missing filter") + exposedFilter[0] = 'f'.code.toByte() + assertContentEquals("true".encodeToByteArray(), query.filter) + + val sourceFilter = "false".encodeToByteArray() + val sourceQuery = SourceFeatureQueryOptions().apply { this.filter = sourceFilter } + sourceFilter[0] = 't'.code.toByte() + assertContentEquals("false".encodeToByteArray(), sourceQuery.filter) + + val properties = "{}".encodeToByteArray() + val source = GeoJsonSourceOptions().apply { clusterProperties = properties } + properties[0] = '['.code.toByte() + assertContentEquals("{}".encodeToByteArray(), source.clusterProperties) + + val geometry = "{\"type\":\"Point\",\"coordinates\":[0,0]}".encodeToByteArray() + val region = OfflineRegionDefinition.GeometryRegion("style", geometry, 0.0, 1.0, 1f, false) + geometry[0] = '['.code.toByte() + assertContentEquals( + "{\"type\":\"Point\",\"coordinates\":[0,0]}".encodeToByteArray(), + region.geometry, + ) + } @Test - fun styleImageOptionsSnapshotCallerOwnedStretches() { - // BND-069. - val stretchX = mutableListOf(ImageStretch(0.0f, 1.0f)) - val options = - StyleImageOptions().apply { - this.stretchX = stretchX - stretchY = emptyList() - } - val copy = options.copy() + fun styleImageOptionsSnapshotCallerOwnedStretches(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { // BND-069. + val stretchX = mutableListOf(ImageStretch(0.0f, 1.0f)) + val options = + StyleImageOptions().apply { + this.stretchX = stretchX + stretchY = emptyList() + } + val copy = options.copy() - stretchX.add(ImageStretch(1.0f, 2.0f)) + stretchX.add(ImageStretch(1.0f, 2.0f)) - assertEquals(listOf(ImageStretch(0.0f, 1.0f)), options.stretchX) - assertEquals(listOf(ImageStretch(0.0f, 1.0f)), copy.stretchX) - // A present empty list stays distinguishable from an absent one. - assertEquals(emptyList(), copy.stretchY) - } + assertEquals(listOf(ImageStretch(0.0f, 1.0f)), options.stretchX) + assertEquals(listOf(ImageStretch(0.0f, 1.0f)), copy.stretchX) + // A present empty list stays distinguishable from an absent one. + assertEquals(emptyList(), copy.stretchY) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/error/MaplibreExceptionTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/error/MaplibreExceptionTest.kt index 417303f05..e0c8d87ab 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/error/MaplibreExceptionTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/error/MaplibreExceptionTest.kt @@ -13,6 +13,10 @@ class MaplibreExceptionTest { assertEquals(MaplibreStatus.WRONG_THREAD, MaplibreStatus.fromNative(-3)) assertEquals(MaplibreStatus.UNSUPPORTED, MaplibreStatus.fromNative(-4)) assertEquals(MaplibreStatus.NATIVE_ERROR, MaplibreStatus.fromNative(-5)) + assertEquals(MaplibreStatus.CANCELLED, MaplibreStatus.fromNative(-6)) + assertEquals(MaplibreStatus.BUSY, MaplibreStatus.fromNative(-7)) + assertEquals(MaplibreStatus.TARGET_LOST, MaplibreStatus.fromNative(-8)) + assertEquals(MaplibreStatus.NOT_READY, MaplibreStatus.fromNative(-9)) assertEquals(MaplibreStatus(-127), MaplibreStatus.fromNative(-127)) } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateTest.kt index 69e180042..c1f7e3212 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateTest.kt @@ -8,46 +8,49 @@ import kotlin.test.assertTrue class CallbackGateTest { @Test - fun closeWithoutEnteredCallbacksClosesNativeStateOnce() { - var closes = 0 - val gate = CallbackGate("test callbacks") { closes++ } + fun closeWithoutEnteredCallbacksClosesNativeStateOnce(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var closes = 0 + val gate = CallbackGate("test callbacks") { closes++ } - gate.close() - gate.close() + gate.close() + gate.close() - assertTrue(gate.isClosedForTesting()) - assertEquals(1, closes) - assertNull(gate.enter()) - } + assertTrue(gate.isClosedForTesting()) + assertEquals(1, closes) + assertNull(gate.enter()) + } @Test - fun closeFromEnteredCallbackDefersNativeCloseUntilCallbackExits() { - var closes = 0 - val gate = CallbackGate("test callbacks") { closes++ } - val lease = assertNotNull(gate.enter()) + fun closeFromEnteredCallbackDefersNativeCloseUntilCallbackExits(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var closes = 0 + val gate = CallbackGate("test callbacks") { closes++ } + val lease = assertNotNull(gate.enter()) - gate.close() - assertEquals(0, closes) - assertNull(gate.enter()) + gate.close() + assertEquals(0, closes) + assertNull(gate.enter()) - lease.close() + lease.close() - assertTrue(gate.isClosedForTesting()) - assertEquals(1, closes) - } + assertTrue(gate.isClosedForTesting()) + assertEquals(1, closes) + } @Test - fun multipleEnteredCallbacksCanExitBeforeClose() { - var closes = 0 - val gate = CallbackGate("test callbacks") { closes++ } - val first = assertNotNull(gate.enter()) - val second = assertNotNull(gate.enter()) - - first.close() - second.close() - gate.close() - - assertTrue(gate.isClosedForTesting()) - assertEquals(1, closes) - } + fun multipleEnteredCallbacksCanExitBeforeClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var closes = 0 + val gate = CallbackGate("test callbacks") { closes++ } + val first = assertNotNull(gate.enter()) + val second = assertNotNull(gate.enter()) + + first.close() + second.close() + gate.close() + + assertTrue(gate.isClosedForTesting()) + assertEquals(1, closes) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistryTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistryTest.kt deleted file mode 100644 index 0015b8040..000000000 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackRegistryTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.maplibre.nativeffi.internal.callback - -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNull -import kotlin.test.assertSame -import kotlin.test.assertTrue -import org.maplibre.nativeffi.error.MaplibreException -import org.maplibre.nativeffi.error.MaplibreStatus - -@OptIn(ExperimentalAtomicApi::class) -class LogCallbackRegistryTest { - @Test - fun firstSetInstallsNativeCallbackAndReplacementClosesPreviousState() { - val registry = LogCallbackRegistry() - val first = TestCallbackState() - val second = TestCallbackState() - var installs = 0 - - registry.set(first) { - installs++ - MaplibreStatus.OK.nativeCode - } - registry.set(second) { - installs++ - MaplibreStatus.OK.nativeCode - } - - assertEquals(1, installs) - assertTrue(first.isClosed()) - assertSame(second, registry.current()) - } - - @Test - fun failedInitialInstallClosesReplacementAndLeavesCurrentEmpty() { - val registry = LogCallbackRegistry() - val replacement = TestCallbackState() - - val error = - assertFailsWith { - registry.set(replacement) { MaplibreStatus.NATIVE_ERROR.nativeCode } - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, error.status) - assertTrue(replacement.isClosed()) - assertNull(registry.current()) - } - - @Test - fun clearOnlyCallsNativeClearWhenInstalledAndClosesCurrentState() { - val registry = LogCallbackRegistry() - val state = TestCallbackState() - var clears = 0 - - registry.clear { - clears++ - MaplibreStatus.OK.nativeCode - } - registry.set(state) { MaplibreStatus.OK.nativeCode } - registry.clear { - clears++ - MaplibreStatus.OK.nativeCode - } - registry.clear { - clears++ - MaplibreStatus.OK.nativeCode - } - - assertEquals(1, clears) - assertTrue(state.isClosed()) - assertNull(registry.current()) - } - - private class TestCallbackState : AutoCloseable { - private val closed = AtomicInt(0) - - override fun close() { - closed.store(1) - } - - fun isClosed(): Boolean = closed.load() != 0 - } -} diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/BorrowedResourceCoreTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/BorrowedResourceCoreTest.kt index 0af6b662b..1202e827e 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/BorrowedResourceCoreTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/BorrowedResourceCoreTest.kt @@ -6,47 +6,51 @@ import kotlin.test.assertFailsWith class BorrowedResourceCoreTest { @Test - fun closeWithoutBorrowsReleasesNativeOnce() { - var releases = 0 - val core = BorrowedResourceCore("TestResource") { releases++ } + fun closeWithoutBorrowsReleasesNativeOnce(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = BorrowedResourceCore("TestResource") { releases++ } - core.close() - core.close() - core.releaseNativeForCleaner() + core.close() + core.close() + core.releaseNativeForCleaner() - assertEquals(1, releases) - } + assertEquals(1, releases) + } @Test - fun closeDuringBorrowDefersNativeReleaseUntilBorrowEnds() { - var releases = 0 - val core = BorrowedResourceCore("TestResource") { releases++ } + fun closeDuringBorrowDefersNativeReleaseUntilBorrowEnds(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = BorrowedResourceCore("TestResource") { releases++ } - core.withOpenResource { - core.close() - assertEquals(0, releases) - } + core.withOpenResource { + core.close() + assertEquals(0, releases) + } - assertEquals(1, releases) - } + assertEquals(1, releases) + } @Test - fun borrowAfterCloseFails() { - val core = BorrowedResourceCore("TestResource") {} + fun borrowAfterCloseFails(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val core = BorrowedResourceCore("TestResource") {} - core.close() + core.close() - assertFailsWith { core.withOpenResource {} } - } + assertFailsWith { core.withOpenResource {} } + } @Test - fun cleanerReleaseIsIdempotentWithClose() { - var releases = 0 - val core = BorrowedResourceCore("TestResource") { releases++ } + fun cleanerReleaseIsIdempotentWithClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = BorrowedResourceCore("TestResource") { releases++ } - core.releaseNativeForCleaner() - core.close() + core.releaseNativeForCleaner() + core.close() - assertEquals(1, releases) - } + assertEquals(1, releases) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreTest.kt index 08c6e64a9..e5342c26b 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreTest.kt @@ -11,190 +11,126 @@ import org.maplibre.nativeffi.error.NativeErrorException class HandleStateCoreTest { @Test - fun failedNativeDestroyLeavesHandleLiveAndRetryable() { - val state = HandleStateCore("TestHandle", 0x1234) - var attempts = 0 - - val failure = - assertFailsWith { - state.closeOnce( - destroy = { - attempts += 1 - MaplibreStatus.NATIVE_ERROR.nativeCode - } - ) - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, failure.status) - assertEquals(1, attempts) - assertFalse(state.isReleased()) - state.requireLive() - - state.closeOnce( - destroy = { - attempts += 1 - MaplibreStatus.OK.nativeCode - } - ) - - assertEquals(2, attempts) - assertTrue(state.isReleased()) - - state.closeOnce( - destroy = { - attempts += 1 - error("destroy must not be called after release") - } - ) - - assertEquals(2, attempts) - } - - @Test - fun releasingHandleRejectsPublicAccessAndReentrantRelease() { - val state = HandleStateCore("TestHandle", 0x1234) - var attempts = 0 - - state.closeOnce( - destroy = { - attempts += 1 - val accessError = assertFailsWith { state.requireLive() } - assertEquals(MaplibreStatus.INVALID_STATE, accessError.status) - assertEquals("TestHandle is currently releasing", accessError.diagnostic) - - val closeError = - assertFailsWith { - state.closeOnce( - destroy = { - attempts += 1 - MaplibreStatus.OK.nativeCode - } - ) - } - assertEquals(MaplibreStatus.INVALID_STATE, closeError.status) - assertEquals("TestHandle is currently releasing", closeError.diagnostic) - MaplibreStatus.OK.nativeCode - } - ) - - assertEquals(1, attempts) - assertTrue(state.isReleased()) - } - - @Test - fun liveChildrenBlockParentCloseUntilReleased() { - val state = HandleStateCore("ParentHandle", 0x1234) - val child = state.retainChild("ChildHandle") - var attempts = 0 - - val error = - assertFailsWith { - state.closeOnce( - destroy = { - attempts += 1 - MaplibreStatus.OK.nativeCode - } - ) - } - - assertEquals(MaplibreStatus.INVALID_STATE, error.status) - assertEquals("ParentHandle has 1 live child handle(s): ChildHandle", error.diagnostic) - assertEquals(0, attempts) - assertFalse(state.isReleased()) - - child.close() - child.close() - - state.closeOnce( - destroy = { - attempts += 1 - MaplibreStatus.OK.nativeCode - } - ) - - assertEquals(1, attempts) - assertTrue(state.isReleased()) - } + fun failedNativeDestroyLeavesHandleLiveAndRetryable(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = HandleStateCore("TestHandle", 0x1234) + var attempts = 0 + + val failure = + assertFailsWith { + state.closeOnce( + destroy = { + attempts += 1 + MaplibreStatus.NATIVE_ERROR.nativeCode + } + ) + } + + assertEquals(MaplibreStatus.NATIVE_ERROR, failure.status) + assertEquals(1, attempts) + assertFalse(state.isReleased()) + state.requireLive() + + state.closeOnce( + destroy = { + attempts += 1 + MaplibreStatus.OK.nativeCode + } + ) + + assertEquals(2, attempts) + assertTrue(state.isReleased()) + + state.closeOnce( + destroy = { + attempts += 1 + error("destroy must not be called after release") + } + ) + + assertEquals(2, attempts) + } @Test - fun blockedParentCloseNamesEachLiveChildTypeUntilItIsReleased() { - val state = HandleStateCore("MapHandle", 0x1234) - val session = state.retainChild("RenderSessionHandle") - val projection = state.retainChild("MapProjectionHandle") - var attempts = 0 - val destroy = { - attempts += 1 - MaplibreStatus.OK.nativeCode + fun releasingHandleRejectsPublicAccessAndReentrantRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = HandleStateCore("TestHandle", 0x1234) + var attempts = 0 + + state.closeOnce( + destroy = { + attempts += 1 + val accessError = assertFailsWith { state.requireLive() } + assertEquals(MaplibreStatus.INVALID_STATE, accessError.status) + assertEquals("TestHandle is currently releasing", accessError.diagnostic) + + val closeError = + assertFailsWith { + state.closeOnce( + destroy = { + attempts += 1 + MaplibreStatus.OK.nativeCode + } + ) + } + assertEquals(MaplibreStatus.INVALID_STATE, closeError.status) + assertEquals("TestHandle is currently releasing", closeError.diagnostic) + MaplibreStatus.OK.nativeCode + } + ) + + assertEquals(1, attempts) + assertTrue(state.isReleased()) } - val bothLive = assertFailsWith { state.closeOnce(destroy) } - assertEquals( - "MapHandle has 2 live child handle(s): MapProjectionHandle, RenderSessionHandle", - bothLive.diagnostic, - ) - - session.close() - - val projectionLive = assertFailsWith { state.closeOnce(destroy) } - assertEquals( - "MapHandle has 1 live child handle(s): MapProjectionHandle", - projectionLive.diagnostic, - ) - assertEquals(0, attempts) - - projection.close() - state.closeOnce(destroy) - - assertEquals(1, attempts) - assertTrue(state.isReleased()) - } - @Test - fun leakReportReportsOnlyUnreleasedHandles() { - val reports = mutableListOf() - val unreleased = HandleStateCore.LeakReport("RuntimeHandle", 0x1234L, reports::add) + fun leakReportReportsOnlyUnreleasedHandles(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val reports = mutableListOf() + val unreleased = HandleStateCore.LeakReport("RuntimeHandle", 0x1234L, reports::add) - unreleased.report() + unreleased.report() - assertEquals( - listOf("Leaked RuntimeHandle native handle 0x1234; " + "close handles explicitly."), - reports, - ) + assertEquals( + listOf("Leaked RuntimeHandle native handle 0x1234; close it explicitly."), + reports, + ) - val released = HandleStateCore.LeakReport("MapHandle", 0x5678L, reports::add) - released.markReleased() - released.report() + val released = HandleStateCore.LeakReport("MapHandle", 0x5678L, reports::add) + released.markReleased() + released.report() - assertEquals(1, reports.size) - } + assertEquals(1, reports.size) + } @Test - fun aUseStartingAfterCloseBeginsIsRefused() { - val state = HandleStateCore("TestHandle", 0x1234) - var refusal: Throwable? = null - - // Runs while closeOnce holds the releasing state, the window a use on another thread - // would land in. - state.closeOnce( - destroy = { - refusal = assertFailsWith { state.withLive {} } - MaplibreStatus.OK.nativeCode - } - ) - - assertTrue(refusal!!.message!!.contains("releasing")) - assertFailsWith { state.withLive {} } - } + fun aUseStartingAfterCloseBeginsIsRefused(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = HandleStateCore("TestHandle", 0x1234) + var refusal: Throwable? = null + + // Runs while closeOnce holds the releasing state, the window a use on another thread + // would land in. + state.closeOnce( + destroy = { + refusal = assertFailsWith { state.withLive {} } + MaplibreStatus.OK.nativeCode + } + ) + + assertTrue(refusal!!.message!!.contains("releasing")) + assertFailsWith { state.withLive {} } + } @Test - fun withLiveReturnsTheBlockResultAndPropagatesFailures() { - val state = HandleStateCore("TestHandle", 0x1234) + fun withLiveReturnsTheBlockResultAndPropagatesFailures(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = HandleStateCore("TestHandle", 0x1234) - assertEquals(7, state.withLive { 7 }) - assertFailsWith { state.withLive { error("boom") } } + assertEquals(7, state.withLive { 7 }) + assertFailsWith { state.withLive { error("boom") } } - // A block that threw must not leave a use counted, or close would wait forever. - state.closeOnce(destroy = { MaplibreStatus.OK.nativeCode }) - assertTrue(state.isReleased()) - } + state.closeOnce(destroy = { MaplibreStatus.OK.nativeCode }) + assertTrue(state.isReleased()) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/SyntheticHandles.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/SyntheticHandles.kt index b3ec1f7cd..a49816155 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/SyntheticHandles.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/SyntheticHandles.kt @@ -26,8 +26,6 @@ internal object SyntheticHandles { fun styleIdList(ordinal: Long = 1): NativeStyleIdList = NativeStyleIdList(kind(0x08) or ordinal) - fun wakeSource(ordinal: Long = 1): NativeWakeSource = NativeWakeSource(kind(0x0B) or ordinal) - fun resourceRequest(ordinal: Long = 1): NativeResourceRequest = NativeResourceRequest(kind(0x0C) or ordinal) diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceRegistryTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceRegistryTest.kt index e14ae63c3..72d61536b 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceRegistryTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceRegistryTest.kt @@ -6,31 +6,33 @@ import kotlin.test.assertFailsWith class CustomGeometrySourceRegistryTest { @Test - fun failedReplacementPreservesExistingCallbackState() { - val released = mutableListOf() - val registry = registry(released) - registry.install("source", State("existing")) {} - - assertFailsWith { - registry.install("source", State("replacement")) { error("native install failed") } + fun failedReplacementPreservesExistingCallbackState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val released = mutableListOf() + val registry = registry(released) + registry.install("source", State("existing")) {} + + assertFailsWith { + registry.install("source", State("replacement")) { error("native install failed") } + } + + assertEquals(1, registry.size) + assertEquals(listOf("replacement"), released) } - assertEquals(1, registry.size) - assertEquals(listOf("replacement"), released) - } - @Test - fun clearingReleasesEveryRemainingCallbackState() { - val released = mutableListOf() - val registry = registry(released) - registry.install("first", State("first")) {} - registry.install("second", State("second")) {} + fun clearingReleasesEveryRemainingCallbackState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val released = mutableListOf() + val registry = registry(released) + registry.install("first", State("first")) {} + registry.install("second", State("second")) {} - registry.clear() + registry.clear() - assertEquals(0, registry.size) - assertEquals(listOf("first", "second"), released) - } + assertEquals(0, registry.size) + assertEquals(listOf("first", "second"), released) + } private fun registry(released: MutableList): CustomGeometrySourceRegistry = CustomGeometrySourceRegistry { state -> diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt index 01064b89a..32a52b68c 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt @@ -25,100 +25,103 @@ import org.maplibre.nativeffi.runtime.RuntimeOptions class DescriptorValidationTest { @Test - fun signedCarriersRejectNegativeUnsignedValues() { - assertFailsWith { - MapOptions().apply { - width = -1 - height = 1 + fun signedCarriersRejectNegativeUnsignedValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertFailsWith { + MapOptions().apply { + width = -1 + height = 1 + } + } + assertFailsWith { TileOptions().prefetchZoomDelta = -1 } + assertFailsWith { NativeBuffer.allocate(-1) } + val nullPointer = NativePointer.NULL_POINTER + assertFailsWith { RenderTargetExtent(-1, 1, 1.0) } + assertFailsWith { RenderTargetExtent(1, 1, 1.0).width = -1 } + assertFailsWith { + vulkanContext(nullPointer, graphicsQueueFamilyIndex = -1) + } + assertFailsWith { + vulkanContext(nullPointer).graphicsQueueFamilyIndex = -1 + } + assertFailsWith { + vulkanBorrowedTextureDescriptor(nullPointer, format = -1) + } + assertFailsWith { + vulkanBorrowedTextureDescriptor(nullPointer).format = -1 } } - assertFailsWith { TileOptions().prefetchZoomDelta = -1 } - assertFailsWith { NativeBuffer.allocate(-1) } - val nullPointer = NativePointer.NULL_POINTER - assertFailsWith { RenderTargetExtent(-1, 1, 1.0) } - assertFailsWith { RenderTargetExtent(1, 1, 1.0).width = -1 } - assertFailsWith { - vulkanContext(nullPointer, graphicsQueueFamilyIndex = -1) - } - assertFailsWith { - vulkanContext(nullPointer).graphicsQueueFamilyIndex = -1 - } - assertFailsWith { - vulkanBorrowedTextureDescriptor(nullPointer, format = -1) - } - assertFailsWith { - vulkanBorrowedTextureDescriptor(nullPointer).format = -1 - } - } @Test - fun mapAndProjectionInputsPropagateNativeCoordinateValidation() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = MapHandle.create(runtime, mapOptions()) - var projection: MapProjectionHandle? = null - try { - val invalidCoordinate = LatLng(Double.NaN, 0.0) - assertInvalidCoordinateDiagnostic { map.pixelForLatLng(invalidCoordinate) } - projection = map.createProjection() - assertInvalidCoordinateDiagnostic { projection.pixelForLatLng(invalidCoordinate) } - assertInvalidCoordinateDiagnostic { Maplibre.projectedMetersForLatLng(invalidCoordinate) } - } finally { - projection?.close() - map.close() - runtime.close() + fun mapAndProjectionInputsPropagateNativeCoordinateValidation(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = MapHandle.create(runtime, mapOptions()).await() + var projection: MapProjectionHandle? = null + try { + val invalidCoordinate = LatLng(Double.NaN, 0.0) + val createdProjection = map.createProjection().await() + projection = createdProjection + assertInvalidCoordinateDiagnostic { createdProjection.pixelForLatLng(invalidCoordinate) } + assertInvalidCoordinateDiagnostic { Maplibre.projectedMetersForLatLng(invalidCoordinate) } + } finally { + projection?.close() + map.close() + runtime.close() + } } - } @Test - fun unsupportedRenderBackendsRejectAttachBeforeSessionCreation() { - val supported = Maplibre.supportedRenderBackends() - assertTrue(supported.isNotEmpty()) - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = MapHandle.create(runtime, mapOptions()) - try { - val pointer = NativePointer.ofAddress(0x10L) - val extent = RenderTargetExtent(256, 256, 1.0) - if (RenderBackend.METAL !in supported) { - assertEquals( - MaplibreStatus.UNSUPPORTED, - assertFailsWith { - map.attachMetalOwnedTexture( - MetalOwnedTextureDescriptor(extent, MetalContextDescriptor(pointer)) - ) - } - .status, - ) - } - if (RenderBackend.VULKAN !in supported) { - assertEquals( - MaplibreStatus.UNSUPPORTED, - assertFailsWith { - map.attachVulkanOwnedTexture( - VulkanOwnedTextureDescriptor(extent, context = vulkanContext(pointer)) - ) - } - .status, - ) - } - if (RenderBackend.OPENGL !in supported) { - assertEquals( - MaplibreStatus.UNSUPPORTED, - assertFailsWith { - map.attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor( - extent, - context = WglContextDescriptor(pointer, pointer, pointer), + fun unsupportedRenderBackendsRejectAttachBeforeSessionCreation(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val supported = Maplibre.supportedRenderBackends() + assertTrue(supported.isNotEmpty()) + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = MapHandle.create(runtime, mapOptions()).await() + try { + val pointer = NativePointer.ofAddress(0x10L) + val extent = RenderTargetExtent(256, 256, 1.0) + if (RenderBackend.METAL !in supported) { + assertEquals( + MaplibreStatus.UNSUPPORTED, + assertFailsWith { + map.attachMetalOwnedTexture( + MetalOwnedTextureDescriptor(extent, MetalContextDescriptor(pointer)) ) - ) - } - .status, - ) + } + .status, + ) + } + if (RenderBackend.VULKAN !in supported) { + assertEquals( + MaplibreStatus.UNSUPPORTED, + assertFailsWith { + map.attachVulkanOwnedTexture( + VulkanOwnedTextureDescriptor(extent, context = vulkanContext(pointer)) + ) + } + .status, + ) + } + if (RenderBackend.OPENGL !in supported) { + assertEquals( + MaplibreStatus.UNSUPPORTED, + assertFailsWith { + map.attachOpenGLOwnedTexture( + OpenGLOwnedTextureDescriptor( + extent, + context = WglContextDescriptor(pointer, pointer, pointer), + ) + ) + } + .status, + ) + } + } finally { + map.close() + runtime.close() } - } finally { - map.close() - runtime.close() } - } private fun mapOptions(): MapOptions = MapOptions().apply { @@ -126,8 +129,14 @@ class DescriptorValidationTest { height = 128 } - private fun assertInvalidCoordinateDiagnostic(block: () -> Unit) { - val error = assertFailsWith { block() } + private suspend fun assertInvalidCoordinateDiagnostic(block: suspend () -> Unit) { + val error = + try { + block() + error("expected InvalidArgumentException") + } catch (error: InvalidArgumentException) { + error + } assertEquals(MaplibreStatus.INVALID_ARGUMENT, error.status) assertTrue(error.diagnostic.contains("latitude must be finite")) } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapHandleTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapHandleTest.kt index f2ec9bfb3..be056d499 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapHandleTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapHandleTest.kt @@ -4,32 +4,28 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse -import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame import kotlin.test.assertTrue -import org.maplibre.nativeffi.camera.AnimationOptions import org.maplibre.nativeffi.camera.BoundOptions import org.maplibre.nativeffi.camera.BoundsConstraint -import org.maplibre.nativeffi.camera.CameraFitOptions -import org.maplibre.nativeffi.camera.CameraOptions -import org.maplibre.nativeffi.camera.EdgeInsets -import org.maplibre.nativeffi.camera.UnitBezier +import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.error.InvalidArgumentException import org.maplibre.nativeffi.error.InvalidStateException +import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds import org.maplibre.nativeffi.geo.Quaternion -import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.geo.Vec3 +import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.render.PremultipliedRgba8Image -import org.maplibre.nativeffi.runtime.CameraChangeMode -import org.maplibre.nativeffi.runtime.RuntimeEventPayload -import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.runtime.CommandCompletion +import org.maplibre.nativeffi.runtime.CommandDisposition import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.runtime.use import org.maplibre.nativeffi.style.CustomGeometrySourceCallback import org.maplibre.nativeffi.style.CustomGeometrySourceOptions import org.maplibre.nativeffi.style.CustomMvtVectorSourceCallback @@ -49,108 +45,159 @@ import org.maplibre.nativeffi.style.VectorTileEncoding class MapHandleTest { @Test - fun layerBaseAccessorsReachNativeThroughDowncalls() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - .use { map -> - map.setStyleJson( - ("{\"version\":8,\"sources\":{\"geo\":{\"type\":\"geojson\",\"data\":" + - "{\"type\":\"FeatureCollection\",\"features\":[]}}},\"layers\":[" + - "{\"id\":\"bg\",\"type\":\"background\"}," + - "{\"id\":\"fill\",\"type\":\"fill\",\"source\":\"geo\"}]}") - .encodeToByteArray() + fun layerBaseAccessorsReachNativeThroughDowncalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, ) + .use { map -> + assertCommandCommitted( + runtime, + map + .setStyleJson( + ("{\"version\":8,\"sources\":{\"geo\":{\"type\":\"geojson\",\"data\":" + + "{\"type\":\"FeatureCollection\",\"features\":[]}}},\"layers\":[" + + "{\"id\":\"bg\",\"type\":\"background\"}," + + "{\"id\":\"fill\",\"type\":\"fill\",\"source\":\"geo\"}]}") + .encodeToByteArray() + ) + .await(), + ) - assertEquals("", map.layerSourceLayer("fill")) - map.setLayerSourceLayer("fill", "roads") - assertEquals("roads", map.layerSourceLayer("fill")) - assertEquals("geo", map.layerSourceId("fill")) - - // A background layer takes no source. - assertFailsWith { map.setLayerSourceLayer("bg", "roads") } - - // An unset zoom range crosses the boundary as infinities. - assertEquals(Double.NEGATIVE_INFINITY, map.layerMinZoom("fill")) - assertEquals(Double.POSITIVE_INFINITY, map.layerMaxZoom("fill")) - map.setLayerMinZoom("fill", 4.0) - map.setLayerMaxZoom("fill", 12.5) - assertEquals(4.0, map.layerMinZoom("fill")) - assertEquals(12.5, map.layerMaxZoom("fill")) - - assertEquals(StyleLayerVisibility.VISIBLE, map.layerVisibility("fill")) - map.setLayerVisibility("fill", StyleLayerVisibility.NONE) - assertEquals(StyleLayerVisibility.NONE, map.layerVisibility("fill")) + assertEquals("", map.layerSourceLayer("fill").await()) + assertCommandCommitted(runtime, map.setLayerSourceLayer("fill", "roads").await()) + assertEquals("roads", map.layerSourceLayer("fill").await()) + assertEquals("geo", map.layerSourceId("fill").await()) - assertFailsWith { - map.setLayerVisibility("fill", StyleLayerVisibility(900)) + // A background layer takes no source. + assertCommandFailed( + map.setLayerSourceLayer("bg", "roads").await(), + MaplibreStatus.INVALID_ARGUMENT, + ) + assertEquals("", map.layerSourceLayer("bg").await()) + + // An unset zoom range crosses the boundary as infinities. + val unbounded = assertNotNull(map.styleLayerInfo("fill").await()) + assertEquals("fill", unbounded.type) + assertEquals(Double.NEGATIVE_INFINITY, unbounded.minZoom) + assertEquals(Double.POSITIVE_INFINITY, unbounded.maxZoom) + assertEquals(StyleLayerVisibility.VISIBLE, unbounded.visibility) + // The info's source flags feed the copy operations. + assertEquals("geo", unbounded.sourceId) + assertEquals("roads", unbounded.sourceLayer) + + assertCommandCommitted(runtime, map.setLayerMinZoom("fill", 4.0).await()) + assertCommandCommitted(runtime, map.setLayerMaxZoom("fill", 12.5).await()) + assertCommandCommitted( + runtime, + map.setLayerVisibility("fill", StyleLayerVisibility.NONE).await(), + ) + val bounded = assertNotNull(map.styleLayerInfo("fill").await()) + assertEquals(4.0, bounded.minZoom) + assertEquals(12.5, bounded.maxZoom) + assertEquals(StyleLayerVisibility.NONE, bounded.visibility) + + // A sourceless layer reports no source fields. + val background = assertNotNull(map.styleLayerInfo("bg").await()) + assertEquals("background", background.type) + assertNull(background.sourceId) + assertNull(background.sourceLayer) + + // No layer carries this ID. + assertNull(map.styleLayerInfo("missing").await()) + + assertCommandFailed( + map.setLayerVisibility("fill", StyleLayerVisibility(900)).await(), + MaplibreStatus.INVALID_ARGUMENT, + ) + assertEquals(StyleLayerVisibility.NONE, map.styleLayerInfo("fill").await()?.visibility) } - } + } } - } @Test - fun styleTransitionOptionsRoundTripThroughDowncalls() { - val transitionStyleJson = - "{\"version\":8,\"transition\":{\"duration\":750,\"delay\":100}," + - "\"sources\":{},\"layers\":[]}" - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - .use { map -> - // Duration and delay are absent until a style loads; the placement flag always - // holds a value. - val empty = map.styleTransitionOptions() - assertNull(empty.durationMs) - assertNull(empty.delayMs) - assertEquals(true, empty.enablePlacementTransitions) - - // The style parser supplies a 300ms default duration. - map.setStyleJson("{\"version\":8,\"sources\":{},\"layers\":[]}".encodeToByteArray()) - val parsed = map.styleTransitionOptions() - assertEquals(300.0, parsed.durationMs) - assertNull(parsed.delayMs) - - map.setStyleJson(transitionStyleJson.encodeToByteArray()) - val declared = map.styleTransitionOptions() - assertEquals(750.0, declared.durationMs) - assertEquals(100.0, declared.delayMs) - assertEquals(true, declared.enablePlacementTransitions) - - // A present zero stays distinguishable from an absent field, and an absent field - // clears what the style declared. - val options = - StyleTransitionOptions().apply { - durationMs = 0.0 - enablePlacementTransitions = false - } - map.setStyleTransitionOptions(options) - assertEquals(options, map.styleTransitionOptions()) + fun styleTransitionOptionsRoundTripThroughDowncalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val transitionStyleJson = + "{\"version\":8,\"transition\":{\"duration\":750,\"delay\":100}," + + "\"sources\":{},\"layers\":[]}" + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .use { map -> + // Duration and delay are absent until a style loads; the placement flag always + // holds a value. + val empty = map.styleTransitionOptions().await() + assertNull(empty.durationMs) + assertNull(empty.delayMs) + assertEquals(true, empty.enablePlacementTransitions) + + // The style parser supplies a 300ms default duration. + assertCommandCommitted( + runtime, + map + .setStyleJson("{\"version\":8,\"sources\":{},\"layers\":[]}".encodeToByteArray()) + .await(), + ) + val parsed = map.styleTransitionOptions().await() + assertEquals(300.0, parsed.durationMs) + assertNull(parsed.delayMs) - // Omitting the flag leaves the cross-fade on. - map.setStyleTransitionOptions(StyleTransitionOptions().apply { durationMs = 250.0 }) - assertEquals(true, map.styleTransitionOptions().enablePlacementTransitions) + assertCommandCommitted( + runtime, + map.setStyleJson(transitionStyleJson.encodeToByteArray()).await(), + ) + val declared = map.styleTransitionOptions().await() + assertEquals(750.0, declared.durationMs) + assertEquals(100.0, declared.delayMs) + assertEquals(true, declared.enablePlacementTransitions) + + // A present zero stays distinguishable from an absent field, and an absent field + // clears what the style declared. + val options = + StyleTransitionOptions().apply { + durationMs = 0.0 + enablePlacementTransitions = false + } + assertCommandCommitted(runtime, map.setStyleTransitionOptions(options).await()) + assertEquals(options, map.styleTransitionOptions().await()) + + // Omitting the flag leaves the cross-fade on. + assertCommandCommitted( + runtime, + map + .setStyleTransitionOptions(StyleTransitionOptions().apply { durationMs = 250.0 }) + .await(), + ) + assertEquals(true, map.styleTransitionOptions().await().enablePlacementTransitions) - // Loading a style replaces the override with what that style declares. - map.setStyleJson(transitionStyleJson.encodeToByteArray()) - assertEquals(declared, map.styleTransitionOptions()) + // Loading a style replaces the override with what that style declares. + assertCommandCommitted( + runtime, + map.setStyleJson(transitionStyleJson.encodeToByteArray()).await(), + ) + assertEquals(declared, map.styleTransitionOptions().await()) - assertFailsWith { - map.setStyleTransitionOptions(StyleTransitionOptions().apply { delayMs = -1.0 }) + assertCommandFailed( + map + .setStyleTransitionOptions(StyleTransitionOptions().apply { delayMs = -1.0 }) + .await(), + MaplibreStatus.INVALID_ARGUMENT, + ) + assertEquals(declared, map.styleTransitionOptions().await()) } - } + } } - } @Test fun canonicalTileIdRejectsOutOfRangeInputs() { @@ -158,1026 +205,810 @@ class MapHandleTest { } @Test - fun mapCreateStyleAndCloseRetainsRuntime() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - scaleFactor = 1.0 - mapMode = MapMode.STATIC - }, - ) - - assertFalse(map.isClosed) - assertSame(runtime, map.runtime()) - assertFailsWith { runtime.close() } - - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.setStyleUrl("https://example.com/style.json") - map.close() - map.close() - - assertTrue(map.isClosed) - assertFailsWith { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - } - runtime.close() - assertTrue(runtime.isClosed) - } + fun mapCreateStyleAndCloseRetainsRuntime(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + scaleFactor = 1.0 + mapMode = MapMode.STATIC + }, + ) + .await() - @Test - fun mapSizeReportsCreationExtentAndPixelRatio() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 512 - height = 256 - scaleFactor = 2.0 - }, - ) - - val size = map.size - assertEquals(512, size.width) - assertEquals(256, size.height) - assertEquals(2.0, size.scaleFactor) - assertEquals(MapSize(512, 256, 2.0), size) - assertEquals(MapSize(512, 256, 2.0).hashCode(), size.hashCode()) - - map.close() - runtime.close() - } + assertFalse(map.isClosed) + assertSame(runtime, map.runtime()) + assertFailsWith { + org.maplibre.nativeffi.runtime.runSuspendTest { runtime.close() } + } - @Test - fun styleSourceJsonCanBeAddedInspectedListedAndRemoved() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addStyleSourceJson("places", geoJsonSource()) - - assertTrue(map.styleSourceExists("places")) - assertEquals(SourceType.GEOJSON, map.styleSourceType("places")) - assertEquals(SourceType.GEOJSON, map.styleSourceInfo("places")?.type) - assertTrue(map.styleSourceIds().contains("places")) - assertTrue(map.removeStyleSource("places")) - assertFalse(map.styleSourceExists("places")) - assertFalse(map.removeStyleSource("places")) - } finally { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map.setStyleUrl("https://example.com/style.json").await() + map.close() map.close() + + assertTrue(map.isClosed) + assertFailsWith { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + } runtime.close() + assertTrue(runtime.isClosed) } - } - - // BND-109. @Test - fun styleSourceInfoCopiesUrlAndInlineTileMetadataPastNativeLifetime() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - lateinit var retainedInfo: SourceInfo - val tileUrls = - listOf("https://a.example.com/{z}/{x}/{y}.pbf", "https://b.example.com/{z}/{x}/{y}.pbf") - val bounds = LatLngBounds(LatLng(-5.0, -10.0), LatLng(15.0, 20.0)) - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addVectorSourceUrl("remote", "https://example.com/vector.json", null) - val remote = assertNotNull(map.styleSourceInfo("remote")) - assertEquals("https://example.com/vector.json", remote.url) - assertNull(remote.tileJson) - - map.addVectorSourceTiles( - "inline", - tileUrls, - TileSourceOptions().apply { - minZoom = 0.0 - maxZoom = 12.0 - attribution = "inline attribution" - scheme = TileScheme.TMS - this.bounds = bounds - tileSize = 256 - vectorEncoding = VectorTileEncoding.MLT - }, - ) - - retainedInfo = assertNotNull(map.styleSourceInfo("inline")) - assertNull(retainedInfo.url) - assertEquals("inline attribution", retainedInfo.attribution) - assertEquals(tileUrls, retainedInfo.tileJson?.tileUrls) - assertEquals(0.0, retainedInfo.tileJson?.minZoom) - assertEquals(12.0, retainedInfo.tileJson?.maxZoom) - assertEquals(TileScheme.TMS, retainedInfo.tileJson?.scheme) - assertEquals(bounds, retainedInfo.tileJson?.bounds) - assertEquals(512, retainedInfo.tileSize) - assertEquals(VectorTileEncoding.MLT, retainedInfo.vectorEncoding) - assertNull(retainedInfo.rasterDemEncoding) - assertTrue(map.removeStyleSource("inline")) - } finally { + fun mapSizeReportsCreationExtentAndPixelRatio(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 512 + height = 256 + scaleFactor = 2.0 + }, + ) + .await() + + val snapshot = map.snapshot() + val size = snapshot.size + assertEquals(512, size.width) + assertEquals(256, size.height) + assertEquals(2.0, size.scaleFactor) + assertEquals(MapSize(512, 256, 2.0), size) + assertEquals(MapSize(512, 256, 2.0).hashCode(), size.hashCode()) + map.close() runtime.close() } - assertEquals(tileUrls, retainedInfo.tileJson?.tileUrls) - assertEquals(bounds, retainedInfo.tileJson?.bounds) - } - @Test - fun geoJsonSourcesCanBeAddedAndUpdated() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addGeoJsonSourceUrl("remote-places", "https://example.com/places.geojson", null) - assertEquals(SourceType.GEOJSON, map.styleSourceType("remote-places")) - map.setGeoJsonSourceUrl("remote-places", "https://example.com/updated.geojson") - - val options = - GeoJsonSourceOptions().apply { - minZoom = 0.0 - maxZoom = 14.0 - tolerance = 0.5 - tileSize = 256 - buffer = 64 - lineMetrics = true - } - GeoJsonSourceDataHandle.create(geoJsonData(), options).use { initialData -> - map.addGeoJsonSourceData("inline-places", initialData) - } - assertEquals(SourceType.GEOJSON, map.styleSourceType("inline-places")) - GeoJsonSourceDataHandle.create( - "{\"type\":\"LineString\",\"coordinates\":[[0,0],[1,1]]}".encodeToByteArray(), - options, - ) - .use { updatedData -> map.setGeoJsonSourceData("inline-places", updatedData) } - map.setGeoJsonSourceSynchronousTiling("inline-places", true) - map.setGeoJsonSourceSynchronousTiling("inline-places", false) + fun styleSourceJsonCanBeAddedInspectedListedAndRemoved(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - GeoJsonSourceDataHandle.create(nearbyPoints(), clusterOptions()).use { clusteredData -> - map.addGeoJsonSourceData("clustered-places", clusteredData) - } - assertEquals(SourceType.GEOJSON, map.styleSourceType("clustered-places")) - assertFailsWith { - map.setGeoJsonSourceSynchronousTiling("no-such-source", true) + try { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map.addStyleSourceJson("places", geoJsonSource()).await() + + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("places").await()?.type) + assertTrue(map.styleSourceIds().await().contains("places")) + assertCommandCommitted(runtime, map.removeStyleSource("places").await()) + assertNull(map.styleSourceInfo("places").await()) + assertCommandFailed(map.removeStyleSource("places").await(), MaplibreStatus.NOT_FOUND) + } finally { + map.close() + runtime.close() } + } - // Option values reach native validation rather than being dropped by the binding. - assertFailsWith { - map.addGeoJsonSourceUrl( - "invalid-zooms", - "https://example.com/places.geojson", - GeoJsonSourceOptions().apply { - minZoom = 12.0 - maxZoom = 4.0 - }, + // BND-109. + + @Test + fun styleSourceInfoCopiesUrlAndInlineTileMetadataPastNativeLifetime(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() + lateinit var retainedInfo: SourceInfo + val tileUrls = + listOf("https://a.example.com/{z}/{x}/{y}.pbf", "https://b.example.com/{z}/{x}/{y}.pbf") + val bounds = LatLngBounds(LatLng(-5.0, -10.0), LatLng(15.0, 20.0)) + try { + assertCommandCommitted( + runtime, + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await(), ) - } - assertFalse(map.styleSourceExists("invalid-zooms")) - assertFailsWith { - map.addGeoJsonSourceUrl( - "invalid-cluster-properties", - "https://example.com/places.geojson", - GeoJsonSourceOptions().apply { - clusterProperties = "\"not an object\"".encodeToByteArray() - }, + assertCommandCommitted( + runtime, + map.addVectorSourceUrl("remote", "https://example.com/vector.json", null).await(), + ) + val remote = assertNotNull(map.styleSourceInfo("remote").await()) + assertEquals("https://example.com/vector.json", remote.url) + assertNull(remote.tileJson) + + assertCommandCommitted( + runtime, + map + .addVectorSourceTiles( + "inline", + tileUrls, + TileSourceOptions().apply { + minZoom = 0.0 + maxZoom = 12.0 + attribution = "inline attribution" + scheme = TileScheme.TMS + this.bounds = bounds + tileSize = 256 + vectorEncoding = VectorTileEncoding.MLT + }, + ) + .await(), ) + + retainedInfo = assertNotNull(map.styleSourceInfo("inline").await()) + assertNull(retainedInfo.url) + assertEquals("inline attribution", retainedInfo.attribution) + assertEquals(tileUrls, retainedInfo.tileJson?.tileUrls) + assertEquals(0.0, retainedInfo.tileJson?.minZoom) + assertEquals(12.0, retainedInfo.tileJson?.maxZoom) + assertEquals(TileScheme.TMS, retainedInfo.tileJson?.scheme) + assertEquals(bounds, retainedInfo.tileJson?.bounds) + assertEquals(512, retainedInfo.tileSize) + assertEquals(VectorTileEncoding.MLT, retainedInfo.vectorEncoding) + assertNull(retainedInfo.rasterDemEncoding) + assertCommandCommitted(runtime, map.removeStyleSource("inline").await()) + } finally { + map.close() + runtime.close() } - } finally { - map.close() - runtime.close() + + assertEquals(tileUrls, retainedInfo.tileJson?.tileUrls) + assertEquals(bounds, retainedInfo.tileJson?.bounds) } - } @Test - fun customGeometrySourcesCanBeManaged() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addCustomGeometrySource( - "custom-places", - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) {} - } + fun geoJsonSourcesCanBeAddedAndUpdated(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, ) - .apply { + .await() + + try { + assertCommandCommitted( + runtime, + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await(), + ) + assertCommandCommitted( + runtime, + map + .addGeoJsonSourceUrl("remote-places", "https://example.com/places.geojson", null) + .await(), + ) + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("remote-places").await()?.type) + assertCommandCommitted( + runtime, + map.setGeoJsonSourceUrl("remote-places", "https://example.com/updated.geojson").await(), + ) + + val inlineOptions = + GeoJsonSourceOptions().apply { minZoom = 0.0 maxZoom = 14.0 - tolerance = 0.375 - tileSize = 512 + tolerance = 0.5 + tileSize = 256 buffer = 64 - clip = true - wrap = false - }, - ) - - assertTrue(map.styleSourceExists("custom-places")) - assertEquals(SourceType.CUSTOM_VECTOR, map.styleSourceType("custom-places")) - - val tileId = CanonicalTileId(0, 0, 0) - map.setCustomGeometrySourceTileData("custom-places", tileId, geoJsonData()) - map.invalidateCustomGeometrySourceTile("custom-places", tileId) - map.invalidateCustomGeometrySourceRegion( - "custom-places", - LatLngBounds(LatLng(-1.0, -1.0), LatLng(1.0, 1.0)), - ) - - assertTrue(map.removeStyleSource("custom-places")) - assertFalse(map.styleSourceExists("custom-places")) - } finally { - map.close() - runtime.close() + lineMetrics = true + } + GeoJsonSourceDataHandle.create(geoJsonData(), inlineOptions).use { data -> + assertCommandCommitted(runtime, map.addGeoJsonSourceData("inline-places", data).await()) + } + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("inline-places").await()?.type) + GeoJsonSourceDataHandle.create( + ("{\"type\":\"Feature\",\"geometry\":{\"type\":\"LineString\"," + + "\"coordinates\":[[0,0],[1,1]]},\"properties\":{}}") + .encodeToByteArray(), + inlineOptions, + ) + .use { update -> + assertCommandCommitted( + runtime, + map.setGeoJsonSourceData("inline-places", update).await(), + ) + } + + GeoJsonSourceDataHandle.create(nearbyPoints(), clusterOptions()).use { clustered -> + assertCommandCommitted( + runtime, + map.addGeoJsonSourceData("clustered-places", clustered).await(), + ) + } + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("clustered-places").await()?.type) + + assertFailsWith { + map + .addGeoJsonSourceUrl( + "invalid-zooms", + "https://example.com/places.geojson", + GeoJsonSourceOptions().apply { + minZoom = 12.0 + maxZoom = 4.0 + }, + ) + .await() + } + assertNull(map.styleSourceInfo("invalid-zooms").await()) + assertCommandFailed( + map + .addGeoJsonSourceUrl( + "invalid-cluster-properties", + "https://example.com/places.geojson", + GeoJsonSourceOptions().apply { + clusterProperties = "\"not an object\"".encodeToByteArray() + }, + ) + .await(), + MaplibreStatus.INVALID_ARGUMENT, + ) + assertNull(map.styleSourceInfo("invalid-cluster-properties").await()) + } finally { + map.close() + runtime.close() + } } - } @Test - fun customMvtVectorSourcesCanBeManaged() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addCustomMvtVectorSource( - "custom-mvt", - CustomMvtVectorSourceOptions( - object : CustomMvtVectorSourceCallback { - override fun fetchTile(tileId: CanonicalTileId) {} - } + fun customGeometrySourcesCanBeManaged(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, ) - .apply { - minZoom = 0.0 - maxZoom = 14.0 - }, - ) + .await() - assertTrue(map.styleSourceExists("custom-mvt")) - assertEquals(SourceType.CUSTOM_MVT_VECTOR, map.styleSourceType("custom-mvt")) + try { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map + .addCustomGeometrySource( + "custom-places", + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) {} + } + ) + .apply { + minZoom = 0.0 + maxZoom = 14.0 + tolerance = 0.375 + tileSize = 512 + buffer = 64 + clip = true + wrap = false + }, + ) + .await() - val tileId = CanonicalTileId(0, 0, 0) - map.setCustomMvtVectorSourceTileData("custom-mvt", tileId, ByteArray(0)) - map.setCustomMvtVectorSourceTileError("custom-mvt", tileId, "tile missing") - map.invalidateCustomMvtVectorSourceTile("custom-mvt", tileId) + assertEquals(SourceType.CUSTOM_VECTOR, map.styleSourceInfo("custom-places").await()?.type) - assertFailsWith { - map.addCustomMvtVectorSource( - "custom-mvt", - CustomMvtVectorSourceOptions( - object : CustomMvtVectorSourceCallback { - override fun fetchTile(tileId: CanonicalTileId) {} - } - ), - ) - } + val tileId = CanonicalTileId(0, 0, 0) + map.setCustomGeometrySourceTileData("custom-places", tileId, geoJsonData()) + map.invalidateCustomGeometrySourceTile("custom-places", tileId).await() + map + .invalidateCustomGeometrySourceRegion( + "custom-places", + LatLngBounds(LatLng(-1.0, -1.0), LatLng(1.0, 1.0)), + ) + .await() - assertTrue(map.removeStyleSource("custom-mvt")) - assertFalse(map.styleSourceExists("custom-mvt")) - } finally { - map.close() - runtime.close() + assertCommandCommitted(runtime, map.removeStyleSource("custom-places").await()) + assertNull(map.styleSourceInfo("custom-places").await()) + } finally { + map.close() + runtime.close() + } } - } @Test - fun sixLiveCustomGeometrySourcesStayRegistered() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - .use { map -> - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - val options = - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) {} - } - ) - val ids = (1..6).map { "custom-$it" } - ids.forEach { map.addCustomGeometrySource(it, options) } - ids.forEach { id -> assertTrue(map.styleSourceExists(id), id) } - - assertTrue(map.removeStyleSource("custom-1")) - map.addCustomGeometrySource("custom-7", options) - assertTrue(map.styleSourceExists("custom-7")) - assertFalse(map.styleSourceExists("custom-1")) - } - } - } + fun customMvtVectorSourcesCanBeManaged(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - @Test - fun sixLiveCustomMvtVectorSourcesStayRegistered() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - .use { map -> - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - val options = + try { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map + .addCustomMvtVectorSource( + "custom-mvt", CustomMvtVectorSourceOptions( - object : CustomMvtVectorSourceCallback { - override fun fetchTile(tileId: CanonicalTileId) {} - } + object : CustomMvtVectorSourceCallback { + override fun fetchTile(tileId: CanonicalTileId) {} + } + ) + .apply { + minZoom = 0.0 + maxZoom = 14.0 + }, + ) + .await() + + assertEquals(SourceType.CUSTOM_MVT_VECTOR, map.styleSourceInfo("custom-mvt").await()?.type) + + val tileId = CanonicalTileId(0, 0, 0) + map.setCustomMvtVectorSourceTileData("custom-mvt", tileId, ByteArray(0)).await() + map.setCustomMvtVectorSourceTileError("custom-mvt", tileId, "tile missing").await() + map.invalidateCustomMvtVectorSourceTile("custom-mvt", tileId).await() + + // A second source under the same ID is rejected, and the first one stays installed. + assertCommandFailed( + map + .addCustomMvtVectorSource( + "custom-mvt", + CustomMvtVectorSourceOptions( + object : CustomMvtVectorSourceCallback { + override fun fetchTile(tileId: CanonicalTileId) {} + } + ), ) - val ids = (1..6).map { "custom-mvt-$it" } - ids.forEach { map.addCustomMvtVectorSource(it, options) } - ids.forEach { id -> assertTrue(map.styleSourceExists(id), id) } - - assertTrue(map.removeStyleSource("custom-mvt-1")) - map.addCustomMvtVectorSource("custom-mvt-7", options) - assertTrue(map.styleSourceExists("custom-mvt-7")) - assertFalse(map.styleSourceExists("custom-mvt-1")) - } - } - } + .await(), + MaplibreStatus.INVALID_ARGUMENT, + ) - @Test - fun tileSourcesCanBeAddedAndInspected() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addVectorSourceUrl( - "roads", - "https://example.com/vector.json", - TileSourceOptions().apply { - minZoom = 1.0 - maxZoom = 12.0 - attribution = "vector attribution" - scheme = TileScheme.XYZ - vectorEncoding = VectorTileEncoding.MVT - }, - ) - map.addRasterSourceTiles( - "satellite", - listOf("https://example.com/raster/{z}/{x}/{y}.png"), - TileSourceOptions().apply { tileSize = 256 }, - ) - map.addRasterDemSourceTiles( - "terrain", - listOf("https://example.com/terrain/{z}/{x}/{y}.png"), - TileSourceOptions().apply { - tileSize = 512 - rasterDemEncoding = RasterDemEncoding.TERRARIUM - }, - ) - - assertEquals(SourceType.VECTOR, map.styleSourceType("roads")) - val rasterInfo = assertNotNull(map.styleSourceInfo("satellite")) - assertEquals(SourceType.RASTER, rasterInfo.type) - assertEquals(256, rasterInfo.tileSize) - assertEquals(SourceType.RASTER_DEM, map.styleSourceType("terrain")) - assertEquals(RasterDemEncoding.TERRARIUM, map.styleSourceInfo("terrain")?.rasterDemEncoding) - assertTrue(map.styleSourceIds().containsAll(listOf("roads", "satellite", "terrain"))) - } finally { - map.close() - runtime.close() + assertCommandCommitted(runtime, map.removeStyleSource("custom-mvt").await()) + assertNull(map.styleSourceInfo("custom-mvt").await()) + } finally { + map.close() + runtime.close() + } } - } - @Test - fun styleLayerJsonCanBeAddedInspectedListedAndRemoved() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addStyleLayerJson(backgroundLayer(), "") - map.addLocationIndicatorLayer("puck", "") - map.setLocationIndicatorLocation("puck", LatLng(12.0, 34.0), 56.0) - map.setLocationIndicatorBearing("puck", 78.0) - map.setLocationIndicatorAccuracyRadius("puck", 9.0) - map.moveStyleLayer("puck", "background") - - assertTrue(map.styleLayerExists("background")) - assertEquals("background", map.styleLayerType("background")) - assertTrue(map.styleLayerExists("puck")) - assertTrue(map.styleLayerIds().contains("background")) - assertTrue(map.styleLayerIds().contains("puck")) - assertTrue( - map.styleLayerJson("background")!!.decodeToString().contains("\"type\":\"background\"") - ) - map.setLayerProperty("background", "background-opacity", "0.5".encodeToByteArray()) - assertEquals("0.5", map.layerProperty("background", "background-opacity")?.decodeToString()) - map.setStyleLightProperty("anchor", "\"viewport\"".encodeToByteArray()) - assertEquals("\"viewport\"", map.styleLightProperty("anchor")?.decodeToString()) - assertTrue(map.removeStyleLayer("background")) - assertTrue(map.removeStyleLayer("puck")) - assertFalse(map.styleLayerExists("background")) - assertFalse(map.removeStyleLayer("background")) - } finally { - map.close() - runtime.close() - } - } + // Every live custom source keeps its own callback state, past the ten-slot JavaCPP + // function-pointer pool that per-source thunks would exhaust on Android. @Test - fun styleImageCanBeSetCopiedInspectedAndRemoved() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - val image = PremultipliedRgba8Image(1, 1, 4, byteArrayOf(1, 2, 3, 4)) - val options = - StyleImageOptions().apply { - pixelRatio = 2.0f - sdf = true - } + fun elevenLiveCustomSourcesStayRegistered(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.setStyleImage("dot", image, options) - - assertTrue(map.styleImageExists("dot")) - val info = map.styleImageInfo("dot") - assertEquals(1, info?.width) - assertEquals(1, info?.height) - assertEquals(4, info?.stride) - assertEquals(4, info?.byteLength) - assertEquals(2.0f, info?.pixelRatio) - assertEquals(true, info?.sdf) - assertEquals(image, map.copyStyleImagePremultipliedRgba8("dot")?.image) - assertEquals(2.0f, map.copyStyleImagePremultipliedRgba8("dot")?.pixelRatio) - assertEquals(true, map.copyStyleImagePremultipliedRgba8("dot")?.sdf) - assertTrue(map.removeStyleImage("dot")) - assertFalse(map.styleImageExists("dot")) - assertFalse(map.removeStyleImage("dot")) - } finally { - map.close() - runtime.close() + try { + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + val geometryOptions = + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) {} + } + ) + val mvtOptions = + CustomMvtVectorSourceOptions( + object : CustomMvtVectorSourceCallback { + override fun fetchTile(tileId: CanonicalTileId) {} + } + ) + val geometryIds = (1..11).map { "custom-$it" } + val mvtIds = (1..11).map { "custom-mvt-$it" } + geometryIds.forEach { map.addCustomGeometrySource(it, geometryOptions).await() } + mvtIds.forEach { map.addCustomMvtVectorSource(it, mvtOptions).await() } + (geometryIds + mvtIds).forEach { id -> assertNotNull(map.styleSourceInfo(id).await(), id) } + + assertCommandCommitted(runtime, map.removeStyleSource("custom-1").await()) + assertCommandCommitted(runtime, map.removeStyleSource("custom-mvt-1").await()) + map.addCustomGeometrySource("custom-12", geometryOptions).await() + map.addCustomMvtVectorSource("custom-mvt-12", mvtOptions).await() + assertNotNull(map.styleSourceInfo("custom-12").await()) + assertNotNull(map.styleSourceInfo("custom-mvt-12").await()) + assertNull(map.styleSourceInfo("custom-1").await()) + assertNull(map.styleSourceInfo("custom-mvt-1").await()) + } finally { + map.close() + runtime.close() + } } - } @Test - fun imageSourcesCanBeAddedUpdatedAndInspected() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - val image = PremultipliedRgba8Image(1, 1, 4, byteArrayOf(1, 2, 3, 4)) - val coordinates = imageCoordinates() - val moved = listOf(LatLng(1.0, 0.0), LatLng(1.0, 1.0), LatLng(0.0, 1.0), LatLng(0.0, 0.0)) - - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - map.addImageSourceUrl("overlay", coordinates, "https://example.com/image.png") - - assertEquals(SourceType.IMAGE, map.styleSourceType("overlay")) - assertEquals(coordinates, map.imageSourceCoordinates("overlay")) - map.setImageSourceUrl("overlay", "https://example.com/updated-image.png") - map.setImageSourceImage("overlay", image) - map.setImageSourceCoordinates("overlay", moved) - assertEquals(moved, map.imageSourceCoordinates("overlay")) - assertEquals(null, map.imageSourceCoordinates("missing-overlay")) - - map.addImageSourceImage("inline-overlay", coordinates, image) - assertEquals(SourceType.IMAGE, map.styleSourceInfo("inline-overlay")?.type) - } finally { - map.close() - runtime.close() - } - } + fun featureStateRoundTripsThroughTheMapStore(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - @Test - fun mapDebugControlsCanBeReadAndWritten() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.CONTINUOUS - }, - ) - - try { - assertTrue(map.debugOptions.isEmpty()) - map.debugOptions = setOf(DebugOption.TILE_BORDERS, DebugOption.TIMESTAMPS) - assertEquals(setOf(DebugOption.TILE_BORDERS, DebugOption.TIMESTAMPS), map.debugOptions) - map.debugOptions = emptySet() - assertTrue(map.debugOptions.isEmpty()) - - assertFalse(map.isRenderingStatsViewEnabled) - map.isRenderingStatsViewEnabled = true - assertTrue(map.isRenderingStatsViewEnabled) - map.isRenderingStatsViewEnabled = false - assertFalse(map.isRenderingStatsViewEnabled) - - map.requestRepaint() - map.isFullyLoaded - map.dumpDebugLogs() - } finally { - map.close() - runtime.close() - } - } + try { + val selector = FeatureStateSelector("point").apply { featureId = "feature-1" } - @Test - fun mapOptionPropertiesCanBeRoundTripped() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - mapMode = MapMode.STATIC - }, - ) - - try { - map.viewportOptions = - ViewportOptions().apply { - northOrientation = NorthOrientation.UP - constrainMode = ConstrainMode.HEIGHT_ONLY - viewportMode = ViewportMode.DEFAULT - frustumOffset = EdgeInsets.ZERO - } - val viewport = map.viewportOptions - assertEquals(NorthOrientation.UP, viewport.northOrientation) - assertEquals(ConstrainMode.HEIGHT_ONLY, viewport.constrainMode) - assertEquals(ViewportMode.DEFAULT, viewport.viewportMode) - assertEquals(EdgeInsets.ZERO, viewport.frustumOffset) - - map.tileOptions = - TileOptions().apply { - prefetchZoomDelta = 2 - lodMinRadius = 1.5 - lodScale = 2.5 - lodPitchThreshold = 30.0 - lodZoomShift = 1.0 - lodMode = TileLodMode.DEFAULT - } - val tile = map.tileOptions - assertEquals(2, tile.prefetchZoomDelta) - assertEquals(1.5, tile.lodMinRadius) - assertEquals(2.5, tile.lodScale) - assertEquals(30.0, tile.lodPitchThreshold) - assertEquals(1.0, tile.lodZoomShift) - assertEquals(TileLodMode.DEFAULT, tile.lodMode) - - map.projectionMode = - ProjectionModeOptions().apply { - axonometric = false - xSkew = 0.0 - ySkew = 0.0 + // The store answers before any source loads, and missing state reads as an empty object. + assertEquals("{}", map.getFeatureState(selector).await().decodeToString()) + + assertCommandCommitted( + runtime, + map + .setFeatureState(selector, """{"hover":true,"radius":20}""".encodeToByteArray()) + .await(), + ) + val stored = map.getFeatureState(selector).await().decodeToString() + assertTrue(stored.contains("\"hover\":true"), stored) + assertTrue(stored.contains("\"radius\":20"), stored) + + // State must be one JSON object. + assertFailsWith { + map.setFeatureState(selector, "[]".encodeToByteArray()).await() } - val projectionMode = map.projectionMode - assertEquals(false, projectionMode.axonometric) - assertEquals(0.0, projectionMode.xSkew) - assertEquals(0.0, projectionMode.ySkew) - } finally { - map.close() - runtime.close() + + // A state key narrows the removal to that one member. + assertCommandCommitted( + runtime, + map + .removeFeatureState( + FeatureStateSelector("point").apply { + featureId = "feature-1" + stateKey = "hover" + } + ) + .await(), + ) + val afterRemove = map.getFeatureState(selector).await().decodeToString() + assertFalse(afterRemove.contains("hover"), afterRemove) + assertTrue(afterRemove.contains("\"radius\":20"), afterRemove) + + assertCommandCommitted(runtime, map.removeFeatureState(selector).await()) + assertEquals("{}", map.getFeatureState(selector).await().decodeToString()) + } finally { + map.close() + runtime.close() + } } - } @Test - fun cameraCommandsAndProjectionCameraCanBeUsed() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - mapMode = MapMode.STATIC - }, - ) - - try { - val camera = - CameraOptions().apply { - center = LatLng(10.0, 20.0) - zoom = 3.0 - bearing = 15.0 - pitch = 20.0 - padding = EdgeInsets.ZERO - anchor = ScreenPoint(64.0, 64.0) - } - map.jumpTo(camera) - assertNear(requireNotNull(camera.center), requireNotNull(map.camera.center)) - assertEquals(3.0, map.camera.zoom ?: 0.0, 1e-6) - // BND-070: successive snapshots of an unchanged camera compare equal. - assertEquals(map.camera, map.camera) - - val animation = - AnimationOptions().apply { - durationMs = 0.0 - minZoom = 2.0 - easing = UnitBezier(0.0, 0.0, 1.0, 1.0) - } - map.easeTo(CameraOptions().apply { zoom = 3.25 }, animation) - map.flyTo(CameraOptions().apply { zoom = 3.5 }, null) - map.moveBy(0.0, 0.0) - map.moveByAnimated(0.0, 0.0, null) - map.scaleBy(1.0, null) - map.scaleByAnimated(1.0, ScreenPoint(64.0, 64.0), animation) - map.rotateBy(ScreenPoint(0.0, 0.0), ScreenPoint(1.0, 1.0)) - map.rotateByAnimated(ScreenPoint(0.0, 0.0), ScreenPoint(1.0, 1.0), null) - map.pitchBy(0.0) - map.pitchByAnimated(0.0, null) - map.cancelTransitions() - - assertFalse(map.isGestureInProgress) - map.isGestureInProgress = true - map.moveBy(8.0, -4.0) - assertTrue(map.isGestureInProgress) - map.isGestureInProgress = false - assertFalse(map.isGestureInProgress) - - val fit = - CameraFitOptions().apply { - padding = EdgeInsets.ZERO - bearing = 0.0 - pitch = 0.0 - } - val bounds = LatLngBounds(LatLng(-1.0, -1.0), LatLng(1.0, 1.0)) - assertTrue(map.cameraForLatLngBounds(bounds, fit).center != null) - assertTrue( - map.cameraForLatLngs(listOf(bounds.southwest, bounds.northeast), null).center != null - ) - assertTrue(map.cameraForGeometry(pointGeometry(), fit).center != null) - assertTrue(map.latLngBoundsForCamera(camera).southwest.latitude.isFinite()) - assertTrue(map.latLngBoundsForCameraUnwrapped(camera).southwest.latitude.isFinite()) - - val projection = map.createProjection() + fun tileSourcesCanBeAddedAndInspected(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() + try { - projection.setCamera(camera) - assertNear(requireNotNull(camera.center), requireNotNull(projection.camera.center)) - projection.setVisibleCoordinates( - listOf(bounds.southwest, bounds.northeast), - EdgeInsets.ZERO, + assertCommandCommitted( + runtime, + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await(), + ) + assertCommandCommitted( + runtime, + map + .addVectorSourceUrl( + "roads", + "https://example.com/vector.json", + TileSourceOptions().apply { + minZoom = 1.0 + maxZoom = 12.0 + attribution = "vector attribution" + scheme = TileScheme.XYZ + vectorEncoding = VectorTileEncoding.MVT + }, + ) + .await(), + ) + assertCommandCommitted( + runtime, + map + .addRasterSourceTiles( + "satellite", + listOf("https://example.com/raster/{z}/{x}/{y}.png"), + TileSourceOptions().apply { tileSize = 256 }, + ) + .await(), + ) + assertCommandCommitted( + runtime, + map + .addRasterDemSourceTiles( + "terrain", + listOf("https://example.com/terrain/{z}/{x}/{y}.png"), + TileSourceOptions().apply { + tileSize = 512 + rasterDemEncoding = RasterDemEncoding.TERRARIUM + }, + ) + .await(), + ) + + assertEquals(SourceType.VECTOR, map.styleSourceInfo("roads").await()?.type) + val rasterInfo = assertNotNull(map.styleSourceInfo("satellite").await()) + assertEquals(SourceType.RASTER, rasterInfo.type) + assertEquals(256, rasterInfo.tileSize) + assertEquals(SourceType.RASTER_DEM, map.styleSourceInfo("terrain").await()?.type) + assertEquals( + RasterDemEncoding.TERRARIUM, + map.styleSourceInfo("terrain").await()?.rasterDemEncoding, + ) + assertTrue( + map.styleSourceIds().await().containsAll(listOf("roads", "satellite", "terrain")) ) - assertTrue(projection.camera.center != null) - projection.setVisibleGeometry(pointGeometry(), EdgeInsets.ZERO) - assertTrue(projection.camera.center != null) } finally { - projection.close() + map.close() + runtime.close() } - - map.bounds = - BoundOptions().apply { - this.bounds = BoundsConstraint.Bounded(bounds) - minZoom = 1.0 - maxZoom = 10.0 - minPitch = 0.0 - maxPitch = 45.0 - } - val boundOptions = map.bounds - assertEquals(BoundsConstraint.Bounded(bounds), boundOptions.bounds) - assertEquals(1.0, boundOptions.minZoom ?: 0.0, 1e-6) - assertEquals(10.0, boundOptions.maxZoom ?: 0.0, 1e-6) - assertEquals(0.0, boundOptions.minPitch ?: -1.0, 1e-6) - assertEquals(45.0, boundOptions.maxPitch ?: 0.0, 1e-6) - - map.freeCameraOptions = - org.maplibre.nativeffi.camera.FreeCameraOptions().apply { - position = Vec3(0.0, 0.0, 1.0) - orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - } - assertTrue(map.freeCameraOptions.position != null) - assertTrue(map.freeCameraOptions.orientation != null) - } finally { - map.close() - runtime.close() } - } - - // BND-087, BND-102. @Test - fun cameraTransitionIdsReportEveryTerminalOutcomeOnce() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - - try { - // A zero-duration ease resolves inside the call. An id above Long.MAX_VALUE - // round-trips as the unsigned bit pattern the caller passed in. - val instantId = (Long.MAX_VALUE.toULong() + 1UL).toLong() - map.easeTo(CameraOptions().apply { zoom = 2.0 }, transitionAnimation(instantId, 0.0)) - val instant = drainCameraEvents(runtime) - assertEquals(listOf(instantId), instant.finishedTransitionIds) - assertEquals(CameraChangeMode.IMMEDIATE, instant.lastChangeMode) - - map.easeTo(CameraOptions().apply { zoom = 12.0 }, transitionAnimation(11L, 5_000.0)) - assertEquals(emptyList(), drainCameraEvents(runtime).finishedTransitionIds) - - // A later camera command supersedes the running transition, ending it. - map.easeTo(CameraOptions().apply { zoom = 13.0 }, transitionAnimation(12L, 5_000.0)) - val superseded = drainCameraEvents(runtime) - assertEquals(listOf(11L), superseded.finishedTransitionIds) - assertEquals(CameraChangeMode.ANIMATED, superseded.lastChangeMode) - - map.cancelTransitions() - assertEquals(listOf(12L), drainCameraEvents(runtime).finishedTransitionIds) - - // Omitting the id leaves the transition silent. - map.easeTo( - CameraOptions().apply { zoom = 14.0 }, - AnimationOptions().apply { durationMs = 0.0 }, - ) - assertEquals(emptyList(), drainCameraEvents(runtime).finishedTransitionIds) - } finally { - map.close() - runtime.close() - } - } + fun styleLayerJsonCanBeAddedInspectedListedAndRemoved(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - @Test - fun completedCameraTransitionReportsItsIdOnceAndReachesTheRequestedCamera() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) - - try { - map.easeTo(CameraOptions().apply { zoom = 5.0 }, transitionAnimation(21L, 5_000.0)) - // A still-image request runs a static map's pending transitions to their end. - map.requestStillImage() - - val finished = mutableListOf() - val cameraEventTypes = mutableListOf() - var rounds = 0 - while (finished.isEmpty() && rounds < 10_000) { - runtime.pump(0) - val events = drainCameraEvents(runtime) - finished += events.finishedTransitionIds - cameraEventTypes += events.cameraEventTypes - rounds++ - runtime.pump(1) - } - assertEquals(listOf(21L), finished) - assertEquals(5.0, map.camera.zoom ?: 0.0, 1e-6) - - // The transition reports its end once; later pumping adds nothing. - repeat(100) { - runtime.pump(0) - val events = drainCameraEvents(runtime) - finished += events.finishedTransitionIds - cameraEventTypes += events.cameraEventTypes + try { + assertCommandCommitted( + runtime, + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await(), + ) + assertCommandCommitted(runtime, map.addStyleLayerJson(backgroundLayer(), "").await()) + assertCommandCommitted(runtime, map.addLocationIndicatorLayer("puck", "").await()) + assertCommandCommitted( + runtime, + map.setLocationIndicatorLocation("puck", LatLng(12.0, 34.0), 56.0).await(), + ) + assertCommandCommitted(runtime, map.setLocationIndicatorBearing("puck", 78.0).await()) + assertCommandCommitted(runtime, map.setLocationIndicatorAccuracyRadius("puck", 9.0).await()) + assertCommandCommitted(runtime, map.moveStyleLayer("puck", "background").await()) + + assertEquals("background", map.styleLayerInfo("background").await()?.type) + assertNotNull(map.styleLayerInfo("puck").await()) + assertTrue(map.styleLayerIds().await().contains("background")) + assertTrue(map.styleLayerIds().await().contains("puck")) + assertTrue( + map + .styleLayerJson("background") + .await()!! + .decodeToString() + .contains("\"type\":\"background\"") + ) + assertCommandCommitted( + runtime, + map + .setLayerProperty("background", "background-opacity", "0.5".encodeToByteArray()) + .await(), + ) + assertEquals( + "0.5", + map.layerProperty("background", "background-opacity").await()?.decodeToString(), + ) + assertCommandCommitted( + runtime, + map.setStyleLightProperty("anchor", "\"viewport\"".encodeToByteArray()).await(), + ) + assertEquals("\"viewport\"", map.styleLightProperty("anchor").await()?.decodeToString()) + assertCommandCommitted(runtime, map.removeStyleLayer("background").await()) + assertCommandCommitted(runtime, map.removeStyleLayer("puck").await()) + assertNull(map.styleLayerInfo("background").await()) + assertCommandFailed(map.removeStyleLayer("background").await(), MaplibreStatus.NOT_FOUND) + } finally { + map.close() + runtime.close() } - assertEquals(listOf(21L), finished) - - // The transition reports its end ahead of the camera change that settles it. - val finishedIndex = cameraEventTypes.indexOf(RuntimeEventType.MAP_CAMERA_TRANSITION_FINISHED) - assertTrue( - cameraEventTypes.drop(finishedIndex + 1).contains(RuntimeEventType.MAP_CAMERA_DID_CHANGE), - "camera-did-change did not follow the transition: $cameraEventTypes", - ) - } finally { - map.close() - runtime.close() } - } - private fun transitionAnimation(transitionId: Long, durationMs: Double): AnimationOptions = - AnimationOptions().apply { - this.transitionId = transitionId - this.durationMs = durationMs - } + @Test + fun styleImageCanBeSetCopiedInspectedAndRemoved(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - private class CameraEvents( - val finishedTransitionIds: List, - val lastChangeMode: CameraChangeMode?, - /** Camera event types in queue order, so a test can assert their order. */ - val cameraEventTypes: List, - ) - - private fun drainCameraEvents(runtime: RuntimeHandle): CameraEvents { - val finished = mutableListOf() - val types = mutableListOf() - var lastChangeMode: CameraChangeMode? = null - for (event in runtime.drainEvents().events) { - when (event.type) { - RuntimeEventType.MAP_CAMERA_TRANSITION_FINISHED -> { - finished += - assertIs(event.payload).transitionId - types += event.type - } - RuntimeEventType.MAP_CAMERA_DID_CHANGE -> { - lastChangeMode = CameraChangeMode(event.code) - types += event.type - } + try { + val image = PremultipliedRgba8Image(1, 1, 4, byteArrayOf(1, 2, 3, 4)) + val options = + StyleImageOptions().apply { + pixelRatio = 2.0f + sdf = true + } + + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map.setStyleImage("dot", image, options) + + val info = map.styleImageInfo("dot").await() + assertEquals(1, info?.width) + assertEquals(1, info?.height) + assertEquals(4, info?.stride) + assertEquals(4, info?.byteLength) + assertEquals(2.0f, info?.pixelRatio) + assertEquals(true, info?.sdf) + assertEquals(image, map.copyStyleImagePremultipliedRgba8("dot").await()?.image) + assertEquals(2.0f, map.copyStyleImagePremultipliedRgba8("dot").await()?.pixelRatio) + assertEquals(true, map.copyStyleImagePremultipliedRgba8("dot").await()?.sdf) + assertCommandCommitted(runtime, map.removeStyleImage("dot").await()) + assertNull(map.styleImageInfo("dot").await()) + assertCommandFailed(map.removeStyleImage("dot").await(), MaplibreStatus.NOT_FOUND) + } finally { + map.close() + runtime.close() } } - return CameraEvents(finished, lastChangeMode, types) - } @Test - fun boundsConstraintDistinguishesUnboundedFromWorldBounds() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - mapMode = MapMode.STATIC - }, - ) - - fun jumpedLongitude(longitude: Double): Double { - map.jumpTo( - CameraOptions().apply { - center = LatLng(0.0, longitude) - zoom = 2.0 - } - ) - return requireNotNull(map.camera.center).longitude - } + fun imageSourcesCanBeAddedUpdatedAndInspected(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() - try { - // An unbounded map wraps across the antimeridian. - assertEquals(BoundsConstraint.Unbounded, map.bounds.bounds) - assertEquals(-160.0, jumpedLongitude(200.0), 1e-6) - - val world = LatLngBounds(LatLng(-90.0, -180.0), LatLng(90.0, 180.0)) - map.bounds = BoundOptions().apply { bounds = BoundsConstraint.Bounded(world) } - assertEquals(BoundsConstraint.Bounded(world), map.bounds.bounds) - // World bounds clamp at the antimeridian instead of wrapping. - assertEquals(180.0, jumpedLongitude(200.0), 1e-6) - - map.bounds = BoundOptions().apply { bounds = BoundsConstraint.Unbounded } - assertEquals(BoundsConstraint.Unbounded, map.bounds.bounds) - // Releasing the constraint restores antimeridian wrapping. - assertEquals(-160.0, jumpedLongitude(200.0), 1e-6) - } finally { - map.close() - runtime.close() + try { + val image = PremultipliedRgba8Image(1, 1, 4, byteArrayOf(1, 2, 3, 4)) + val coordinates = imageCoordinates() + val moved = listOf(LatLng(1.0, 0.0), LatLng(1.0, 1.0), LatLng(0.0, 1.0), LatLng(0.0, 0.0)) + + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + map.addImageSourceUrl("overlay", coordinates, "https://example.com/image.png").await() + + assertEquals(SourceType.IMAGE, map.styleSourceInfo("overlay").await()?.type) + assertEquals(coordinates, map.imageSourceCoordinates("overlay").await()) + map.setImageSourceUrl("overlay", "https://example.com/updated-image.png").await() + map.setImageSourceImage("overlay", image).await() + map.setImageSourceCoordinates("overlay", moved).await() + assertEquals(moved, map.imageSourceCoordinates("overlay").await()) + assertEquals(null, map.imageSourceCoordinates("missing-overlay").await()) + + map.addImageSourceImage("inline-overlay", coordinates, image) + assertEquals(SourceType.IMAGE, map.styleSourceInfo("inline-overlay").await()?.type) + } finally { + map.close() + runtime.close() + } } - } + // The command completion generation fences a later snapshot: a snapshot at or past it + // observes the commit. @Test - fun mapCoordinateConversionsCanBeRoundTripped() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - mapMode = MapMode.STATIC - }, - ) - - try { - val coordinate = LatLng(0.0, 0.0) - val point = map.pixelForLatLng(coordinate) - val roundTrip = map.latLngForPixel(point) - assertNear(coordinate, roundTrip) - - val coordinates = listOf(LatLng(0.0, 0.0), LatLng(10.0, 20.0)) - val points = map.pixelsForLatLngs(coordinates) - assertEquals(coordinates.size, points.size) - val coordinateRoundTrips = map.latLngsForPixels(points) - assertEquals(coordinates.size, coordinateRoundTrips.size) - coordinates.zip(coordinateRoundTrips).forEach { (expected, actual) -> - assertNear(expected, actual) + fun committedCommandGenerationFencesTheSnapshot(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + .use { map -> + val debug = setOf(DebugOption.TILE_BORDERS, DebugOption.OVERDRAW) + val generation = map.setDebugOptions(debug).await().generation.toLong() + assertTrue(generation > 0L, "committed command must publish a generation") + val snapshot = map.snapshot() + assertTrue(snapshot.generation >= generation) + assertEquals(debug, snapshot.debugOptions) + } } - - assertEquals(emptyList(), map.pixelsForLatLngs(emptyList())) - assertEquals(emptyList(), map.latLngsForPixels(emptyList())) - } finally { - map.close() - runtime.close() } - } @Test - fun mapProjectionCoordinateConversionsCanBeRoundTrippedAndClosed() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - mapMode = MapMode.STATIC - }, - ) - - try { - val projection = map.createProjection() - assertFalse(projection.isClosed) - val coordinate = LatLng(0.0, 0.0) - val point = projection.pixelForLatLng(coordinate) - val roundTrip = projection.latLngForPixel(point) - assertNear(coordinate, roundTrip) - - map.close() - val detachedPoint = projection.pixelForLatLng(coordinate) - assertNear(coordinate, projection.latLngForPixel(detachedPoint)) - - projection.close() - projection.close() - assertTrue(projection.isClosed) - assertFailsWith { projection.pixelForLatLng(coordinate) } - } finally { - map.close() - runtime.close() + fun snapshotFieldsRoundTripThroughTheirSetCommands(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + .use { map -> + assertEquals(emptySet(), map.snapshot().debugOptions) + assertFalse(map.snapshot().renderingStatsViewEnabled) + + assertCommandCommitted(runtime, map.setRenderingStatsViewEnabled(true).await()) + assertTrue(map.snapshot().renderingStatsViewEnabled) + + val viewport = ViewportOptions().apply { northOrientation = NorthOrientation.DOWN } + assertCommandCommitted(runtime, map.setViewportOptions(viewport).await()) + assertEquals(NorthOrientation.DOWN, map.snapshot().viewportOptions.northOrientation) + + val tile = + TileOptions().apply { + prefetchZoomDelta = 3 + lodScale = 1.5 + } + assertCommandCommitted(runtime, map.setTileOptions(tile).await()) + val tileSnapshot = map.snapshot().tileOptions + assertEquals(3, tileSnapshot.prefetchZoomDelta) + assertEquals(1.5, tileSnapshot.lodScale) + + val bounds = + BoundOptions().apply { + minZoom = 2.0 + maxZoom = 15.0 + this.bounds = + BoundsConstraint.Bounded(LatLngBounds(LatLng(-10.0, -10.0), LatLng(10.0, 10.0))) + } + assertCommandCommitted(runtime, map.setBounds(bounds).await()) + val boundsSnapshot = map.snapshot().bounds + assertEquals(2.0, boundsSnapshot.minZoom) + assertEquals(15.0, boundsSnapshot.maxZoom) + assertEquals(bounds.bounds, boundsSnapshot.bounds) + + val freeCamera = + FreeCameraOptions().apply { + position = Vec3(0.5, 0.5, 0.5) + orientation = Quaternion(0.0, 0.0, 0.0, 1.0) + } + assertCommandCommitted(runtime, map.setFreeCameraOptions(freeCamera).await()) + val freeCameraSnapshot = map.snapshot().freeCameraOptions + kotlin.test.assertNotNull(freeCameraSnapshot.position) + kotlin.test.assertNotNull(freeCameraSnapshot.orientation) + } + } } - } private fun geoJsonSource(): ByteArray = "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\",\"features\":[]}}" @@ -1213,44 +1044,52 @@ class MapHandleTest { private fun backgroundLayer(): ByteArray = "{\"id\":\"background\",\"type\":\"background\"}".encodeToByteArray() - private fun pointGeometry(): ByteArray = - "{\"type\":\"Point\",\"coordinates\":[0,0]}".encodeToByteArray() - private fun imageCoordinates(): List = listOf(LatLng(0.0, 0.0), LatLng(0.0, 1.0), LatLng(1.0, 1.0), LatLng(1.0, 0.0)) - private fun assertNear(expected: LatLng, actual: LatLng) { - assertEquals(expected.latitude, actual.latitude, 1e-6) - assertEquals(expected.longitude, actual.longitude, 1e-6) + private fun assertCommandCommitted( + @Suppress("UNUSED_PARAMETER") runtime: RuntimeHandle, + completion: CommandCompletion, + ) { + assertEquals(CommandDisposition.COMMITTED, completion.disposition) + assertEquals(MaplibreStatus.OK, completion.status) + } + + private fun assertCommandFailed(completion: CommandCompletion, status: MaplibreStatus) { + assertEquals(CommandDisposition.FAILED, completion.disposition) + assertEquals(status, completion.status) + assertTrue(completion.diagnostic.isNotEmpty()) } @Test - fun loadedStyleDocumentAndUrlReadBackWhatWasLoaded() { - val styleJson = "{\"version\":8,\"sources\":{},\"layers\":[]}" - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - .use { map -> - assertTrue(map.loadedStyleJson().isEmpty()) - assertEquals("", map.styleUrl()) - - // The document reads back byte-for-byte. - map.setStyleJson(styleJson.encodeToByteArray()) - assertEquals(styleJson, map.loadedStyleJson().decodeToString()) - // Inline JSON clears the URL. - assertEquals("", map.styleUrl()) - - // setStyleUrl records request state before the load can succeed; the document - // still reports the style that last parsed. - map.setStyleUrl("https://example.com/style.json") - assertEquals("https://example.com/style.json", map.styleUrl()) - assertEquals(styleJson, map.loadedStyleJson().decodeToString()) - } + fun loadedStyleDocumentAndUrlReadBackWhatWasLoaded(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val styleJson = "{\"version\":8,\"sources\":{},\"layers\":[]}" + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + .use { map -> + assertTrue(map.loadedStyleJson().await().isEmpty()) + assertEquals("", map.styleUrl().await()) + + // The document reads back byte-for-byte. + map.setStyleJson(styleJson.encodeToByteArray()).await() + assertEquals(styleJson, map.loadedStyleJson().await().decodeToString()) + // Inline JSON clears the URL. + assertEquals("", map.styleUrl().await()) + + // setStyleUrl records request state before the load can succeed; the document + // still reports the style that last parsed. + map.setStyleUrl("https://example.com/style.json").await() + assertEquals("https://example.com/style.json", map.styleUrl().await()) + assertEquals(styleJson, map.loadedStyleJson().await().decodeToString()) + } + } } - } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapProjectionHandleTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapProjectionHandleTest.kt index 91ce0c565..a57a4b0b1 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapProjectionHandleTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/map/MapProjectionHandleTest.kt @@ -6,13 +6,17 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.EdgeInsets -import org.maplibre.nativeffi.error.InvalidStateException import org.maplibre.nativeffi.geo.LatLng +import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.runOnBackgroundThread import org.maplibre.nativeffi.runtime.RuntimeHandle @@ -21,31 +25,40 @@ class MapProjectionHandleTest { // BND-043, BND-103. @Test - fun projectionOwnsStandaloneSnapshotAndClosesIndependently() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - try { + fun projectionOwnsStandaloneSnapshotAndClosesIndependently(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) val map = MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - scaleFactor = 1.0 - }, - ) - val projection = map.createProjection() + runtime, + MapOptions().apply { + width = 64 + height = 64 + scaleFactor = 1.0 + }, + ) + .await() + val projection = map.createProjection().await() assertFalse(projection.isClosed) + // Every projection call is synchronous: a setter is applied before it returns, so the + // next read or conversion observes it. projection.setCamera( CameraOptions().apply { center = LatLng(0.0, 0.0) zoom = 2.0 } ) - val camera = projection.camera - kotlin.test.assertNotNull(camera.center) - kotlin.test.assertNotNull(camera.zoom) - projection.setVisibleCoordinates(listOf(LatLng(0.0, 0.0), LatLng(1.0, 1.0)), EdgeInsets.ZERO) + val camera = projection.camera() + assertNotNull(camera.center) + assertEquals(2.0, camera.zoom) + val zoomedIn = projection.pixelForLatLng(LatLng(10.0, 10.0)) + projection.setVisibleCoordinates( + listOf(LatLng(-60.0, -170.0), LatLng(60.0, 170.0)), + EdgeInsets.ZERO, + ) + val zoomedOut = projection.pixelForLatLng(LatLng(10.0, 10.0)) + assertFalse(zoomedIn == zoomedOut, "a setter changes later conversions") projection.setVisibleGeometry( "{\"type\":\"LineString\",\"coordinates\":[[0,0],[1,1]]}".encodeToByteArray(), EdgeInsets.ZERO, @@ -59,40 +72,106 @@ class MapProjectionHandleTest { assertEquals(0.0, projectedCoordinate.latitude, 0.000001) assertEquals(0.0, projectedCoordinate.longitude, 0.000001) + // A projection is usable from another thread. + val offThreadRoundTrip = + withContext(Dispatchers.Default) { + projection.latLngForPixel(projection.pixelForLatLng(LatLng(0.0, 0.0))) + } + assertEquals(0.0, offThreadRoundTrip.latitude, 0.000001) + assertEquals(0.0, offThreadRoundTrip.longitude, 0.000001) + map.close() - kotlin.test.assertNotNull(projection.camera.zoom) - val pointAfterMapClose = projection.pixelForLatLng(LatLng(0.0, 0.0)) - val coordinateAfterMapClose = projection.latLngForPixel(pointAfterMapClose) - assertEquals(0.0, coordinateAfterMapClose.latitude, 0.000001) - assertEquals(0.0, coordinateAfterMapClose.longitude, 0.000001) + runtime.close() + // The projection owns its snapshot independently of both source handles. + assertNotNull(projection.camera().zoom) projection.close() assertTrue(projection.isClosed) projection.close() - assertFailsWith { projection.camera } - } finally { - runtime.close() + assertFailsWith { projection.camera() } } - } @Test - fun projectionRemainsUsableOnAnotherThreadAfterMapClose() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val map = MapHandle.create(runtime, MapOptions()) - val projection = map.createProjection() - map.close() - runtime.close() - val failure = AtomicReference(null) - - runOnBackgroundThread { + fun projectionObservesMapCommandsAcceptedBeforeCreation(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + scaleFactor = 1.0 + }, + ) + .await() try { - projection.camera - projection.close() - } catch (error: Throwable) { - failure.store(error) + map + .updateCamera( + CameraUpdate( + camera = + CameraOptions().apply { + center = LatLng(30.0, 40.0) + zoom = 5.0 + } + ) + ) + .await() + + // Creation copies the map transform after every earlier map command. + val projection = map.createProjection().await() + try { + assertEquals(5.0, projection.camera().zoom) + assertEquals(30.0, assertNotNull(projection.camera().center).latitude, 0.000001) + + // A later map camera command is never observed by the existing projection. + map.updateCamera(CameraUpdate(camera = CameraOptions().apply { zoom = 9.0 })).await() + map.queryCamera().await() + assertEquals(5.0, projection.camera().zoom) + + // The commands accepted before this call reached the map: the center maps back to + // itself through the projection's own frozen transform. + val center = projection.latLngForPixel(ScreenPoint(32.0, 32.0)) + assertEquals(30.0, center.latitude, 0.5) + assertEquals(40.0, center.longitude, 0.5) + } finally { + projection.close() + } + } finally { + map.close() + runtime.close() } } - assertNull(failure.load()) - } + @Test + fun projectionRemainsUsableOnAnotherThreadAfterMapClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + scaleFactor = 1.0 + }, + ) + .await() + val projection = map.createProjection().await() + map.close() + runtime.close() + val failure = AtomicReference(null) + + // A projection stays usable, and closable, on a thread that never touched the map. + runOnBackgroundThread { + try { + projection.camera() + projection.close() + } catch (error: Throwable) { + failure.store(error) + } + } + + assertNull(failure.load()) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/ActiveFrameStateTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/ActiveFrameStateTest.kt index 4c02d7747..79c653ec1 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/ActiveFrameStateTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/ActiveFrameStateTest.kt @@ -10,28 +10,30 @@ class ActiveFrameStateTest { // BND-170. @Test - fun activeFrameRejectsForbiddenSessionOperations() { - val state = ActiveFrameState() + fun activeFrameRejectsForbiddenSessionOperations(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = ActiveFrameState() - state.beginAcquire() - val error = assertFailsWith { state.ensureInactive("render") } - assertEquals(MaplibreStatus.INVALID_STATE, error.status) - assertEquals(MaplibreStatus.INVALID_STATE.nativeCode, error.nativeStatusCode) - assertEquals( - "RenderSessionHandle cannot render while a texture frame is acquired", - error.diagnostic, - ) - assertFailsWith { state.beginAcquire() } - } + state.beginAcquire() + val error = assertFailsWith { state.ensureInactive("render") } + assertEquals(MaplibreStatus.INVALID_STATE, error.status) + assertEquals(MaplibreStatus.INVALID_STATE.nativeCode, error.nativeStatusCode) + assertEquals( + "RenderSessionHandle cannot render while a texture frame is acquired", + error.diagnostic, + ) + assertFailsWith { state.beginAcquire() } + } @Test - fun endingFrameBorrowAllowsLaterOperationsAndAcquisition() { - val state = ActiveFrameState() + fun endingFrameBorrowAllowsLaterOperationsAndAcquisition(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = ActiveFrameState() - state.beginAcquire() - state.endBorrow() + state.beginAcquire() + state.endBorrow() - state.ensureInactive("render") - state.beginAcquire() - } + state.ensureInactive("render") + state.beginAcquire() + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt index f45c443c7..7fafe951e 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt @@ -7,82 +7,86 @@ import kotlin.test.assertSame class FrameAcquirePolicyTest { @Test - fun wrapperFailureAfterNativeAcquireReleasesNativeFrameAndClosesLocalState() { - val failure = IllegalArgumentException("frame metadata overflow") - var released = 0 - var closed = 0 + fun wrapperFailureAfterNativeAcquireReleasesNativeFrameAndClosesLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val failure = IllegalArgumentException("frame metadata overflow") + var released = 0 + var closed = 0 - val thrown = - assertFailsWith { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired = true, - releaseNative = { released += 1 }, - closeLocal = { closed += 1 }, - failure = failure, - ) - } + val thrown = + assertFailsWith { + FrameAcquirePolicy.cleanupAfterWrapperFailure( + acquired = true, + releaseNative = { released += 1 }, + closeLocal = { closed += 1 }, + failure = failure, + ) + } - assertSame(failure, thrown) - assertEquals(1, released) - assertEquals(1, closed) - } + assertSame(failure, thrown) + assertEquals(1, released) + assertEquals(1, closed) + } @Test - fun wrapperFailureBeforeNativeAcquireOnlyClosesLocalState() { - val failure = IllegalArgumentException("native acquire failed") - var released = 0 - var closed = 0 + fun wrapperFailureBeforeNativeAcquireOnlyClosesLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val failure = IllegalArgumentException("native acquire failed") + var released = 0 + var closed = 0 - val thrown = - assertFailsWith { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired = false, - releaseNative = { released += 1 }, - closeLocal = { closed += 1 }, - failure = failure, - ) - } + val thrown = + assertFailsWith { + FrameAcquirePolicy.cleanupAfterWrapperFailure( + acquired = false, + releaseNative = { released += 1 }, + closeLocal = { closed += 1 }, + failure = failure, + ) + } - assertSame(failure, thrown) - assertEquals(0, released) - assertEquals(1, closed) - } + assertSame(failure, thrown) + assertEquals(0, released) + assertEquals(1, closed) + } @Test - fun wrapperFailureStillClosesLocalStateWhenNativeFrameReleaseFails() { - val failure = IllegalArgumentException("frame copy failed") - var closed = 0 + fun wrapperFailureStillClosesLocalStateWhenNativeFrameReleaseFails(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val failure = IllegalArgumentException("frame copy failed") + var closed = 0 - val thrown = - assertFailsWith { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired = true, - releaseNative = { throw IllegalStateException("wrong thread") }, - closeLocal = { closed += 1 }, - failure = failure, - ) - } + val thrown = + assertFailsWith { + FrameAcquirePolicy.cleanupAfterWrapperFailure( + acquired = true, + releaseNative = { throw IllegalStateException("wrong thread") }, + closeLocal = { closed += 1 }, + failure = failure, + ) + } - assertSame(failure, thrown) - assertEquals(1, closed) - } + assertSame(failure, thrown) + assertEquals(1, closed) + } @Test - fun localCleanupFailureDoesNotReplaceOriginalFailure() { - val failure = IllegalArgumentException("frame copy failed") - var released = 0 + fun localCleanupFailureDoesNotReplaceOriginalFailure(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val failure = IllegalArgumentException("frame copy failed") + var released = 0 - val thrown = - assertFailsWith { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired = true, - releaseNative = { released += 1 }, - closeLocal = { throw IllegalStateException("cleanup failed") }, - failure = failure, - ) - } + val thrown = + assertFailsWith { + FrameAcquirePolicy.cleanupAfterWrapperFailure( + acquired = true, + releaseNative = { released += 1 }, + closeLocal = { throw IllegalStateException("cleanup failed") }, + failure = failure, + ) + } - assertSame(failure, thrown) - assertEquals(1, released) - } + assertSame(failure, thrown) + assertEquals(1, released) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameReleasePolicyTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameReleasePolicyTest.kt index 7381f5a1f..781da7b94 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameReleasePolicyTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameReleasePolicyTest.kt @@ -7,56 +7,59 @@ import kotlin.test.assertSame class FrameReleasePolicyTest { @Test - fun successfulReleaseClosesLocalState() { - var released = false - var locallyClosed = false + fun successfulReleaseClosesLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var released = false + var locallyClosed = false - FrameReleasePolicy.close( - isClosed = { locallyClosed }, - releaseNative = { released = true }, - ownerClosed = { false }, - closeLocal = { locallyClosed = true }, - ) + FrameReleasePolicy.close( + isClosed = { locallyClosed }, + releaseNative = { released = true }, + ownerClosed = { false }, + closeLocal = { locallyClosed = true }, + ) - assertEquals(true, released) - assertEquals(true, locallyClosed) - } + assertEquals(true, released) + assertEquals(true, locallyClosed) + } @Test - fun liveOwnerReleaseFailureLeavesLocalStateRetryable() { - val failure = IllegalStateException("wrong thread") - var attempts = 0 - var locallyClosed = false - - val thrown = - assertFailsWith { - FrameReleasePolicy.close( - isClosed = { locallyClosed }, - releaseNative = { - attempts += 1 - throw failure - }, - ownerClosed = { false }, - closeLocal = { locallyClosed = true }, - ) - } - - assertSame(failure, thrown) - assertEquals(1, attempts) - assertEquals(false, locallyClosed) - } + fun liveOwnerReleaseFailureLeavesLocalStateRetryable(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val failure = IllegalStateException("wrong thread") + var attempts = 0 + var locallyClosed = false + + val thrown = + assertFailsWith { + FrameReleasePolicy.close( + isClosed = { locallyClosed }, + releaseNative = { + attempts += 1 + throw failure + }, + ownerClosed = { false }, + closeLocal = { locallyClosed = true }, + ) + } + + assertSame(failure, thrown) + assertEquals(1, attempts) + assertEquals(false, locallyClosed) + } @Test - fun closedOwnerReleaseFailureConsumesLocalState() { - var locallyClosed = false - - FrameReleasePolicy.close( - isClosed = { locallyClosed }, - releaseNative = { throw IllegalStateException("owner closed") }, - ownerClosed = { true }, - closeLocal = { locallyClosed = true }, - ) - - assertEquals(true, locallyClosed) - } + fun closedOwnerReleaseFailureConsumesLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var locallyClosed = false + + FrameReleasePolicy.close( + isClosed = { locallyClosed }, + releaseNative = { throw IllegalStateException("owner closed") }, + ownerClosed = { true }, + closeLocal = { locallyClosed = true }, + ) + + assertEquals(true, locallyClosed) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameScopeTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameScopeTest.kt index 229c39140..07e627a9a 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameScopeTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/FrameScopeTest.kt @@ -8,96 +8,99 @@ class FrameScopeTest { // BND-168, BND-173. @Test - fun metalFrameRejectsAccessAfterScopeCloses() { - val scope = FrameScope() - val highBit = Long.MAX_VALUE.toULong() + 1UL - val frame = - MetalOwnedTextureFrame( - scope, - highBit.toLong(), - 2, - 3, - 2.0, - ULong.MAX_VALUE.toLong(), - NativePointer.ofAddress(0x10L), - NativePointer.ofAddress(0x20L), - highBit.toLong(), - ) + fun metalFrameRejectsAccessAfterScopeCloses(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val scope = FrameScope() + val highBit = Long.MAX_VALUE.toULong() + 1UL + val frame = + MetalOwnedTextureFrame( + scope, + highBit.toLong(), + 2, + 3, + 2.0, + ULong.MAX_VALUE.toLong(), + NativePointer.ofAddress(0x10L), + NativePointer.ofAddress(0x20L), + highBit.toLong(), + ) - assertEquals(highBit.toLong(), frame.generation()) - assertEquals(2, frame.width()) - assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) - assertEquals(highBit.toLong(), frame.pixelFormat()) - assertEquals(NativePointer.ofAddress(0x10L), frame.texture()) - val retainedTexture = NativePointer.scoped(0x10L, scope) - scope.close() - assertFailsWith { frame.width() } - assertFailsWith { frame.texture() } - assertFailsWith { retainedTexture.address } - assertFailsWith { retainedTexture.toString() } - assertFailsWith { retainedTexture.hashCode() } - assertFailsWith { - retainedTexture.equals(NativePointer.ofAddress(0x10L)) + assertEquals(highBit.toLong(), frame.generation()) + assertEquals(2, frame.width()) + assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) + assertEquals(highBit.toLong(), frame.pixelFormat()) + assertEquals(NativePointer.ofAddress(0x10L), frame.texture()) + val retainedTexture = NativePointer.scoped(0x10L, scope) + scope.close() + assertFailsWith { frame.width() } + assertFailsWith { frame.texture() } + assertFailsWith { retainedTexture.address } + assertFailsWith { retainedTexture.toString() } + assertFailsWith { retainedTexture.hashCode() } + assertFailsWith { + retainedTexture.equals(NativePointer.ofAddress(0x10L)) + } } - } @Test - fun vulkanFrameRejectsAccessAfterScopeCloses() { - val scope = FrameScope() - val highBit = Long.MAX_VALUE.toULong() + 1UL - val frame = - VulkanOwnedTextureFrame( - scope, - highBit.toLong(), - 2, - 3, - 2.0, - ULong.MAX_VALUE.toLong(), - NativePointer.ofAddress(0x10L), - NativePointer.ofAddress(0x20L), - NativePointer.ofAddress(0x30L), - UInt.MAX_VALUE.toInt(), - 0x8000_0000U.toInt(), - ) + fun vulkanFrameRejectsAccessAfterScopeCloses(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val scope = FrameScope() + val highBit = Long.MAX_VALUE.toULong() + 1UL + val frame = + VulkanOwnedTextureFrame( + scope, + highBit.toLong(), + 2, + 3, + 2.0, + ULong.MAX_VALUE.toLong(), + NativePointer.ofAddress(0x10L), + NativePointer.ofAddress(0x20L), + NativePointer.ofAddress(0x30L), + UInt.MAX_VALUE.toInt(), + 0x8000_0000U.toInt(), + ) - assertEquals(highBit.toLong(), frame.generation()) - assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) - assertEquals(UInt.MAX_VALUE.toInt(), frame.format()) - assertEquals(0x8000_0000U.toInt(), frame.layout()) - assertEquals(NativePointer.ofAddress(0x20L), frame.imageView()) - scope.close() - assertFailsWith { frame.format() } - assertFailsWith { frame.imageView() } - } + assertEquals(highBit.toLong(), frame.generation()) + assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) + assertEquals(UInt.MAX_VALUE.toInt(), frame.format()) + assertEquals(0x8000_0000U.toInt(), frame.layout()) + assertEquals(NativePointer.ofAddress(0x20L), frame.imageView()) + scope.close() + assertFailsWith { frame.format() } + assertFailsWith { frame.imageView() } + } @Test - fun openglFramePreservesHighBitTextureValues() { - val scope = FrameScope() - val highBit = Long.MAX_VALUE.toULong() + 1UL - val frame = - OpenGLOwnedTextureFrame( - scope, - highBit.toLong(), - 2, - 3, - 2.0, - ULong.MAX_VALUE.toLong(), - UInt.MAX_VALUE.toInt(), - 0x0de1, - 0x8000_8058U.toInt(), - 0x8000_1908U.toInt(), - 0x8000_1401U.toInt(), - ) + fun openglFramePreservesHighBitTextureValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val scope = FrameScope() + val highBit = Long.MAX_VALUE.toULong() + 1UL + val frame = + OpenGLOwnedTextureFrame( + scope, + highBit.toLong(), + 2, + 3, + 2.0, + ULong.MAX_VALUE.toLong(), + UInt.MAX_VALUE.toInt(), + 0x0de1, + 0x8000_8058U.toInt(), + 0x8000_1908U.toInt(), + 0x8000_1401U.toInt(), + ) - assertEquals(highBit.toLong(), frame.generation()) - assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) - assertEquals(UInt.MAX_VALUE.toInt(), frame.texture()) - assertEquals(0x8000_8058U.toInt(), frame.internalFormat()) - assertEquals(0x8000_1908U.toInt(), frame.format()) - assertEquals(0x8000_1401U.toInt(), frame.type()) - scope.close() - assertFailsWith { frame.width() } - assertFailsWith { frame.texture() } - assertFailsWith { frame.internalFormat() } - } + assertEquals(highBit.toLong(), frame.generation()) + assertEquals(ULong.MAX_VALUE.toLong(), frame.frameId()) + assertEquals(UInt.MAX_VALUE.toInt(), frame.texture()) + assertEquals(0x8000_8058U.toInt(), frame.internalFormat()) + assertEquals(0x8000_1908U.toInt(), frame.format()) + assertEquals(0x8000_1401U.toInt(), frame.type()) + scope.close() + assertFailsWith { frame.width() } + assertFailsWith { frame.texture() } + assertFailsWith { frame.internalFormat() } + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/NativeBufferTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/NativeBufferTest.kt index bcb35d16b..2d6386b86 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/NativeBufferTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/NativeBufferTest.kt @@ -9,36 +9,39 @@ class NativeBufferTest { // BND-166. @Test - fun nativeBufferTracksLengthAndRejectsAfterClose() { - val buffer = NativeBuffer.allocate(4L) + fun nativeBufferTracksLengthAndRejectsAfterClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val buffer = NativeBuffer.allocate(4L) - assertEquals(4L, buffer.byteLength()) - assertEquals(4, buffer.toByteArray().size) + assertEquals(4L, buffer.byteLength()) + assertEquals(4, buffer.toByteArray().size) - buffer.close() - buffer.close() - assertFailsWith { buffer.byteLength() } - } + buffer.close() + buffer.close() + assertFailsWith { buffer.byteLength() } + } @Test - fun zeroLengthBufferHasNoBytes() { - NativeBuffer.allocate(0L).use { buffer -> - assertEquals(0L, buffer.byteLength()) - assertEquals(0, buffer.toByteArray().size) + fun zeroLengthBufferHasNoBytes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeBuffer.allocate(0L).use { buffer -> + assertEquals(0L, buffer.byteLength()) + assertEquals(0, buffer.toByteArray().size) + } } - } // BND-069. @Test - fun premultipliedImagePixelsSnapshotAndReturnCopies() { - val source = byteArrayOf(1, 2, 3, 4) - val image = PremultipliedRgba8Image(1, 1, 4, source) - source[0] = 9 - - val first = image.pixels - assertContentEquals(byteArrayOf(1, 2, 3, 4), first) - first[0] = 8 - assertContentEquals(byteArrayOf(1, 2, 3, 4), image.pixels) - } + fun premultipliedImagePixelsSnapshotAndReturnCopies(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val source = byteArrayOf(1, 2, 3, 4) + val image = PremultipliedRgba8Image(1, 1, 4, source) + source[0] = 9 + + val first = image.pixels + assertContentEquals(byteArrayOf(1, 2, 3, 4), first) + first[0] = 8 + assertContentEquals(byteArrayOf(1, 2, 3, 4), image.pixels) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCoreTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCoreTest.kt index 430273ed0..21e8436f5 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCoreTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureFrameHandleCoreTest.kt @@ -10,83 +10,87 @@ import org.maplibre.nativeffi.error.MaplibreStatus class OwnedTextureFrameHandleCoreTest { @Test - fun closeReleasesNativeThenLocalState() { - val core = OwnedTextureFrameHandleCore("TestFrameHandle") - var nativeReleases = 0 - var localReleases = 0 + fun closeReleasesNativeThenLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val core = OwnedTextureFrameHandleCore("TestFrameHandle") + var nativeReleases = 0 + var localReleases = 0 - core.close( - releaseNative = { nativeReleases++ }, - ownerClosed = { false }, - releaseLocal = { localReleases++ }, - ) - core.close( - releaseNative = { nativeReleases++ }, - ownerClosed = { false }, - releaseLocal = { localReleases++ }, - ) + core.close( + releaseNative = { nativeReleases++ }, + ownerClosed = { false }, + releaseLocal = { localReleases++ }, + ) + core.close( + releaseNative = { nativeReleases++ }, + ownerClosed = { false }, + releaseLocal = { localReleases++ }, + ) - assertTrue(core.isClosed()) - assertEquals(1, nativeReleases) - assertEquals(1, localReleases) - } + assertTrue(core.isClosed()) + assertEquals(1, nativeReleases) + assertEquals(1, localReleases) + } @Test - fun liveOwnerNativeReleaseFailureLeavesLocalStateRetryable() { - val core = OwnedTextureFrameHandleCore("TestFrameHandle") - val failure = IllegalStateException("wrong thread") - var nativeReleases = 0 - var localReleases = 0 + fun liveOwnerNativeReleaseFailureLeavesLocalStateRetryable(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val core = OwnedTextureFrameHandleCore("TestFrameHandle") + val failure = IllegalStateException("wrong thread") + var nativeReleases = 0 + var localReleases = 0 - val thrown = - assertFailsWith { - core.close( - releaseNative = { - nativeReleases++ - throw failure - }, - ownerClosed = { false }, - releaseLocal = { localReleases++ }, - ) - } + val thrown = + assertFailsWith { + core.close( + releaseNative = { + nativeReleases++ + throw failure + }, + ownerClosed = { false }, + releaseLocal = { localReleases++ }, + ) + } - assertSame(failure, thrown) - assertEquals(1, nativeReleases) - assertEquals(0, localReleases) - assertEquals(false, core.isClosed()) - } + assertSame(failure, thrown) + assertEquals(1, nativeReleases) + assertEquals(0, localReleases) + assertEquals(false, core.isClosed()) + } @Test - fun closedOwnerNativeReleaseFailureConsumesLocalState() { - val core = OwnedTextureFrameHandleCore("TestFrameHandle") - var localReleases = 0 + fun closedOwnerNativeReleaseFailureConsumesLocalState(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val core = OwnedTextureFrameHandleCore("TestFrameHandle") + var localReleases = 0 - core.close( - releaseNative = { throw IllegalStateException("owner closed") }, - ownerClosed = { true }, - releaseLocal = { localReleases++ }, - ) + core.close( + releaseNative = { throw IllegalStateException("owner closed") }, + ownerClosed = { true }, + releaseLocal = { localReleases++ }, + ) - assertTrue(core.isClosed()) - assertEquals(1, localReleases) - } + assertTrue(core.isClosed()) + assertEquals(1, localReleases) + } @Test - fun closedFrameRejectsUseWithTheSharedReleasedHandleError() { - val core = OwnedTextureFrameHandleCore("TestFrameHandle") - val leaks = mutableListOf() + fun closedFrameRejectsUseWithTheSharedReleasedHandleError(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val core = OwnedTextureFrameHandleCore("TestFrameHandle") + val leaks = mutableListOf() - core.reportLeak { leaks += it } - core.close(releaseNative = {}, ownerClosed = { false }, releaseLocal = {}) - val error = assertFailsWith { core.ensureOpen() } + core.reportLeak { leaks += it } + core.close(releaseNative = {}, ownerClosed = { false }, releaseLocal = {}) + val error = assertFailsWith { core.ensureOpen() } - assertEquals(MaplibreStatus.INVALID_STATE, error.status) - assertEquals("TestFrameHandle is already closed", error.diagnostic) - assertEquals( - listOf( - "Leaked TestFrameHandle; close frame handles explicitly on the render session owner thread." - ), - leaks, - ) - } + assertEquals(MaplibreStatus.INVALID_STATE, error.status) + assertEquals("TestFrameHandle is already closed", error.diagnostic) + assertEquals( + listOf( + "Leaked TestFrameHandle; close frame handles explicitly on the render session graphics thread." + ), + leaks, + ) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.kt index 470463cc6..3e88438da 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.kt @@ -1,33 +1,38 @@ package org.maplibre.nativeffi.render +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.error.MaplibreException +import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapMode import org.maplibre.nativeffi.map.MapOptions import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.sleepMillis -/** Backend-owned texture frame valid only while this handle is open. */ -internal interface OwnedTextureTestFrame : AutoCloseable { - val width: Int - val height: Int - val isClosed: Boolean -} +/** Backend texture size read from an acquired frame. */ +internal data class OwnedTextureFrameSize(val width: Int, val height: Int) /** Backend-owned texture session plus the graphics context that keeps it valid. */ internal interface OwnedTextureTestSession : AutoCloseable { - val session: RenderSessionHandle + /** The attached session and the deferred result of its attachment. */ + val attachment: RenderSessionAttachment - fun acquireFrame(): OwnedTextureTestFrame + val session: RenderSessionHandle + get() = attachment.session /** * Attaches a second owned-texture session on [session]'s map, reusing this fixture's graphics * context. Native rejects the second attach; the fixture keeps the live context current. */ - fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionHandle + fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionAttachment + + /** Reads the backend texture size behind an acquired frame. */ + fun frameSize(frame: AcquiredFrameHandle): OwnedTextureFrameSize } /** - * Attaches an owned-texture session for common render tests. + * Attaches a caller-graphics-thread owned-texture session for common render tests. * * Returns null when this source set has no fixture for the loaded native backend, so the common * session tests stay compiled on every target. @@ -36,18 +41,102 @@ internal expect object OwnedTextureTestSupport { fun attach(map: MapHandle, width: Int, height: Int): OwnedTextureTestSession? } -internal inline fun withOwnedTextureSession( +/** Attach options every fixture uses: the fixture's context is current on the calling thread. */ +internal val OWNED_TEXTURE_ATTACH_OPTIONS: RenderSessionAttachOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD) + +/** Completes [deferred] while servicing the driver work only this thread can run. */ +internal suspend fun RenderSessionHandle.completeOnDriver(deferred: Deferred): T { + while (!deferred.isCompleted) serviceDriverWork() + return deferred.await() +} + +/** Demands one frame and services driver work until the demand reaches a terminal result. */ +internal fun RenderSessionHandle.renderOneFrame( + demand: FrameDemand = FrameDemand(ifNeeded = false) +): RenderFrameResult { + requestFrame(demand) + while (true) { + serviceDriverWork() + val results = drainFrameResultsOrEmpty() + if (results.isNotEmpty()) return results.last() + } +} + +/** Renders until the map settles, and returns the frame that asked for no repaint. */ +internal fun RenderSessionHandle.renderUntilSettled(attempts: Int = 500): RenderFrameResult { + var last: RenderFrameResult? = null + repeat(attempts) { + val result = renderOneFrame() + if (result.disposition == RenderResult.RENDERED && !result.needsRepaint) return result + last = result + // The map applies a new extent and its style on the runtime worker, so leave that + // worker room to publish between demands. + sleepMillis(1) + } + error("the map never settled; last frame result: $last") +} + +/** + * Demands one frame from a core-worker session and waits for the map to render it. + * + * A core-worker session renders on the runtime, so the caller only polls the frame-result queue. + * SIZE_PENDING means the map has not applied this target's extent yet. + */ +internal fun RenderSessionHandle.awaitRenderedFrame(attempts: Int = 1000): RenderFrameResult { + var last: RenderFrameResult? = null + val present = capabilities().presentation + for (attempt in 0 until attempts) { + val token = snapshot().latestDemandToken + 1uL + requestFrame(FrameDemand(ifNeeded = false, present = present, token = token)) + for (poll in 0 until 10) { + val result = drainFrameResultsOrEmpty().lastOrNull { it.token == token } + if (result != null) { + if (result.disposition == RenderResult.RENDERED) return result + last = result + break + } + sleepMillis(1) + } + } + error("the session never rendered a frame; last frame result: $last") +} + +/** Treats an empty native frame-result queue as an empty Kotlin collection. */ +private fun RenderSessionHandle.drainFrameResultsOrEmpty(): List = + try { + drainFrameResults() + } catch (error: MaplibreException) { + if (error.status == MaplibreStatus.NOT_READY) emptyList() else throw error + } + +/** Leaves a session closable: an attached session is abandoned rather than leaked. */ +internal fun RenderSessionHandle.abandonAndClose() { + if (isClosed) return + val state = snapshot().state + if (state != RenderSessionState.DETACHED && state != RenderSessionState.ABANDONED) { + abandon() + } + close() +} + +/** + * Runs [block] against a fresh runtime, map, and owned-texture session. + * + * Teardown always runs, and a teardown failure never hides a failure from [block]. + */ +internal suspend fun withOwnedTextureSession( width: Int = 32, height: Int = 16, - mapWidth: Int = 64, - mapHeight: Int = 64, + mapWidth: Int = width, + mapHeight: Int = height, mapMode: MapMode = MapMode.CONTINUOUS, - crossinline block: (RuntimeHandle, MapHandle, OwnedTextureTestSession) -> Unit, + block: suspend (RuntimeHandle, MapHandle, OwnedTextureTestSession) -> Unit, ) { + val failures = mutableListOf() val runtime = RuntimeHandle.create(RuntimeOptions()) - try { - val map = - MapHandle.create( + val map = + MapHandle.create( runtime, MapOptions().apply { this.width = mapWidth @@ -55,13 +144,25 @@ internal inline fun withOwnedTextureSession( this.mapMode = mapMode }, ) + .await() + val owned = OwnedTextureTestSupport.attach(map, width, height) + if (owned != null) { try { - val owned = OwnedTextureTestSupport.attach(map, width, height) ?: return - owned.use { block(runtime, map, owned) } - } finally { - map.close() + owned.session.completeOnDriver(owned.attachment.completed) + block(runtime, map, owned) + } catch (error: Throwable) { + failures += error } - } finally { - runtime.close() + failures.addIfFailed { owned.close() } + // Session teardown finishes on the runtime worker, so the map owns no session by the + // time the map itself closes. + failures.addIfFailed { runtime.barrier().await() } } + failures.addIfFailed { map.close() } + failures.addIfFailed { runtime.close() } + failures.firstOrNull()?.let { throw it } +} + +private inline fun MutableList.addIfFailed(block: () -> Unit) { + runCatching(block).exceptionOrNull()?.let { add(it) } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/QueryTestSupport.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/QueryTestSupport.kt index 4b389bff1..f1f52b200 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/QueryTestSupport.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/QueryTestSupport.kt @@ -1,10 +1,7 @@ package org.maplibre.nativeffi.render -import kotlin.test.assertEquals -import org.maplibre.nativeffi.map.MapHandle +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.query.QueriedFeature -import org.maplibre.nativeffi.runtime.RuntimeEventType -import org.maplibre.nativeffi.runtime.RuntimeHandle internal const val QUERY_STYLE_JSON = """ @@ -36,48 +33,19 @@ internal const val QUERY_STYLE_JSON = internal fun jsonBytes(value: String): ByteArray = value.trimIndent().encodeToByteArray() -internal fun waitForMapEvent( - runtime: RuntimeHandle, - map: MapHandle, - eventType: RuntimeEventType, -): Boolean { - repeat(10_000) { - runtime.pump(1) - if (runtime.drainEvents().events.any { it.type == eventType && it.mapSource == map }) { - return true - } - } - return false -} - -internal fun waitForQueriedFeature( - runtime: RuntimeHandle, - map: MapHandle, +internal suspend fun waitForQueriedFeature( session: RenderSessionHandle, - query: () -> List, + attempts: Int = 500, + query: () -> Deferred>, ): QueriedFeature { - repeat(100) { - val feature = query().firstOrNull() + repeat(attempts) { + session.renderOneFrame() + val feature = session.completeOnDriver(query()).firstOrNull() if (feature != null) return feature - renderIfAvailable(runtime, map, session) } error("query returned no features") } -internal fun renderIfAvailable( - runtime: RuntimeHandle, - map: MapHandle, - session: RenderSessionHandle, -) { - runtime.pump(1) - for (event in runtime.drainEvents().events) { - if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE && event.mapSource == map) { - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - return - } - } -} - internal fun featureStringProperty(feature: ByteArray, key: String): String? = rawMember(feature, "properties")?.let { stringMember(it, key) } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderClusterQueryTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderClusterQueryTest.kt index 0884c0544..12d541b32 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderClusterQueryTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderClusterQueryTest.kt @@ -4,15 +4,14 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue -import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.ScreenBox import org.maplibre.nativeffi.geo.ScreenPoint -import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry -import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.runtime.runSuspendTest import org.maplibre.nativeffi.style.GeoJsonSourceDataHandle import org.maplibre.nativeffi.style.GeoJsonSourceOptions @@ -21,53 +20,61 @@ class RenderClusterQueryTest { // unsigned leaves limit bounds the returned features. @Test - fun clusterFeatureExtensionQueriesResolveUnsignedClusterIdAndLimit() { - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) - try { - withOwnedTextureSession(width = 64, height = 64) { runtime, map, owned -> - val session = owned.session - map.jumpTo( - CameraOptions().apply { - center = LatLng(0.0, 0.0) - zoom = 0.0 - } + fun clusterFeatureExtensionQueriesResolveUnsignedClusterIdAndLimit(): Unit = runSuspendTest { + withOwnedTextureSession(width = 64, height = 64) { runtime, map, owned -> + val session = owned.session + session.completeOnDriver( + map.updateCamera( + CameraUpdate( + camera = + CameraOptions().apply { + center = LatLng(0.0, 0.0) + zoom = 0.0 + } + ) ) - map.setStyleJson(CLUSTER_STYLE_JSON.encodeToByteArray()) - GeoJsonSourceDataHandle.create(clusterPoints(), clusterSourceOptions()).use { clusterData -> - map.addGeoJsonSourceData("cluster-source", clusterData) - } - map.addStyleLayerJson(clusterCircleLayer(), "") - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE)) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) + ) + session.completeOnDriver(map.setStyleJson(CLUSTER_STYLE_JSON.encodeToByteArray())) + GeoJsonSourceDataHandle.create(clusterPoints(), clusterSourceOptions()).use { clusterData -> + session.completeOnDriver(map.addGeoJsonSourceData("cluster-source", clusterData)) + } + session.completeOnDriver(map.addStyleLayerJson(clusterCircleLayer(), "")) + session.completeOnDriver(runtime.barrier()) - val queryPoint = map.pixelForLatLng(LatLng(0.0, 0.0)) - val queryGeometry = - RenderedQueryGeometry.Box( - ScreenBox( - ScreenPoint(queryPoint.x - 30.0, queryPoint.y - 30.0), - ScreenPoint(queryPoint.x + 30.0, queryPoint.y + 30.0), - ) + val projection = session.completeOnDriver(map.createProjection()) + val queryPoint = + try { + projection.pixelForLatLng(LatLng(0.0, 0.0)) + } finally { + projection.close() + } + val queryGeometry = + RenderedQueryGeometry.Box( + ScreenBox( + ScreenPoint(queryPoint.x - 30.0, queryPoint.y - 30.0), + ScreenPoint(queryPoint.x + 30.0, queryPoint.y + 30.0), + ) + ) + val cluster = + waitForQueriedFeature(session) { + session.queryRenderedFeatures( + queryGeometry, + RenderedFeatureQueryOptions().apply { layerIds = listOf("cluster-circle") }, ) - val cluster = - waitForQueriedFeature(runtime, map, session) { - session.queryRenderedFeatures( - queryGeometry, - RenderedFeatureQueryOptions().apply { layerIds = listOf("cluster-circle") }, - ) - } - val clusterProperties = - rawMember(cluster.feature, "properties") ?: error("feature has no properties") - // The serialized feature must keep cluster_id as an integral value so - // MapLibre can resolve it when the bytes are passed back in. - assertTrue(numberMember(clusterProperties, "cluster_id") != null) + } + val clusterProperties = + rawMember(cluster.feature, "properties") ?: error("feature has no properties") + // The serialized feature must keep cluster_id as an integral value so + // MapLibre can resolve it when the bytes are passed back in. + assertTrue(numberMember(clusterProperties, "cluster_id") != null) - // The rendered cluster exists because GeoJsonSourceOptions enables - // clustering, and weightSum comes from the byte-encoded aggregation. - assertEquals(3.0, numberMember(clusterProperties, "point_count")) - assertEquals(6.0, numberMember(clusterProperties, "weightSum")) + // The rendered cluster exists because GeoJsonSourceOptions enables + // clustering, and weightSum comes from the byte-encoded aggregation. + assertEquals(3.0, numberMember(clusterProperties, "point_count")) + assertEquals(6.0, numberMember(clusterProperties, "weightSum")) - val children = + val children = + session.completeOnDriver( session.queryFeatureExtension( "cluster-source", cluster.feature, @@ -75,9 +82,11 @@ class RenderClusterQueryTest { "children", null, ) - assertTrue(firstFeature(children) != null) + ) + assertTrue(firstFeature(children) != null) - val expansionZoom = + val expansionZoom = + session.completeOnDriver( session.queryFeatureExtension( "cluster-source", cluster.feature, @@ -85,36 +94,37 @@ class RenderClusterQueryTest { "expansion-zoom", null, ) - assertTrue(expansionZoom.decodeToString().toULongOrNull() != null) + ) + assertTrue(expansionZoom.decodeToString().toULongOrNull() != null) - // An unsigned limit bounds the collection, and an unsigned offset - // selects a later leaf. Native ignores arguments of another type and - // falls back to ten leaves at offset zero, so both bounds must move - // the observed result. - val feature = cluster.feature - val first = singleClusterLeaf(session, feature, 0) - val second = singleClusterLeaf(session, feature, 1) - assertNotEquals(featureStringProperty(first, "name"), featureStringProperty(second, "name")) - } - } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + // An unsigned limit bounds the collection, and an unsigned offset + // selects a later leaf. Native ignores arguments of another type and + // falls back to ten leaves at offset zero, so both bounds must move + // the observed result. + val feature = cluster.feature + val first = singleClusterLeaf(session, feature, 0) + val second = singleClusterLeaf(session, feature, 1) + assertNotEquals(featureStringProperty(first, "name"), featureStringProperty(second, "name")) + + session.completeOnDriver(session.detach()) } } /** Returns the one leaf at [offset] through a bounded supercluster query. */ - private fun singleClusterLeaf( + private suspend fun singleClusterLeaf( session: RenderSessionHandle, feature: ByteArray, offset: Long, ): ByteArray { val leaves = - session.queryFeatureExtension( - "cluster-source", - feature, - "supercluster", - "leaves", - jsonBytes("""{"limit":1,"offset":$offset}"""), + session.completeOnDriver( + session.queryFeatureExtension( + "cluster-source", + feature, + "supercluster", + "leaves", + jsonBytes("""{"limit":1,"offset":$offset}"""), + ) ) return firstFeature(leaves) ?: error("expected one leaf") } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderDriverContractTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderDriverContractTest.kt new file mode 100644 index 000000000..23aee7701 --- /dev/null +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderDriverContractTest.kt @@ -0,0 +1,57 @@ +package org.maplibre.nativeffi.render + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.maplibre.nativeffi.error.InvalidArgumentException + +class RenderDriverContractTest { + @Test + fun defaultDemandIsRenderIfNeededWithoutPresentation() { + val demand = FrameDemand() + assertTrue(demand.ifNeeded) + assertFalse(demand.present) + assertEquals(0uL, demand.token) + assertEquals(0uL, demand.timeoutNanoseconds) + } + + @Test + fun openResultDomainIncludesCoalescingAndTimeoutTerminals() { + assertEquals(4, RenderResult.SUPERSEDED.nativeValue) + assertEquals(5, RenderResult.DEADLINE_MISSED.nativeValue) + } + + @Test + fun cpuCompleteIsTheDefaultReleaseSynchronization() { + assertEquals(GpuSyncKind.CPU_COMPLETE, GpuSync().kind) + assertEquals(0uL, GpuSync().objectHandle) + } + + @Test + fun attachOptionsRepresentBothNativeDriverWorkflows() { + assertEquals(RenderDriver.CORE_WORKER, RenderSessionAttachOptions().driver) + assertEquals( + RenderDriver.CALLER_GRAPHICS_THREAD, + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD).driver, + ) + } + + @Test + fun textureRingDepthRejectsValuesOutsideNativeRange() { + assertFailsWith { + RenderSessionAttachOptions(requestedTextureRingDepth = -1) + } + assertFailsWith { + RenderSessionAttachOptions(requestedTextureRingDepth = 4) + } + } + + @Test + fun abandonmentReportsQuarantineCount() { + val result = RenderAbandonResult(RenderAbandonDisposition.QUARANTINED, 3) + assertEquals(RenderAbandonDisposition.QUARANTINED, result.disposition) + assertEquals(3, result.quarantinedResourceCount) + } +} diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderQueryTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderQueryTest.kt index 1f3bdf738..4e7bf2c20 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderQueryTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderQueryTest.kt @@ -2,108 +2,96 @@ package org.maplibre.nativeffi.render import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue -import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.ScreenBox import org.maplibre.nativeffi.geo.ScreenPoint -import org.maplibre.nativeffi.log.LogCallback -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapOptions import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.query.SourceFeatureQueryOptions -import org.maplibre.nativeffi.runtime.RuntimeEventType -import org.maplibre.nativeffi.runtime.RuntimeHandle -import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.runtime.runSuspendTest class RenderQueryTest { // BND-106: rendered and source queries copy GeoJSON, source IDs, and feature state. @Test - fun renderedAndSourceQueriesReturnCopiedQueriedFeatures() { - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) - try { - val runtime = RuntimeHandle.create(RuntimeOptions()) - try { - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 + fun renderedAndSourceQueriesReturnCopiedQueriedFeatures(): Unit = runSuspendTest { + withOwnedTextureSession { runtime, map, owned -> + val session = owned.session + val featureCoordinate = LatLng(37.7749, -122.4194) + session.completeOnDriver( + map.updateCamera( + CameraUpdate(camera = CameraOptions().apply { center = featureCoordinate }) + ) + ) + session.completeOnDriver(map.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray())) + session.completeOnDriver(runtime.barrier()) + + val projection = session.completeOnDriver(map.createProjection()) + val queryPoint = + try { + projection.pixelForLatLng(featureCoordinate) + } finally { + projection.close() + } + val queryGeometry = + RenderedQueryGeometry.Box( + ScreenBox( + ScreenPoint(queryPoint.x - 20.0, queryPoint.y - 20.0), + ScreenPoint(queryPoint.x + 20.0, queryPoint.y + 20.0), + ) + ) + val filter = jsonBytes("""["==",["get","kind"],"capital"]""") + val rendered = + waitForQueriedFeature(session) { + session.queryRenderedFeatures( + queryGeometry, + RenderedFeatureQueryOptions().apply { + layerIds = listOf("point-circle") + this.filter = filter }, ) - try { - val owned = OwnedTextureTestSupport.attach(map, 32, 16) ?: return - owned.use { - val session = owned.session - val featureCoordinate = LatLng(37.7749, -122.4194) - map.jumpTo(CameraOptions().apply { center = featureCoordinate }) - map.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE)) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - - val queryPoint = map.pixelForLatLng(featureCoordinate) - val queryGeometry = - RenderedQueryGeometry.Box( - ScreenBox( - ScreenPoint(queryPoint.x - 20.0, queryPoint.y - 20.0), - ScreenPoint(queryPoint.x + 20.0, queryPoint.y + 20.0), - ) - ) - val filter = jsonBytes("""["==",["get","kind"],"capital"]""") - val rendered = - waitForQueriedFeature(runtime, map, session) { - session.queryRenderedFeatures( - queryGeometry, - RenderedFeatureQueryOptions().apply { - layerIds = listOf("point-circle") - this.filter = filter - }, - ) - } - assertEquals("point", rendered.sourceId) - assertEquals("capital", featureStringProperty(rendered.feature, "kind")) - - val source = - waitForQueriedFeature(runtime, map, session) { - session.querySourceFeatures( - "point", - SourceFeatureQueryOptions().apply { this.filter = filter }, - ) - } - assertEquals("point", source.sourceId) - assertEquals("capital", featureStringProperty(source.feature, "kind")) + } + assertEquals("point", rendered.sourceId) + assertEquals("capital", featureStringProperty(rendered.feature, "kind")) - map.setFeatureState(featureStateSelector(), featureState()) - renderIfAvailable(runtime, map, session) - val renderedWithState = - waitForQueriedFeature(runtime, map, session) { - session.queryRenderedFeatures( - queryGeometry, - RenderedFeatureQueryOptions().apply { - layerIds = listOf("point-circle") - this.filter = filter - }, - ) - } - val renderedState = renderedWithState.state ?: error("missing state") - assertEquals("true", rawMember(renderedState, "hover")?.decodeToString()) - assertEquals(20.0, numberMember(renderedState, "radius")) - } - } finally { - map.close() + val source = + waitForQueriedFeature(session) { + session.querySourceFeatures( + "point", + SourceFeatureQueryOptions().apply { this.filter = filter }, + ) } - } finally { - runtime.close() + assertEquals("point", source.sourceId) + assertEquals("capital", featureStringProperty(source.feature, "kind")) + + // Feature state belongs to the map; the renderer observes it on the next update. + session.completeOnDriver(map.setFeatureState(featureStateSelector(), featureState())) + var renderedState: ByteArray? = null + for (attempt in 0 until 500) { + session.renderOneFrame() + renderedState = + session + .completeOnDriver( + session.queryRenderedFeatures( + queryGeometry, + RenderedFeatureQueryOptions().apply { + layerIds = listOf("point-circle") + this.filter = filter + }, + ) + ) + .firstOrNull() + ?.state + if (renderedState != null) break } - } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + renderedState = renderedState ?: error("missing state") + assertEquals("true", rawMember(renderedState, "hover")?.decodeToString()) + assertEquals(20.0, numberMember(renderedState, "radius")) + + session.completeOnDriver(session.detach()) } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderSessionHandleTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderSessionHandleTest.kt index 2fe3fe262..045da5bc1 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderSessionHandleTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/render/RenderSessionHandleTest.kt @@ -2,232 +2,140 @@ package org.maplibre.nativeffi.render import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertSame import kotlin.test.assertTrue -import org.maplibre.nativeffi.Maplibre +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.camera.AnimationOptions import org.maplibre.nativeffi.camera.CameraOptions -import org.maplibre.nativeffi.error.InvalidArgumentException -import org.maplibre.nativeffi.error.InvalidStateException +import org.maplibre.nativeffi.camera.CameraUpdate +import org.maplibre.nativeffi.camera.CameraUpdateMode +import org.maplibre.nativeffi.error.MaplibreException import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.error.UnsupportedFeatureException -import org.maplibre.nativeffi.error.WrongThreadException -import org.maplibre.nativeffi.failureFromBackgroundThread -import org.maplibre.nativeffi.geo.LatLng -import org.maplibre.nativeffi.geo.ScreenBox -import org.maplibre.nativeffi.geo.ScreenPoint -import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.map.MapMode -import org.maplibre.nativeffi.query.FeatureStateSelector -import org.maplibre.nativeffi.query.RenderedQueryGeometry -import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.runtime.runSuspendTest import org.maplibre.nativeffi.sleepMillis class RenderSessionHandleTest { // BND-160, BND-161, BND-163, BND-164, BND-165, BND-166, BND-167, BND-168, - // BND-169, BND-170: owned-texture rendering, readback, frames, and - // owner-thread checks. + // BND-169, BND-170: owned-texture attachment, frame demands, frame leases, + // readback, and detachment on a caller-driven graphics thread. @Test - fun renderUpdateWithoutPendingUpdateReportsNoUpdateAndKeepsSessionLive() { - withOwnedTextureSession(mapMode = MapMode.STATIC) { _, _, owned -> + fun ownedTextureSessionRendersReadsBackAcquiresAFrameAndDetaches(): Unit = runSuspendTest { + withOwnedTextureSession { runtime, map, owned -> val session = owned.session - assertEquals(RenderResult.NO_UPDATE, session.renderUpdate().result) - session.resize(32, 16, 1.0) + assertSame(map, session.map()) + assertEquals(RenderDriver.CALLER_GRAPHICS_THREAD, session.capabilities().driver) + assertTrue(session.capabilities().readback) + assertEquals(RenderSessionState.ATTACHED, session.snapshot().state) + + session.completeOnDriver(map.setStyleJson(BACKGROUND_STYLE_JSON.encodeToByteArray())) + session.completeOnDriver(runtime.barrier()) + + // A second owned texture on the same map is rejected while this one is attached. + val secondFailure = + runCatching { + val second = owned.attachAnotherOwnedTexture(16, 8) + try { + session.completeOnDriver(second.completed) + } finally { + second.session.abandonAndClose() + } + } + .exceptionOrNull() + assertTrue(secondFailure is MaplibreException, "second attach must fail: $secondFailure") + + val rendered = session.renderUntilSettled() + assertEquals(RenderResult.RENDERED, rendered.disposition) + + val readback = session.completeOnDriver(session.readPremultipliedRgba8()) + assertEquals(32, readback.info.width) + assertEquals(16, readback.info.height) + assertEquals(32 * 4, readback.info.stride) + assertEquals( + readback.info.stride.toLong() * readback.info.height.toLong(), + readback.info.byteLength, + ) + assertEquals(readback.info.byteLength.toInt(), readback.bytes.size) + + val frame = assertNotNull(session.acquireFrame()) + assertEquals(rendered.frameGeneration, frame.result().frameGeneration) + assertEquals(OwnedTextureFrameSize(32, 16), owned.frameSize(frame)) + frame.release() + assertTrue(frame.isReleased) + + // Resizing hands the new logical size to the map, so the next frames report + // SIZE_PENDING until the map publishes an update matching the new target. + session.completeOnDriver(session.resize(RenderTargetExtent(16, 8, 2.0))) + session.renderUntilSettled() + val resized = session.snapshot().extent + assertEquals(16, resized.width) + assertEquals(8, resized.height) + assertEquals(2.0, resized.scaleFactor) + + session.completeOnDriver(session.barrier()) + session.completeOnDriver(session.detach()) + assertEquals(RenderSessionState.DETACHED, session.snapshot().state) + assertTrue(!session.isClosed) } } @Test - fun renderUpdateReportsNeedsRepaintDuringCameraTransition() { - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) - try { - withOwnedTextureSession { runtime, map, owned -> - val session = owned.session - map.setStyleJson(BACKGROUND_STYLE_JSON.encodeToByteArray()) - - // The session owns this thread, which is also the map's, so the map - // only reaches the style when this loop pumps the runtime. Render until - // the map settles: the last frame asks for no repaint. - var update = session.renderUpdate() - for (attempt in 0 until 500) { - if (update.result == RenderResult.RENDERED && !update.needsRepaint) break - runtime.pump(0) - update = session.renderUpdate() - if (update.result != RenderResult.RENDERED || update.needsRepaint) { - sleepMillis(1) - } - } - assertEquals(RenderResult.RENDERED, update.result) - assertFalse(update.needsRepaint) - - map.easeTo( - CameraOptions().apply { zoom = 4.0 }, - AnimationOptions().apply { durationMs = 60_000.0 }, + fun renderedFrameResultsReportNeedsRepaintDuringCameraTransition(): Unit = runSuspendTest { + withOwnedTextureSession { runtime, map, owned -> + val session = owned.session + session.completeOnDriver(map.setStyleJson(BACKGROUND_STYLE_JSON.encodeToByteArray())) + session.completeOnDriver(runtime.barrier()) + session.renderUntilSettled() + + session.completeOnDriver( + map.updateCamera( + CameraUpdate( + mode = CameraUpdateMode.EASE, + camera = CameraOptions().apply { zoom = 4.0 }, + animation = AnimationOptions().apply { durationMs = 60_000.0 }, + ) ) - - var sawRepaintRequest = false - for (attempt in 0 until 500) { - runtime.pump(0) - update = session.renderUpdate() - if (update.result == RenderResult.RENDERED && update.needsRepaint) { - sawRepaintRequest = true - break - } - sleepMillis(1) + ) + session.completeOnDriver(runtime.barrier()) + + var sawRepaintRequest = false + for (attempt in 0 until 500) { + val result = session.renderOneFrame() + sleepMillis(1) + if (result.disposition == RenderResult.RENDERED && result.needsRepaint) { + sawRepaintRequest = true + break } - assertTrue(sawRepaintRequest) } - } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + assertTrue(sawRepaintRequest) + + session.completeOnDriver(session.detach()) } } @Test - fun ownedTextureSessionRendersReadsBackAcquiresFrameAndDetaches() { - Maplibre.setLogCallback(LogCallback { true }) - Maplibre.setAsyncLogSeverities(emptySet()) - try { - withOwnedTextureSession { runtime, map, owned -> - val session = owned.session - val featureCoordinate = LatLng(37.7749, -122.4194) - map.jumpTo(CameraOptions().apply { center = featureCoordinate }) - assertSame(map, session.map()) - assertFailsWith { session.textureImageInfo() } - map.setFeatureState(featureStateSelector(), featureState()) - val queuedState = map.getFeatureState(featureStateSelector()) - assertEquals("true", rawMember(queuedState, "hover")?.decodeToString()) - assertEquals(20.0, numberMember(queuedState, "radius")) - assertFailsWith { map.close() } - assertFailsWith { owned.attachAnotherOwnedTexture(16, 8).close() } - - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE)) - session.renderUpdate() - val beforeStyle = map.getFeatureState(featureStateSelector()) - assertEquals("true", rawMember(beforeStyle, "hover")?.decodeToString()) - assertEquals(20.0, numberMember(beforeStyle, "radius")) - - map.setStyleJson(QUERY_STYLE_JSON.encodeToByteArray()) - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE)) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - - val sessionCallWrongThread = failureFromBackgroundThread { session.renderUpdate() } - if (sessionCallWrongThread !is WrongThreadException) throw sessionCallWrongThread - val sessionCallDiagnostic = sessionCallWrongThread.diagnostic - assertEquals(MaplibreStatus.WRONG_THREAD, sessionCallWrongThread.status) - assertTrue(sessionCallDiagnostic.isNotBlank()) - - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - assertEquals(sessionCallDiagnostic, sessionCallWrongThread.diagnostic) - - val sessionCloseWrongThread = failureFromBackgroundThread { session.close() } - if (sessionCloseWrongThread !is WrongThreadException) throw sessionCloseWrongThread - assertEquals(MaplibreStatus.WRONG_THREAD, sessionCloseWrongThread.status) - assertFalse(session.isClosed) - - val info = session.textureImageInfo() - assertEquals(32, info.width) - assertEquals(16, info.height) - assertEquals(32 * 4, info.stride) - assertEquals(info.stride.toLong() * info.height.toLong(), info.byteLength) - - NativeBuffer.allocate(4).use { small -> - assertFailsWith { session.readPremultipliedRgba8(small) } - } - NativeBuffer.allocate(info.byteLength).use { buffer -> - assertEquals(info, session.readPremultipliedRgba8(buffer)) - assertEquals(info.byteLength.toInt(), buffer.toByteArray().size) - } - - assertFailsWith { - map.setFeatureState(featureStateSelector(), jsonBytes("[]")) - } - map.setFeatureState(featureStateSelector(), featureState()) - val copiedState = map.getFeatureState(featureStateSelector()) - assertEquals("true", rawMember(copiedState, "hover")?.decodeToString()) - assertEquals(20.0, numberMember(copiedState, "radius")) - - renderIfAvailable(runtime, map, session) - map.removeFeatureState( - FeatureStateSelector("point").apply { - featureId = "feature-1" - stateKey = "hover" - } - ) - renderIfAvailable(runtime, map, session) - val afterRemove = map.getFeatureState(featureStateSelector()) - assertEquals(null, rawMember(afterRemove, "hover")) - assertEquals(20.0, numberMember(afterRemove, "radius")) - - val queryGeometry = - RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(0.0, 0.0), ScreenPoint(1.0, 1.0))) - val frame = owned.acquireFrame() - try { - assertEquals(32, frame.width) - assertEquals(16, frame.height) - assertFalse(frame.isClosed) - assertFailsWith { session.renderUpdate() } - assertFailsWith { session.resize(16, 8, 2.0) } - assertFailsWith { - session.setMetalBorrowedTextureTarget(dummyMetalBorrowedTexture()) - } - assertFailsWith { - session.setVulkanBorrowedTextureTarget(dummyVulkanBorrowedTexture()) - } - assertFailsWith { - session.setOpenGLBorrowedTextureTarget(dummyOpenGLBorrowedTexture()) - } - assertFailsWith { session.detach() } - assertFailsWith { session.reduceMemoryUse() } - assertFailsWith { session.clearData() } - assertFailsWith { session.dumpDebugLogs() } - assertFailsWith { - session.queryRenderedFeatures(queryGeometry, null) - } - assertFailsWith { session.querySourceFeatures("point", null) } - assertFailsWith { - session.queryFeatureExtension( - "point", - jsonBytes("""{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]}}"""), - "supercluster", - "children", - null, - ) - } - assertFailsWith { session.textureImageInfo() } - NativeBuffer.allocate(1).use { buffer -> - assertFailsWith { session.readPremultipliedRgba8(buffer) } - } - assertFailsWith { owned.acquireFrame() } - assertFailsWith { session.close() } - - val closeError = failureFromBackgroundThread { frame.close() } - assertTrue(closeError is WrongThreadException) - assertFalse(frame.isClosed) - assertFailsWith { session.renderUpdate() } - } finally { - frame.close() - } - assertTrue(frame.isClosed) - assertFailsWith { frame.width } - - // Resizing hands the new logical size to the map's owner thread, so - // the map publishes an update matching the new target only once - // pumped. - session.resize(16, 8, 2.0) - assertEquals(RenderResult.SIZE_PENDING, session.renderUpdate().result) - runtime.pump(0) - assertEquals(RenderResult.RENDERED, session.renderUpdate().result) - session.detach() - assertFailsWith { session.renderUpdate() } - assertFalse(session.isClosed) - } - } finally { - Maplibre.clearLogCallback() - Maplibre.restoreDefaultAsyncLogSeverities() + fun abandonReportsItsDispositionAndFailsPendingDriverWorkAsTargetLost(): Unit = runSuspendTest { + withOwnedTextureSession(width = 8, height = 8) { runtime, map, owned -> + val session = owned.session + session.completeOnDriver(map.setStyleJson(BACKGROUND_STYLE_JSON.encodeToByteArray())) + session.completeOnDriver(runtime.barrier()) + session.renderUntilSettled() + + val frame = assertNotNull(session.acquireFrame()) + val pending = session.reduceMemoryUse() + val abandoned = session.abandon() + assertTrue( + abandoned.disposition == RenderAbandonDisposition.CLEAN || + abandoned.disposition == RenderAbandonDisposition.QUARANTINED + ) + val failure = runCatching { pending.await() }.exceptionOrNull() + assertTrue(failure is MaplibreException, "expected a target-lost failure: $failure") + assertEquals(MaplibreStatus.TARGET_LOST, failure.status) + frame.release() + assertTrue(frame.isReleased) + assertEquals(RenderSessionState.ABANDONED, session.snapshot().state) } } @@ -236,36 +144,38 @@ class RenderSessionHandleTest { // before it reads GPU objects. @Test - fun ownedTextureSetTargetReportsUnsupportedForOtherTargetKinds() { + fun ownedTextureSetTargetReportsUnsupportedForOtherTargetKinds(): Unit = runSuspendTest { withOwnedTextureSession(mapMode = MapMode.STATIC) { _, _, owned -> val session = owned.session - val error = - assertFailsWith { - session.setMetalBorrowedTextureTarget(dummyMetalBorrowedTexture()) - } - assertEquals(MaplibreStatus.UNSUPPORTED, error.status) - assertFailsWith { - session.setVulkanBorrowedTextureTarget(dummyVulkanBorrowedTexture()) + assertUnsupported(session, "metal texture") { + session.setMetalBorrowedTextureTarget(metalBorrowedTexture()) } - assertFailsWith { - session.setOpenGLBorrowedTextureTarget(dummyOpenGLBorrowedTexture()) + assertUnsupported(session, "vulkan texture") { + session.setVulkanBorrowedTextureTarget(vulkanBorrowedTexture()) } - assertFailsWith { - session.setMetalSurfaceTarget(dummyMetalSurface()) + assertUnsupported(session, "opengl texture") { + session.setOpenGLBorrowedTextureTarget(openGLBorrowedTexture()) } - assertFailsWith { - session.setVulkanSurfaceTarget(dummyVulkanSurface()) + assertUnsupported(session, "metal surface") { session.setMetalSurfaceTarget(metalSurface()) } + assertUnsupported(session, "vulkan surface") { + session.setVulkanSurfaceTarget(vulkanSurface()) } - assertFailsWith { - session.setOpenGLSurfaceTarget(dummyOpenGLSurface()) + assertUnsupported(session, "opengl surface") { + session.setOpenGLSurfaceTarget(openGLSurface()) } } } - private fun featureStateSelector(): FeatureStateSelector = - FeatureStateSelector("point").apply { featureId = "feature-1" } - - private fun featureState(): ByteArray = jsonBytes("""{"hover":true,"radius":20}""") + /** A target-kind mismatch fails either at submission or at completion. */ + private suspend fun assertUnsupported( + session: RenderSessionHandle, + label: String, + submit: () -> Deferred, + ) { + val failure = runCatching { session.completeOnDriver(submit()) }.exceptionOrNull() + assertTrue(failure is MaplibreException, "$label: expected an unsupported failure: $failure") + assertEquals(MaplibreStatus.UNSUPPORTED, failure.status, "$label: ${failure.diagnostic}") + } private companion object { private const val BACKGROUND_STYLE_JSON = @@ -275,17 +185,17 @@ class RenderSessionHandleTest { private fun dummyPointer(): NativePointer = NativePointer.ofAddress(1) -private fun dummyMetalBorrowedTexture(): MetalBorrowedTextureDescriptor = +private fun metalBorrowedTexture(): MetalBorrowedTextureDescriptor = MetalBorrowedTextureDescriptor(RenderTargetExtent(16, 8, 1.0), 16, 8, dummyPointer()) -private fun dummyMetalSurface(): MetalSurfaceDescriptor = +private fun metalSurface(): MetalSurfaceDescriptor = MetalSurfaceDescriptor( RenderTargetExtent(16, 8, 1.0), MetalContextDescriptor(dummyPointer()), dummyPointer(), ) -private fun dummyVulkanContext(): VulkanContextDescriptor = +private fun vulkanContext(): VulkanContextDescriptor = VulkanContextDescriptor( dummyPointer(), dummyPointer(), @@ -296,33 +206,27 @@ private fun dummyVulkanContext(): VulkanContextDescriptor = dummyPointer(), ) -private fun dummyVulkanBorrowedTexture(): VulkanBorrowedTextureDescriptor = +private fun vulkanBorrowedTexture(): VulkanBorrowedTextureDescriptor = VulkanBorrowedTextureDescriptor( RenderTargetExtent(16, 8, 1.0), 16, 8, - dummyVulkanContext(), + vulkanContext(), dummyPointer(), dummyPointer(), - 0, - 0, + // VK_FORMAT_R8G8B8A8_UNORM and VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. + 37, + 5, ) -private fun dummyVulkanSurface(): VulkanSurfaceDescriptor = - VulkanSurfaceDescriptor(RenderTargetExtent(16, 8, 1.0), dummyVulkanContext(), dummyPointer()) +private fun vulkanSurface(): VulkanSurfaceDescriptor = + VulkanSurfaceDescriptor(RenderTargetExtent(16, 8, 1.0), vulkanContext(), dummyPointer()) -private fun dummyEglContext(): EglContextDescriptor = +private fun eglContext(): EglContextDescriptor = EglContextDescriptor(dummyPointer(), dummyPointer(), dummyPointer(), NativePointer.NULL_POINTER) -private fun dummyOpenGLBorrowedTexture(): OpenGLBorrowedTextureDescriptor = - OpenGLBorrowedTextureDescriptor( - RenderTargetExtent(16, 8, 1.0), - 16, - 8, - dummyEglContext(), - 1, - 0x0DE1, - ) +private fun openGLBorrowedTexture(): OpenGLBorrowedTextureDescriptor = + OpenGLBorrowedTextureDescriptor(RenderTargetExtent(16, 8, 1.0), 16, 8, eglContext(), 1, 0x0DE1) -private fun dummyOpenGLSurface(): OpenGLSurfaceDescriptor = - OpenGLSurfaceDescriptor(RenderTargetExtent(16, 8, 1.0), dummyEglContext(), dummyPointer()) +private fun openGLSurface(): OpenGLSurfaceDescriptor = + OpenGLSurfaceDescriptor(RenderTargetExtent(16, 8, 1.0), eglContext(), dummyPointer()) diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleCoreTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleCoreTest.kt index 77a8ea1be..c51e7ae4c 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleCoreTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleCoreTest.kt @@ -8,148 +8,157 @@ import org.maplibre.nativeffi.error.MaplibreStatus class ResourceRequestHandleCoreTest { @Test - fun providerOwnedHandleReleasesAfterCloseExactlyOnce() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } - - assertEquals( - ResourceProviderDecision.HANDLE, - core.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - core.close() - core.close() - core.releaseIfOwned() - - assertEquals(1, releases) - } + fun providerOwnedHandleReleasesAfterCloseExactlyOnce(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } + + assertEquals( + ResourceProviderDecision.HANDLE, + core.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + core.close() + core.close() + core.releaseIfOwned() + + assertEquals(1, releases) + } @Test - fun passThroughDecisionLetsNativeOwnRelease() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } - - assertEquals( - ResourceProviderDecision.PASS_THROUGH, - core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), - ) - core.close() - core.releaseIfOwned() - - assertEquals(0, releases) - } + fun passThroughDecisionLetsNativeOwnRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } + + assertEquals( + ResourceProviderDecision.PASS_THROUGH, + core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), + ) + core.close() + core.releaseIfOwned() + + assertEquals(0, releases) + } @Test - fun completionBeforeProviderDecisionForcesProviderOwnership() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } - - core.beginComplete().use { it.markCompleted() } - assertEquals( - ResourceProviderDecision.HANDLE, - core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), - ) - - assertEquals(1, releases) - } + fun completionBeforeProviderDecisionForcesProviderOwnership(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } + + core.beginComplete().use { it.markCompleted() } + assertEquals( + ResourceProviderDecision.HANDLE, + core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), + ) + + assertEquals(1, releases) + } @Test - fun failedCompletionBeforeNativeCallLeavesHandleRetryable() { - var completions = 0 - val core = ResourceRequestHandleCore {} + fun failedCompletionBeforeNativeCallLeavesHandleRetryable(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var completions = 0 + val core = ResourceRequestHandleCore {} - val first = core.beginComplete() - first.markNotReachedNative() - first.close() + val first = core.beginComplete() + first.markNotReachedNative() + first.close() - core.beginComplete().use { - completions++ - it.markCompleted() - } + core.beginComplete().use { + completions++ + it.markCompleted() + } - assertEquals(1, completions) - } + assertEquals(1, completions) + } @Test - fun completedHandleRejectsFurtherCompletion() { - var nativeCalls = 0 - val core = ResourceRequestHandleCore {} - - core.beginComplete().use { - nativeCalls += 1 - it.markCompleted() + fun completedHandleRejectsFurtherCompletion(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var nativeCalls = 0 + val core = ResourceRequestHandleCore {} + + core.beginComplete().use { + nativeCalls += 1 + it.markCompleted() + } + val error = assertFailsWith { core.beginComplete() } + + assertEquals(MaplibreStatus.INVALID_STATE, error.status) + assertEquals(1, nativeCalls) } - val error = assertFailsWith { core.beginComplete() } - - assertEquals(MaplibreStatus.INVALID_STATE, error.status) - assertEquals(1, nativeCalls) - } @Test - fun closeDuringLiveOperationDefersProviderOwnedReleaseUntilOperationExits() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } + fun closeDuringLiveOperationDefersProviderOwnedReleaseUntilOperationExits(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } - assertEquals( - ResourceProviderDecision.HANDLE, - core.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - val operation = core.beginComplete() - core.close() + assertEquals( + ResourceProviderDecision.HANDLE, + core.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + val operation = core.beginComplete() + core.close() - assertEquals(0, releases) + assertEquals(0, releases) - operation.markCompleted() - operation.close() + operation.markCompleted() + operation.close() - assertEquals(1, releases) - } + assertEquals(1, releases) + } @Test - fun providerOwnedHandleClosedBeforeDecisionReleasesAfterDecisionExactlyOnce() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } - - core.close() - assertEquals( - ResourceProviderDecision.HANDLE, - core.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - core.close() - core.releaseIfOwned() - - assertEquals(1, releases) - } + fun providerOwnedHandleClosedBeforeDecisionReleasesAfterDecisionExactlyOnce(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } + + core.close() + assertEquals( + ResourceProviderDecision.HANDLE, + core.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + core.close() + core.releaseIfOwned() + + assertEquals(1, releases) + } @Test - fun retainedPassThroughHandleCannotStartLaterOperations() { - var releases = 0 - val core = ResourceRequestHandleCore { releases++ } + fun retainedPassThroughHandleCannotStartLaterOperations(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + val core = ResourceRequestHandleCore { releases++ } - assertEquals( - ResourceProviderDecision.PASS_THROUGH, - core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), - ) + assertEquals( + ResourceProviderDecision.PASS_THROUGH, + core.finishProviderDecision(ResourceProviderDecision.PASS_THROUGH), + ) - assertFailsWith { core.beginComplete() } - assertFailsWith { core.withLiveHandle {} } - core.close() + assertFailsWith { core.beginComplete() } + assertFailsWith { core.withLiveHandle {} } + core.close() - assertEquals(0, releases) - } + assertEquals(0, releases) + } @Test - fun closeRejectsCompletionAndCancellationBeforeNativeCalls() { - var releases = 0 - var nativeCalls = 0 - val core = ResourceRequestHandleCore { releases++ } + fun closeRejectsCompletionAndCancellationBeforeNativeCalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var releases = 0 + var nativeCalls = 0 + val core = ResourceRequestHandleCore { releases++ } - core.finishProviderDecision(ResourceProviderDecision.HANDLE) - core.close() + core.finishProviderDecision(ResourceProviderDecision.HANDLE) + core.close() - assertFailsWith { core.beginComplete().use { nativeCalls += 1 } } - assertFailsWith { core.withLiveHandle { nativeCalls += 1 } } + assertFailsWith { core.beginComplete().use { nativeCalls += 1 } } + assertFailsWith { core.withLiveHandle { nativeCalls += 1 } } - assertEquals(0, nativeCalls) - assertEquals(1, releases) - } + assertEquals(0, nativeCalls) + assertEquals(1, releases) + } } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/DeferredMapUse.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/DeferredMapUse.kt new file mode 100644 index 000000000..1791e1933 --- /dev/null +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/DeferredMapUse.kt @@ -0,0 +1,8 @@ +package org.maplibre.nativeffi.runtime + +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.map.MapHandle + +/** Test-scoped lifecycle helper for the eager map-creation result. */ +internal suspend inline fun Deferred.use(block: suspend (MapHandle) -> T): T = + await().use(block) diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCoreTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCoreTest.kt deleted file mode 100644 index fd73b0b26..000000000 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandleCoreTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import org.maplibre.nativeffi.error.InvalidStateException - -class OfflineOperationHandleCoreTest { - @Test - fun takeValidatesOperationKindBeforeReturningTheNativeId() { - val runtime = Any() - val core = operation(runtime) - - assertFailsWith { - core.requireLive( - runtime, - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) - } - - assertFalse(core.isClosed) - assertEquals( - 7L, - core.requireLive( - runtime, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ), - ) - } - - @Test - fun failedTakeLeavesOperationRetryable() { - val runtime = Any() - val core = operation(runtime) - - repeat(2) { - assertEquals(7L, core.requireLive(runtime)) - assertFailsWith { error("native take failed") } - assertFalse(core.isClosed) - } - } - - @Test - fun consumedOperationCloseIsIdempotent() { - var retentionReleases = 0 - val runtime = Any() - val core = operation(runtime) { retentionReleases += 1 } - - core.markConsumed() - core.markConsumed() - - assertTrue(core.isClosed) - assertEquals(1, retentionReleases) - assertFailsWith { core.requireLive(runtime) } - } - - @Test - fun liveOperationRetainsRuntimeUntilItIsConsumed() { - var retentionReleases = 0 - val core = operation(Any()) { retentionReleases += 1 } - - assertEquals(0, retentionReleases) - core.markConsumed() - assertEquals(1, retentionReleases) - } - - private fun operation( - runtime: Any, - releaseRuntimeRetention: () -> Unit = {}, - ): OfflineOperationHandleCore = - OfflineOperationHandleCore( - runtime, - 7L, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - releaseRuntimeRetention, - ) -} diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OwnerThreadTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OwnerThreadTest.kt deleted file mode 100644 index c4d58da16..000000000 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/OwnerThreadTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.error.WrongThreadException -import org.maplibre.nativeffi.failureFromBackgroundThread -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapOptions - -class OwnerThreadTest { - @Test - fun runtimeAndMapCallsFromAnotherHostThreadCopyTheirDiagnostics() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - val runtimeFailure = failureFromBackgroundThread { runtime.pump(0) } - val runtimeWrongThread = runtimeFailure as? WrongThreadException ?: throw runtimeFailure - assertEquals(MaplibreStatus.WRONG_THREAD, runtimeWrongThread.status) - val runtimeDiagnostic = runtimeWrongThread.diagnostic - assertTrue(runtimeDiagnostic.isNotBlank()) - - val mapFailure = failureFromBackgroundThread { - map.setStyleJson("{\"version\":8,\"sources\":{},\"layers\":[]}".encodeToByteArray()) - } - val mapWrongThread = mapFailure as? WrongThreadException ?: throw mapFailure - assertEquals(MaplibreStatus.WRONG_THREAD, mapWrongThread.status) - val mapDiagnostic = mapWrongThread.diagnostic - assertTrue(mapDiagnostic.isNotBlank()) - - runtime.pump(0) - assertEquals(runtimeDiagnostic, runtimeWrongThread.diagnostic) - assertEquals(mapDiagnostic, mapWrongThread.diagnostic) - } finally { - map.close() - runtime.close() - } - } -} diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RunSuspendTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RunSuspendTest.kt new file mode 100644 index 000000000..089bdad05 --- /dev/null +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RunSuspendTest.kt @@ -0,0 +1,5 @@ +package org.maplibre.nativeffi.runtime + +import kotlinx.coroutines.runBlocking + +internal fun runSuspendTest(block: suspend () -> T): T = runBlocking { block() } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleTest.kt index 301183cdb..6bafdf0bd 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleTest.kt @@ -12,7 +12,6 @@ import kotlin.test.assertTrue import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.error.InvalidArgumentException import org.maplibre.nativeffi.error.InvalidStateException -import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapOptions import org.maplibre.nativeffi.offline.OfflineRegionDefinition @@ -29,575 +28,595 @@ import org.maplibre.nativeffi.resource.ResourceTransformCallback @OptIn(ExperimentalAtomicApi::class) class RuntimeHandleTest { @Test - fun runtimeRunsOnceAndCloses() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - - assertFalse(runtime.isClosed) - runtime.pump(0) - runtime.close() - runtime.close() - - assertTrue(runtime.isClosed) - assertFailsWith { runtime.pump(0) } - } - - @Test - fun freshRuntimeDrainsAnEmptyBatch() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val batch = runtime.drainEvents() - assertEquals(emptyList(), batch.events) - assertEquals(0L, batch.remainingCount) + fun runtimeRunsOnceAndCloses(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + + assertFalse(runtime.isClosed) + runtime.barrier().await() + val tornDown = runtime.close() + // A second close reports the same teardown instead of starting another one. + assertEquals(tornDown, runtime.close()) + + assertTrue(runtime.isClosed) + assertFailsWith { runSuspendTest { runtime.barrier().await() } } } - } + // BND-040. @Test - fun ambientCacheOperationRetainsRuntimeUntilDiscarded() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val operation = runtime.startAmbientCacheOperation(AmbientCacheOperation.INVALIDATE) - - assertFalse(operation.isClosed) - assertFailsWith { runtime.close() } + fun runtimeCloseReportsTheEndOfNativeTeardown(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + map.setStyleUrl("custom://never-served.json").await() + map.close() - operation.close() - operation.close() + // The report arrives only after the released map's teardown finishes too. + runtime.close().await() - assertTrue(operation.isClosed) - runtime.close() - assertTrue(runtime.isClosed) - } + assertTrue(runtime.isClosed) + assertTrue(map.isClosed) + } @Test - fun setMaximumAmbientCacheSizeReachesNativeAndRejectsNegativeSize() { - val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) - val operation = runtime.startSetMaximumAmbientCacheSize(8L shl 20) + fun freshRuntimeDrainsAnEmptyBatch(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val batch = runtime.drainEvents() + assertEquals(emptyList(), batch.events) + } + } - assertEquals(OfflineOperationKind.SET_MAXIMUM_AMBIENT_CACHE_SIZE, operation.kind) - assertFalse(operation.isClosed) - operation.close() + @Test + fun ambientCacheOperationRemainsUsableAfterRuntimeClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val completion = runtime.runAmbientCacheOperation(AmbientCacheOperation.INVALIDATE) + runtime.close() + assertTrue(runtime.isClosed) + completion.await() + } - // Binding-owned validation fails before crossing into C. - assertFailsWith { runtime.startSetMaximumAmbientCacheSize(-1L) } - runtime.close() - } + @Test + fun setMaximumAmbientCacheSizeReachesNativeAndRejectsNegativeSize(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) + runtime.setMaximumAmbientCacheSize(8L shl 20).await() + + // Binding-owned validation fails before crossing into C. + assertFailsWith { runtime.setMaximumAmbientCacheSize(-1L) } + runtime.close() + } @Test - fun offlineDownloadStateUnknownRawValueRejectsBeforeNativeCall() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - assertFailsWith { - runtime.startSetOfflineRegionDownloadState(1, OfflineRegionDownloadState(900)) + fun offlineDownloadStateUnknownRawValueRejectsBeforeNativeCall(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + assertFailsWith { + runtime.setOfflineRegionDownloadState(1, OfflineRegionDownloadState(900)) + } } } - } @Test - fun geometryOfflineRegionDefinitionStartsOperation() { - RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }).use { runtime -> - val operation = - runtime.startCreateOfflineRegion( - OfflineRegionDefinition.GeometryRegion( - "custom://style.json", - "{\"type\":\"Point\",\"coordinates\":[2,1]}".encodeToByteArray(), - 0.0, - 1.0, - 1.0f, - false, - ), - ByteArray(0), - ) - assertEquals(OfflineOperationKind.REGION_CREATE, operation.kind) - assertEquals(OfflineOperationResultKind.REGION, operation.resultKind) - operation.close() - assertTrue(operation.isClosed) + fun geometryOfflineRegionDefinitionStartsOperation(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }).use { runtime -> + val region = + runtime + .createOfflineRegion( + OfflineRegionDefinition.GeometryRegion( + "custom://style.json", + "{\"type\":\"Point\",\"coordinates\":[2,1]}".encodeToByteArray(), + 0.0, + 1.0, + 1.0f, + false, + ), + ByteArray(0), + ) + .await() + assertTrue(region.id > 0) + } } - } @Test - fun offlineRegionsListCompletesAndConsumesOperation() { - RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }).use { runtime -> - val operation = runtime.startOfflineRegions() - - val completed = waitForOperation(runtime, operation) - assertEquals(OfflineOperationKind.REGIONS_LIST, completed.operationKind) - assertEquals(OfflineOperationResultKind.REGION_LIST, completed.resultKind) - - assertTrue(runtime.takeOfflineRegionsResult(operation).isEmpty()) - assertTrue(operation.isClosed) - assertFailsWith { runtime.takeOfflineRegionsResult(operation) } + fun offlineRegionsListCompletesAndConsumesOperation(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }).use { runtime -> + assertTrue(runtime.offlineRegions().await().isEmpty()) + } } - } // BND-155. @Test - fun resourceProviderSeesSchemeAliasAndItsResolvedUrl() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val resolvedUrl = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != "maplibre://maps/style") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - resolvedUrl.store(request.resolvedUrl) - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() + fun resourceProviderSeesSchemeAliasAndItsResolvedUrl(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val resolvedUrl = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != "maplibre://maps/style") { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + resolvedUrl.store(request.resolvedUrl) + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) + ResourceProviderDecision.HANDLE } ) - ResourceProviderDecision.HANDLE + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("maplibre://maps/style").await() + assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_STYLE_LOADED)) + assertEquals("https://demotiles.maplibre.org/style.json", resolvedUrl.load()) + } finally { + map.close() } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("maplibre://maps/style") - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_STYLE_LOADED)) - assertEquals("https://demotiles.maplibre.org/style.json", resolvedUrl.load()) - } finally { - map.close() } } - } @Test - fun resourceProviderCompletesStyleRequestThroughRuntime() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val calls = AtomicInt(0) - val callbackError = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - try { - if (request.requestedUrl != "custom://style.json") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - calls.addAndFetch(1) - assertEquals(ResourceKind.STYLE, request.kind) - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() + fun resourceProviderCompletesStyleRequestThroughRuntime(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val calls = AtomicInt(0) + val callbackError = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + try { + if (request.requestedUrl != "custom://style.json") { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + calls.addAndFetch(1) + assertEquals(ResourceKind.STYLE, request.kind) + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) + ResourceProviderDecision.HANDLE + } catch (error: Throwable) { + callbackError.store(error) + throw error } + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, ) - ResourceProviderDecision.HANDLE - } catch (error: Throwable) { - callbackError.store(error) - throw error + .await() + try { + map.setStyleUrl("custom://style.json").await() + val event = waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_STYLE_LOADED) + val copiedMessage = event.message + assertEquals(RuntimeEventSourceType.MAP, event.sourceType) + assertEquals(map, event.mapSource) + assertNull(event.runtimeSource) + assertEquals(RuntimeEventPayload.None, event.payload) + // A drained value stays readable after the next drain ends the batch window. + runtime.drainEvents() + assertEquals(copiedMessage, event.message) + callbackError.load()?.let { + throw AssertionError("resource provider callback failed", it) } + assertEquals(1, calls.load()) + } finally { + map.close() } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("custom://style.json") - val event = waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_STYLE_LOADED) - val copiedMessage = event.message - assertEquals(RuntimeEventSourceType.MAP, event.sourceType) - assertEquals(map, event.mapSource) - assertNull(event.runtimeSource) - assertEquals(RuntimeEventPayload.None, event.payload) - // A drained value stays readable after the next drain ends the batch window. - runtime.drainEvents() - assertEquals(copiedMessage, event.message) - callbackError.load()?.let { throw AssertionError("resource provider callback failed", it) } - assertEquals(1, calls.load()) - } finally { - map.close() } } - } @Test - fun handledResourceRequestCanCompleteAfterTheProviderReturns() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val handledRequest = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != "custom://deferred-style.json") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - handledRequest.store(handle) - ResourceProviderDecision.HANDLE - } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("custom://deferred-style.json") - val handle = waitForHandledRequest(runtime, handledRequest) - assertFalse(handle.isCancelled()) - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() + fun handledResourceRequestCanCompleteAfterTheProviderReturns(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val handledRequest = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != "custom://deferred-style.json") { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + handledRequest.store(handle) + ResourceProviderDecision.HANDLE + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("custom://deferred-style.json").await() + val handle = waitForHandledRequest(runtime, handledRequest) + assertFalse(handle.isCancelled()) + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) + assertFailsWith { handle.isCancelled() } + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - ) - assertFailsWith { handle.isCancelled() } - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_STYLE_LOADED)) + } finally { + map.close() } - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_STYLE_LOADED)) - } finally { - map.close() } } - } @Test - fun resourceErrorBecomesACopiedMapLoadingFailureEvent() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != "custom://error-style.json") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - handle.complete( - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason.NOT_FOUND - errorMessage = "custom style failed" + fun resourceErrorBecomesACopiedMapLoadingFailureEvent(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != "custom://error-style.json") { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + handle.complete( + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason.NOT_FOUND + errorMessage = "custom style failed" + } + ) + ResourceProviderDecision.HANDLE } ) - ResourceProviderDecision.HANDLE + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("custom://error-style.json").await() + val event = waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_LOADING_FAILED) + val copiedMessage = event.message + assertEquals(RuntimeEventSourceType.MAP, event.sourceType) + assertEquals(map, event.mapSource) + assertNull(event.runtimeSource) + assertTrue(copiedMessage.contains("custom style failed")) + // A drained value stays readable after the next drain ends the batch window. + runtime.drainEvents() + assertEquals(copiedMessage, event.message) + } finally { + map.close() } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("custom://error-style.json") - val event = waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_LOADING_FAILED) - val copiedMessage = event.message - assertEquals(RuntimeEventSourceType.MAP, event.sourceType) - assertEquals(map, event.mapSource) - assertNull(event.runtimeSource) - assertTrue(copiedMessage.contains("custom style failed")) - // A drained value stays readable after the next drain ends the batch window. - runtime.drainEvents() - assertEquals(copiedMessage, event.message) - } finally { - map.close() } } - } @Test - fun closingAMapCancelsItsOutstandingResourceRequest() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val handledRequest = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != "custom://cancelled-style.json") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - handledRequest.store(handle) - ResourceProviderDecision.HANDLE - } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - map.setStyleUrl("custom://cancelled-style.json") - val handle = waitForHandledRequest(runtime, handledRequest) - - map.close() + fun closingAMapCancelsItsOutstandingResourceRequest(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val handledRequest = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != "custom://cancelled-style.json") { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + handledRequest.store(handle) + ResourceProviderDecision.HANDLE + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + map.setStyleUrl("custom://cancelled-style.json").await() + val handle = waitForHandledRequest(runtime, handledRequest) - assertTrue(waitForRequestCancellation(runtime, handle)) - assertFailsWith { - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() - } - ) - } - handle.close() - } - } + map.close() - // BND-154. - @Test - fun resourceProviderIsConsultedUntilClearedWhileMapIsLive() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val firstCalls = AtomicInt(0) - val secondCalls = AtomicInt(0) - runtime.setResourceProvider( - ResourceProviderCallback { _, _ -> - firstCalls.addAndFetch(1) - ResourceProviderDecision.PASS_THROUGH + assertTrue(waitForRequestCancellation(runtime, handle)) + assertFailsWith { + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - loadUnservedStyle(runtime, map, "jar:file:/packaged/first.json") - assertTrue(firstCalls.load() > 0) - - runtime.setResourceProvider( - ResourceProviderCallback { _, _ -> - secondCalls.addAndFetch(1) - ResourceProviderDecision.PASS_THROUGH - } - ) - val firstCallsAfterReplace = firstCalls.load() - loadUnservedStyle(runtime, map, "jar:file:/packaged/second.json") - assertTrue(secondCalls.load() > 0) - assertEquals(firstCallsAfterReplace, firstCalls.load()) - - runtime.clearResourceProvider() - val secondCallsAfterClear = secondCalls.load() - loadUnservedStyle(runtime, map, "jar:file:/packaged/third.json") - assertEquals(firstCallsAfterReplace, firstCalls.load()) - assertEquals(secondCallsAfterClear, secondCalls.load()) - - // Clearing an already cleared provider stays a successful no-op. - runtime.clearResourceProvider() - } finally { - map.close() + handle.close() } } - } + // BND-154. @Test - fun resourceTransformRewritesStyleRequestsUntilCleared() { - val previousNetworkStatus = Maplibre.networkStatus - Maplibre.setNetworkStatus(NetworkStatus.ONLINE) - try { + fun resourceProviderIsConsultedUntilClearedWhileMapIsLive(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val calls = AtomicInt(0) - val lastUrl = AtomicReference(null) - val lastKind = AtomicReference(null) - runtime.setResourceTransform( - ResourceTransformCallback { request -> - calls.addAndFetch(1) - lastUrl.store(request.url) - lastKind.store(request.kind) - "unsupported://rewritten-style.json" - } - ) + val firstCalls = AtomicInt(0) + val secondCalls = AtomicInt(0) + runtime + .setResourceProvider( + ResourceProviderCallback { _, _ -> + firstCalls.addAndFetch(1) + ResourceProviderDecision.PASS_THROUGH + } + ) + .await() val map = MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() try { - map.setStyleUrl("http://example.invalid/original-style.json") - assertTrue( - waitForCondition { - runtime.pump(1) - calls.load() > 0 - } - ) - assertEquals(1, calls.load()) - assertEquals("http://example.invalid/original-style.json", lastUrl.load()) - assertEquals(ResourceKind.STYLE, lastKind.load()) - - runtime.clearResourceTransform() - map.setStyleUrl("unsupported://after-clear-style.json") - repeat(100) { - runtime.pump(1) - // Keep native loading moving while proving the retired transform stays retired. - runtime.drainEvents() - assertEquals(1, calls.load()) - } + loadUnservedStyle(runtime, map, "jar:file:/packaged/first.json") + assertTrue(firstCalls.load() > 0) + + runtime + .setResourceProvider( + ResourceProviderCallback { _, _ -> + secondCalls.addAndFetch(1) + ResourceProviderDecision.PASS_THROUGH + } + ) + .await() + val firstCallsAfterReplace = firstCalls.load() + loadUnservedStyle(runtime, map, "jar:file:/packaged/second.json") + assertTrue(secondCalls.load() > 0) + assertEquals(firstCallsAfterReplace, firstCalls.load()) + + runtime.clearResourceProvider().await() + val secondCallsAfterClear = secondCalls.load() + loadUnservedStyle(runtime, map, "jar:file:/packaged/third.json") + assertEquals(firstCallsAfterReplace, firstCalls.load()) + assertEquals(secondCallsAfterClear, secondCalls.load()) + + // Clearing an already cleared provider stays a successful no-op. + runtime.clearResourceProvider().await() } finally { map.close() } } - } finally { - Maplibre.setNetworkStatus(previousNetworkStatus) } - } @Test - fun passThroughResourceRequestExpiresAfterTheProviderReturns() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val requestHandle = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl == "custom://pass-through-style.json") { - requestHandle.store(handle) - } - ResourceProviderDecision.PASS_THROUGH - } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) + fun resourceTransformRewritesStyleRequestsUntilCleared(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val previousNetworkStatus = Maplibre.networkStatus + Maplibre.setNetworkStatus(NetworkStatus.ONLINE) try { - map.setStyleUrl("custom://pass-through-style.json") - val handle = waitForHandledRequest(runtime, requestHandle) - assertEquals( - RuntimeEventType.MAP_LOADING_FAILED, - waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_LOADING_FAILED).type, - ) - assertFailsWith { handle.isCancelled() } - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val calls = AtomicInt(0) + val lastUrl = AtomicReference(null) + val lastKind = AtomicReference(null) + runtime + .setResourceTransform( + ResourceTransformCallback { request -> + calls.addAndFetch(1) + lastUrl.store(request.url) + lastKind.store(request.kind) + "unsupported://rewritten-style.json" + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("http://example.invalid/original-style.json").await() + waitForMapLoadingFailure(runtime, map) + val callsBeforeClear = calls.load() + assertTrue(callsBeforeClear > 0) + assertEquals("http://example.invalid/original-style.json", lastUrl.load()) + assertEquals(ResourceKind.STYLE, lastKind.load()) + + runtime.clearResourceTransform().await() + map.setStyleUrl("unsupported://after-clear-style.json").await() + waitForMapLoadingFailure(runtime, map, "unsupported://after-clear-style.json") + assertEquals(callsBeforeClear, calls.load()) + } finally { + map.close().await() + } } } finally { - map.close() + Maplibre.setNetworkStatus(previousNetworkStatus) } } - } @Test - fun runtimeCloseDuringResourceProviderCallbackRejectsBeforeNativeDestroy() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val closeError = AtomicReference(null) - runtime.setResourceProvider( - ResourceProviderCallback { request, _ -> - if (request.requestedUrl == "custom://close-during-provider.json") { - closeError.store(assertFailsWith { runtime.close() }) + fun passThroughResourceRequestExpiresAfterTheProviderReturns(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val requestHandle = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl == "custom://pass-through-style.json") { + requestHandle.store(handle) + } + ResourceProviderDecision.PASS_THROUGH + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("custom://pass-through-style.json").await() + val handle = waitForHandledRequest(runtime, requestHandle) + assertEquals( + RuntimeEventType.MAP_LOADING_FAILED, + waitForMapEventRecord(runtime, map, RuntimeEventType.MAP_LOADING_FAILED).type, + ) + assertFailsWith { handle.isCancelled() } + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - ResourceProviderDecision.PASS_THROUGH + } finally { + map.close() } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("custom://close-during-provider.json") - assertTrue( - waitForCondition { - runtime.pump(1) - closeError.load() != null - } - ) - assertFalse(runtime.isClosed) - } finally { - map.close() } } - } @Test - fun runtimeCloseDuringResourceTransformCallbackRejectsBeforeNativeDestroy() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val closeError = AtomicReference(null) - runtime.setResourceTransform( - ResourceTransformCallback { request -> - if (request.url == "http://example.invalid/close-during-transform.json") { - closeError.store(assertFailsWith { runtime.close() }) - } - "unsupported://close-during-transform.json" + fun runtimeCloseDuringResourceProviderCallbackRejectsBeforeNativeDestroy(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val closeError = AtomicReference(null) + runtime + .setResourceProvider( + ResourceProviderCallback { request, _ -> + if (request.requestedUrl == "custom://close-during-provider.json") { + closeError.store( + assertFailsWith { runSuspendTest { runtime.close() } } + ) + } + ResourceProviderDecision.PASS_THROUGH + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("custom://close-during-provider.json").await() + assertTrue(waitForCondition { closeError.load() != null }) + assertFalse(runtime.isClosed) + } finally { + map.close() } - ) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl("http://example.invalid/close-during-transform.json") - assertTrue( - waitForCondition { - runtime.pump(1) - closeError.load() != null - } - ) - assertFalse(runtime.isClosed) - } finally { - map.close() } } - } - private fun waitForOperation( - runtime: RuntimeHandle, - operation: OfflineOperationHandle<*>, - ): RuntimeEventPayload.OfflineOperationCompleted { - repeat(10_000) { - runtime.pump(0) - for (event in runtime.drainEvents().events) { - val completed = event.payload as? RuntimeEventPayload.OfflineOperationCompleted ?: continue - if (completed.operationId != operation.id) continue - assertEquals(RuntimeEventType.OFFLINE_OPERATION_COMPLETED, event.type) - assertEquals(operation.kind, completed.operationKind) - assertEquals(operation.resultKind, completed.resultKind) - assertEquals(MaplibreStatus.OK.nativeCode, completed.resultStatus) - return completed + @Test + fun runtimeCloseDuringResourceTransformCallbackRejectsBeforeNativeDestroy(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val closeError = AtomicReference(null) + runtime + .setResourceTransform( + ResourceTransformCallback { request -> + if (request.url == "http://example.invalid/close-during-transform.json") { + closeError.store( + assertFailsWith { runSuspendTest { runtime.close() } } + ) + } + "unsupported://close-during-transform.json" + } + ) + .await() + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl("http://example.invalid/close-during-transform.json").await() + assertTrue(waitForCondition { closeError.load() != null }) + assertFalse(runtime.isClosed) + } finally { + map.close() + } } - runtime.pump(1) - waitForAsyncTestWork() } - error("offline operation did not complete: ${operation.id}") - } - private fun waitForMapEvent( + private suspend fun waitForMapEvent( runtime: RuntimeHandle, map: MapHandle, type: RuntimeEventType, ): Boolean { repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() if (runtime.drainEvents().events.any { it.type == type && it.mapSource == map }) return true - runtime.pump(1) + runtime.barrier().await() waitForAsyncTestWork() } return false } - private fun waitForMapEventRecord( + private suspend fun waitForMapEventRecord( runtime: RuntimeHandle, map: MapHandle, type: RuntimeEventType, ): RuntimeEvent { repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() for (event in runtime.drainEvents().events) { if (event.type == type && event.mapSource == map) return event } - runtime.pump(1) + runtime.barrier().await() waitForAsyncTestWork() } error("runtime event $type did not arrive") } - private fun waitForHandledRequest( + private suspend fun waitForHandledRequest( runtime: RuntimeHandle, handledRequest: AtomicReference, ): ResourceRequestHandle { @@ -605,19 +624,19 @@ class RuntimeHandleTest { handledRequest.load()?.let { return it } - runtime.pump(1) + runtime.barrier().await() waitForAsyncTestWork() } error("resource provider did not receive handled request") } - private fun waitForRequestCancellation( + private suspend fun waitForRequestCancellation( runtime: RuntimeHandle, handle: ResourceRequestHandle, ): Boolean { repeat(10_000) { if (handle.isCancelled()) return true - runtime.pump(1) + runtime.barrier().await() waitForAsyncTestWork() } return false @@ -627,35 +646,35 @@ class RuntimeHandleTest { * Loads a style URL whose scheme no file source serves; the failure names the scheme and URL, * proving the request reached the network file source. */ - private fun loadUnservedStyle(runtime: RuntimeHandle, map: MapHandle, styleUrl: String) { - map.setStyleUrl(styleUrl) + private suspend fun loadUnservedStyle(runtime: RuntimeHandle, map: MapHandle, styleUrl: String) { + map.setStyleUrl(styleUrl).await() val message = waitForMapLoadingFailure(runtime, map, styleUrl) assertTrue(message.contains("\"jar\""), "unexpected loading failure message: $message") } - private fun waitForMapLoadingFailure( + private suspend fun waitForMapLoadingFailure( runtime: RuntimeHandle, map: MapHandle, - styleUrl: String, + styleUrl: String? = null, ): String { repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() for (event in runtime.drainEvents().events) { if ( event.type == RuntimeEventType.MAP_LOADING_FAILED && event.mapSource == map && - event.message.contains(styleUrl) + (styleUrl == null || event.message.contains(styleUrl)) ) { return event.message } } - runtime.pump(1) + runtime.barrier().await() waitForAsyncTestWork() } error("map loading failure for $styleUrl did not arrive") } - private fun waitForCondition(condition: () -> Boolean): Boolean { + private suspend fun waitForCondition(condition: suspend () -> Boolean): Boolean { repeat(10_000) { if (condition()) return true waitForAsyncTestWork() diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeOfflineConformanceTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeOfflineConformanceTest.kt index d4c433e26..c6437f1ed 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeOfflineConformanceTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeOfflineConformanceTest.kt @@ -6,7 +6,6 @@ import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNull import kotlin.test.assertTrue -import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds import org.maplibre.nativeffi.offline.OfflineRegionDefinition @@ -18,114 +17,72 @@ class RuntimeOfflineConformanceTest { private val drained = mutableListOf() @Test - fun offlineRegionApisCreateObserveAndCopyPublicEvents() { - val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) - try { - val definition = tileDefinition() - val createMetadata = byteArrayOf(1, 2, 3) - val createOperation = runtime.startCreateOfflineRegion(definition, createMetadata) - createMetadata[0] = 9 - waitForOperation(runtime, createOperation) - val created = runtime.takeCreateOfflineRegionResult(createOperation) - assertTrue(created.id > 0) - assertEquals(definition, created.definition) - assertContentEquals(byteArrayOf(1, 2, 3), created.metadata) - val copiedMetadata = created.metadata - copiedMetadata[0] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), created.metadata) - - assertEquals(created, offlineRegion(runtime, created.id)) - assertTrue(offlineRegions(runtime).contains(created)) - - val updateMetadata = byteArrayOf(4, 5) - val updateOperation = runtime.startUpdateOfflineRegionMetadata(created.id, updateMetadata) - updateMetadata[0] = 9 - waitForOperation(runtime, updateOperation) - val updated = runtime.takeUpdateOfflineRegionMetadataResult(updateOperation) - assertEquals(created.id, updated.id) - assertContentEquals(byteArrayOf(4, 5), updated.metadata) - - val status = offlineRegionStatus(runtime, created.id) - assertEquals(OfflineRegionDownloadState.INACTIVE, status.downloadState) - - completeVoidOperation(runtime, runtime.startSetOfflineRegionObserved(created.id, true)) - completeVoidOperation( - runtime, - runtime.startSetOfflineRegionDownloadState(created.id, OfflineRegionDownloadState.ACTIVE), - ) - val observed = waitForObservedOfflineRegionEvent(runtime, created.id) - assertEquals(RuntimeEventSourceType.RUNTIME, observed.sourceType) - assertEquals(runtime, observed.runtimeSource) - assertNull(observed.mapSource) - assertObservedOfflineRegionStatusPayload(created.id, observed.payload) - // A drained value stays readable after the next drain ends the batch window. - val copiedMessage = observed.message - runtime.drainEvents() - assertEquals(copiedMessage, observed.message) - - completeVoidOperation(runtime, runtime.startSetOfflineRegionObserved(created.id, false)) - completeVoidOperation( - runtime, - runtime.startSetOfflineRegionDownloadState(created.id, OfflineRegionDownloadState.INACTIVE), - ) - completeVoidOperation(runtime, runtime.startInvalidateOfflineRegion(created.id)) - completeVoidOperation(runtime, runtime.startDeleteOfflineRegion(created.id)) - assertNull(offlineRegion(runtime, created.id)) - } finally { - runtime.close() - } - } - - private fun waitForOperation( - runtime: RuntimeHandle, - operation: OfflineOperationHandle<*>, - ): RuntimeEventPayload.OfflineOperationCompleted { - repeat(10_000) { - drain(runtime) - for (event in drained) { - val completed = event.payload as? RuntimeEventPayload.OfflineOperationCompleted - if (completed == null || completed.operationId != operation.id) { - continue - } - assertEquals(operation.kind, completed.operationKind) - assertEquals(operation.resultKind, completed.resultKind) - if (completed.resultStatus != MaplibreStatus.OK.nativeCode) { - error("offline operation failed: ${event.message}") - } - return completed + fun offlineRegionApisCreateObserveAndCopyPublicEvents(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) + try { + val definition = tileDefinition() + val createMetadata = byteArrayOf(1, 2, 3) + val create = runtime.createOfflineRegion(definition, createMetadata) + createMetadata[0] = 9 + val created = create.await() + assertTrue(created.id > 0) + assertEquals(definition, created.definition) + assertContentEquals(byteArrayOf(1, 2, 3), created.metadata) + val copiedMetadata = created.metadata + copiedMetadata[0] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), created.metadata) + + assertEquals(created, offlineRegion(runtime, created.id)) + assertTrue(offlineRegions(runtime).contains(created)) + + val updateMetadata = byteArrayOf(4, 5) + val update = runtime.updateOfflineRegionMetadata(created.id, updateMetadata) + updateMetadata[0] = 9 + val updated = update.await() + assertEquals(created.id, updated.id) + assertContentEquals(byteArrayOf(4, 5), updated.metadata) + + val status = offlineRegionStatus(runtime, created.id) + assertEquals(OfflineRegionDownloadState.INACTIVE, status.downloadState) + + runtime.setOfflineRegionObserved(created.id, true).await() + runtime.setOfflineRegionDownloadState(created.id, OfflineRegionDownloadState.ACTIVE).await() + val observed = waitForObservedOfflineRegionEvent(runtime, created.id) + assertEquals(RuntimeEventSourceType.RUNTIME, observed.sourceType) + assertEquals(runtime, observed.runtimeSource) + assertNull(observed.mapSource) + assertObservedOfflineRegionStatusPayload(created.id, observed.payload) + // A drained value stays readable after the next drain ends the batch window. + val copiedMessage = observed.message + runtime.drainEvents() + assertEquals(copiedMessage, observed.message) + + runtime.setOfflineRegionObserved(created.id, false).await() + runtime + .setOfflineRegionDownloadState(created.id, OfflineRegionDownloadState.INACTIVE) + .await() + runtime.invalidateOfflineRegion(created.id).await() + runtime.deleteOfflineRegion(created.id).await() + assertNull(offlineRegion(runtime, created.id)) + } finally { + runtime.close().await() } - runtime.pump(1) } - error("offline operation did not complete: ${operation.id}") - } - - private fun completeVoidOperation( - runtime: RuntimeHandle, - operation: OfflineOperationHandle, - ) { - waitForOperation(runtime, operation) - operation.close() - } - private fun offlineRegion(runtime: RuntimeHandle, id: Long): OfflineRegionInfo? { - val operation = runtime.startOfflineRegion(id) - waitForOperation(runtime, operation) - return runtime.takeOfflineRegionResult(operation) + private suspend fun offlineRegion(runtime: RuntimeHandle, id: Long): OfflineRegionInfo? { + return runtime.offlineRegion(id).await() } - private fun offlineRegions(runtime: RuntimeHandle): List { - val operation = runtime.startOfflineRegions() - waitForOperation(runtime, operation) - return runtime.takeOfflineRegionsResult(operation) + private suspend fun offlineRegions(runtime: RuntimeHandle): List { + return runtime.offlineRegions().await() } - private fun offlineRegionStatus(runtime: RuntimeHandle, id: Long): OfflineRegionStatus { - val operation = runtime.startOfflineRegionStatus(id) - waitForOperation(runtime, operation) - return runtime.takeOfflineRegionStatusResult(operation) + private suspend fun offlineRegionStatus(runtime: RuntimeHandle, id: Long): OfflineRegionStatus { + return runtime.offlineRegionStatus(id).await() } - private fun waitForObservedOfflineRegionEvent( + private suspend fun waitForObservedOfflineRegionEvent( runtime: RuntimeHandle, regionId: Long, ): RuntimeEvent { @@ -142,18 +99,18 @@ class RuntimeOfflineConformanceTest { else -> Unit } } - runtime.pump(1) + runtime.barrier().await() } error("offline region observation event did not arrive for region $regionId") } /** - * Pumps once and keeps every event this test has drained. One offline step drains the events of + * Drains once and keeps every event this test has observed. One offline step drains the events of * the steps before it, so the waiters below scan what the whole test has seen rather than one * batch. */ - private fun drain(runtime: RuntimeHandle) { - runtime.pump(0) + private suspend fun drain(runtime: RuntimeHandle) { + runtime.barrier().await() drained += runtime.drainEvents().events } diff --git a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandleTest.kt b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandleTest.kt index 760c7bb48..b8ea5959c 100644 --- a/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandleTest.kt +++ b/bindings/kotlin/src/commonTest/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandleTest.kt @@ -7,11 +7,14 @@ import kotlin.test.assertTrue import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.error.InvalidArgumentException import org.maplibre.nativeffi.error.InvalidStateException +import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapMode import org.maplibre.nativeffi.map.MapOptions +import org.maplibre.nativeffi.runtime.CommandDisposition import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.runtime.runSuspendTest class GeoJsonSourceDataHandleTest { @Test @@ -37,93 +40,100 @@ class GeoJsonSourceDataHandleTest { } @Test - fun onePreparedHandleInstallsOnManySources() { - withMap { map -> + fun onePreparedHandleInstallsOnManySources(): Unit = runSuspendTest { + withMap { _, map -> GeoJsonSourceDataHandle.create(featureCollection(), baseOptions()).use { data -> - map.addGeoJsonSourceData("places-a", data) - map.addGeoJsonSourceData("places-b", data) - assertEquals(SourceType.GEOJSON, map.styleSourceType("places-a")) - assertEquals(SourceType.GEOJSON, map.styleSourceType("places-b")) - map.setGeoJsonSourceData("places-a", data) + map.addGeoJsonSourceData("places-a", data).await() + map.addGeoJsonSourceData("places-b", data).await() + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("places-a").await()?.type) + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("places-b").await()?.type) + map.setGeoJsonSourceData("places-a", data).await() } } } @Test - fun releaseNeverInvalidatesAnInstalledSource() { - withMap { map -> + fun releaseNeverInvalidatesAnInstalledSource(): Unit = runSuspendTest { + withMap { _, map -> val data = GeoJsonSourceDataHandle.create(featureCollection()) - map.addGeoJsonSourceData("places", data) + map.addGeoJsonSourceData("places", data).await() data.close() // The source keeps its own reference and remains usable after the handle is gone. - assertEquals(SourceType.GEOJSON, map.styleSourceType("places")) + assertEquals(SourceType.GEOJSON, map.styleSourceInfo("places").await()?.type) GeoJsonSourceDataHandle.create(featureCollection()).use { replacement -> - map.setGeoJsonSourceData("places", replacement) + map.setGeoJsonSourceData("places", replacement).await() } // A released handle is no longer installable. - assertFailsWith { map.addGeoJsonSourceData("more-places", data) } + assertFailsWith { + map.addGeoJsonSourceData("more-places", data).await() + } } } @Test - fun setRejectsDataPreparedWithMismatchedOptions() { - withMap { map -> + fun setRejectsDataPreparedWithMismatchedOptions(): Unit = runSuspendTest { + withMap { _, map -> GeoJsonSourceDataHandle.create(featureCollection(), baseOptions()).use { data -> - map.addGeoJsonSourceData("places", data) + map.addGeoJsonSourceData("places", data).await() } - assertFailsWith { - GeoJsonSourceDataHandle.create(featureCollection(), baseOptions().copy { buffer = 32 }) - .use { mismatched -> map.setGeoJsonSourceData("places", mismatched) } + GeoJsonSourceDataHandle.create(featureCollection(), baseOptions().copy { buffer = 32 }).use { + mismatched -> + assertCommandFailed(map.setGeoJsonSourceData("places", mismatched).await()) } // Cluster aggregations are part of the options comparison, so data // prepared with different clusterProperties is rejected too. - assertFailsWith { - GeoJsonSourceDataHandle.create( - featureCollection(), - baseOptions().copy { - clusterProperties = "{\"total\":[\"+\",[\"get\",\"rank\"]]}".encodeToByteArray() - }, - ) - .use { mismatched -> map.setGeoJsonSourceData("places", mismatched) } - } + GeoJsonSourceDataHandle.create( + featureCollection(), + baseOptions().copy { + clusterProperties = "{\"total\":[\"+\",[\"get\",\"rank\"]]}".encodeToByteArray() + }, + ) + .use { mismatched -> + assertCommandFailed(map.setGeoJsonSourceData("places", mismatched).await()) + } } } @Test - fun synchronousTilingOverridesALiveSource() { - withMap { map -> + fun synchronousTilingOverridesALiveSource(): Unit = runSuspendTest { + withMap { _, map -> GeoJsonSourceDataHandle.create(featureCollection()).use { data -> - map.addGeoJsonSourceData("places", data) - map.setGeoJsonSourceSynchronousTiling("places", true) + map.addGeoJsonSourceData("places", data).await() + map.setGeoJsonSourceSynchronousTiling("places", true).await() GeoJsonSourceDataHandle.create(featureCollection()).use { update -> - map.setGeoJsonSourceData("places", update) + map.setGeoJsonSourceData("places", update).await() } - map.setGeoJsonSourceSynchronousTiling("places", false) - } - assertFailsWith { - map.setGeoJsonSourceSynchronousTiling("missing", true) + map.setGeoJsonSourceSynchronousTiling("places", false).await() } + assertCommandFailed(map.setGeoJsonSourceSynchronousTiling("missing", true).await()) } } - private fun withMap(block: (MapHandle) -> Unit) { + private fun assertCommandFailed(completion: org.maplibre.nativeffi.runtime.CommandCompletion) { + assertEquals(CommandDisposition.FAILED, completion.disposition) + assertEquals(MaplibreStatus.INVALID_ARGUMENT, completion.status) + assertTrue(completion.diagnostic.isNotEmpty()) + } + + private suspend fun withMap(block: suspend (RuntimeHandle, MapHandle) -> Unit) { val runtime = RuntimeHandle.create(RuntimeOptions()) val map = MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - mapMode = MapMode.STATIC - }, - ) + runtime, + MapOptions().apply { + width = 64 + height = 64 + mapMode = MapMode.STATIC + }, + ) + .await() try { - map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()) - block(map) + map.setStyleJson("""{"version":8,"sources":{},"layers":[]}""".encodeToByteArray()).await() + block(runtime, map) } finally { map.close() runtime.close() diff --git a/bindings/kotlin/src/jextract/maplibre-native-c.includes b/bindings/kotlin/src/jextract/maplibre-native-c.includes index 651747d69..36d56ba4d 100644 --- a/bindings/kotlin/src/jextract/maplibre-native-c.includes +++ b/bindings/kotlin/src/jextract/maplibre-native-c.includes @@ -1,89 +1,77 @@ -# Generated from the Kotlin binding jextract include task. -# Keep declarations from include/maplibre_native_c/** only. - -# include/maplibre_native_c/android.h --include-function mln_android_init - -# include/maplibre_native_c/base.h +--include-constant MLN_HANDLE_NULL --include-constant MLN_RENDER_BACKEND_FLAG_METAL --include-constant MLN_RENDER_BACKEND_FLAG_OPENGL --include-constant MLN_RENDER_BACKEND_FLAG_VULKAN --include-constant MLN_RENDER_BACKEND_FLAG_WEBGPU +--include-constant MLN_STATUS_CANCELLED --include-constant MLN_STATUS_INVALID_ARGUMENT --include-constant MLN_STATUS_INVALID_STATE --include-constant MLN_STATUS_NATIVE_ERROR --include-constant MLN_STATUS_OK --include-constant MLN_STATUS_UNSUPPORTED +--include-constant MLN_STATUS_BUSY +--include-constant MLN_STATUS_NOT_READY +--include-constant MLN_STATUS_NOT_FOUND +--include-constant MLN_STATUS_TARGET_LOST --include-constant MLN_STATUS_WRONG_THREAD ---include-function mln_map_copy_layer_source_id ---include-function mln_map_copy_layer_source_layer +--include-struct mln_buffer_view +--include-struct mln_completion +--include-struct mln_completion_result +--include-struct mln_wake +--include-typedef mln_completion_callback +--include-typedef mln_completion_release +--include-typedef mln_wake_callback +--include-typedef mln_wake_release --include-function mln_buffer_destroy --include-function mln_buffer_get --include-function mln_c_version --include-function mln_supported_render_backend_mask - -# include/maplibre_native_c/camera.h +--include-typedef mln_adapter_log_queue +--include-typedef mln_adapter_resource_request_queue +--include-typedef mln_acquired_frame +--include-typedef mln_buffer +--include-typedef mln_event_batch +--include-typedef mln_map +--include-typedef mln_map_projection +--include-typedef mln_offline_region_list +--include-typedef mln_offline_region_snapshot +--include-typedef mln_render_frame_batch +--include-typedef mln_render_session +--include-typedef mln_resource_request_handle +--include-typedef mln_runtime --include-function mln_animation_options_default --include-function mln_bound_options_default --include-function mln_camera_fit_options_default --include-function mln_camera_options_default +--include-function mln_camera_delta_default +--include-function mln_camera_update_default --include-function mln_free_camera_options_default --include-function mln_map_camera_for_geometry --include-function mln_map_camera_for_lat_lng_bounds --include-function mln_map_camera_for_lat_lngs ---include-function mln_map_cancel_transitions +--include-function mln_map_camera_query +--include-function mln_map_camera_snapshot_get +--include-function mln_map_apply_camera_delta --include-function mln_map_dump_debug_logs ---include-function mln_map_ease_to ---include-function mln_map_fly_to ---include-function mln_map_get_bounds ---include-function mln_map_get_camera ---include-function mln_map_get_debug_options ---include-function mln_map_get_free_camera_options ---include-function mln_map_get_projection_mode ---include-function mln_map_get_rendering_stats_view_enabled ---include-function mln_map_get_size ---include-function mln_map_get_tile_options ---include-function mln_map_get_viewport_options ---include-function mln_map_is_fully_loaded ---include-function mln_map_is_gesture_in_progress ---include-function mln_map_jump_to --include-function mln_map_lat_lng_bounds_for_camera --include-function mln_map_lat_lng_bounds_for_camera_unwrapped --include-function mln_map_lat_lng_for_pixel --include-function mln_map_lat_lngs_for_pixels ---include-function mln_map_move_by ---include-function mln_map_move_by_animated ---include-function mln_map_pitch_by ---include-function mln_map_pitch_by_animated --include-function mln_map_pixel_for_lat_lng --include-function mln_map_pixels_for_lat_lngs ---include-function mln_map_rotate_by ---include-function mln_map_rotate_by_animated ---include-function mln_map_scale_by ---include-function mln_map_scale_by_animated --include-function mln_map_set_bounds --include-function mln_map_set_debug_options --include-function mln_map_set_free_camera_options ---include-function mln_map_set_gesture_in_progress ---include-function mln_map_get_layer_max_zoom ---include-function mln_map_get_layer_min_zoom ---include-function mln_map_get_layer_visibility --include-function mln_map_set_projection_mode ---include-function mln_map_copy_style_image_stretches --include-function mln_map_set_rendering_stats_view_enabled --include-function mln_map_set_tile_options --include-function mln_map_set_viewport_options --include-function mln_map_tile_options_default ---include-function mln_map_set_layer_source_id ---include-function mln_map_set_layer_source_layer ---include-function mln_map_set_layer_visibility +--include-function mln_map_update_camera --include-function mln_map_viewport_options_default --include-function mln_projection_mode_default - -# include/maplibre_native_c/diagnostics.h --include-function mln_thread_last_error_message - -# include/maplibre_native_c/logging.h --include-constant MLN_LOG_EVENT_ANDROID --include-constant MLN_LOG_EVENT_CRASH --include-constant MLN_LOG_EVENT_DATABASE @@ -113,10 +101,7 @@ --include-function mln_log_set_async_severity_mask --include-function mln_log_set_callback --include-typedef mln_log_callback ---include-function mln_map_set_layer_max_zoom ---include-function mln_map_set_layer_min_zoom - -# include/maplibre_native_c/map.h +--include-typedef mln_log_callback_release --include-constant MLN_ANIMATION_OPTION_DURATION --include-constant MLN_ANIMATION_OPTION_EASING --include-constant MLN_ANIMATION_OPTION_MIN_ZOOM @@ -140,39 +125,23 @@ --include-constant MLN_CAMERA_OPTION_PITCH --include-constant MLN_CAMERA_OPTION_ROLL --include-constant MLN_CAMERA_OPTION_ZOOM +--include-constant MLN_CAMERA_UPDATE_MODE_EASE +--include-constant MLN_CAMERA_UPDATE_MODE_FLY +--include-constant MLN_CAMERA_UPDATE_MODE_JUMP --include-constant MLN_CONSTRAIN_MODE_HEIGHT_ONLY --include-constant MLN_CONSTRAIN_MODE_NONE --include-constant MLN_CONSTRAIN_MODE_SCREEN --include-constant MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT ---include-constant MLN_FEATURE_IDENTIFIER_TYPE_DOUBLE ---include-constant MLN_FEATURE_IDENTIFIER_TYPE_INT ---include-constant MLN_FEATURE_IDENTIFIER_TYPE_NULL ---include-constant MLN_FEATURE_IDENTIFIER_TYPE_STRING ---include-constant MLN_FEATURE_IDENTIFIER_TYPE_UINT --include-constant MLN_FEATURE_STATE_SELECTOR_FEATURE_ID --include-constant MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID --include-constant MLN_FEATURE_STATE_SELECTOR_STATE_KEY --include-constant MLN_FREE_CAMERA_OPTION_ORIENTATION --include-constant MLN_FREE_CAMERA_OPTION_POSITION ---include-constant MLN_GEOJSON_TYPE_FEATURE ---include-constant MLN_GEOJSON_TYPE_FEATURE_COLLECTION ---include-constant MLN_GEOJSON_TYPE_GEOMETRY ---include-constant MLN_GEOMETRY_TYPE_EMPTY ---include-constant MLN_GEOMETRY_TYPE_GEOMETRY_COLLECTION ---include-constant MLN_GEOMETRY_TYPE_LINE_STRING ---include-constant MLN_GEOMETRY_TYPE_MULTI_LINE_STRING ---include-constant MLN_GEOMETRY_TYPE_MULTI_POINT ---include-constant MLN_GEOMETRY_TYPE_MULTI_POLYGON ---include-constant MLN_GEOMETRY_TYPE_POINT ---include-constant MLN_GEOMETRY_TYPE_POLYGON ---include-constant MLN_JSON_VALUE_TYPE_ARRAY ---include-constant MLN_JSON_VALUE_TYPE_BOOL ---include-constant MLN_JSON_VALUE_TYPE_DOUBLE ---include-constant MLN_JSON_VALUE_TYPE_INT ---include-constant MLN_JSON_VALUE_TYPE_NULL ---include-constant MLN_JSON_VALUE_TYPE_OBJECT ---include-constant MLN_JSON_VALUE_TYPE_STRING ---include-constant MLN_JSON_VALUE_TYPE_UINT +--include-constant MLN_GESTURE_PHASE_BEGIN +--include-constant MLN_GESTURE_PHASE_CANCEL +--include-constant MLN_GESTURE_PHASE_END +--include-constant MLN_GESTURE_PHASE_NONE +--include-constant MLN_GESTURE_PHASE_UPDATE --include-constant MLN_MAP_DEBUG_COLLISION --include-constant MLN_MAP_DEBUG_DEPTH_BUFFER --include-constant MLN_MAP_DEBUG_OVERDRAW @@ -204,54 +173,21 @@ --include-constant MLN_TILE_LOD_MODE_DISTANCE --include-constant MLN_VIEWPORT_MODE_DEFAULT --include-constant MLN_VIEWPORT_MODE_FLIPPED_Y ---include-function mln_map_copy_loaded_style_json ---include-function mln_map_copy_style_url ---include-function mln_map_create ---include-function mln_map_destroy ---include-function mln_map_get_event_mask ---include-function mln_map_options_default ---include-function mln_map_request_repaint ---include-function mln_map_request_still_image ---include-function mln_map_set_event_mask ---include-function mln_map_set_feature_state ---include-function mln_map_get_feature_state ---include-function mln_map_remove_feature_state ---include-function mln_map_set_style_json ---include-function mln_map_set_style_url ---include-function mln_offline_region_list_count ---include-function mln_offline_region_list_destroy ---include-function mln_offline_region_list_get ---include-function mln_offline_region_snapshot_destroy ---include-function mln_offline_region_snapshot_get ---include-function mln_runtime_offline_region_create_start ---include-function mln_runtime_offline_region_create_take_result ---include-function mln_runtime_offline_region_delete_start ---include-function mln_runtime_offline_region_get_start ---include-function mln_runtime_offline_region_get_status_start ---include-function mln_runtime_offline_region_get_status_take_result ---include-function mln_runtime_offline_region_get_take_result ---include-function mln_runtime_offline_region_invalidate_start ---include-function mln_runtime_offline_region_set_download_state_start ---include-function mln_runtime_offline_region_set_observed_start ---include-function mln_runtime_offline_region_update_metadata_start ---include-function mln_runtime_offline_region_update_metadata_take_result ---include-function mln_runtime_offline_regions_list_start ---include-function mln_runtime_offline_regions_list_take_result ---include-function mln_runtime_offline_regions_merge_database_start ---include-function mln_runtime_offline_regions_merge_database_take_result --include-struct mln_animation_options --include-struct mln_bound_options ---include-struct mln_buffer_view --include-struct mln_camera_fit_options +--include-struct mln_camera_delta --include-struct mln_camera_options +--include-struct mln_camera_query_result +--include-struct mln_camera_update --include-struct mln_edge_insets --include-struct mln_feature_state_selector --include-struct mln_free_camera_options ---include-struct mln_image_content ---include-struct mln_image_stretch --include-struct mln_lat_lng --include-struct mln_lat_lng_bounds +--include-struct mln_logical_extent --include-struct mln_map_options +--include-struct mln_map_snapshot --include-struct mln_map_tile_options --include-struct mln_map_viewport_options --include-struct mln_offline_geometry_region_definition @@ -264,21 +200,39 @@ --include-struct mln_screen_point --include-struct mln_unit_bezier --include-struct mln_vec3 - -# include/maplibre_native_c/projection.h ---include-constant MLN_STYLE_IMAGE_OPTION_CONTENT +--include-function mln_map_release +--include-function mln_map_create +--include-function mln_map_loaded_style_json +--include-function mln_map_options_default +--include-function mln_map_request_repaint +--include-function mln_map_resize +--include-function mln_map_request_still_image +--include-function mln_map_set_event_mask +--include-function mln_map_set_feature_state +--include-function mln_map_get_feature_state +--include-function mln_map_remove_feature_state +--include-function mln_map_set_style_json +--include-function mln_map_set_style_url +--include-function mln_map_snapshot_get +--include-function mln_map_style_url +--include-function mln_offline_region_list_count +--include-function mln_offline_region_list_destroy +--include-function mln_offline_region_list_get +--include-function mln_offline_region_snapshot_destroy +--include-function mln_offline_region_snapshot_get +--include-function mln_runtime_offline_region_create +--include-function mln_runtime_offline_region_delete +--include-function mln_runtime_offline_region_get +--include-function mln_runtime_offline_region_get_status +--include-function mln_runtime_offline_region_invalidate +--include-function mln_runtime_offline_region_set_download_state +--include-function mln_runtime_offline_region_set_observed +--include-function mln_runtime_offline_region_update_metadata +--include-function mln_runtime_offline_regions_list +--include-function mln_runtime_offline_regions_merge_database --include-function mln_lat_lng_for_projected_meters ---include-constant MLN_STYLE_LAYER_VISIBILITY_NONE ---include-constant MLN_STYLE_IMAGE_OPTION_STRETCH_X ---include-constant MLN_STYLE_IMAGE_OPTION_STRETCH_Y ---include-constant MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT ---include-constant MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH ---include-constant MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL ---include-constant MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY ---include-constant MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK ---include-constant MLN_STYLE_LAYER_VISIBILITY_VISIBLE +--include-function mln_map_projection_close --include-function mln_map_projection_create ---include-function mln_map_projection_destroy --include-function mln_map_projection_get_camera --include-function mln_map_projection_lat_lng_for_pixel --include-function mln_map_projection_pixel_for_lat_lng @@ -314,17 +268,68 @@ --include-struct mln_screen_box --include-struct mln_screen_line_string --include-struct mln_source_feature_query_options - -# include/maplibre_native_c/render_session.h +--include-constant MLN_RENDER_RESULT_DEADLINE_MISSED +--include-constant MLN_RENDER_RESULT_NO_UPDATE +--include-constant MLN_RENDER_RESULT_RENDERED +--include-constant MLN_RENDER_RESULT_SIZE_PENDING +--include-constant MLN_RENDER_RESULT_SUPERSEDED +--include-constant MLN_RENDER_RESULT_TARGET_NOT_READY +--include-constant MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD +--include-constant MLN_RENDER_DRIVER_CORE_WORKER +--include-constant MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC +--include-constant MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION +--include-constant MLN_RENDER_SESSION_CAPABILITY_PRESENTATION +--include-constant MLN_RENDER_SESSION_CAPABILITY_READBACK +--include-constant MLN_RENDER_SESSION_STATE_ABANDONED +--include-constant MLN_RENDER_SESSION_STATE_ATTACHED +--include-constant MLN_RENDER_SESSION_STATE_ATTACHING +--include-constant MLN_RENDER_SESSION_STATE_DETACHED +--include-constant MLN_RENDER_SESSION_STATE_DETACHING +--include-constant MLN_RENDER_SESSION_STATE_TARGET_LOST +--include-constant MLN_RENDER_ABANDON_DISPOSITION_CLEAN +--include-constant MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED +--include-constant MLN_FRAME_DEMAND_IF_NEEDED +--include-constant MLN_FRAME_DEMAND_PRESENT +--include-constant MLN_GPU_SYNC_CPU_COMPLETE +--include-constant MLN_GPU_SYNC_METAL_SHARED_EVENT +--include-constant MLN_GPU_SYNC_OPENGL_FENCE +--include-constant MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE +--include-constant MLN_GPU_SYNC_WEBGPU_TOKEN +--include-struct mln_frame_demand +--include-struct mln_gpu_sync +--include-struct mln_render_abandon_result +--include-struct mln_render_frame_result +--include-struct mln_render_session_attach_options +--include-struct mln_render_session_capabilities +--include-struct mln_render_session_snapshot +--include-function mln_frame_demand_default +--include-function mln_render_session_attach_options_default +--include-function mln_gpu_sync_default +--include-function mln_render_session_get_capabilities +--include-function mln_render_session_get_snapshot +--include-function mln_render_session_request_frame +--include-function mln_render_session_drain_frame_results +--include-function mln_render_frame_batch_count +--include-function mln_render_frame_batch_get +--include-function mln_render_frame_batch_release +--include-function mln_render_session_acquire_frame +--include-function mln_acquired_frame_get_result +--include-function mln_acquired_frame_get_producer_sync +--include-function mln_acquired_frame_release +--include-function mln_render_session_resize +--include-function mln_render_session_barrier --include-function mln_render_session_clear_data ---include-function mln_render_session_destroy --include-function mln_render_session_detach --include-function mln_render_session_dump_debug_logs --include-function mln_render_session_reduce_memory_use ---include-function mln_render_session_render_update ---include-function mln_render_session_resize - -# include/maplibre_native_c/render_target.h +--include-function mln_render_session_service_driver_work +--include-function mln_render_session_abandon +--include-function mln_render_session_destroy +--include-constant MLN_OPENGL_CLIENT_API_GL +--include-constant MLN_OPENGL_CLIENT_API_GLES +--include-constant MLN_OPENGL_CLIENT_API_UNSPECIFIED +--include-constant MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED +--include-constant MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED --include-constant MLN_OPENGL_CONTEXT_PLATFORM_EGL --include-constant MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED --include-constant MLN_OPENGL_CONTEXT_PLATFORM_WEBGL @@ -332,41 +337,30 @@ --include-constant MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL --include-constant MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL --include-constant MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL ---include-function mln_opengl_supported_context_provider_mask ---include-function mln_render_target_extent_physical_size --include-struct mln_egl_context_descriptor --include-struct mln_metal_context_descriptor --include-struct mln_opengl_context_descriptor --include-struct mln_render_target_extent --include-struct mln_vulkan_context_descriptor --include-struct mln_webgl_context_descriptor +--include-constant MLN_WEBGL_CONTEXT_EXISTING +--include-constant MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS --include-struct mln_webgpu_context_descriptor --include-struct mln_wgl_context_descriptor - -# include/maplibre_native_c/runtime.h +--include-function mln_opengl_supported_context_provider_mask +--include-function mln_render_target_extent_physical_size --include-constant MLN_AMBIENT_CACHE_OPERATION_CLEAR --include-constant MLN_AMBIENT_CACHE_OPERATION_INVALIDATE --include-constant MLN_AMBIENT_CACHE_OPERATION_PACK_DATABASE --include-constant MLN_AMBIENT_CACHE_OPERATION_RESET_DATABASE +--include-constant MLN_CAMERA_CHANGE_MODE_ANIMATED +--include-constant MLN_CAMERA_CHANGE_MODE_IMMEDIATE +--include-constant MLN_COMMAND_DISPOSITION_CANCELLED +--include-constant MLN_COMMAND_DISPOSITION_COMMITTED +--include-constant MLN_COMMAND_DISPOSITION_FAILED +--include-constant MLN_COMMAND_DISPOSITION_SUPERSEDED --include-constant MLN_NETWORK_STATUS_OFFLINE --include-constant MLN_NETWORK_STATUS_ONLINE ---include-constant MLN_OFFLINE_OPERATION_AMBIENT_CACHE ---include-constant MLN_OFFLINE_OPERATION_REGIONS_LIST ---include-constant MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE ---include-constant MLN_OFFLINE_OPERATION_REGION_CREATE ---include-constant MLN_OFFLINE_OPERATION_REGION_DELETE ---include-constant MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE ---include-constant MLN_OFFLINE_OPERATION_REGION_GET ---include-constant MLN_OFFLINE_OPERATION_REGION_GET_STATUS ---include-constant MLN_OFFLINE_OPERATION_REGION_INVALIDATE ---include-constant MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE ---include-constant MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED ---include-constant MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA ---include-constant MLN_OFFLINE_OPERATION_RESULT_NONE ---include-constant MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION ---include-constant MLN_OFFLINE_OPERATION_RESULT_REGION ---include-constant MLN_OFFLINE_OPERATION_RESULT_REGION_LIST ---include-constant MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS --include-constant MLN_OFFLINE_REGION_DEFINITION_GEOMETRY --include-constant MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID --include-constant MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE @@ -444,17 +438,14 @@ --include-constant MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED --include-constant MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION --include-constant MLN_RUNTIME_EVENT_MASK_NONE ---include-constant MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED --include-constant MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR --include-constant MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED --include-constant MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED ---include-constant MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED --include-constant MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR --include-constant MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED --include-constant MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED --include-constant MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED --include-constant MLN_RUNTIME_EVENT_PAYLOAD_NONE ---include-constant MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED --include-constant MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR --include-constant MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS --include-constant MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT @@ -472,33 +463,9 @@ --include-constant MLN_TILE_OPERATION_REQUESTED_FROM_CACHE --include-constant MLN_TILE_OPERATION_REQUESTED_FROM_NETWORK --include-constant MLN_TILE_OPERATION_START_PARSE ---include-function mln_network_status_get ---include-function mln_network_status_set ---include-function mln_resource_request_cancelled ---include-function mln_resource_request_complete ---include-function mln_resource_request_release ---include-function mln_resource_transform_response_set_url ---include-function mln_http_header_transform_response_set ---include-function mln_runtime_clear_http_header_transform ---include-function mln_runtime_clear_resource_provider ---include-function mln_runtime_clear_resource_transform ---include-function mln_runtime_create ---include-function mln_runtime_destroy ---include-function mln_runtime_drain_events ---include-function mln_runtime_event_batch_default ---include-function mln_runtime_get_event_mask ---include-function mln_runtime_offline_operation_discard ---include-function mln_runtime_options_default ---include-function mln_runtime_run_ambient_cache_operation_start ---include-function mln_runtime_set_event_mask ---include-function mln_runtime_set_maximum_ambient_cache_size_start ---include-function mln_runtime_set_resource_provider ---include-function mln_runtime_set_resource_transform ---include-function mln_runtime_set_http_header_transform ---include-function mln_runtime_pump ---include-function mln_runtime_wake_source_acquire ---include-function mln_wake_source_destroy ---include-function mln_wake_source_signal +--include-union mln_runtime_event_payload +--include-struct mln_http_header_transform +--include-struct mln_http_header_transform_response --include-struct mln_offline_region_status --include-struct mln_rendering_stats --include-struct mln_resource_provider @@ -506,12 +473,9 @@ --include-struct mln_resource_response --include-struct mln_resource_transform --include-struct mln_resource_transform_response ---include-struct mln_http_header_transform ---include-struct mln_http_header_transform_response --include-struct mln_runtime_event ---include-struct mln_runtime_event_batch +--include-struct mln_runtime_event_batch_view --include-struct mln_runtime_event_camera_transition_finished ---include-struct mln_runtime_event_offline_operation_completed --include-struct mln_runtime_event_offline_region_response_error --include-struct mln_runtime_event_offline_region_status --include-struct mln_runtime_event_offline_region_tile_count_limit @@ -520,14 +484,36 @@ --include-struct mln_runtime_event_tile_action --include-struct mln_runtime_options --include-struct mln_tile_id ---include-union mln_runtime_event_payload ---include-typedef mln_offline_operation_id +--include-function mln_event_batch_get +--include-function mln_event_batch_release +--include-function mln_http_header_transform_response_set +--include-function mln_network_status_get +--include-function mln_network_status_set +--include-function mln_resource_request_cancelled +--include-function mln_resource_request_complete +--include-function mln_resource_request_release +--include-function mln_resource_request_wait_until_retired +--include-function mln_resource_transform_response_set_url +--include-function mln_runtime_barrier +--include-function mln_runtime_clear_http_header_transform +--include-function mln_runtime_clear_resource_provider +--include-function mln_runtime_clear_resource_transform +--include-function mln_runtime_release +--include-function mln_runtime_create +--include-function mln_runtime_drain_events +--include-function mln_runtime_get_event_mask +--include-function mln_runtime_options_default +--include-function mln_runtime_run_ambient_cache_operation +--include-function mln_runtime_set_event_mask +--include-function mln_runtime_set_http_header_transform +--include-function mln_runtime_set_maximum_ambient_cache_size +--include-function mln_runtime_set_resource_provider +--include-function mln_runtime_set_resource_transform +--include-typedef mln_http_header_transform_callback --include-typedef mln_offline_region_id --include-typedef mln_resource_provider_callback --include-typedef mln_resource_transform_callback ---include-typedef mln_http_header_transform_callback - -# include/maplibre_native_c/style.h +--include-typedef mln_runtime_callback_release --include-constant MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_BUFFER --include-constant MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_CLIP --include-constant MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM @@ -552,8 +538,18 @@ --include-constant MLN_LOCATION_INDICATOR_IMAGE_KIND_BEARING --include-constant MLN_LOCATION_INDICATOR_IMAGE_KIND_SHADOW --include-constant MLN_LOCATION_INDICATOR_IMAGE_KIND_TOP +--include-constant MLN_STYLE_IMAGE_OPTION_CONTENT --include-constant MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO --include-constant MLN_STYLE_IMAGE_OPTION_SDF +--include-constant MLN_STYLE_IMAGE_OPTION_STRETCH_X +--include-constant MLN_STYLE_IMAGE_OPTION_STRETCH_Y +--include-constant MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT +--include-constant MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH +--include-constant MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL +--include-constant MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY +--include-constant MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK +--include-constant MLN_STYLE_LAYER_VISIBILITY_NONE +--include-constant MLN_STYLE_LAYER_VISIBILITY_VISIBLE --include-constant MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX --include-constant MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM --include-constant MLN_STYLE_SOURCE_INFO_BOUNDS @@ -584,8 +580,26 @@ --include-constant MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING --include-constant MLN_STYLE_TRANSITION_OPTION_DELAY --include-constant MLN_STYLE_TRANSITION_OPTION_DURATION +--include-constant MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS --include-constant MLN_STYLE_VECTOR_TILE_ENCODING_MLT --include-constant MLN_STYLE_VECTOR_TILE_ENCODING_MVT +--include-struct mln_canonical_tile_id +--include-struct mln_custom_geometry_source_options +--include-struct mln_custom_mvt_vector_source_options +--include-struct mln_geojson_source_options +--include-struct mln_image_content +--include-struct mln_image_stretch +--include-struct mln_premultiplied_rgba8_image +--include-struct mln_style_image_info +--include-struct mln_style_image_result +--include-struct mln_style_image_stretches_result +--include-struct mln_style_image_options +--include-struct mln_style_source_info +--include-struct mln_style_source_result +--include-struct mln_style_layer_info +--include-struct mln_style_layer_result +--include-struct mln_style_tile_source_options +--include-struct mln_style_transition_options --include-function mln_custom_geometry_source_options_default --include-function mln_custom_mvt_vector_source_options_default --include-function mln_geojson_source_data_create @@ -608,7 +622,10 @@ --include-function mln_map_add_style_source_json --include-function mln_map_add_vector_source_tiles --include-function mln_map_add_vector_source_url +--include-function mln_map_copy_layer_source_id +--include-function mln_map_copy_layer_source_layer --include-function mln_map_copy_style_image_premultiplied_rgba8 +--include-function mln_map_copy_style_image_stretches --include-function mln_map_copy_style_source_attribution --include-function mln_map_copy_style_source_url --include-function mln_map_get_image_source_coordinates @@ -616,11 +633,10 @@ --include-function mln_map_get_layer_property --include-function mln_map_get_style_image_info --include-function mln_map_get_style_layer_json ---include-function mln_map_get_style_layer_type +--include-function mln_map_get_style_layer_info --include-function mln_map_get_style_light_property --include-function mln_map_get_style_source_info --include-function mln_map_get_style_source_tile_urls ---include-function mln_map_get_style_source_type --include-function mln_map_get_style_transition_options --include-function mln_map_invalidate_custom_geometry_source_region --include-function mln_map_invalidate_custom_geometry_source_tile @@ -641,7 +657,12 @@ --include-function mln_map_set_image_source_image --include-function mln_map_set_image_source_url --include-function mln_map_set_layer_filter +--include-function mln_map_set_layer_max_zoom +--include-function mln_map_set_layer_min_zoom --include-function mln_map_set_layer_property +--include-function mln_map_set_layer_source_id +--include-function mln_map_set_layer_source_layer +--include-function mln_map_set_layer_visibility --include-function mln_map_set_location_indicator_accuracy_radius --include-function mln_map_set_location_indicator_bearing --include-function mln_map_set_location_indicator_image_name @@ -650,36 +671,27 @@ --include-function mln_map_set_style_light_json --include-function mln_map_set_style_light_property --include-function mln_map_set_style_transition_options ---include-function mln_map_style_image_exists ---include-function mln_map_style_layer_exists ---include-function mln_map_style_source_exists --include-function mln_premultiplied_rgba8_image_default --include-function mln_style_id_list_count --include-function mln_style_id_list_destroy --include-function mln_style_id_list_get +--include-function mln_style_image_info_default +--include-function mln_style_image_options_default --include-function mln_style_string_list_count --include-function mln_style_string_list_destroy --include-function mln_style_string_list_get ---include-function mln_style_image_info_default ---include-function mln_style_image_options_default --include-function mln_style_tile_source_options_default --include-function mln_style_transition_options_default ---include-struct mln_canonical_tile_id ---include-struct mln_custom_geometry_source_options ---include-struct mln_custom_mvt_vector_source_options ---include-struct mln_geojson_source_options ---include-struct mln_premultiplied_rgba8_image ---include-struct mln_style_image_info ---include-struct mln_style_image_options ---include-struct mln_style_source_info ---include-struct mln_style_tile_source_options ---include-struct mln_style_transition_options --include-typedef mln_custom_geometry_source_release_callback --include-typedef mln_custom_geometry_source_tile_callback --include-typedef mln_custom_mvt_vector_source_release_callback --include-typedef mln_custom_mvt_vector_source_tile_callback - -# include/maplibre_native_c/surface.h +--include-typedef mln_style_id_list +--include-typedef mln_style_string_list +--include-struct mln_metal_surface_descriptor +--include-struct mln_opengl_surface_descriptor +--include-struct mln_vulkan_surface_descriptor +--include-struct mln_webgpu_surface_descriptor --include-function mln_metal_surface_attach --include-function mln_metal_surface_descriptor_default --include-function mln_metal_surface_set_target @@ -692,52 +704,43 @@ --include-function mln_webgpu_surface_attach --include-function mln_webgpu_surface_descriptor_default --include-function mln_webgpu_surface_set_target ---include-struct mln_metal_surface_descriptor ---include-struct mln_opengl_surface_descriptor ---include-struct mln_vulkan_surface_descriptor ---include-struct mln_webgpu_surface_descriptor - -# include/maplibre_native_c/texture.h +--include-struct mln_metal_borrowed_texture_descriptor +--include-struct mln_metal_owned_texture_descriptor +--include-struct mln_metal_owned_texture_frame +--include-struct mln_opengl_borrowed_texture_descriptor +--include-struct mln_opengl_owned_texture_descriptor +--include-struct mln_opengl_owned_texture_frame +--include-struct mln_texture_image_info +--include-struct mln_texture_readback_result +--include-struct mln_vulkan_borrowed_texture_descriptor +--include-struct mln_vulkan_owned_texture_descriptor +--include-struct mln_vulkan_owned_texture_frame +--include-struct mln_webgpu_borrowed_texture_descriptor +--include-struct mln_webgpu_owned_texture_descriptor +--include-struct mln_webgpu_owned_texture_frame --include-function mln_metal_borrowed_texture_attach --include-function mln_metal_borrowed_texture_descriptor_default --include-function mln_metal_borrowed_texture_set_target ---include-function mln_metal_owned_texture_acquire_frame --include-function mln_metal_owned_texture_attach --include-function mln_metal_owned_texture_descriptor_default ---include-function mln_metal_owned_texture_release_frame --include-function mln_opengl_borrowed_texture_attach --include-function mln_opengl_borrowed_texture_descriptor_default --include-function mln_opengl_borrowed_texture_set_target ---include-function mln_opengl_owned_texture_acquire_frame --include-function mln_opengl_owned_texture_attach --include-function mln_opengl_owned_texture_descriptor_default ---include-function mln_opengl_owned_texture_release_frame --include-function mln_texture_image_info_default --include-function mln_texture_read_premultiplied_rgba8 --include-function mln_vulkan_borrowed_texture_attach --include-function mln_vulkan_borrowed_texture_descriptor_default --include-function mln_vulkan_borrowed_texture_set_target ---include-function mln_vulkan_owned_texture_acquire_frame --include-function mln_vulkan_owned_texture_attach --include-function mln_vulkan_owned_texture_descriptor_default ---include-function mln_vulkan_owned_texture_release_frame --include-function mln_webgpu_borrowed_texture_attach --include-function mln_webgpu_borrowed_texture_descriptor_default --include-function mln_webgpu_borrowed_texture_set_target ---include-function mln_webgpu_owned_texture_acquire_frame --include-function mln_webgpu_owned_texture_attach --include-function mln_webgpu_owned_texture_descriptor_default ---include-function mln_webgpu_owned_texture_release_frame ---include-struct mln_metal_borrowed_texture_descriptor ---include-struct mln_metal_owned_texture_descriptor ---include-struct mln_metal_owned_texture_frame ---include-struct mln_opengl_borrowed_texture_descriptor ---include-struct mln_opengl_owned_texture_descriptor ---include-struct mln_opengl_owned_texture_frame ---include-struct mln_texture_image_info ---include-struct mln_vulkan_borrowed_texture_descriptor ---include-struct mln_vulkan_owned_texture_descriptor ---include-struct mln_vulkan_owned_texture_frame ---include-struct mln_webgpu_borrowed_texture_descriptor ---include-struct mln_webgpu_owned_texture_descriptor ---include-struct mln_webgpu_owned_texture_frame +--include-function mln_acquired_frame_get_metal_texture +--include-function mln_acquired_frame_get_opengl_texture +--include-function mln_acquired_frame_get_vulkan_texture +--include-function mln_acquired_frame_get_webgpu_texture diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/MapLibreNativeC.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/MapLibreNativeC.java index ef274ad94..8200678cc 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/MapLibreNativeC.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/MapLibreNativeC.java @@ -77,6 +77,51 @@ public static int MLN_STATUS_UNSUPPORTED() { public static int MLN_STATUS_NATIVE_ERROR() { return MLN_STATUS_NATIVE_ERROR; } + private static final int MLN_STATUS_CANCELLED = (int)-6L; + /** + * {@snippet lang=c : + * enum mln_status.MLN_STATUS_CANCELLED = -6 + * } + */ + public static int MLN_STATUS_CANCELLED() { + return MLN_STATUS_CANCELLED; + } + private static final int MLN_STATUS_BUSY = (int)-7L; + /** + * {@snippet lang=c : + * enum mln_status.MLN_STATUS_BUSY = -7 + * } + */ + public static int MLN_STATUS_BUSY() { + return MLN_STATUS_BUSY; + } + private static final int MLN_STATUS_TARGET_LOST = (int)-8L; + /** + * {@snippet lang=c : + * enum mln_status.MLN_STATUS_TARGET_LOST = -8 + * } + */ + public static int MLN_STATUS_TARGET_LOST() { + return MLN_STATUS_TARGET_LOST; + } + private static final int MLN_STATUS_NOT_READY = (int)-9L; + /** + * {@snippet lang=c : + * enum mln_status.MLN_STATUS_NOT_READY = -9 + * } + */ + public static int MLN_STATUS_NOT_READY() { + return MLN_STATUS_NOT_READY; + } + private static final int MLN_STATUS_NOT_FOUND = (int)-10L; + /** + * {@snippet lang=c : + * enum mln_status.MLN_STATUS_NOT_FOUND = -10 + * } + */ + public static int MLN_STATUS_NOT_FOUND() { + return MLN_STATUS_NOT_FOUND; + } private static final int MLN_RENDER_BACKEND_FLAG_METAL = (int)1L; /** * {@snippet lang=c : @@ -113,6 +158,84 @@ public static int MLN_RENDER_BACKEND_FLAG_OPENGL() { public static int MLN_RENDER_BACKEND_FLAG_WEBGPU() { return MLN_RENDER_BACKEND_FLAG_WEBGPU; } + /** + * {@snippet lang=c : + * typedef uint64_t mln_runtime + * } + */ + public static final OfLong mln_runtime = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_map + * } + */ + public static final OfLong mln_map = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_map_projection + * } + */ + public static final OfLong mln_map_projection = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_offline_region_snapshot + * } + */ + public static final OfLong mln_offline_region_snapshot = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_offline_region_list + * } + */ + public static final OfLong mln_offline_region_list = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_buffer + * } + */ + public static final OfLong mln_buffer = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_resource_request_handle + * } + */ + public static final OfLong mln_resource_request_handle = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_render_session + * } + */ + public static final OfLong mln_render_session = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_event_batch + * } + */ + public static final OfLong mln_event_batch = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_adapter_resource_request_queue + * } + */ + public static final OfLong mln_adapter_resource_request_queue = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_adapter_log_queue + * } + */ + public static final OfLong mln_adapter_log_queue = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_acquired_frame + * } + */ + public static final OfLong mln_acquired_frame = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_render_frame_batch + * } + */ + public static final OfLong mln_render_frame_batch = MapLibreNativeC.C_LONG; private static class mln_buffer_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( @@ -411,6 +534,42 @@ public static int mln_android_init(MemorySegment jni_env, MemorySegment jni_clas throw new AssertionError("should not reach here", ex$); } } + private static final int MLN_COMMAND_DISPOSITION_COMMITTED = (int)0L; + /** + * {@snippet lang=c : + * enum mln_command_disposition.MLN_COMMAND_DISPOSITION_COMMITTED = 0 + * } + */ + public static int MLN_COMMAND_DISPOSITION_COMMITTED() { + return MLN_COMMAND_DISPOSITION_COMMITTED; + } + private static final int MLN_COMMAND_DISPOSITION_SUPERSEDED = (int)1L; + /** + * {@snippet lang=c : + * enum mln_command_disposition.MLN_COMMAND_DISPOSITION_SUPERSEDED = 1 + * } + */ + public static int MLN_COMMAND_DISPOSITION_SUPERSEDED() { + return MLN_COMMAND_DISPOSITION_SUPERSEDED; + } + private static final int MLN_COMMAND_DISPOSITION_FAILED = (int)2L; + /** + * {@snippet lang=c : + * enum mln_command_disposition.MLN_COMMAND_DISPOSITION_FAILED = 2 + * } + */ + public static int MLN_COMMAND_DISPOSITION_FAILED() { + return MLN_COMMAND_DISPOSITION_FAILED; + } + private static final int MLN_COMMAND_DISPOSITION_CANCELLED = (int)3L; + /** + * {@snippet lang=c : + * enum mln_command_disposition.MLN_COMMAND_DISPOSITION_CANCELLED = 3 + * } + */ + public static int MLN_COMMAND_DISPOSITION_CANCELLED() { + return MLN_COMMAND_DISPOSITION_CANCELLED; + } private static final int MLN_NETWORK_STATUS_ONLINE = (int)1L; /** * {@snippet lang=c : @@ -507,165 +666,6 @@ public static int MLN_OFFLINE_REGION_DOWNLOAD_INACTIVE() { public static int MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE() { return MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE; } - /** - * {@snippet lang=c : - * typedef uint64_t mln_offline_operation_id - * } - */ - public static final OfLong mln_offline_operation_id = MapLibreNativeC.C_LONG; - private static final int MLN_OFFLINE_OPERATION_AMBIENT_CACHE = (int)1L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_AMBIENT_CACHE = 1 - * } - */ - public static int MLN_OFFLINE_OPERATION_AMBIENT_CACHE() { - return MLN_OFFLINE_OPERATION_AMBIENT_CACHE; - } - private static final int MLN_OFFLINE_OPERATION_REGION_CREATE = (int)2L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_CREATE = 2 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_CREATE() { - return MLN_OFFLINE_OPERATION_REGION_CREATE; - } - private static final int MLN_OFFLINE_OPERATION_REGION_GET = (int)3L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_GET = 3 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_GET() { - return MLN_OFFLINE_OPERATION_REGION_GET; - } - private static final int MLN_OFFLINE_OPERATION_REGIONS_LIST = (int)4L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGIONS_LIST = 4 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGIONS_LIST() { - return MLN_OFFLINE_OPERATION_REGIONS_LIST; - } - private static final int MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE = (int)5L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE = 5 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE() { - return MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE; - } - private static final int MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA = (int)6L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA = 6 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA() { - return MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA; - } - private static final int MLN_OFFLINE_OPERATION_REGION_GET_STATUS = (int)7L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_GET_STATUS = 7 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_GET_STATUS() { - return MLN_OFFLINE_OPERATION_REGION_GET_STATUS; - } - private static final int MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED = (int)8L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED = 8 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED() { - return MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED; - } - private static final int MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE = (int)9L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE = 9 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE() { - return MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE; - } - private static final int MLN_OFFLINE_OPERATION_REGION_INVALIDATE = (int)10L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_INVALIDATE = 10 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_INVALIDATE() { - return MLN_OFFLINE_OPERATION_REGION_INVALIDATE; - } - private static final int MLN_OFFLINE_OPERATION_REGION_DELETE = (int)11L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_REGION_DELETE = 11 - * } - */ - public static int MLN_OFFLINE_OPERATION_REGION_DELETE() { - return MLN_OFFLINE_OPERATION_REGION_DELETE; - } - private static final int MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE = (int)12L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_kind.MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE = 12 - * } - */ - public static int MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE() { - return MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE; - } - private static final int MLN_OFFLINE_OPERATION_RESULT_NONE = (int)0L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_result_kind.MLN_OFFLINE_OPERATION_RESULT_NONE = 0 - * } - */ - public static int MLN_OFFLINE_OPERATION_RESULT_NONE() { - return MLN_OFFLINE_OPERATION_RESULT_NONE; - } - private static final int MLN_OFFLINE_OPERATION_RESULT_REGION = (int)1L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_result_kind.MLN_OFFLINE_OPERATION_RESULT_REGION = 1 - * } - */ - public static int MLN_OFFLINE_OPERATION_RESULT_REGION() { - return MLN_OFFLINE_OPERATION_RESULT_REGION; - } - private static final int MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION = (int)2L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_result_kind.MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION = 2 - * } - */ - public static int MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION() { - return MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION; - } - private static final int MLN_OFFLINE_OPERATION_RESULT_REGION_LIST = (int)3L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_result_kind.MLN_OFFLINE_OPERATION_RESULT_REGION_LIST = 3 - * } - */ - public static int MLN_OFFLINE_OPERATION_RESULT_REGION_LIST() { - return MLN_OFFLINE_OPERATION_RESULT_REGION_LIST; - } - private static final int MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS = (int)4L; - /** - * {@snippet lang=c : - * enum mln_offline_operation_result_kind.MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS = 4 - * } - */ - public static int MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS() { - return MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS; - } private static final int MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE = (int)1L; /** * {@snippet lang=c : @@ -855,19 +855,10 @@ public static int MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR() { public static int MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED() { return MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED; } - private static final int MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED = (int)22L; - /** - * {@snippet lang=c : - * enum mln_runtime_event_type.MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED = 22 - * } - */ - public static int MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED() { - return MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED; - } - private static final int MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = (int)23L; + private static final int MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = (int)22L; /** * {@snippet lang=c : - * enum mln_runtime_event_type.MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 23 + * enum mln_runtime_event_type.MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 22 * } */ public static int MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED() { @@ -1044,10 +1035,10 @@ public static long MLN_RUNTIME_EVENT_MASK_MAP_STYLE_IMAGE_MISSING() { public static long MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION() { return MLN_RUNTIME_EVENT_MASK_MAP_TILE_ACTION; } - private static final long MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED = 8388608L; + private static final long MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED = 4194304L; /** * {@snippet lang=c : - * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED = 8388608 + * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED = 4194304 * } */ public static long MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED() { @@ -1080,37 +1071,28 @@ public static long MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR() { public static long MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED() { return MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED; } - private static final long MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED = 4194304L; - /** - * {@snippet lang=c : - * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED = 4194304 - * } - */ - public static long MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED() { - return MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED; - } - private static final long MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = 8912894L; + private static final long MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = 4718590L; /** * {@snippet lang=c : - * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = 8912894 + * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = 4718590 * } */ public static long MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS() { return MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS; } - private static final long MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = 7864320L; + private static final long MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = 3670016L; /** * {@snippet lang=c : - * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = 7864320 + * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = 3670016 * } */ public static long MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS() { return MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS; } - private static final long MLN_RUNTIME_EVENT_MASK_ALL = 16777214L; + private static final long MLN_RUNTIME_EVENT_MASK_ALL = 8388606L; /** * {@snippet lang=c : - * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL = 16777214 + * enum mln_runtime_event_mask.MLN_RUNTIME_EVENT_MASK_ALL = 8388606 * } */ public static long MLN_RUNTIME_EVENT_MASK_ALL() { @@ -1197,23 +1179,32 @@ public static int MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR() { public static int MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT() { return MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT; } - private static final int MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED = (int)8L; + private static final int MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = (int)8L; /** * {@snippet lang=c : - * enum mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED = 8 + * enum mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 8 * } */ - public static int MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED() { - return MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED; + public static int MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED() { + return MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED; } - private static final int MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = (int)9L; + private static final int MLN_CAMERA_CHANGE_MODE_IMMEDIATE = (int)0L; /** * {@snippet lang=c : - * enum mln_runtime_event_payload_type.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 9 + * enum mln_camera_change_mode.MLN_CAMERA_CHANGE_MODE_IMMEDIATE = 0 * } */ - public static int MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED() { - return MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED; + public static int MLN_CAMERA_CHANGE_MODE_IMMEDIATE() { + return MLN_CAMERA_CHANGE_MODE_IMMEDIATE; + } + private static final int MLN_CAMERA_CHANGE_MODE_ANIMATED = (int)1L; + /** + * {@snippet lang=c : + * enum mln_camera_change_mode.MLN_CAMERA_CHANGE_MODE_ANIMATED = 1 + * } + */ + public static int MLN_CAMERA_CHANGE_MODE_ANIMATED() { + return MLN_CAMERA_CHANGE_MODE_ANIMATED; } private static final int MLN_RENDER_MODE_PARTIAL = (int)0L; /** @@ -1945,6 +1936,7 @@ private static class mln_runtime_set_resource_provider { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -1956,7 +1948,7 @@ private static class mln_runtime_set_resource_provider { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider) + * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider, const mln_completion *completion) * } */ public static FunctionDescriptor mln_runtime_set_resource_provider$descriptor() { @@ -1966,7 +1958,7 @@ private static class mln_runtime_set_resource_provider { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider) + * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider, const mln_completion *completion) * } */ public static MethodHandle mln_runtime_set_resource_provider$handle() { @@ -1976,7 +1968,7 @@ private static class mln_runtime_set_resource_provider { /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider) + * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider, const mln_completion *completion) * } */ public static MemorySegment mln_runtime_set_resource_provider$address() { @@ -1985,16 +1977,16 @@ private static class mln_runtime_set_resource_provider { /** * {@snippet lang=c : - * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider) + * mln_status mln_runtime_set_resource_provider(mln_runtime runtime, const mln_resource_provider *provider, const mln_completion *completion) * } */ - public static int mln_runtime_set_resource_provider(long runtime, MemorySegment provider) { + public static int mln_runtime_set_resource_provider(long runtime, MemorySegment provider, MemorySegment completion) { var mh$ = mln_runtime_set_resource_provider.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_set_resource_provider", runtime, provider); + traceDowncall("mln_runtime_set_resource_provider", runtime, provider, completion); } - return (int)mh$.invokeExact(runtime, provider); + return (int)mh$.invokeExact(runtime, provider, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2005,7 +1997,8 @@ public static int mln_runtime_set_resource_provider(long runtime, MemorySegment private static class mln_runtime_clear_resource_provider { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_clear_resource_provider"); @@ -2016,7 +2009,7 @@ private static class mln_runtime_clear_resource_provider { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime) + * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime, const mln_completion *completion) * } */ public static FunctionDescriptor mln_runtime_clear_resource_provider$descriptor() { @@ -2026,7 +2019,7 @@ private static class mln_runtime_clear_resource_provider { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime) + * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime, const mln_completion *completion) * } */ public static MethodHandle mln_runtime_clear_resource_provider$handle() { @@ -2036,7 +2029,7 @@ private static class mln_runtime_clear_resource_provider { /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime) + * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime, const mln_completion *completion) * } */ public static MemorySegment mln_runtime_clear_resource_provider$address() { @@ -2045,16 +2038,16 @@ private static class mln_runtime_clear_resource_provider { /** * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime) + * mln_status mln_runtime_clear_resource_provider(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_clear_resource_provider(long runtime) { + public static int mln_runtime_clear_resource_provider(long runtime, MemorySegment completion) { var mh$ = mln_runtime_clear_resource_provider.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_clear_resource_provider", runtime); + traceDowncall("mln_runtime_clear_resource_provider", runtime, completion); } - return (int)mh$.invokeExact(runtime); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2243,14 +2236,13 @@ public static void mln_resource_request_release(long handle) { } } - private static class mln_runtime_set_resource_transform { + private static class mln_resource_request_wait_until_retired { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_resource_transform"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_resource_request_wait_until_retired"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2258,45 +2250,45 @@ private static class mln_runtime_set_resource_transform { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform) + * mln_status mln_resource_request_wait_until_retired(mln_resource_request_handle handle) * } */ - public static FunctionDescriptor mln_runtime_set_resource_transform$descriptor() { - return mln_runtime_set_resource_transform.DESC; + public static FunctionDescriptor mln_resource_request_wait_until_retired$descriptor() { + return mln_resource_request_wait_until_retired.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform) + * mln_status mln_resource_request_wait_until_retired(mln_resource_request_handle handle) * } */ - public static MethodHandle mln_runtime_set_resource_transform$handle() { - return mln_runtime_set_resource_transform.HANDLE; + public static MethodHandle mln_resource_request_wait_until_retired$handle() { + return mln_resource_request_wait_until_retired.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform) + * mln_status mln_resource_request_wait_until_retired(mln_resource_request_handle handle) * } */ - public static MemorySegment mln_runtime_set_resource_transform$address() { - return mln_runtime_set_resource_transform.ADDR; + public static MemorySegment mln_resource_request_wait_until_retired$address() { + return mln_resource_request_wait_until_retired.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform) + * mln_status mln_resource_request_wait_until_retired(mln_resource_request_handle handle) * } */ - public static int mln_runtime_set_resource_transform(long runtime, MemorySegment transform) { - var mh$ = mln_runtime_set_resource_transform.HANDLE; + public static int mln_resource_request_wait_until_retired(long handle) { + var mh$ = mln_resource_request_wait_until_retired.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_set_resource_transform", runtime, transform); + traceDowncall("mln_resource_request_wait_until_retired", handle); } - return (int)mh$.invokeExact(runtime, transform); + return (int)mh$.invokeExact(handle); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2304,13 +2296,15 @@ public static int mln_runtime_set_resource_transform(long runtime, MemorySegment } } - private static class mln_runtime_clear_resource_transform { + private static class mln_runtime_set_resource_transform { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_clear_resource_transform"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_resource_transform"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2318,45 +2312,45 @@ private static class mln_runtime_clear_resource_transform { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime) + * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_clear_resource_transform$descriptor() { - return mln_runtime_clear_resource_transform.DESC; + public static FunctionDescriptor mln_runtime_set_resource_transform$descriptor() { + return mln_runtime_set_resource_transform.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime) + * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_clear_resource_transform$handle() { - return mln_runtime_clear_resource_transform.HANDLE; + public static MethodHandle mln_runtime_set_resource_transform$handle() { + return mln_runtime_set_resource_transform.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime) + * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_clear_resource_transform$address() { - return mln_runtime_clear_resource_transform.ADDR; + public static MemorySegment mln_runtime_set_resource_transform$address() { + return mln_runtime_set_resource_transform.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime) + * mln_status mln_runtime_set_resource_transform(mln_runtime runtime, const mln_resource_transform *transform, const mln_completion *completion) * } */ - public static int mln_runtime_clear_resource_transform(long runtime) { - var mh$ = mln_runtime_clear_resource_transform.HANDLE; + public static int mln_runtime_set_resource_transform(long runtime, MemorySegment transform, MemorySegment completion) { + var mh$ = mln_runtime_set_resource_transform.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_clear_resource_transform", runtime); + traceDowncall("mln_runtime_set_resource_transform", runtime, transform, completion); } - return (int)mh$.invokeExact(runtime); + return (int)mh$.invokeExact(runtime, transform, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2364,14 +2358,14 @@ public static int mln_runtime_clear_resource_transform(long runtime) { } } - private static class mln_runtime_set_http_header_transform { + private static class mln_runtime_clear_resource_transform { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_http_header_transform"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_clear_resource_transform"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2379,45 +2373,45 @@ private static class mln_runtime_set_http_header_transform { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform) + * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_set_http_header_transform$descriptor() { - return mln_runtime_set_http_header_transform.DESC; + public static FunctionDescriptor mln_runtime_clear_resource_transform$descriptor() { + return mln_runtime_clear_resource_transform.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform) + * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_set_http_header_transform$handle() { - return mln_runtime_set_http_header_transform.HANDLE; + public static MethodHandle mln_runtime_clear_resource_transform$handle() { + return mln_runtime_clear_resource_transform.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform) + * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_set_http_header_transform$address() { - return mln_runtime_set_http_header_transform.ADDR; + public static MemorySegment mln_runtime_clear_resource_transform$address() { + return mln_runtime_clear_resource_transform.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform) + * mln_status mln_runtime_clear_resource_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_set_http_header_transform(long runtime, MemorySegment transform) { - var mh$ = mln_runtime_set_http_header_transform.HANDLE; + public static int mln_runtime_clear_resource_transform(long runtime, MemorySegment completion) { + var mh$ = mln_runtime_clear_resource_transform.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_set_http_header_transform", runtime, transform); + traceDowncall("mln_runtime_clear_resource_transform", runtime, completion); } - return (int)mh$.invokeExact(runtime, transform); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2425,13 +2419,15 @@ public static int mln_runtime_set_http_header_transform(long runtime, MemorySegm } } - private static class mln_runtime_clear_http_header_transform { + private static class mln_runtime_set_http_header_transform { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_clear_http_header_transform"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_http_header_transform"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2439,45 +2435,45 @@ private static class mln_runtime_clear_http_header_transform { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime) + * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_clear_http_header_transform$descriptor() { - return mln_runtime_clear_http_header_transform.DESC; + public static FunctionDescriptor mln_runtime_set_http_header_transform$descriptor() { + return mln_runtime_set_http_header_transform.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime) + * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_clear_http_header_transform$handle() { - return mln_runtime_clear_http_header_transform.HANDLE; + public static MethodHandle mln_runtime_set_http_header_transform$handle() { + return mln_runtime_set_http_header_transform.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime) + * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_clear_http_header_transform$address() { - return mln_runtime_clear_http_header_transform.ADDR; + public static MemorySegment mln_runtime_set_http_header_transform$address() { + return mln_runtime_set_http_header_transform.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime) + * mln_status mln_runtime_set_http_header_transform(mln_runtime runtime, const mln_http_header_transform *transform, const mln_completion *completion) * } */ - public static int mln_runtime_clear_http_header_transform(long runtime) { - var mh$ = mln_runtime_clear_http_header_transform.HANDLE; + public static int mln_runtime_set_http_header_transform(long runtime, MemorySegment transform, MemorySegment completion) { + var mh$ = mln_runtime_set_http_header_transform.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_clear_http_header_transform", runtime); + traceDowncall("mln_runtime_set_http_header_transform", runtime, transform, completion); } - return (int)mh$.invokeExact(runtime); + return (int)mh$.invokeExact(runtime, transform, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2485,15 +2481,14 @@ public static int mln_runtime_clear_http_header_transform(long runtime) { } } - private static class mln_runtime_run_ambient_cache_operation_start { + private static class mln_runtime_clear_http_header_transform { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_INT, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_run_ambient_cache_operation_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_clear_http_header_transform"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2501,45 +2496,45 @@ private static class mln_runtime_run_ambient_cache_operation_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_run_ambient_cache_operation_start(mln_runtime runtime, uint32_t operation, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_run_ambient_cache_operation_start$descriptor() { - return mln_runtime_run_ambient_cache_operation_start.DESC; + public static FunctionDescriptor mln_runtime_clear_http_header_transform$descriptor() { + return mln_runtime_clear_http_header_transform.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_run_ambient_cache_operation_start(mln_runtime runtime, uint32_t operation, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_run_ambient_cache_operation_start$handle() { - return mln_runtime_run_ambient_cache_operation_start.HANDLE; + public static MethodHandle mln_runtime_clear_http_header_transform$handle() { + return mln_runtime_clear_http_header_transform.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_run_ambient_cache_operation_start(mln_runtime runtime, uint32_t operation, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_run_ambient_cache_operation_start$address() { - return mln_runtime_run_ambient_cache_operation_start.ADDR; + public static MemorySegment mln_runtime_clear_http_header_transform$address() { + return mln_runtime_clear_http_header_transform.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_run_ambient_cache_operation_start(mln_runtime runtime, uint32_t operation, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_clear_http_header_transform(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_run_ambient_cache_operation_start(long runtime, int operation, MemorySegment out_operation_id) { - var mh$ = mln_runtime_run_ambient_cache_operation_start.HANDLE; + public static int mln_runtime_clear_http_header_transform(long runtime, MemorySegment completion) { + var mh$ = mln_runtime_clear_http_header_transform.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_run_ambient_cache_operation_start", runtime, operation, out_operation_id); + traceDowncall("mln_runtime_clear_http_header_transform", runtime, completion); } - return (int)mh$.invokeExact(runtime, operation, out_operation_id); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2547,15 +2542,15 @@ public static int mln_runtime_run_ambient_cache_operation_start(long runtime, in } } - private static class mln_runtime_set_maximum_ambient_cache_size_start { + private static class mln_runtime_run_ambient_cache_operation { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, + MapLibreNativeC.C_INT, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_maximum_ambient_cache_size_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_run_ambient_cache_operation"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2563,45 +2558,45 @@ private static class mln_runtime_set_maximum_ambient_cache_size_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_set_maximum_ambient_cache_size_start(mln_runtime runtime, uint64_t size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_run_ambient_cache_operation(mln_runtime runtime, uint32_t operation, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_set_maximum_ambient_cache_size_start$descriptor() { - return mln_runtime_set_maximum_ambient_cache_size_start.DESC; + public static FunctionDescriptor mln_runtime_run_ambient_cache_operation$descriptor() { + return mln_runtime_run_ambient_cache_operation.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_set_maximum_ambient_cache_size_start(mln_runtime runtime, uint64_t size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_run_ambient_cache_operation(mln_runtime runtime, uint32_t operation, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_set_maximum_ambient_cache_size_start$handle() { - return mln_runtime_set_maximum_ambient_cache_size_start.HANDLE; + public static MethodHandle mln_runtime_run_ambient_cache_operation$handle() { + return mln_runtime_run_ambient_cache_operation.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_set_maximum_ambient_cache_size_start(mln_runtime runtime, uint64_t size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_run_ambient_cache_operation(mln_runtime runtime, uint32_t operation, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_set_maximum_ambient_cache_size_start$address() { - return mln_runtime_set_maximum_ambient_cache_size_start.ADDR; + public static MemorySegment mln_runtime_run_ambient_cache_operation$address() { + return mln_runtime_run_ambient_cache_operation.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_set_maximum_ambient_cache_size_start(mln_runtime runtime, uint64_t size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_run_ambient_cache_operation(mln_runtime runtime, uint32_t operation, const mln_completion *completion) * } */ - public static int mln_runtime_set_maximum_ambient_cache_size_start(long runtime, long size, MemorySegment out_operation_id) { - var mh$ = mln_runtime_set_maximum_ambient_cache_size_start.HANDLE; + public static int mln_runtime_run_ambient_cache_operation(long runtime, int operation, MemorySegment completion) { + var mh$ = mln_runtime_run_ambient_cache_operation.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_set_maximum_ambient_cache_size_start", runtime, size, out_operation_id); + traceDowncall("mln_runtime_run_ambient_cache_operation", runtime, operation, completion); } - return (int)mh$.invokeExact(runtime, size, out_operation_id); + return (int)mh$.invokeExact(runtime, operation, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2609,14 +2604,15 @@ public static int mln_runtime_set_maximum_ambient_cache_size_start(long runtime, } } - private static class mln_runtime_offline_operation_discard { + private static class mln_runtime_set_maximum_ambient_cache_size { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_operation_discard"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_set_maximum_ambient_cache_size"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2624,45 +2620,45 @@ private static class mln_runtime_offline_operation_discard { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_operation_discard(mln_runtime runtime, mln_offline_operation_id operation_id) + * mln_status mln_runtime_set_maximum_ambient_cache_size(mln_runtime runtime, uint64_t size, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_operation_discard$descriptor() { - return mln_runtime_offline_operation_discard.DESC; + public static FunctionDescriptor mln_runtime_set_maximum_ambient_cache_size$descriptor() { + return mln_runtime_set_maximum_ambient_cache_size.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_operation_discard(mln_runtime runtime, mln_offline_operation_id operation_id) + * mln_status mln_runtime_set_maximum_ambient_cache_size(mln_runtime runtime, uint64_t size, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_operation_discard$handle() { - return mln_runtime_offline_operation_discard.HANDLE; + public static MethodHandle mln_runtime_set_maximum_ambient_cache_size$handle() { + return mln_runtime_set_maximum_ambient_cache_size.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_operation_discard(mln_runtime runtime, mln_offline_operation_id operation_id) + * mln_status mln_runtime_set_maximum_ambient_cache_size(mln_runtime runtime, uint64_t size, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_operation_discard$address() { - return mln_runtime_offline_operation_discard.ADDR; + public static MemorySegment mln_runtime_set_maximum_ambient_cache_size$address() { + return mln_runtime_set_maximum_ambient_cache_size.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_operation_discard(mln_runtime runtime, mln_offline_operation_id operation_id) + * mln_status mln_runtime_set_maximum_ambient_cache_size(mln_runtime runtime, uint64_t size, const mln_completion *completion) * } */ - public static int mln_runtime_offline_operation_discard(long runtime, long operation_id) { - var mh$ = mln_runtime_offline_operation_discard.HANDLE; + public static int mln_runtime_set_maximum_ambient_cache_size(long runtime, long size, MemorySegment completion) { + var mh$ = mln_runtime_set_maximum_ambient_cache_size.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_operation_discard", runtime, operation_id); + traceDowncall("mln_runtime_set_maximum_ambient_cache_size", runtime, size, completion); } - return (int)mh$.invokeExact(runtime, operation_id); + return (int)mh$.invokeExact(runtime, size, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2670,13 +2666,14 @@ public static int mln_runtime_offline_operation_discard(long runtime, long opera } } - private static class mln_runtime_destroy { + private static class mln_runtime_barrier { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_barrier"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2684,45 +2681,45 @@ private static class mln_runtime_destroy { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_destroy(mln_runtime runtime) + * mln_status mln_runtime_barrier(mln_runtime runtime, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_destroy$descriptor() { - return mln_runtime_destroy.DESC; + public static FunctionDescriptor mln_runtime_barrier$descriptor() { + return mln_runtime_barrier.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_destroy(mln_runtime runtime) + * mln_status mln_runtime_barrier(mln_runtime runtime, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_destroy$handle() { - return mln_runtime_destroy.HANDLE; + public static MethodHandle mln_runtime_barrier$handle() { + return mln_runtime_barrier.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_destroy(mln_runtime runtime) + * mln_status mln_runtime_barrier(mln_runtime runtime, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_destroy$address() { - return mln_runtime_destroy.ADDR; + public static MemorySegment mln_runtime_barrier$address() { + return mln_runtime_barrier.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_destroy(mln_runtime runtime) + * mln_status mln_runtime_barrier(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_destroy(long runtime) { - var mh$ = mln_runtime_destroy.HANDLE; + public static int mln_runtime_barrier(long runtime, MemorySegment completion) { + var mh$ = mln_runtime_barrier.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_destroy", runtime); + traceDowncall("mln_runtime_barrier", runtime, completion); } - return (int)mh$.invokeExact(runtime); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2730,15 +2727,14 @@ public static int mln_runtime_destroy(long runtime) { } } - private static class mln_runtime_pump { + private static class mln_runtime_release { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_pump"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_release"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2746,45 +2742,45 @@ private static class mln_runtime_pump { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_pump(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) + * mln_status mln_runtime_release(mln_runtime runtime, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_pump$descriptor() { - return mln_runtime_pump.DESC; + public static FunctionDescriptor mln_runtime_release$descriptor() { + return mln_runtime_release.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_pump(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) + * mln_status mln_runtime_release(mln_runtime runtime, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_pump$handle() { - return mln_runtime_pump.HANDLE; + public static MethodHandle mln_runtime_release$handle() { + return mln_runtime_release.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_pump(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) + * mln_status mln_runtime_release(mln_runtime runtime, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_pump$address() { - return mln_runtime_pump.ADDR; + public static MemorySegment mln_runtime_release$address() { + return mln_runtime_release.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_pump(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) + * mln_status mln_runtime_release(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_pump(long runtime, long timeout_ms, long budget_ms) { - var mh$ = mln_runtime_pump.HANDLE; + public static int mln_runtime_release(long runtime, MemorySegment completion) { + var mh$ = mln_runtime_release.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_pump", runtime, timeout_ms, budget_ms); + traceDowncall("mln_runtime_release", runtime, completion); } - return (int)mh$.invokeExact(runtime, timeout_ms, budget_ms); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2792,14 +2788,14 @@ public static int mln_runtime_pump(long runtime, long timeout_ms, long budget_ms } } - private static class mln_runtime_wake_source_acquire { + private static class mln_runtime_drain_events { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_wake_source_acquire"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_drain_events"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2807,45 +2803,45 @@ private static class mln_runtime_wake_source_acquire { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_wake_source_acquire(mln_runtime runtime, mln_wake_source *out_source) + * mln_status mln_runtime_drain_events(mln_runtime runtime, mln_event_batch *out_batch) * } */ - public static FunctionDescriptor mln_runtime_wake_source_acquire$descriptor() { - return mln_runtime_wake_source_acquire.DESC; + public static FunctionDescriptor mln_runtime_drain_events$descriptor() { + return mln_runtime_drain_events.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_wake_source_acquire(mln_runtime runtime, mln_wake_source *out_source) + * mln_status mln_runtime_drain_events(mln_runtime runtime, mln_event_batch *out_batch) * } */ - public static MethodHandle mln_runtime_wake_source_acquire$handle() { - return mln_runtime_wake_source_acquire.HANDLE; + public static MethodHandle mln_runtime_drain_events$handle() { + return mln_runtime_drain_events.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_wake_source_acquire(mln_runtime runtime, mln_wake_source *out_source) + * mln_status mln_runtime_drain_events(mln_runtime runtime, mln_event_batch *out_batch) * } */ - public static MemorySegment mln_runtime_wake_source_acquire$address() { - return mln_runtime_wake_source_acquire.ADDR; + public static MemorySegment mln_runtime_drain_events$address() { + return mln_runtime_drain_events.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_wake_source_acquire(mln_runtime runtime, mln_wake_source *out_source) + * mln_status mln_runtime_drain_events(mln_runtime runtime, mln_event_batch *out_batch) * } */ - public static int mln_runtime_wake_source_acquire(long runtime, MemorySegment out_source) { - var mh$ = mln_runtime_wake_source_acquire.HANDLE; + public static int mln_runtime_drain_events(long runtime, MemorySegment out_batch) { + var mh$ = mln_runtime_drain_events.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_wake_source_acquire", runtime, out_source); + traceDowncall("mln_runtime_drain_events", runtime, out_batch); } - return (int)mh$.invokeExact(runtime, out_source); + return (int)mh$.invokeExact(runtime, out_batch); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2853,13 +2849,14 @@ public static int mln_runtime_wake_source_acquire(long runtime, MemorySegment ou } } - private static class mln_wake_source_signal { + private static class mln_event_batch_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_wake_source_signal"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_event_batch_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -2867,45 +2864,45 @@ private static class mln_wake_source_signal { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_wake_source_signal(mln_wake_source source) + * mln_status mln_event_batch_get(mln_event_batch batch, mln_runtime_event_batch_view *out_view) * } */ - public static FunctionDescriptor mln_wake_source_signal$descriptor() { - return mln_wake_source_signal.DESC; + public static FunctionDescriptor mln_event_batch_get$descriptor() { + return mln_event_batch_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_wake_source_signal(mln_wake_source source) + * mln_status mln_event_batch_get(mln_event_batch batch, mln_runtime_event_batch_view *out_view) * } */ - public static MethodHandle mln_wake_source_signal$handle() { - return mln_wake_source_signal.HANDLE; + public static MethodHandle mln_event_batch_get$handle() { + return mln_event_batch_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_wake_source_signal(mln_wake_source source) + * mln_status mln_event_batch_get(mln_event_batch batch, mln_runtime_event_batch_view *out_view) * } */ - public static MemorySegment mln_wake_source_signal$address() { - return mln_wake_source_signal.ADDR; + public static MemorySegment mln_event_batch_get$address() { + return mln_event_batch_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_wake_source_signal(mln_wake_source source) + * mln_status mln_event_batch_get(mln_event_batch batch, mln_runtime_event_batch_view *out_view) * } */ - public static int mln_wake_source_signal(long source) { - var mh$ = mln_wake_source_signal.HANDLE; + public static int mln_event_batch_get(long batch, MemorySegment out_view) { + var mh$ = mln_event_batch_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_wake_source_signal", source); + traceDowncall("mln_event_batch_get", batch, out_view); } - return (int)mh$.invokeExact(source); + return (int)mh$.invokeExact(batch, out_view); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -2913,132 +2910,12 @@ public static int mln_wake_source_signal(long source) { } } - private static class mln_wake_source_destroy { + private static class mln_event_batch_release { public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_wake_source_destroy"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * void mln_wake_source_destroy(mln_wake_source source) - * } - */ - public static FunctionDescriptor mln_wake_source_destroy$descriptor() { - return mln_wake_source_destroy.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * void mln_wake_source_destroy(mln_wake_source source) - * } - */ - public static MethodHandle mln_wake_source_destroy$handle() { - return mln_wake_source_destroy.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * void mln_wake_source_destroy(mln_wake_source source) - * } - */ - public static MemorySegment mln_wake_source_destroy$address() { - return mln_wake_source_destroy.ADDR; - } - - /** - * {@snippet lang=c : - * void mln_wake_source_destroy(mln_wake_source source) - * } - */ - public static void mln_wake_source_destroy(long source) { - var mh$ = mln_wake_source_destroy.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_wake_source_destroy", source); - } - mh$.invokeExact(source); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_runtime_event_batch_default { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_runtime_event_batch.layout() ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_event_batch_default"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_runtime_event_batch mln_runtime_event_batch_default() - * } - */ - public static FunctionDescriptor mln_runtime_event_batch_default$descriptor() { - return mln_runtime_event_batch_default.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_runtime_event_batch mln_runtime_event_batch_default() - * } - */ - public static MethodHandle mln_runtime_event_batch_default$handle() { - return mln_runtime_event_batch_default.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_runtime_event_batch mln_runtime_event_batch_default() - * } - */ - public static MemorySegment mln_runtime_event_batch_default$address() { - return mln_runtime_event_batch_default.ADDR; - } - - /** - * {@snippet lang=c : - * mln_runtime_event_batch mln_runtime_event_batch_default() - * } - */ - public static MemorySegment mln_runtime_event_batch_default(SegmentAllocator allocator) { - var mh$ = mln_runtime_event_batch_default.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_event_batch_default", allocator); - } - return (MemorySegment)mh$.invokeExact(allocator); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_runtime_drain_events { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_drain_events"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_event_batch_release"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -3046,45 +2923,45 @@ private static class mln_runtime_drain_events { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_drain_events(mln_runtime runtime, size_t max_events, mln_runtime_event_batch *out_batch) + * void mln_event_batch_release(mln_event_batch batch) * } */ - public static FunctionDescriptor mln_runtime_drain_events$descriptor() { - return mln_runtime_drain_events.DESC; + public static FunctionDescriptor mln_event_batch_release$descriptor() { + return mln_event_batch_release.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_drain_events(mln_runtime runtime, size_t max_events, mln_runtime_event_batch *out_batch) + * void mln_event_batch_release(mln_event_batch batch) * } */ - public static MethodHandle mln_runtime_drain_events$handle() { - return mln_runtime_drain_events.HANDLE; + public static MethodHandle mln_event_batch_release$handle() { + return mln_event_batch_release.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_drain_events(mln_runtime runtime, size_t max_events, mln_runtime_event_batch *out_batch) + * void mln_event_batch_release(mln_event_batch batch) * } */ - public static MemorySegment mln_runtime_drain_events$address() { - return mln_runtime_drain_events.ADDR; + public static MemorySegment mln_event_batch_release$address() { + return mln_event_batch_release.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_drain_events(mln_runtime runtime, size_t max_events, mln_runtime_event_batch *out_batch) + * void mln_event_batch_release(mln_event_batch batch) * } */ - public static int mln_runtime_drain_events(long runtime, long max_events, MemorySegment out_batch) { - var mh$ = mln_runtime_drain_events.HANDLE; + public static void mln_event_batch_release(long batch) { + var mh$ = mln_event_batch_release.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_drain_events", runtime, max_events, out_batch); + traceDowncall("mln_event_batch_release", batch); } - return (int)mh$.invokeExact(runtime, max_events, out_batch); + mh$.invokeExact(batch); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -3753,6 +3630,78 @@ public static int MLN_MAP_MODE_STATIC() { public static int MLN_MAP_MODE_TILE() { return MLN_MAP_MODE_TILE; } + private static final int MLN_CAMERA_UPDATE_MODE_JUMP = (int)0L; + /** + * {@snippet lang=c : + * enum mln_camera_update_mode.MLN_CAMERA_UPDATE_MODE_JUMP = 0 + * } + */ + public static int MLN_CAMERA_UPDATE_MODE_JUMP() { + return MLN_CAMERA_UPDATE_MODE_JUMP; + } + private static final int MLN_CAMERA_UPDATE_MODE_EASE = (int)1L; + /** + * {@snippet lang=c : + * enum mln_camera_update_mode.MLN_CAMERA_UPDATE_MODE_EASE = 1 + * } + */ + public static int MLN_CAMERA_UPDATE_MODE_EASE() { + return MLN_CAMERA_UPDATE_MODE_EASE; + } + private static final int MLN_CAMERA_UPDATE_MODE_FLY = (int)2L; + /** + * {@snippet lang=c : + * enum mln_camera_update_mode.MLN_CAMERA_UPDATE_MODE_FLY = 2 + * } + */ + public static int MLN_CAMERA_UPDATE_MODE_FLY() { + return MLN_CAMERA_UPDATE_MODE_FLY; + } + private static final int MLN_GESTURE_PHASE_NONE = (int)0L; + /** + * {@snippet lang=c : + * enum mln_gesture_phase.MLN_GESTURE_PHASE_NONE = 0 + * } + */ + public static int MLN_GESTURE_PHASE_NONE() { + return MLN_GESTURE_PHASE_NONE; + } + private static final int MLN_GESTURE_PHASE_BEGIN = (int)1L; + /** + * {@snippet lang=c : + * enum mln_gesture_phase.MLN_GESTURE_PHASE_BEGIN = 1 + * } + */ + public static int MLN_GESTURE_PHASE_BEGIN() { + return MLN_GESTURE_PHASE_BEGIN; + } + private static final int MLN_GESTURE_PHASE_UPDATE = (int)2L; + /** + * {@snippet lang=c : + * enum mln_gesture_phase.MLN_GESTURE_PHASE_UPDATE = 2 + * } + */ + public static int MLN_GESTURE_PHASE_UPDATE() { + return MLN_GESTURE_PHASE_UPDATE; + } + private static final int MLN_GESTURE_PHASE_END = (int)3L; + /** + * {@snippet lang=c : + * enum mln_gesture_phase.MLN_GESTURE_PHASE_END = 3 + * } + */ + public static int MLN_GESTURE_PHASE_END() { + return MLN_GESTURE_PHASE_END; + } + private static final int MLN_GESTURE_PHASE_CANCEL = (int)4L; + /** + * {@snippet lang=c : + * enum mln_gesture_phase.MLN_GESTURE_PHASE_CANCEL = 4 + * } + */ + public static int MLN_GESTURE_PHASE_CANCEL() { + return MLN_GESTURE_PHASE_CANCEL; + } private static final int MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID = (int)1L; /** * {@snippet lang=c : @@ -3781,7 +3730,7 @@ public static int MLN_FEATURE_STATE_SELECTOR_STATE_KEY() { return MLN_FEATURE_STATE_SELECTOR_STATE_KEY; } - private static class mln_runtime_offline_region_create_start { + private static class mln_runtime_offline_region_create { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -3791,7 +3740,7 @@ private static class mln_runtime_offline_region_create_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_create_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_create"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -3799,45 +3748,45 @@ private static class mln_runtime_offline_region_create_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_start(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_create(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_create_start$descriptor() { - return mln_runtime_offline_region_create_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_create$descriptor() { + return mln_runtime_offline_region_create.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_start(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_create(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_create_start$handle() { - return mln_runtime_offline_region_create_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_create$handle() { + return mln_runtime_offline_region_create.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_start(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_create(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_create_start$address() { - return mln_runtime_offline_region_create_start.ADDR; + public static MemorySegment mln_runtime_offline_region_create$address() { + return mln_runtime_offline_region_create.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_start(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_create(mln_runtime runtime, const mln_offline_region_definition *definition, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_create_start(long runtime, MemorySegment definition, MemorySegment metadata, long metadata_size, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_create_start.HANDLE; + public static int mln_runtime_offline_region_create(long runtime, MemorySegment definition, MemorySegment metadata, long metadata_size, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_create.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_create_start", runtime, definition, metadata, metadata_size, out_operation_id); + traceDowncall("mln_runtime_offline_region_create", runtime, definition, metadata, metadata_size, completion); } - return (int)mh$.invokeExact(runtime, definition, metadata, metadata_size, out_operation_id); + return (int)mh$.invokeExact(runtime, definition, metadata, metadata_size, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -3845,7 +3794,7 @@ public static int mln_runtime_offline_region_create_start(long runtime, MemorySe } } - private static class mln_runtime_offline_region_get_start { + private static class mln_runtime_offline_region_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -3853,7 +3802,7 @@ private static class mln_runtime_offline_region_get_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -3861,45 +3810,45 @@ private static class mln_runtime_offline_region_get_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_get_start$descriptor() { - return mln_runtime_offline_region_get_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_get$descriptor() { + return mln_runtime_offline_region_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_get_start$handle() { - return mln_runtime_offline_region_get_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_get$handle() { + return mln_runtime_offline_region_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_get_start$address() { - return mln_runtime_offline_region_get_start.ADDR; + public static MemorySegment mln_runtime_offline_region_get$address() { + return mln_runtime_offline_region_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_get_start(long runtime, long region_id, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_get_start.HANDLE; + public static int mln_runtime_offline_region_get(long runtime, long region_id, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_get_start", runtime, region_id, out_operation_id); + traceDowncall("mln_runtime_offline_region_get", runtime, region_id, completion); } - return (int)mh$.invokeExact(runtime, region_id, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -3907,14 +3856,14 @@ public static int mln_runtime_offline_region_get_start(long runtime, long region } } - private static class mln_runtime_offline_regions_list_start { + private static class mln_runtime_offline_regions_list { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_list_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_list"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -3922,45 +3871,45 @@ private static class mln_runtime_offline_regions_list_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_start(mln_runtime runtime, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_list(mln_runtime runtime, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_regions_list_start$descriptor() { - return mln_runtime_offline_regions_list_start.DESC; + public static FunctionDescriptor mln_runtime_offline_regions_list$descriptor() { + return mln_runtime_offline_regions_list.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_start(mln_runtime runtime, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_list(mln_runtime runtime, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_regions_list_start$handle() { - return mln_runtime_offline_regions_list_start.HANDLE; + public static MethodHandle mln_runtime_offline_regions_list$handle() { + return mln_runtime_offline_regions_list.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_start(mln_runtime runtime, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_list(mln_runtime runtime, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_regions_list_start$address() { - return mln_runtime_offline_regions_list_start.ADDR; + public static MemorySegment mln_runtime_offline_regions_list$address() { + return mln_runtime_offline_regions_list.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_start(mln_runtime runtime, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_list(mln_runtime runtime, const mln_completion *completion) * } */ - public static int mln_runtime_offline_regions_list_start(long runtime, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_regions_list_start.HANDLE; + public static int mln_runtime_offline_regions_list(long runtime, MemorySegment completion) { + var mh$ = mln_runtime_offline_regions_list.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_regions_list_start", runtime, out_operation_id); + traceDowncall("mln_runtime_offline_regions_list", runtime, completion); } - return (int)mh$.invokeExact(runtime, out_operation_id); + return (int)mh$.invokeExact(runtime, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -3968,7 +3917,7 @@ public static int mln_runtime_offline_regions_list_start(long runtime, MemorySeg } } - private static class mln_runtime_offline_regions_merge_database_start { + private static class mln_runtime_offline_regions_merge_database { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -3976,7 +3925,7 @@ private static class mln_runtime_offline_regions_merge_database_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_merge_database_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_merge_database"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -3984,45 +3933,45 @@ private static class mln_runtime_offline_regions_merge_database_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_start(mln_runtime runtime, const char *side_database_path, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_merge_database(mln_runtime runtime, const char *side_database_path, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_regions_merge_database_start$descriptor() { - return mln_runtime_offline_regions_merge_database_start.DESC; + public static FunctionDescriptor mln_runtime_offline_regions_merge_database$descriptor() { + return mln_runtime_offline_regions_merge_database.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_start(mln_runtime runtime, const char *side_database_path, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_merge_database(mln_runtime runtime, const char *side_database_path, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_regions_merge_database_start$handle() { - return mln_runtime_offline_regions_merge_database_start.HANDLE; + public static MethodHandle mln_runtime_offline_regions_merge_database$handle() { + return mln_runtime_offline_regions_merge_database.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_start(mln_runtime runtime, const char *side_database_path, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_merge_database(mln_runtime runtime, const char *side_database_path, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_regions_merge_database_start$address() { - return mln_runtime_offline_regions_merge_database_start.ADDR; + public static MemorySegment mln_runtime_offline_regions_merge_database$address() { + return mln_runtime_offline_regions_merge_database.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_start(mln_runtime runtime, const char *side_database_path, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_regions_merge_database(mln_runtime runtime, const char *side_database_path, const mln_completion *completion) * } */ - public static int mln_runtime_offline_regions_merge_database_start(long runtime, MemorySegment side_database_path, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_regions_merge_database_start.HANDLE; + public static int mln_runtime_offline_regions_merge_database(long runtime, MemorySegment side_database_path, MemorySegment completion) { + var mh$ = mln_runtime_offline_regions_merge_database.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_regions_merge_database_start", runtime, side_database_path, out_operation_id); + traceDowncall("mln_runtime_offline_regions_merge_database", runtime, side_database_path, completion); } - return (int)mh$.invokeExact(runtime, side_database_path, out_operation_id); + return (int)mh$.invokeExact(runtime, side_database_path, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4030,7 +3979,7 @@ public static int mln_runtime_offline_regions_merge_database_start(long runtime, } } - private static class mln_runtime_offline_region_update_metadata_start { + private static class mln_runtime_offline_region_update_metadata { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4040,7 +3989,7 @@ private static class mln_runtime_offline_region_update_metadata_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_update_metadata_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_update_metadata"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4048,45 +3997,45 @@ private static class mln_runtime_offline_region_update_metadata_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_start(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_update_metadata(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_update_metadata_start$descriptor() { - return mln_runtime_offline_region_update_metadata_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_update_metadata$descriptor() { + return mln_runtime_offline_region_update_metadata.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_start(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_update_metadata(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_update_metadata_start$handle() { - return mln_runtime_offline_region_update_metadata_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_update_metadata$handle() { + return mln_runtime_offline_region_update_metadata.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_start(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_update_metadata(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_update_metadata_start$address() { - return mln_runtime_offline_region_update_metadata_start.ADDR; + public static MemorySegment mln_runtime_offline_region_update_metadata$address() { + return mln_runtime_offline_region_update_metadata.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_start(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_update_metadata(mln_runtime runtime, mln_offline_region_id region_id, const uint8_t *metadata, size_t metadata_size, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_update_metadata_start(long runtime, long region_id, MemorySegment metadata, long metadata_size, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_update_metadata_start.HANDLE; + public static int mln_runtime_offline_region_update_metadata(long runtime, long region_id, MemorySegment metadata, long metadata_size, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_update_metadata.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_update_metadata_start", runtime, region_id, metadata, metadata_size, out_operation_id); + traceDowncall("mln_runtime_offline_region_update_metadata", runtime, region_id, metadata, metadata_size, completion); } - return (int)mh$.invokeExact(runtime, region_id, metadata, metadata_size, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, metadata, metadata_size, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4094,7 +4043,7 @@ public static int mln_runtime_offline_region_update_metadata_start(long runtime, } } - private static class mln_runtime_offline_region_get_status_start { + private static class mln_runtime_offline_region_get_status { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4102,7 +4051,7 @@ private static class mln_runtime_offline_region_get_status_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get_status_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get_status"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4110,45 +4059,45 @@ private static class mln_runtime_offline_region_get_status_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get_status(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_get_status_start$descriptor() { - return mln_runtime_offline_region_get_status_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_get_status$descriptor() { + return mln_runtime_offline_region_get_status.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get_status(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_get_status_start$handle() { - return mln_runtime_offline_region_get_status_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_get_status$handle() { + return mln_runtime_offline_region_get_status.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get_status(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_get_status_start$address() { - return mln_runtime_offline_region_get_status_start.ADDR; + public static MemorySegment mln_runtime_offline_region_get_status$address() { + return mln_runtime_offline_region_get_status.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_get_status(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_get_status_start(long runtime, long region_id, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_get_status_start.HANDLE; + public static int mln_runtime_offline_region_get_status(long runtime, long region_id, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_get_status.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_get_status_start", runtime, region_id, out_operation_id); + traceDowncall("mln_runtime_offline_region_get_status", runtime, region_id, completion); } - return (int)mh$.invokeExact(runtime, region_id, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4156,7 +4105,7 @@ public static int mln_runtime_offline_region_get_status_start(long runtime, long } } - private static class mln_runtime_offline_region_set_observed_start { + private static class mln_runtime_offline_region_set_observed { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4165,7 +4114,7 @@ private static class mln_runtime_offline_region_set_observed_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_set_observed_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_set_observed"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4173,45 +4122,45 @@ private static class mln_runtime_offline_region_set_observed_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_observed_start(mln_runtime runtime, mln_offline_region_id region_id, bool observed, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_observed(mln_runtime runtime, mln_offline_region_id region_id, bool observed, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_set_observed_start$descriptor() { - return mln_runtime_offline_region_set_observed_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_set_observed$descriptor() { + return mln_runtime_offline_region_set_observed.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_observed_start(mln_runtime runtime, mln_offline_region_id region_id, bool observed, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_observed(mln_runtime runtime, mln_offline_region_id region_id, bool observed, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_set_observed_start$handle() { - return mln_runtime_offline_region_set_observed_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_set_observed$handle() { + return mln_runtime_offline_region_set_observed.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_observed_start(mln_runtime runtime, mln_offline_region_id region_id, bool observed, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_observed(mln_runtime runtime, mln_offline_region_id region_id, bool observed, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_set_observed_start$address() { - return mln_runtime_offline_region_set_observed_start.ADDR; + public static MemorySegment mln_runtime_offline_region_set_observed$address() { + return mln_runtime_offline_region_set_observed.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_observed_start(mln_runtime runtime, mln_offline_region_id region_id, bool observed, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_observed(mln_runtime runtime, mln_offline_region_id region_id, bool observed, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_set_observed_start(long runtime, long region_id, boolean observed, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_set_observed_start.HANDLE; + public static int mln_runtime_offline_region_set_observed(long runtime, long region_id, boolean observed, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_set_observed.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_set_observed_start", runtime, region_id, observed, out_operation_id); + traceDowncall("mln_runtime_offline_region_set_observed", runtime, region_id, observed, completion); } - return (int)mh$.invokeExact(runtime, region_id, observed, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, observed, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4219,7 +4168,7 @@ public static int mln_runtime_offline_region_set_observed_start(long runtime, lo } } - private static class mln_runtime_offline_region_set_download_state_start { + private static class mln_runtime_offline_region_set_download_state { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4228,7 +4177,7 @@ private static class mln_runtime_offline_region_set_download_state_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_set_download_state_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_set_download_state"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4236,45 +4185,45 @@ private static class mln_runtime_offline_region_set_download_state_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_download_state_start(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_download_state(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_set_download_state_start$descriptor() { - return mln_runtime_offline_region_set_download_state_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_set_download_state$descriptor() { + return mln_runtime_offline_region_set_download_state.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_download_state_start(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_download_state(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_set_download_state_start$handle() { - return mln_runtime_offline_region_set_download_state_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_set_download_state$handle() { + return mln_runtime_offline_region_set_download_state.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_download_state_start(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_download_state(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_set_download_state_start$address() { - return mln_runtime_offline_region_set_download_state_start.ADDR; + public static MemorySegment mln_runtime_offline_region_set_download_state$address() { + return mln_runtime_offline_region_set_download_state.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_set_download_state_start(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_set_download_state(mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_set_download_state_start(long runtime, long region_id, int state, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_set_download_state_start.HANDLE; + public static int mln_runtime_offline_region_set_download_state(long runtime, long region_id, int state, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_set_download_state.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_set_download_state_start", runtime, region_id, state, out_operation_id); + traceDowncall("mln_runtime_offline_region_set_download_state", runtime, region_id, state, completion); } - return (int)mh$.invokeExact(runtime, region_id, state, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, state, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4282,7 +4231,7 @@ public static int mln_runtime_offline_region_set_download_state_start(long runti } } - private static class mln_runtime_offline_region_invalidate_start { + private static class mln_runtime_offline_region_invalidate { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4290,7 +4239,7 @@ private static class mln_runtime_offline_region_invalidate_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_invalidate_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_invalidate"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4298,45 +4247,45 @@ private static class mln_runtime_offline_region_invalidate_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_invalidate_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_invalidate(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_invalidate_start$descriptor() { - return mln_runtime_offline_region_invalidate_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_invalidate$descriptor() { + return mln_runtime_offline_region_invalidate.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_invalidate_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_invalidate(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_invalidate_start$handle() { - return mln_runtime_offline_region_invalidate_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_invalidate$handle() { + return mln_runtime_offline_region_invalidate.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_invalidate_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_invalidate(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_invalidate_start$address() { - return mln_runtime_offline_region_invalidate_start.ADDR; + public static MemorySegment mln_runtime_offline_region_invalidate$address() { + return mln_runtime_offline_region_invalidate.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_invalidate_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_invalidate(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_invalidate_start(long runtime, long region_id, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_invalidate_start.HANDLE; + public static int mln_runtime_offline_region_invalidate(long runtime, long region_id, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_invalidate.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_invalidate_start", runtime, region_id, out_operation_id); + traceDowncall("mln_runtime_offline_region_invalidate", runtime, region_id, completion); } - return (int)mh$.invokeExact(runtime, region_id, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4344,7 +4293,7 @@ public static int mln_runtime_offline_region_invalidate_start(long runtime, long } } - private static class mln_runtime_offline_region_delete_start { + private static class mln_runtime_offline_region_delete { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4352,7 +4301,7 @@ private static class mln_runtime_offline_region_delete_start { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_delete_start"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_delete"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4360,45 +4309,45 @@ private static class mln_runtime_offline_region_delete_start { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_delete_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_delete(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_runtime_offline_region_delete_start$descriptor() { - return mln_runtime_offline_region_delete_start.DESC; + public static FunctionDescriptor mln_runtime_offline_region_delete$descriptor() { + return mln_runtime_offline_region_delete.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_delete_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_delete(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MethodHandle mln_runtime_offline_region_delete_start$handle() { - return mln_runtime_offline_region_delete_start.HANDLE; + public static MethodHandle mln_runtime_offline_region_delete$handle() { + return mln_runtime_offline_region_delete.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_delete_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_delete(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static MemorySegment mln_runtime_offline_region_delete_start$address() { - return mln_runtime_offline_region_delete_start.ADDR; + public static MemorySegment mln_runtime_offline_region_delete$address() { + return mln_runtime_offline_region_delete.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_delete_start(mln_runtime runtime, mln_offline_region_id region_id, mln_offline_operation_id *out_operation_id) + * mln_status mln_runtime_offline_region_delete(mln_runtime runtime, mln_offline_region_id region_id, const mln_completion *completion) * } */ - public static int mln_runtime_offline_region_delete_start(long runtime, long region_id, MemorySegment out_operation_id) { - var mh$ = mln_runtime_offline_region_delete_start.HANDLE; + public static int mln_runtime_offline_region_delete(long runtime, long region_id, MemorySegment completion) { + var mh$ = mln_runtime_offline_region_delete.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_delete_start", runtime, region_id, out_operation_id); + traceDowncall("mln_runtime_offline_region_delete", runtime, region_id, completion); } - return (int)mh$.invokeExact(runtime, region_id, out_operation_id); + return (int)mh$.invokeExact(runtime, region_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4406,15 +4355,14 @@ public static int mln_runtime_offline_region_delete_start(long runtime, long reg } } - private static class mln_runtime_offline_region_create_take_result { + private static class mln_offline_region_snapshot_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_create_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_snapshot_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4422,45 +4370,45 @@ private static class mln_runtime_offline_region_create_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) * } */ - public static FunctionDescriptor mln_runtime_offline_region_create_take_result$descriptor() { - return mln_runtime_offline_region_create_take_result.DESC; + public static FunctionDescriptor mln_offline_region_snapshot_get$descriptor() { + return mln_offline_region_snapshot_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) * } */ - public static MethodHandle mln_runtime_offline_region_create_take_result$handle() { - return mln_runtime_offline_region_create_take_result.HANDLE; + public static MethodHandle mln_offline_region_snapshot_get$handle() { + return mln_offline_region_snapshot_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) * } */ - public static MemorySegment mln_runtime_offline_region_create_take_result$address() { - return mln_runtime_offline_region_create_take_result.ADDR; + public static MemorySegment mln_offline_region_snapshot_get$address() { + return mln_offline_region_snapshot_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_create_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) * } */ - public static int mln_runtime_offline_region_create_take_result(long runtime, long operation_id, MemorySegment out_region) { - var mh$ = mln_runtime_offline_region_create_take_result.HANDLE; + public static int mln_offline_region_snapshot_get(long snapshot, MemorySegment out_info) { + var mh$ = mln_offline_region_snapshot_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_create_take_result", runtime, operation_id, out_region); + traceDowncall("mln_offline_region_snapshot_get", snapshot, out_info); } - return (int)mh$.invokeExact(runtime, operation_id, out_region); + return (int)mh$.invokeExact(snapshot, out_info); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4468,16 +4416,12 @@ public static int mln_runtime_offline_region_create_take_result(long runtime, lo } } - private static class mln_runtime_offline_region_get_take_result { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER + private static class mln_offline_region_snapshot_destroy { + public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_snapshot_destroy"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4485,45 +4429,45 @@ private static class mln_runtime_offline_region_get_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region, bool *out_found) + * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) * } */ - public static FunctionDescriptor mln_runtime_offline_region_get_take_result$descriptor() { - return mln_runtime_offline_region_get_take_result.DESC; + public static FunctionDescriptor mln_offline_region_snapshot_destroy$descriptor() { + return mln_offline_region_snapshot_destroy.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region, bool *out_found) + * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) * } */ - public static MethodHandle mln_runtime_offline_region_get_take_result$handle() { - return mln_runtime_offline_region_get_take_result.HANDLE; + public static MethodHandle mln_offline_region_snapshot_destroy$handle() { + return mln_offline_region_snapshot_destroy.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region, bool *out_found) + * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) * } */ - public static MemorySegment mln_runtime_offline_region_get_take_result$address() { - return mln_runtime_offline_region_get_take_result.ADDR; + public static MemorySegment mln_offline_region_snapshot_destroy$address() { + return mln_offline_region_snapshot_destroy.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region, bool *out_found) + * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) * } */ - public static int mln_runtime_offline_region_get_take_result(long runtime, long operation_id, MemorySegment out_region, MemorySegment out_found) { - var mh$ = mln_runtime_offline_region_get_take_result.HANDLE; + public static void mln_offline_region_snapshot_destroy(long snapshot) { + var mh$ = mln_offline_region_snapshot_destroy.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_get_take_result", runtime, operation_id, out_region, out_found); + traceDowncall("mln_offline_region_snapshot_destroy", snapshot); } - return (int)mh$.invokeExact(runtime, operation_id, out_region, out_found); + mh$.invokeExact(snapshot); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4531,15 +4475,14 @@ public static int mln_runtime_offline_region_get_take_result(long runtime, long } } - private static class mln_runtime_offline_regions_list_take_result { + private static class mln_offline_region_list_count { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_list_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_count"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4547,45 +4490,45 @@ private static class mln_runtime_offline_regions_list_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) * } */ - public static FunctionDescriptor mln_runtime_offline_regions_list_take_result$descriptor() { - return mln_runtime_offline_regions_list_take_result.DESC; + public static FunctionDescriptor mln_offline_region_list_count$descriptor() { + return mln_offline_region_list_count.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) * } */ - public static MethodHandle mln_runtime_offline_regions_list_take_result$handle() { - return mln_runtime_offline_regions_list_take_result.HANDLE; + public static MethodHandle mln_offline_region_list_count$handle() { + return mln_offline_region_list_count.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) * } */ - public static MemorySegment mln_runtime_offline_regions_list_take_result$address() { - return mln_runtime_offline_regions_list_take_result.ADDR; + public static MemorySegment mln_offline_region_list_count$address() { + return mln_offline_region_list_count.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_list_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) * } */ - public static int mln_runtime_offline_regions_list_take_result(long runtime, long operation_id, MemorySegment out_regions) { - var mh$ = mln_runtime_offline_regions_list_take_result.HANDLE; + public static int mln_offline_region_list_count(long list, MemorySegment out_count) { + var mh$ = mln_offline_region_list_count.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_regions_list_take_result", runtime, operation_id, out_regions); + traceDowncall("mln_offline_region_list_count", list, out_count); } - return (int)mh$.invokeExact(runtime, operation_id, out_regions); + return (int)mh$.invokeExact(list, out_count); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4593,7 +4536,7 @@ public static int mln_runtime_offline_regions_list_take_result(long runtime, lon } } - private static class mln_runtime_offline_regions_merge_database_take_result { + private static class mln_offline_region_list_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -4601,7 +4544,7 @@ private static class mln_runtime_offline_regions_merge_database_take_result { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_regions_merge_database_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4609,45 +4552,45 @@ private static class mln_runtime_offline_regions_merge_database_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) * } */ - public static FunctionDescriptor mln_runtime_offline_regions_merge_database_take_result$descriptor() { - return mln_runtime_offline_regions_merge_database_take_result.DESC; + public static FunctionDescriptor mln_offline_region_list_get$descriptor() { + return mln_offline_region_list_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) * } */ - public static MethodHandle mln_runtime_offline_regions_merge_database_take_result$handle() { - return mln_runtime_offline_regions_merge_database_take_result.HANDLE; + public static MethodHandle mln_offline_region_list_get$handle() { + return mln_offline_region_list_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) * } */ - public static MemorySegment mln_runtime_offline_regions_merge_database_take_result$address() { - return mln_runtime_offline_regions_merge_database_take_result.ADDR; + public static MemorySegment mln_offline_region_list_get$address() { + return mln_offline_region_list_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_regions_merge_database_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_list *out_regions) + * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) * } */ - public static int mln_runtime_offline_regions_merge_database_take_result(long runtime, long operation_id, MemorySegment out_regions) { - var mh$ = mln_runtime_offline_regions_merge_database_take_result.HANDLE; + public static int mln_offline_region_list_get(long list, long index, MemorySegment out_info) { + var mh$ = mln_offline_region_list_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_regions_merge_database_take_result", runtime, operation_id, out_regions); + traceDowncall("mln_offline_region_list_get", list, index, out_info); } - return (int)mh$.invokeExact(runtime, operation_id, out_regions); + return (int)mh$.invokeExact(list, index, out_info); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4655,15 +4598,12 @@ public static int mln_runtime_offline_regions_merge_database_take_result(long ru } } - private static class mln_runtime_offline_region_update_metadata_take_result { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER + private static class mln_offline_region_list_destroy { + public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_update_metadata_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_destroy"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4671,45 +4611,45 @@ private static class mln_runtime_offline_region_update_metadata_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * void mln_offline_region_list_destroy(mln_offline_region_list list) * } */ - public static FunctionDescriptor mln_runtime_offline_region_update_metadata_take_result$descriptor() { - return mln_runtime_offline_region_update_metadata_take_result.DESC; + public static FunctionDescriptor mln_offline_region_list_destroy$descriptor() { + return mln_offline_region_list_destroy.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * void mln_offline_region_list_destroy(mln_offline_region_list list) * } */ - public static MethodHandle mln_runtime_offline_region_update_metadata_take_result$handle() { - return mln_runtime_offline_region_update_metadata_take_result.HANDLE; + public static MethodHandle mln_offline_region_list_destroy$handle() { + return mln_offline_region_list_destroy.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * void mln_offline_region_list_destroy(mln_offline_region_list list) * } */ - public static MemorySegment mln_runtime_offline_region_update_metadata_take_result$address() { - return mln_runtime_offline_region_update_metadata_take_result.ADDR; + public static MemorySegment mln_offline_region_list_destroy$address() { + return mln_offline_region_list_destroy.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_update_metadata_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_snapshot *out_region) + * void mln_offline_region_list_destroy(mln_offline_region_list list) * } */ - public static int mln_runtime_offline_region_update_metadata_take_result(long runtime, long operation_id, MemorySegment out_region) { - var mh$ = mln_runtime_offline_region_update_metadata_take_result.HANDLE; + public static void mln_offline_region_list_destroy(long list) { + var mh$ = mln_offline_region_list_destroy.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_update_metadata_take_result", runtime, operation_id, out_region); + traceDowncall("mln_offline_region_list_destroy", list); } - return (int)mh$.invokeExact(runtime, operation_id, out_region); + mh$.invokeExact(list); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4717,15 +4657,11 @@ public static int mln_runtime_offline_region_update_metadata_take_result(long ru } } - private static class mln_runtime_offline_region_get_status_take_result { + private static class mln_map_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_map_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_runtime_offline_region_get_status_take_result"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4733,45 +4669,45 @@ private static class mln_runtime_offline_region_get_status_take_result { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_status *out_status) + * mln_map_options mln_map_options_default() * } */ - public static FunctionDescriptor mln_runtime_offline_region_get_status_take_result$descriptor() { - return mln_runtime_offline_region_get_status_take_result.DESC; + public static FunctionDescriptor mln_map_options_default$descriptor() { + return mln_map_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_status *out_status) + * mln_map_options mln_map_options_default() * } */ - public static MethodHandle mln_runtime_offline_region_get_status_take_result$handle() { - return mln_runtime_offline_region_get_status_take_result.HANDLE; + public static MethodHandle mln_map_options_default$handle() { + return mln_map_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_status *out_status) + * mln_map_options mln_map_options_default() * } */ - public static MemorySegment mln_runtime_offline_region_get_status_take_result$address() { - return mln_runtime_offline_region_get_status_take_result.ADDR; + public static MemorySegment mln_map_options_default$address() { + return mln_map_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_runtime_offline_region_get_status_take_result(mln_runtime runtime, mln_offline_operation_id operation_id, mln_offline_region_status *out_status) + * mln_map_options mln_map_options_default() * } */ - public static int mln_runtime_offline_region_get_status_take_result(long runtime, long operation_id, MemorySegment out_status) { - var mh$ = mln_runtime_offline_region_get_status_take_result.HANDLE; + public static MemorySegment mln_map_options_default(SegmentAllocator allocator) { + var mh$ = mln_map_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_runtime_offline_region_get_status_take_result", runtime, operation_id, out_status); + traceDowncall("mln_map_options_default", allocator); } - return (int)mh$.invokeExact(runtime, operation_id, out_status); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4779,14 +4715,15 @@ public static int mln_runtime_offline_region_get_status_take_result(long runtime } } - private static class mln_offline_region_snapshot_get { + private static class mln_map_create { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_snapshot_get"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_create"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4794,45 +4731,45 @@ private static class mln_offline_region_snapshot_get { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) + * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_offline_region_snapshot_get$descriptor() { - return mln_offline_region_snapshot_get.DESC; + public static FunctionDescriptor mln_map_create$descriptor() { + return mln_map_create.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) + * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_offline_region_snapshot_get$handle() { - return mln_offline_region_snapshot_get.HANDLE; + public static MethodHandle mln_map_create$handle() { + return mln_map_create.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) + * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_offline_region_snapshot_get$address() { - return mln_offline_region_snapshot_get.ADDR; + public static MemorySegment mln_map_create$address() { + return mln_map_create.ADDR; } /** * {@snippet lang=c : - * mln_status mln_offline_region_snapshot_get(mln_offline_region_snapshot snapshot, mln_offline_region_info *out_info) + * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, const mln_completion *completion) * } */ - public static int mln_offline_region_snapshot_get(long snapshot, MemorySegment out_info) { - var mh$ = mln_offline_region_snapshot_get.HANDLE; + public static int mln_map_create(long runtime, MemorySegment options, MemorySegment completion) { + var mh$ = mln_map_create.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_offline_region_snapshot_get", snapshot, out_info); + traceDowncall("mln_map_create", runtime, options, completion); } - return (int)mh$.invokeExact(snapshot, out_info); + return (int)mh$.invokeExact(runtime, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4840,12 +4777,14 @@ public static int mln_offline_region_snapshot_get(long snapshot, MemorySegment o } } - private static class mln_offline_region_snapshot_destroy { - public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - MapLibreNativeC.C_LONG + private static class mln_map_snapshot_get { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_snapshot_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_snapshot_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4853,45 +4792,45 @@ private static class mln_offline_region_snapshot_destroy { /** * Function descriptor for: * {@snippet lang=c : - * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) + * mln_status mln_map_snapshot_get(mln_map map, mln_map_snapshot *out_snapshot) * } */ - public static FunctionDescriptor mln_offline_region_snapshot_destroy$descriptor() { - return mln_offline_region_snapshot_destroy.DESC; + public static FunctionDescriptor mln_map_snapshot_get$descriptor() { + return mln_map_snapshot_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) + * mln_status mln_map_snapshot_get(mln_map map, mln_map_snapshot *out_snapshot) * } */ - public static MethodHandle mln_offline_region_snapshot_destroy$handle() { - return mln_offline_region_snapshot_destroy.HANDLE; + public static MethodHandle mln_map_snapshot_get$handle() { + return mln_map_snapshot_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) + * mln_status mln_map_snapshot_get(mln_map map, mln_map_snapshot *out_snapshot) * } */ - public static MemorySegment mln_offline_region_snapshot_destroy$address() { - return mln_offline_region_snapshot_destroy.ADDR; + public static MemorySegment mln_map_snapshot_get$address() { + return mln_map_snapshot_get.ADDR; } /** * {@snippet lang=c : - * void mln_offline_region_snapshot_destroy(mln_offline_region_snapshot snapshot) + * mln_status mln_map_snapshot_get(mln_map map, mln_map_snapshot *out_snapshot) * } */ - public static void mln_offline_region_snapshot_destroy(long snapshot) { - var mh$ = mln_offline_region_snapshot_destroy.HANDLE; + public static int mln_map_snapshot_get(long map, MemorySegment out_snapshot) { + var mh$ = mln_map_snapshot_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_offline_region_snapshot_destroy", snapshot); + traceDowncall("mln_map_snapshot_get", map, out_snapshot); } - mh$.invokeExact(snapshot); + return (int)mh$.invokeExact(map, out_snapshot); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4899,14 +4838,15 @@ public static void mln_offline_region_snapshot_destroy(long snapshot) { } } - private static class mln_offline_region_list_count { + private static class mln_map_resize { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + mln_logical_extent.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_count"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_resize"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4914,45 +4854,45 @@ private static class mln_offline_region_list_count { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) + * mln_status mln_map_resize(mln_map map, mln_logical_extent extent, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_offline_region_list_count$descriptor() { - return mln_offline_region_list_count.DESC; + public static FunctionDescriptor mln_map_resize$descriptor() { + return mln_map_resize.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) + * mln_status mln_map_resize(mln_map map, mln_logical_extent extent, const mln_completion *completion) * } */ - public static MethodHandle mln_offline_region_list_count$handle() { - return mln_offline_region_list_count.HANDLE; + public static MethodHandle mln_map_resize$handle() { + return mln_map_resize.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) + * mln_status mln_map_resize(mln_map map, mln_logical_extent extent, const mln_completion *completion) * } */ - public static MemorySegment mln_offline_region_list_count$address() { - return mln_offline_region_list_count.ADDR; + public static MemorySegment mln_map_resize$address() { + return mln_map_resize.ADDR; } /** * {@snippet lang=c : - * mln_status mln_offline_region_list_count(mln_offline_region_list list, size_t *out_count) + * mln_status mln_map_resize(mln_map map, mln_logical_extent extent, const mln_completion *completion) * } */ - public static int mln_offline_region_list_count(long list, MemorySegment out_count) { - var mh$ = mln_offline_region_list_count.HANDLE; + public static int mln_map_resize(long map, MemorySegment extent, MemorySegment completion) { + var mh$ = mln_map_resize.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_offline_region_list_count", list, out_count); + traceDowncall("mln_map_resize", map, extent, completion); } - return (int)mh$.invokeExact(list, out_count); + return (int)mh$.invokeExact(map, extent, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -4960,15 +4900,14 @@ public static int mln_offline_region_list_count(long list, MemorySegment out_cou } } - private static class mln_offline_region_list_get { + private static class mln_map_request_repaint { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_get"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_request_repaint"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -4976,45 +4915,45 @@ private static class mln_offline_region_list_get { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) + * mln_status mln_map_request_repaint(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_offline_region_list_get$descriptor() { - return mln_offline_region_list_get.DESC; + public static FunctionDescriptor mln_map_request_repaint$descriptor() { + return mln_map_request_repaint.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) + * mln_status mln_map_request_repaint(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_offline_region_list_get$handle() { - return mln_offline_region_list_get.HANDLE; + public static MethodHandle mln_map_request_repaint$handle() { + return mln_map_request_repaint.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) + * mln_status mln_map_request_repaint(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_offline_region_list_get$address() { - return mln_offline_region_list_get.ADDR; + public static MemorySegment mln_map_request_repaint$address() { + return mln_map_request_repaint.ADDR; } /** * {@snippet lang=c : - * mln_status mln_offline_region_list_get(mln_offline_region_list list, size_t index, mln_offline_region_info *out_info) + * mln_status mln_map_request_repaint(mln_map map, const mln_completion *completion) * } */ - public static int mln_offline_region_list_get(long list, long index, MemorySegment out_info) { - var mh$ = mln_offline_region_list_get.HANDLE; + public static int mln_map_request_repaint(long map, MemorySegment completion) { + var mh$ = mln_map_request_repaint.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_offline_region_list_get", list, index, out_info); + traceDowncall("mln_map_request_repaint", map, completion); } - return (int)mh$.invokeExact(list, index, out_info); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5022,12 +4961,16 @@ public static int mln_offline_region_list_get(long list, long index, MemorySegme } } - private static class mln_offline_region_list_destroy { - public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - MapLibreNativeC.C_LONG + private static class mln_map_set_feature_state { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_offline_region_list_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_feature_state"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5035,45 +4978,45 @@ private static class mln_offline_region_list_destroy { /** * Function descriptor for: * {@snippet lang=c : - * void mln_offline_region_list_destroy(mln_offline_region_list list) + * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_offline_region_list_destroy$descriptor() { - return mln_offline_region_list_destroy.DESC; + public static FunctionDescriptor mln_map_set_feature_state$descriptor() { + return mln_map_set_feature_state.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * void mln_offline_region_list_destroy(mln_offline_region_list list) + * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state, const mln_completion *completion) * } */ - public static MethodHandle mln_offline_region_list_destroy$handle() { - return mln_offline_region_list_destroy.HANDLE; + public static MethodHandle mln_map_set_feature_state$handle() { + return mln_map_set_feature_state.HANDLE; } /** * Address for: * {@snippet lang=c : - * void mln_offline_region_list_destroy(mln_offline_region_list list) + * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state, const mln_completion *completion) * } */ - public static MemorySegment mln_offline_region_list_destroy$address() { - return mln_offline_region_list_destroy.ADDR; + public static MemorySegment mln_map_set_feature_state$address() { + return mln_map_set_feature_state.ADDR; } /** * {@snippet lang=c : - * void mln_offline_region_list_destroy(mln_offline_region_list list) + * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state, const mln_completion *completion) * } */ - public static void mln_offline_region_list_destroy(long list) { - var mh$ = mln_offline_region_list_destroy.HANDLE; + public static int mln_map_set_feature_state(long map, MemorySegment selector, MemorySegment state, MemorySegment completion) { + var mh$ = mln_map_set_feature_state.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_offline_region_list_destroy", list); + traceDowncall("mln_map_set_feature_state", map, selector, state, completion); } - mh$.invokeExact(list); + return (int)mh$.invokeExact(map, selector, state, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5081,11 +5024,15 @@ public static void mln_offline_region_list_destroy(long list) { } } - private static class mln_map_options_default { + private static class mln_map_get_feature_state { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_map_options.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_feature_state"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5093,45 +5040,45 @@ private static class mln_map_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_map_options mln_map_options_default() + * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_options_default$descriptor() { - return mln_map_options_default.DESC; + public static FunctionDescriptor mln_map_get_feature_state$descriptor() { + return mln_map_get_feature_state.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_map_options mln_map_options_default() + * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static MethodHandle mln_map_options_default$handle() { - return mln_map_options_default.HANDLE; + public static MethodHandle mln_map_get_feature_state$handle() { + return mln_map_get_feature_state.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_map_options mln_map_options_default() + * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static MemorySegment mln_map_options_default$address() { - return mln_map_options_default.ADDR; + public static MemorySegment mln_map_get_feature_state$address() { + return mln_map_get_feature_state.ADDR; } /** * {@snippet lang=c : - * mln_map_options mln_map_options_default() + * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static MemorySegment mln_map_options_default(SegmentAllocator allocator) { - var mh$ = mln_map_options_default.HANDLE; + public static int mln_map_get_feature_state(long map, MemorySegment selector, MemorySegment completion) { + var mh$ = mln_map_get_feature_state.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_options_default", allocator); + traceDowncall("mln_map_get_feature_state", map, selector, completion); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(map, selector, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5139,7 +5086,7 @@ public static MemorySegment mln_map_options_default(SegmentAllocator allocator) } } - private static class mln_map_create { + private static class mln_map_remove_feature_state { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -5147,7 +5094,7 @@ private static class mln_map_create { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_create"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_remove_feature_state"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5155,45 +5102,45 @@ private static class mln_map_create { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, mln_map *out_map) + * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_create$descriptor() { - return mln_map_create.DESC; + public static FunctionDescriptor mln_map_remove_feature_state$descriptor() { + return mln_map_remove_feature_state.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, mln_map *out_map) + * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static MethodHandle mln_map_create$handle() { - return mln_map_create.HANDLE; + public static MethodHandle mln_map_remove_feature_state$handle() { + return mln_map_remove_feature_state.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, mln_map *out_map) + * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static MemorySegment mln_map_create$address() { - return mln_map_create.ADDR; + public static MemorySegment mln_map_remove_feature_state$address() { + return mln_map_remove_feature_state.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_create(mln_runtime runtime, const mln_map_options *options, mln_map *out_map) + * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector, const mln_completion *completion) * } */ - public static int mln_map_create(long runtime, MemorySegment options, MemorySegment out_map) { - var mh$ = mln_map_create.HANDLE; + public static int mln_map_remove_feature_state(long map, MemorySegment selector, MemorySegment completion) { + var mh$ = mln_map_remove_feature_state.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_create", runtime, options, out_map); + traceDowncall("mln_map_remove_feature_state", map, selector, completion); } - return (int)mh$.invokeExact(runtime, options, out_map); + return (int)mh$.invokeExact(map, selector, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5201,16 +5148,14 @@ public static int mln_map_create(long runtime, MemorySegment options, MemorySegm } } - private static class mln_map_get_size { + private static class mln_map_request_still_image { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_size"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_request_still_image"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5218,45 +5163,45 @@ private static class mln_map_get_size { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_size(mln_map map, uint32_t *out_width, uint32_t *out_height, double *out_scale_factor) + * mln_status mln_map_request_still_image(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_size$descriptor() { - return mln_map_get_size.DESC; + public static FunctionDescriptor mln_map_request_still_image$descriptor() { + return mln_map_request_still_image.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_size(mln_map map, uint32_t *out_width, uint32_t *out_height, double *out_scale_factor) + * mln_status mln_map_request_still_image(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_size$handle() { - return mln_map_get_size.HANDLE; + public static MethodHandle mln_map_request_still_image$handle() { + return mln_map_request_still_image.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_size(mln_map map, uint32_t *out_width, uint32_t *out_height, double *out_scale_factor) + * mln_status mln_map_request_still_image(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_size$address() { - return mln_map_get_size.ADDR; + public static MemorySegment mln_map_request_still_image$address() { + return mln_map_request_still_image.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_size(mln_map map, uint32_t *out_width, uint32_t *out_height, double *out_scale_factor) + * mln_status mln_map_request_still_image(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_get_size(long map, MemorySegment out_width, MemorySegment out_height, MemorySegment out_scale_factor) { - var mh$ = mln_map_get_size.HANDLE; + public static int mln_map_request_still_image(long map, MemorySegment completion) { + var mh$ = mln_map_request_still_image.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_size", map, out_width, out_height, out_scale_factor); + traceDowncall("mln_map_request_still_image", map, completion); } - return (int)mh$.invokeExact(map, out_width, out_height, out_scale_factor); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5264,13 +5209,14 @@ public static int mln_map_get_size(long map, MemorySegment out_width, MemorySegm } } - private static class mln_map_request_repaint { + private static class mln_map_release { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_request_repaint"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_release"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5278,45 +5224,45 @@ private static class mln_map_request_repaint { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_request_repaint(mln_map map) + * mln_status mln_map_release(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_request_repaint$descriptor() { - return mln_map_request_repaint.DESC; + public static FunctionDescriptor mln_map_release$descriptor() { + return mln_map_release.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_request_repaint(mln_map map) + * mln_status mln_map_release(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_request_repaint$handle() { - return mln_map_request_repaint.HANDLE; + public static MethodHandle mln_map_release$handle() { + return mln_map_release.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_request_repaint(mln_map map) + * mln_status mln_map_release(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_request_repaint$address() { - return mln_map_request_repaint.ADDR; + public static MemorySegment mln_map_release$address() { + return mln_map_release.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_request_repaint(mln_map map) + * mln_status mln_map_release(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_request_repaint(long map) { - var mh$ = mln_map_request_repaint.HANDLE; + public static int mln_map_release(long map, MemorySegment completion) { + var mh$ = mln_map_release.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_request_repaint", map); + traceDowncall("mln_map_release", map, completion); } - return (int)mh$.invokeExact(map); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5324,15 +5270,15 @@ public static int mln_map_request_repaint(long map) { } } - private static class mln_map_set_feature_state { + private static class mln_map_set_style_url { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, - mln_buffer_view.layout() + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_feature_state"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_url"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5340,45 +5286,45 @@ private static class mln_map_set_feature_state { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state) + * mln_status mln_map_set_style_url(mln_map map, const char *url, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_set_feature_state$descriptor() { - return mln_map_set_feature_state.DESC; + public static FunctionDescriptor mln_map_set_style_url$descriptor() { + return mln_map_set_style_url.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state) + * mln_status mln_map_set_style_url(mln_map map, const char *url, const mln_completion *completion) * } */ - public static MethodHandle mln_map_set_feature_state$handle() { - return mln_map_set_feature_state.HANDLE; + public static MethodHandle mln_map_set_style_url$handle() { + return mln_map_set_style_url.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state) + * mln_status mln_map_set_style_url(mln_map map, const char *url, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_feature_state$address() { - return mln_map_set_feature_state.ADDR; + public static MemorySegment mln_map_set_style_url$address() { + return mln_map_set_style_url.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer_view state) + * mln_status mln_map_set_style_url(mln_map map, const char *url, const mln_completion *completion) * } */ - public static int mln_map_set_feature_state(long map, MemorySegment selector, MemorySegment state) { - var mh$ = mln_map_set_feature_state.HANDLE; + public static int mln_map_set_style_url(long map, MemorySegment url, MemorySegment completion) { + var mh$ = mln_map_set_style_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_feature_state", map, selector, state); + traceDowncall("mln_map_set_style_url", map, url, completion); } - return (int)mh$.invokeExact(map, selector, state); + return (int)mh$.invokeExact(map, url, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5386,15 +5332,15 @@ public static int mln_map_set_feature_state(long map, MemorySegment selector, Me } } - private static class mln_map_get_feature_state { + private static class mln_map_set_style_json { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, + mln_buffer_view.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_feature_state"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_json"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5402,45 +5348,45 @@ private static class mln_map_get_feature_state { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer *out_state) + * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_feature_state$descriptor() { - return mln_map_get_feature_state.DESC; + public static FunctionDescriptor mln_map_set_style_json$descriptor() { + return mln_map_set_style_json.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer *out_state) + * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_feature_state$handle() { - return mln_map_get_feature_state.HANDLE; + public static MethodHandle mln_map_set_style_json$handle() { + return mln_map_set_style_json.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer *out_state) + * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_feature_state$address() { - return mln_map_get_feature_state.ADDR; + public static MemorySegment mln_map_set_style_json$address() { + return mln_map_set_style_json.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_feature_state(mln_map map, const mln_feature_state_selector *selector, mln_buffer *out_state) + * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json, const mln_completion *completion) * } */ - public static int mln_map_get_feature_state(long map, MemorySegment selector, MemorySegment out_state) { - var mh$ = mln_map_get_feature_state.HANDLE; + public static int mln_map_set_style_json(long map, MemorySegment json, MemorySegment completion) { + var mh$ = mln_map_set_style_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_feature_state", map, selector, out_state); + traceDowncall("mln_map_set_style_json", map, json, completion); } - return (int)mh$.invokeExact(map, selector, out_state); + return (int)mh$.invokeExact(map, json, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5448,14 +5394,14 @@ public static int mln_map_get_feature_state(long map, MemorySegment selector, Me } } - private static class mln_map_remove_feature_state { + private static class mln_map_loaded_style_json { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_remove_feature_state"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_loaded_style_json"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5463,45 +5409,45 @@ private static class mln_map_remove_feature_state { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector) + * mln_status mln_map_loaded_style_json(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_remove_feature_state$descriptor() { - return mln_map_remove_feature_state.DESC; + public static FunctionDescriptor mln_map_loaded_style_json$descriptor() { + return mln_map_loaded_style_json.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector) + * mln_status mln_map_loaded_style_json(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_remove_feature_state$handle() { - return mln_map_remove_feature_state.HANDLE; + public static MethodHandle mln_map_loaded_style_json$handle() { + return mln_map_loaded_style_json.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector) + * mln_status mln_map_loaded_style_json(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_remove_feature_state$address() { - return mln_map_remove_feature_state.ADDR; + public static MemorySegment mln_map_loaded_style_json$address() { + return mln_map_loaded_style_json.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_remove_feature_state(mln_map map, const mln_feature_state_selector *selector) + * mln_status mln_map_loaded_style_json(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_remove_feature_state(long map, MemorySegment selector) { - var mh$ = mln_map_remove_feature_state.HANDLE; + public static int mln_map_loaded_style_json(long map, MemorySegment completion) { + var mh$ = mln_map_loaded_style_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_remove_feature_state", map, selector); + traceDowncall("mln_map_loaded_style_json", map, completion); } - return (int)mh$.invokeExact(map, selector); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5509,13 +5455,14 @@ public static int mln_map_remove_feature_state(long map, MemorySegment selector) } } - private static class mln_map_request_still_image { + private static class mln_map_style_url { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_request_still_image"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_style_url"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5523,45 +5470,45 @@ private static class mln_map_request_still_image { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_request_still_image(mln_map map) + * mln_status mln_map_style_url(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_request_still_image$descriptor() { - return mln_map_request_still_image.DESC; + public static FunctionDescriptor mln_map_style_url$descriptor() { + return mln_map_style_url.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_request_still_image(mln_map map) + * mln_status mln_map_style_url(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_request_still_image$handle() { - return mln_map_request_still_image.HANDLE; + public static MethodHandle mln_map_style_url$handle() { + return mln_map_style_url.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_request_still_image(mln_map map) + * mln_status mln_map_style_url(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_request_still_image$address() { - return mln_map_request_still_image.ADDR; + public static MemorySegment mln_map_style_url$address() { + return mln_map_style_url.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_request_still_image(mln_map map) + * mln_status mln_map_style_url(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_request_still_image(long map) { - var mh$ = mln_map_request_still_image.HANDLE; + public static int mln_map_style_url(long map, MemorySegment completion) { + var mh$ = mln_map_style_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_request_still_image", map); + traceDowncall("mln_map_style_url", map, completion); } - return (int)mh$.invokeExact(map); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5569,13 +5516,15 @@ public static int mln_map_request_still_image(long map) { } } - private static class mln_map_destroy { + private static class mln_map_set_event_mask { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_event_mask"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5583,45 +5532,45 @@ private static class mln_map_destroy { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_destroy(mln_map map) + * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_destroy$descriptor() { - return mln_map_destroy.DESC; + public static FunctionDescriptor mln_map_set_event_mask$descriptor() { + return mln_map_set_event_mask.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_destroy(mln_map map) + * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask, const mln_completion *completion) * } */ - public static MethodHandle mln_map_destroy$handle() { - return mln_map_destroy.HANDLE; + public static MethodHandle mln_map_set_event_mask$handle() { + return mln_map_set_event_mask.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_destroy(mln_map map) + * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask, const mln_completion *completion) * } */ - public static MemorySegment mln_map_destroy$address() { - return mln_map_destroy.ADDR; + public static MemorySegment mln_map_set_event_mask$address() { + return mln_map_set_event_mask.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_destroy(mln_map map) + * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask, const mln_completion *completion) * } */ - public static int mln_map_destroy(long map) { - var mh$ = mln_map_destroy.HANDLE; + public static int mln_map_set_event_mask(long map, long mask, MemorySegment completion) { + var mh$ = mln_map_set_event_mask.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_destroy", map); + traceDowncall("mln_map_set_event_mask", map, mask, completion); } - return (int)mh$.invokeExact(map); + return (int)mh$.invokeExact(map, mask, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5629,14 +5578,11 @@ public static int mln_map_destroy(long map) { } } - private static class mln_map_set_style_url { + private static class mln_camera_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_camera_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_url"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5644,45 +5590,45 @@ private static class mln_map_set_style_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_url(mln_map map, const char *url) + * mln_camera_options mln_camera_options_default() * } */ - public static FunctionDescriptor mln_map_set_style_url$descriptor() { - return mln_map_set_style_url.DESC; + public static FunctionDescriptor mln_camera_options_default$descriptor() { + return mln_camera_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_url(mln_map map, const char *url) + * mln_camera_options mln_camera_options_default() * } */ - public static MethodHandle mln_map_set_style_url$handle() { - return mln_map_set_style_url.HANDLE; + public static MethodHandle mln_camera_options_default$handle() { + return mln_camera_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_url(mln_map map, const char *url) + * mln_camera_options mln_camera_options_default() * } */ - public static MemorySegment mln_map_set_style_url$address() { - return mln_map_set_style_url.ADDR; + public static MemorySegment mln_camera_options_default$address() { + return mln_camera_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_style_url(mln_map map, const char *url) + * mln_camera_options mln_camera_options_default() * } */ - public static int mln_map_set_style_url(long map, MemorySegment url) { - var mh$ = mln_map_set_style_url.HANDLE; + public static MemorySegment mln_camera_options_default(SegmentAllocator allocator) { + var mh$ = mln_camera_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_url", map, url); + traceDowncall("mln_camera_options_default", allocator); } - return (int)mh$.invokeExact(map, url); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5690,14 +5636,11 @@ public static int mln_map_set_style_url(long map, MemorySegment url) { } } - private static class mln_map_set_style_json { + private static class mln_animation_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout() - ); + mln_animation_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_json"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_animation_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5705,45 +5648,45 @@ private static class mln_map_set_style_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json) + * mln_animation_options mln_animation_options_default() * } */ - public static FunctionDescriptor mln_map_set_style_json$descriptor() { - return mln_map_set_style_json.DESC; + public static FunctionDescriptor mln_animation_options_default$descriptor() { + return mln_animation_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json) + * mln_animation_options mln_animation_options_default() * } */ - public static MethodHandle mln_map_set_style_json$handle() { - return mln_map_set_style_json.HANDLE; + public static MethodHandle mln_animation_options_default$handle() { + return mln_animation_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json) + * mln_animation_options mln_animation_options_default() * } */ - public static MemorySegment mln_map_set_style_json$address() { - return mln_map_set_style_json.ADDR; + public static MemorySegment mln_animation_options_default$address() { + return mln_animation_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_style_json(mln_map map, mln_buffer_view json) + * mln_animation_options mln_animation_options_default() * } */ - public static int mln_map_set_style_json(long map, MemorySegment json) { - var mh$ = mln_map_set_style_json.HANDLE; + public static MemorySegment mln_animation_options_default(SegmentAllocator allocator) { + var mh$ = mln_animation_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_json", map, json); + traceDowncall("mln_animation_options_default", allocator); } - return (int)mh$.invokeExact(map, json); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5751,16 +5694,11 @@ public static int mln_map_set_style_json(long map, MemorySegment json) { } } - private static class mln_map_copy_loaded_style_json { + private static class mln_camera_delta_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_camera_delta.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_copy_loaded_style_json"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_delta_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5768,45 +5706,45 @@ private static class mln_map_copy_loaded_style_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_loaded_style_json(mln_map map, uint8_t *out_json, size_t json_capacity, size_t *out_json_size) + * mln_camera_delta mln_camera_delta_default() * } */ - public static FunctionDescriptor mln_map_copy_loaded_style_json$descriptor() { - return mln_map_copy_loaded_style_json.DESC; + public static FunctionDescriptor mln_camera_delta_default$descriptor() { + return mln_camera_delta_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_loaded_style_json(mln_map map, uint8_t *out_json, size_t json_capacity, size_t *out_json_size) + * mln_camera_delta mln_camera_delta_default() * } */ - public static MethodHandle mln_map_copy_loaded_style_json$handle() { - return mln_map_copy_loaded_style_json.HANDLE; + public static MethodHandle mln_camera_delta_default$handle() { + return mln_camera_delta_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_loaded_style_json(mln_map map, uint8_t *out_json, size_t json_capacity, size_t *out_json_size) + * mln_camera_delta mln_camera_delta_default() * } */ - public static MemorySegment mln_map_copy_loaded_style_json$address() { - return mln_map_copy_loaded_style_json.ADDR; + public static MemorySegment mln_camera_delta_default$address() { + return mln_camera_delta_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_copy_loaded_style_json(mln_map map, uint8_t *out_json, size_t json_capacity, size_t *out_json_size) + * mln_camera_delta mln_camera_delta_default() * } */ - public static int mln_map_copy_loaded_style_json(long map, MemorySegment out_json, long json_capacity, MemorySegment out_json_size) { - var mh$ = mln_map_copy_loaded_style_json.HANDLE; + public static MemorySegment mln_camera_delta_default(SegmentAllocator allocator) { + var mh$ = mln_camera_delta_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_loaded_style_json", map, out_json, json_capacity, out_json_size); + traceDowncall("mln_camera_delta_default", allocator); } - return (int)mh$.invokeExact(map, out_json, json_capacity, out_json_size); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5814,16 +5752,11 @@ public static int mln_map_copy_loaded_style_json(long map, MemorySegment out_jso } } - private static class mln_map_copy_style_url { + private static class mln_camera_update_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_camera_update.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_copy_style_url"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_update_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5831,45 +5764,45 @@ private static class mln_map_copy_style_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_style_url(mln_map map, char *out_url, size_t url_capacity, size_t *out_url_size) + * mln_camera_update mln_camera_update_default() * } */ - public static FunctionDescriptor mln_map_copy_style_url$descriptor() { - return mln_map_copy_style_url.DESC; + public static FunctionDescriptor mln_camera_update_default$descriptor() { + return mln_camera_update_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_style_url(mln_map map, char *out_url, size_t url_capacity, size_t *out_url_size) + * mln_camera_update mln_camera_update_default() * } */ - public static MethodHandle mln_map_copy_style_url$handle() { - return mln_map_copy_style_url.HANDLE; + public static MethodHandle mln_camera_update_default$handle() { + return mln_camera_update_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_style_url(mln_map map, char *out_url, size_t url_capacity, size_t *out_url_size) + * mln_camera_update mln_camera_update_default() * } */ - public static MemorySegment mln_map_copy_style_url$address() { - return mln_map_copy_style_url.ADDR; + public static MemorySegment mln_camera_update_default$address() { + return mln_camera_update_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_copy_style_url(mln_map map, char *out_url, size_t url_capacity, size_t *out_url_size) + * mln_camera_update mln_camera_update_default() * } */ - public static int mln_map_copy_style_url(long map, MemorySegment out_url, long url_capacity, MemorySegment out_url_size) { - var mh$ = mln_map_copy_style_url.HANDLE; + public static MemorySegment mln_camera_update_default(SegmentAllocator allocator) { + var mh$ = mln_camera_update_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_style_url", map, out_url, url_capacity, out_url_size); + traceDowncall("mln_camera_update_default", allocator); } - return (int)mh$.invokeExact(map, out_url, url_capacity, out_url_size); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5877,14 +5810,11 @@ public static int mln_map_copy_style_url(long map, MemorySegment out_url, long u } } - private static class mln_map_set_event_mask { + private static class mln_camera_fit_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG - ); + mln_camera_fit_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_event_mask"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_fit_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5892,45 +5822,45 @@ private static class mln_map_set_event_mask { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask) + * mln_camera_fit_options mln_camera_fit_options_default() * } */ - public static FunctionDescriptor mln_map_set_event_mask$descriptor() { - return mln_map_set_event_mask.DESC; + public static FunctionDescriptor mln_camera_fit_options_default$descriptor() { + return mln_camera_fit_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask) + * mln_camera_fit_options mln_camera_fit_options_default() * } */ - public static MethodHandle mln_map_set_event_mask$handle() { - return mln_map_set_event_mask.HANDLE; + public static MethodHandle mln_camera_fit_options_default$handle() { + return mln_camera_fit_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask) + * mln_camera_fit_options mln_camera_fit_options_default() * } */ - public static MemorySegment mln_map_set_event_mask$address() { - return mln_map_set_event_mask.ADDR; + public static MemorySegment mln_camera_fit_options_default$address() { + return mln_camera_fit_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_event_mask(mln_map map, uint64_t mask) + * mln_camera_fit_options mln_camera_fit_options_default() * } */ - public static int mln_map_set_event_mask(long map, long mask) { - var mh$ = mln_map_set_event_mask.HANDLE; + public static MemorySegment mln_camera_fit_options_default(SegmentAllocator allocator) { + var mh$ = mln_camera_fit_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_event_mask", map, mask); + traceDowncall("mln_camera_fit_options_default", allocator); } - return (int)mh$.invokeExact(map, mask); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5938,14 +5868,11 @@ public static int mln_map_set_event_mask(long map, long mask) { } } - private static class mln_map_get_event_mask { + private static class mln_bound_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_bound_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_event_mask"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_bound_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -5953,45 +5880,45 @@ private static class mln_map_get_event_mask { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_event_mask(mln_map map, uint64_t *out_mask) + * mln_bound_options mln_bound_options_default() * } */ - public static FunctionDescriptor mln_map_get_event_mask$descriptor() { - return mln_map_get_event_mask.DESC; + public static FunctionDescriptor mln_bound_options_default$descriptor() { + return mln_bound_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_event_mask(mln_map map, uint64_t *out_mask) + * mln_bound_options mln_bound_options_default() * } */ - public static MethodHandle mln_map_get_event_mask$handle() { - return mln_map_get_event_mask.HANDLE; + public static MethodHandle mln_bound_options_default$handle() { + return mln_bound_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_event_mask(mln_map map, uint64_t *out_mask) + * mln_bound_options mln_bound_options_default() * } */ - public static MemorySegment mln_map_get_event_mask$address() { - return mln_map_get_event_mask.ADDR; + public static MemorySegment mln_bound_options_default$address() { + return mln_bound_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_event_mask(mln_map map, uint64_t *out_mask) + * mln_bound_options mln_bound_options_default() * } */ - public static int mln_map_get_event_mask(long map, MemorySegment out_mask) { - var mh$ = mln_map_get_event_mask.HANDLE; + public static MemorySegment mln_bound_options_default(SegmentAllocator allocator) { + var mh$ = mln_bound_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_event_mask", map, out_mask); + traceDowncall("mln_bound_options_default", allocator); } - return (int)mh$.invokeExact(map, out_mask); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -5999,11 +5926,11 @@ public static int mln_map_get_event_mask(long map, MemorySegment out_mask) { } } - private static class mln_camera_options_default { + private static class mln_free_camera_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_camera_options.layout() ); + mln_free_camera_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_free_camera_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6011,43 +5938,43 @@ private static class mln_camera_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_camera_options mln_camera_options_default() + * mln_free_camera_options mln_free_camera_options_default() * } */ - public static FunctionDescriptor mln_camera_options_default$descriptor() { - return mln_camera_options_default.DESC; + public static FunctionDescriptor mln_free_camera_options_default$descriptor() { + return mln_free_camera_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_camera_options mln_camera_options_default() + * mln_free_camera_options mln_free_camera_options_default() * } */ - public static MethodHandle mln_camera_options_default$handle() { - return mln_camera_options_default.HANDLE; + public static MethodHandle mln_free_camera_options_default$handle() { + return mln_free_camera_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_camera_options mln_camera_options_default() + * mln_free_camera_options mln_free_camera_options_default() * } */ - public static MemorySegment mln_camera_options_default$address() { - return mln_camera_options_default.ADDR; + public static MemorySegment mln_free_camera_options_default$address() { + return mln_free_camera_options_default.ADDR; } /** * {@snippet lang=c : - * mln_camera_options mln_camera_options_default() + * mln_free_camera_options mln_free_camera_options_default() * } */ - public static MemorySegment mln_camera_options_default(SegmentAllocator allocator) { - var mh$ = mln_camera_options_default.HANDLE; + public static MemorySegment mln_free_camera_options_default(SegmentAllocator allocator) { + var mh$ = mln_free_camera_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_camera_options_default", allocator); + traceDowncall("mln_free_camera_options_default", allocator); } return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { @@ -6057,11 +5984,11 @@ public static MemorySegment mln_camera_options_default(SegmentAllocator allocato } } - private static class mln_animation_options_default { + private static class mln_projection_mode_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_animation_options.layout() ); + mln_projection_mode.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_animation_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_projection_mode_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6069,43 +5996,43 @@ private static class mln_animation_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_animation_options mln_animation_options_default() + * mln_projection_mode mln_projection_mode_default() * } */ - public static FunctionDescriptor mln_animation_options_default$descriptor() { - return mln_animation_options_default.DESC; + public static FunctionDescriptor mln_projection_mode_default$descriptor() { + return mln_projection_mode_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_animation_options mln_animation_options_default() + * mln_projection_mode mln_projection_mode_default() * } */ - public static MethodHandle mln_animation_options_default$handle() { - return mln_animation_options_default.HANDLE; + public static MethodHandle mln_projection_mode_default$handle() { + return mln_projection_mode_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_animation_options mln_animation_options_default() + * mln_projection_mode mln_projection_mode_default() * } */ - public static MemorySegment mln_animation_options_default$address() { - return mln_animation_options_default.ADDR; + public static MemorySegment mln_projection_mode_default$address() { + return mln_projection_mode_default.ADDR; } /** * {@snippet lang=c : - * mln_animation_options mln_animation_options_default() + * mln_projection_mode mln_projection_mode_default() * } */ - public static MemorySegment mln_animation_options_default(SegmentAllocator allocator) { - var mh$ = mln_animation_options_default.HANDLE; + public static MemorySegment mln_projection_mode_default(SegmentAllocator allocator) { + var mh$ = mln_projection_mode_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_animation_options_default", allocator); + traceDowncall("mln_projection_mode_default", allocator); } return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { @@ -6115,11 +6042,11 @@ public static MemorySegment mln_animation_options_default(SegmentAllocator alloc } } - private static class mln_camera_fit_options_default { + private static class mln_map_viewport_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_camera_fit_options.layout() ); + mln_map_viewport_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_camera_fit_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_viewport_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6127,43 +6054,43 @@ private static class mln_camera_fit_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_camera_fit_options mln_camera_fit_options_default() + * mln_map_viewport_options mln_map_viewport_options_default() * } */ - public static FunctionDescriptor mln_camera_fit_options_default$descriptor() { - return mln_camera_fit_options_default.DESC; + public static FunctionDescriptor mln_map_viewport_options_default$descriptor() { + return mln_map_viewport_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_camera_fit_options mln_camera_fit_options_default() + * mln_map_viewport_options mln_map_viewport_options_default() * } */ - public static MethodHandle mln_camera_fit_options_default$handle() { - return mln_camera_fit_options_default.HANDLE; + public static MethodHandle mln_map_viewport_options_default$handle() { + return mln_map_viewport_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_camera_fit_options mln_camera_fit_options_default() + * mln_map_viewport_options mln_map_viewport_options_default() * } */ - public static MemorySegment mln_camera_fit_options_default$address() { - return mln_camera_fit_options_default.ADDR; + public static MemorySegment mln_map_viewport_options_default$address() { + return mln_map_viewport_options_default.ADDR; } /** * {@snippet lang=c : - * mln_camera_fit_options mln_camera_fit_options_default() + * mln_map_viewport_options mln_map_viewport_options_default() * } */ - public static MemorySegment mln_camera_fit_options_default(SegmentAllocator allocator) { - var mh$ = mln_camera_fit_options_default.HANDLE; + public static MemorySegment mln_map_viewport_options_default(SegmentAllocator allocator) { + var mh$ = mln_map_viewport_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_camera_fit_options_default", allocator); + traceDowncall("mln_map_viewport_options_default", allocator); } return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { @@ -6173,11 +6100,11 @@ public static MemorySegment mln_camera_fit_options_default(SegmentAllocator allo } } - private static class mln_bound_options_default { + private static class mln_map_tile_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_bound_options.layout() ); + mln_map_tile_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_bound_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_tile_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6185,43 +6112,43 @@ private static class mln_bound_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_bound_options mln_bound_options_default() + * mln_map_tile_options mln_map_tile_options_default() * } */ - public static FunctionDescriptor mln_bound_options_default$descriptor() { - return mln_bound_options_default.DESC; + public static FunctionDescriptor mln_map_tile_options_default$descriptor() { + return mln_map_tile_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_bound_options mln_bound_options_default() + * mln_map_tile_options mln_map_tile_options_default() * } */ - public static MethodHandle mln_bound_options_default$handle() { - return mln_bound_options_default.HANDLE; + public static MethodHandle mln_map_tile_options_default$handle() { + return mln_map_tile_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_bound_options mln_bound_options_default() + * mln_map_tile_options mln_map_tile_options_default() * } */ - public static MemorySegment mln_bound_options_default$address() { - return mln_bound_options_default.ADDR; + public static MemorySegment mln_map_tile_options_default$address() { + return mln_map_tile_options_default.ADDR; } /** * {@snippet lang=c : - * mln_bound_options mln_bound_options_default() + * mln_map_tile_options mln_map_tile_options_default() * } */ - public static MemorySegment mln_bound_options_default(SegmentAllocator allocator) { - var mh$ = mln_bound_options_default.HANDLE; + public static MemorySegment mln_map_tile_options_default(SegmentAllocator allocator) { + var mh$ = mln_map_tile_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_bound_options_default", allocator); + traceDowncall("mln_map_tile_options_default", allocator); } return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { @@ -6231,11 +6158,15 @@ public static MemorySegment mln_bound_options_default(SegmentAllocator allocator } } - private static class mln_free_camera_options_default { + private static class mln_map_set_debug_options { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_free_camera_options.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_INT, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_free_camera_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_debug_options"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6243,45 +6174,45 @@ private static class mln_free_camera_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_free_camera_options mln_free_camera_options_default() + * mln_status mln_map_set_debug_options(mln_map map, uint32_t options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_free_camera_options_default$descriptor() { - return mln_free_camera_options_default.DESC; + public static FunctionDescriptor mln_map_set_debug_options$descriptor() { + return mln_map_set_debug_options.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_free_camera_options mln_free_camera_options_default() + * mln_status mln_map_set_debug_options(mln_map map, uint32_t options, const mln_completion *completion) * } */ - public static MethodHandle mln_free_camera_options_default$handle() { - return mln_free_camera_options_default.HANDLE; + public static MethodHandle mln_map_set_debug_options$handle() { + return mln_map_set_debug_options.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_free_camera_options mln_free_camera_options_default() + * mln_status mln_map_set_debug_options(mln_map map, uint32_t options, const mln_completion *completion) * } */ - public static MemorySegment mln_free_camera_options_default$address() { - return mln_free_camera_options_default.ADDR; + public static MemorySegment mln_map_set_debug_options$address() { + return mln_map_set_debug_options.ADDR; } /** * {@snippet lang=c : - * mln_free_camera_options mln_free_camera_options_default() + * mln_status mln_map_set_debug_options(mln_map map, uint32_t options, const mln_completion *completion) * } */ - public static MemorySegment mln_free_camera_options_default(SegmentAllocator allocator) { - var mh$ = mln_free_camera_options_default.HANDLE; + public static int mln_map_set_debug_options(long map, int options, MemorySegment completion) { + var mh$ = mln_map_set_debug_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_free_camera_options_default", allocator); + traceDowncall("mln_map_set_debug_options", map, options, completion); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6289,11 +6220,15 @@ public static MemorySegment mln_free_camera_options_default(SegmentAllocator all } } - private static class mln_projection_mode_default { + private static class mln_map_set_rendering_stats_view_enabled { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_projection_mode.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_BOOL, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_projection_mode_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_rendering_stats_view_enabled"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6301,45 +6236,45 @@ private static class mln_projection_mode_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_projection_mode mln_projection_mode_default() + * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_projection_mode_default$descriptor() { - return mln_projection_mode_default.DESC; + public static FunctionDescriptor mln_map_set_rendering_stats_view_enabled$descriptor() { + return mln_map_set_rendering_stats_view_enabled.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_projection_mode mln_projection_mode_default() + * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled, const mln_completion *completion) * } */ - public static MethodHandle mln_projection_mode_default$handle() { - return mln_projection_mode_default.HANDLE; + public static MethodHandle mln_map_set_rendering_stats_view_enabled$handle() { + return mln_map_set_rendering_stats_view_enabled.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_projection_mode mln_projection_mode_default() + * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled, const mln_completion *completion) * } */ - public static MemorySegment mln_projection_mode_default$address() { - return mln_projection_mode_default.ADDR; + public static MemorySegment mln_map_set_rendering_stats_view_enabled$address() { + return mln_map_set_rendering_stats_view_enabled.ADDR; } /** * {@snippet lang=c : - * mln_projection_mode mln_projection_mode_default() + * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled, const mln_completion *completion) * } */ - public static MemorySegment mln_projection_mode_default(SegmentAllocator allocator) { - var mh$ = mln_projection_mode_default.HANDLE; + public static int mln_map_set_rendering_stats_view_enabled(long map, boolean enabled, MemorySegment completion) { + var mh$ = mln_map_set_rendering_stats_view_enabled.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_projection_mode_default", allocator); + traceDowncall("mln_map_set_rendering_stats_view_enabled", map, enabled, completion); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(map, enabled, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6347,11 +6282,14 @@ public static MemorySegment mln_projection_mode_default(SegmentAllocator allocat } } - private static class mln_map_viewport_options_default { + private static class mln_map_dump_debug_logs { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_map_viewport_options.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_viewport_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_dump_debug_logs"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6359,45 +6297,45 @@ private static class mln_map_viewport_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_map_viewport_options mln_map_viewport_options_default() + * mln_status mln_map_dump_debug_logs(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_viewport_options_default$descriptor() { - return mln_map_viewport_options_default.DESC; + public static FunctionDescriptor mln_map_dump_debug_logs$descriptor() { + return mln_map_dump_debug_logs.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_map_viewport_options mln_map_viewport_options_default() + * mln_status mln_map_dump_debug_logs(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_viewport_options_default$handle() { - return mln_map_viewport_options_default.HANDLE; + public static MethodHandle mln_map_dump_debug_logs$handle() { + return mln_map_dump_debug_logs.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_map_viewport_options mln_map_viewport_options_default() + * mln_status mln_map_dump_debug_logs(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_viewport_options_default$address() { - return mln_map_viewport_options_default.ADDR; + public static MemorySegment mln_map_dump_debug_logs$address() { + return mln_map_dump_debug_logs.ADDR; } /** * {@snippet lang=c : - * mln_map_viewport_options mln_map_viewport_options_default() + * mln_status mln_map_dump_debug_logs(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_viewport_options_default(SegmentAllocator allocator) { - var mh$ = mln_map_viewport_options_default.HANDLE; + public static int mln_map_dump_debug_logs(long map, MemorySegment completion) { + var mh$ = mln_map_dump_debug_logs.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_viewport_options_default", allocator); + traceDowncall("mln_map_dump_debug_logs", map, completion); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6405,11 +6343,15 @@ public static MemorySegment mln_map_viewport_options_default(SegmentAllocator al } } - private static class mln_map_tile_options_default { + private static class mln_map_set_viewport_options { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_map_tile_options.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_tile_options_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_viewport_options"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6417,45 +6359,45 @@ private static class mln_map_tile_options_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_map_tile_options mln_map_tile_options_default() + * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_tile_options_default$descriptor() { - return mln_map_tile_options_default.DESC; + public static FunctionDescriptor mln_map_set_viewport_options$descriptor() { + return mln_map_set_viewport_options.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_map_tile_options mln_map_tile_options_default() + * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_tile_options_default$handle() { - return mln_map_tile_options_default.HANDLE; + public static MethodHandle mln_map_set_viewport_options$handle() { + return mln_map_set_viewport_options.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_map_tile_options mln_map_tile_options_default() + * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_tile_options_default$address() { - return mln_map_tile_options_default.ADDR; + public static MemorySegment mln_map_set_viewport_options$address() { + return mln_map_set_viewport_options.ADDR; } /** * {@snippet lang=c : - * mln_map_tile_options mln_map_tile_options_default() + * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_tile_options_default(SegmentAllocator allocator) { - var mh$ = mln_map_tile_options_default.HANDLE; + public static int mln_map_set_viewport_options(long map, MemorySegment options, MemorySegment completion) { + var mh$ = mln_map_set_viewport_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_tile_options_default", allocator); + traceDowncall("mln_map_set_viewport_options", map, options, completion); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6463,14 +6405,15 @@ public static MemorySegment mln_map_tile_options_default(SegmentAllocator alloca } } - private static class mln_map_set_debug_options { + private static class mln_map_set_tile_options { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_INT + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_debug_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_tile_options"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6478,45 +6421,45 @@ private static class mln_map_set_debug_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_debug_options(mln_map map, uint32_t options) + * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_set_debug_options$descriptor() { - return mln_map_set_debug_options.DESC; + public static FunctionDescriptor mln_map_set_tile_options$descriptor() { + return mln_map_set_tile_options.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_debug_options(mln_map map, uint32_t options) + * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_set_debug_options$handle() { - return mln_map_set_debug_options.HANDLE; + public static MethodHandle mln_map_set_tile_options$handle() { + return mln_map_set_tile_options.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_debug_options(mln_map map, uint32_t options) + * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_debug_options$address() { - return mln_map_set_debug_options.ADDR; + public static MemorySegment mln_map_set_tile_options$address() { + return mln_map_set_tile_options.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_debug_options(mln_map map, uint32_t options) + * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options, const mln_completion *completion) * } */ - public static int mln_map_set_debug_options(long map, int options) { - var mh$ = mln_map_set_debug_options.HANDLE; + public static int mln_map_set_tile_options(long map, MemorySegment options, MemorySegment completion) { + var mh$ = mln_map_set_tile_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_debug_options", map, options); + traceDowncall("mln_map_set_tile_options", map, options, completion); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6524,14 +6467,15 @@ public static int mln_map_set_debug_options(long map, int options) { } } - private static class mln_map_get_debug_options { + private static class mln_map_camera_snapshot_get { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_debug_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_snapshot_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6539,45 +6483,45 @@ private static class mln_map_get_debug_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_debug_options(mln_map map, uint32_t *out_options) + * mln_status mln_map_camera_snapshot_get(mln_map map, mln_camera_options *out_camera, uint64_t *out_generation) * } */ - public static FunctionDescriptor mln_map_get_debug_options$descriptor() { - return mln_map_get_debug_options.DESC; + public static FunctionDescriptor mln_map_camera_snapshot_get$descriptor() { + return mln_map_camera_snapshot_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_debug_options(mln_map map, uint32_t *out_options) + * mln_status mln_map_camera_snapshot_get(mln_map map, mln_camera_options *out_camera, uint64_t *out_generation) * } */ - public static MethodHandle mln_map_get_debug_options$handle() { - return mln_map_get_debug_options.HANDLE; + public static MethodHandle mln_map_camera_snapshot_get$handle() { + return mln_map_camera_snapshot_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_debug_options(mln_map map, uint32_t *out_options) + * mln_status mln_map_camera_snapshot_get(mln_map map, mln_camera_options *out_camera, uint64_t *out_generation) * } */ - public static MemorySegment mln_map_get_debug_options$address() { - return mln_map_get_debug_options.ADDR; + public static MemorySegment mln_map_camera_snapshot_get$address() { + return mln_map_camera_snapshot_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_debug_options(mln_map map, uint32_t *out_options) + * mln_status mln_map_camera_snapshot_get(mln_map map, mln_camera_options *out_camera, uint64_t *out_generation) * } */ - public static int mln_map_get_debug_options(long map, MemorySegment out_options) { - var mh$ = mln_map_get_debug_options.HANDLE; + public static int mln_map_camera_snapshot_get(long map, MemorySegment out_camera, MemorySegment out_generation) { + var mh$ = mln_map_camera_snapshot_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_debug_options", map, out_options); + traceDowncall("mln_map_camera_snapshot_get", map, out_camera, out_generation); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(map, out_camera, out_generation); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6585,14 +6529,15 @@ public static int mln_map_get_debug_options(long map, MemorySegment out_options) } } - private static class mln_map_set_rendering_stats_view_enabled { + private static class mln_map_update_camera { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_BOOL + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_rendering_stats_view_enabled"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_update_camera"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6600,45 +6545,45 @@ private static class mln_map_set_rendering_stats_view_enabled { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled) + * mln_status mln_map_update_camera(mln_map map, const mln_camera_update *update, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_set_rendering_stats_view_enabled$descriptor() { - return mln_map_set_rendering_stats_view_enabled.DESC; + public static FunctionDescriptor mln_map_update_camera$descriptor() { + return mln_map_update_camera.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled) + * mln_status mln_map_update_camera(mln_map map, const mln_camera_update *update, const mln_completion *completion) * } */ - public static MethodHandle mln_map_set_rendering_stats_view_enabled$handle() { - return mln_map_set_rendering_stats_view_enabled.HANDLE; + public static MethodHandle mln_map_update_camera$handle() { + return mln_map_update_camera.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled) + * mln_status mln_map_update_camera(mln_map map, const mln_camera_update *update, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_rendering_stats_view_enabled$address() { - return mln_map_set_rendering_stats_view_enabled.ADDR; + public static MemorySegment mln_map_update_camera$address() { + return mln_map_update_camera.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_rendering_stats_view_enabled(mln_map map, bool enabled) + * mln_status mln_map_update_camera(mln_map map, const mln_camera_update *update, const mln_completion *completion) * } */ - public static int mln_map_set_rendering_stats_view_enabled(long map, boolean enabled) { - var mh$ = mln_map_set_rendering_stats_view_enabled.HANDLE; + public static int mln_map_update_camera(long map, MemorySegment update, MemorySegment completion) { + var mh$ = mln_map_update_camera.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_rendering_stats_view_enabled", map, enabled); + traceDowncall("mln_map_update_camera", map, update, completion); } - return (int)mh$.invokeExact(map, enabled); + return (int)mh$.invokeExact(map, update, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6646,14 +6591,15 @@ public static int mln_map_set_rendering_stats_view_enabled(long map, boolean ena } } - private static class mln_map_get_rendering_stats_view_enabled { + private static class mln_map_apply_camera_delta { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_rendering_stats_view_enabled"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_apply_camera_delta"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6661,45 +6607,45 @@ private static class mln_map_get_rendering_stats_view_enabled { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_rendering_stats_view_enabled(mln_map map, bool *out_enabled) + * mln_status mln_map_apply_camera_delta(mln_map map, const mln_camera_delta *delta, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_rendering_stats_view_enabled$descriptor() { - return mln_map_get_rendering_stats_view_enabled.DESC; + public static FunctionDescriptor mln_map_apply_camera_delta$descriptor() { + return mln_map_apply_camera_delta.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_rendering_stats_view_enabled(mln_map map, bool *out_enabled) + * mln_status mln_map_apply_camera_delta(mln_map map, const mln_camera_delta *delta, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_rendering_stats_view_enabled$handle() { - return mln_map_get_rendering_stats_view_enabled.HANDLE; + public static MethodHandle mln_map_apply_camera_delta$handle() { + return mln_map_apply_camera_delta.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_rendering_stats_view_enabled(mln_map map, bool *out_enabled) + * mln_status mln_map_apply_camera_delta(mln_map map, const mln_camera_delta *delta, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_rendering_stats_view_enabled$address() { - return mln_map_get_rendering_stats_view_enabled.ADDR; + public static MemorySegment mln_map_apply_camera_delta$address() { + return mln_map_apply_camera_delta.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_rendering_stats_view_enabled(mln_map map, bool *out_enabled) + * mln_status mln_map_apply_camera_delta(mln_map map, const mln_camera_delta *delta, const mln_completion *completion) * } */ - public static int mln_map_get_rendering_stats_view_enabled(long map, MemorySegment out_enabled) { - var mh$ = mln_map_get_rendering_stats_view_enabled.HANDLE; + public static int mln_map_apply_camera_delta(long map, MemorySegment delta, MemorySegment completion) { + var mh$ = mln_map_apply_camera_delta.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_rendering_stats_view_enabled", map, out_enabled); + traceDowncall("mln_map_apply_camera_delta", map, delta, completion); } - return (int)mh$.invokeExact(map, out_enabled); + return (int)mh$.invokeExact(map, delta, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6707,14 +6653,14 @@ public static int mln_map_get_rendering_stats_view_enabled(long map, MemorySegme } } - private static class mln_map_is_fully_loaded { + private static class mln_map_camera_query { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_is_fully_loaded"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_query"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6722,45 +6668,45 @@ private static class mln_map_is_fully_loaded { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_is_fully_loaded(mln_map map, bool *out_loaded) + * mln_status mln_map_camera_query(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_is_fully_loaded$descriptor() { - return mln_map_is_fully_loaded.DESC; + public static FunctionDescriptor mln_map_camera_query$descriptor() { + return mln_map_camera_query.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_is_fully_loaded(mln_map map, bool *out_loaded) + * mln_status mln_map_camera_query(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_is_fully_loaded$handle() { - return mln_map_is_fully_loaded.HANDLE; + public static MethodHandle mln_map_camera_query$handle() { + return mln_map_camera_query.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_is_fully_loaded(mln_map map, bool *out_loaded) + * mln_status mln_map_camera_query(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_is_fully_loaded$address() { - return mln_map_is_fully_loaded.ADDR; + public static MemorySegment mln_map_camera_query$address() { + return mln_map_camera_query.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_is_fully_loaded(mln_map map, bool *out_loaded) + * mln_status mln_map_camera_query(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_is_fully_loaded(long map, MemorySegment out_loaded) { - var mh$ = mln_map_is_fully_loaded.HANDLE; + public static int mln_map_camera_query(long map, MemorySegment completion) { + var mh$ = mln_map_camera_query.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_is_fully_loaded", map, out_loaded); + traceDowncall("mln_map_camera_query", map, completion); } - return (int)mh$.invokeExact(map, out_loaded); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6768,13 +6714,16 @@ public static int mln_map_is_fully_loaded(long map, MemorySegment out_loaded) { } } - private static class mln_map_dump_debug_logs { + private static class mln_map_camera_for_lat_lng_bounds { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + mln_lat_lng_bounds.layout(), + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_dump_debug_logs"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_lat_lng_bounds"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6782,45 +6731,45 @@ private static class mln_map_dump_debug_logs { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_dump_debug_logs(mln_map map) + * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_dump_debug_logs$descriptor() { - return mln_map_dump_debug_logs.DESC; + public static FunctionDescriptor mln_map_camera_for_lat_lng_bounds$descriptor() { + return mln_map_camera_for_lat_lng_bounds.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_dump_debug_logs(mln_map map) + * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_dump_debug_logs$handle() { - return mln_map_dump_debug_logs.HANDLE; + public static MethodHandle mln_map_camera_for_lat_lng_bounds$handle() { + return mln_map_camera_for_lat_lng_bounds.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_dump_debug_logs(mln_map map) + * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_dump_debug_logs$address() { - return mln_map_dump_debug_logs.ADDR; + public static MemorySegment mln_map_camera_for_lat_lng_bounds$address() { + return mln_map_camera_for_lat_lng_bounds.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_dump_debug_logs(mln_map map) + * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static int mln_map_dump_debug_logs(long map) { - var mh$ = mln_map_dump_debug_logs.HANDLE; + public static int mln_map_camera_for_lat_lng_bounds(long map, MemorySegment bounds, MemorySegment fit_options, MemorySegment completion) { + var mh$ = mln_map_camera_for_lat_lng_bounds.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_dump_debug_logs", map); + traceDowncall("mln_map_camera_for_lat_lng_bounds", map, bounds, fit_options, completion); } - return (int)mh$.invokeExact(map); + return (int)mh$.invokeExact(map, bounds, fit_options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6828,14 +6777,17 @@ public static int mln_map_dump_debug_logs(long map) { } } - private static class mln_map_get_viewport_options { + private static class mln_map_camera_for_lat_lngs { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_viewport_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_lat_lngs"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6843,45 +6795,45 @@ private static class mln_map_get_viewport_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_viewport_options(mln_map map, mln_map_viewport_options *out_options) + * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_viewport_options$descriptor() { - return mln_map_get_viewport_options.DESC; + public static FunctionDescriptor mln_map_camera_for_lat_lngs$descriptor() { + return mln_map_camera_for_lat_lngs.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_viewport_options(mln_map map, mln_map_viewport_options *out_options) + * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_viewport_options$handle() { - return mln_map_get_viewport_options.HANDLE; - } + public static MethodHandle mln_map_camera_for_lat_lngs$handle() { + return mln_map_camera_for_lat_lngs.HANDLE; + } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_viewport_options(mln_map map, mln_map_viewport_options *out_options) + * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_viewport_options$address() { - return mln_map_get_viewport_options.ADDR; + public static MemorySegment mln_map_camera_for_lat_lngs$address() { + return mln_map_camera_for_lat_lngs.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_viewport_options(mln_map map, mln_map_viewport_options *out_options) + * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static int mln_map_get_viewport_options(long map, MemorySegment out_options) { - var mh$ = mln_map_get_viewport_options.HANDLE; + public static int mln_map_camera_for_lat_lngs(long map, MemorySegment coordinates, long coordinate_count, MemorySegment fit_options, MemorySegment completion) { + var mh$ = mln_map_camera_for_lat_lngs.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_viewport_options", map, out_options); + traceDowncall("mln_map_camera_for_lat_lngs", map, coordinates, coordinate_count, fit_options, completion); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(map, coordinates, coordinate_count, fit_options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6889,14 +6841,16 @@ public static int mln_map_get_viewport_options(long map, MemorySegment out_optio } } - private static class mln_map_set_viewport_options { + private static class mln_map_camera_for_geometry { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_viewport_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_geometry"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6904,45 +6858,45 @@ private static class mln_map_set_viewport_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options) + * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_set_viewport_options$descriptor() { - return mln_map_set_viewport_options.DESC; + public static FunctionDescriptor mln_map_camera_for_geometry$descriptor() { + return mln_map_camera_for_geometry.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options) + * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_set_viewport_options$handle() { - return mln_map_set_viewport_options.HANDLE; + public static MethodHandle mln_map_camera_for_geometry$handle() { + return mln_map_camera_for_geometry.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options) + * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_viewport_options$address() { - return mln_map_set_viewport_options.ADDR; + public static MemorySegment mln_map_camera_for_geometry$address() { + return mln_map_camera_for_geometry.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_viewport_options(mln_map map, const mln_map_viewport_options *options) + * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, const mln_completion *completion) * } */ - public static int mln_map_set_viewport_options(long map, MemorySegment options) { - var mh$ = mln_map_set_viewport_options.HANDLE; + public static int mln_map_camera_for_geometry(long map, MemorySegment geometry, MemorySegment fit_options, MemorySegment completion) { + var mh$ = mln_map_camera_for_geometry.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_viewport_options", map, options); + traceDowncall("mln_map_camera_for_geometry", map, geometry, fit_options, completion); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(map, geometry, fit_options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -6950,14 +6904,15 @@ public static int mln_map_set_viewport_options(long map, MemorySegment options) } } - private static class mln_map_get_tile_options { + private static class mln_map_lat_lng_bounds_for_camera { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_tile_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_bounds_for_camera"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -6965,45 +6920,45 @@ private static class mln_map_get_tile_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_tile_options(mln_map map, mln_map_tile_options *out_options) + * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_tile_options$descriptor() { - return mln_map_get_tile_options.DESC; + public static FunctionDescriptor mln_map_lat_lng_bounds_for_camera$descriptor() { + return mln_map_lat_lng_bounds_for_camera.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_tile_options(mln_map map, mln_map_tile_options *out_options) + * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_tile_options$handle() { - return mln_map_get_tile_options.HANDLE; + public static MethodHandle mln_map_lat_lng_bounds_for_camera$handle() { + return mln_map_lat_lng_bounds_for_camera.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_tile_options(mln_map map, mln_map_tile_options *out_options) + * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_tile_options$address() { - return mln_map_get_tile_options.ADDR; + public static MemorySegment mln_map_lat_lng_bounds_for_camera$address() { + return mln_map_lat_lng_bounds_for_camera.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_tile_options(mln_map map, mln_map_tile_options *out_options) + * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static int mln_map_get_tile_options(long map, MemorySegment out_options) { - var mh$ = mln_map_get_tile_options.HANDLE; + public static int mln_map_lat_lng_bounds_for_camera(long map, MemorySegment camera, MemorySegment completion) { + var mh$ = mln_map_lat_lng_bounds_for_camera.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_tile_options", map, out_options); + traceDowncall("mln_map_lat_lng_bounds_for_camera", map, camera, completion); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(map, camera, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7011,14 +6966,15 @@ public static int mln_map_get_tile_options(long map, MemorySegment out_options) } } - private static class mln_map_set_tile_options { + private static class mln_map_lat_lng_bounds_for_camera_unwrapped { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_tile_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_bounds_for_camera_unwrapped"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7026,45 +6982,45 @@ private static class mln_map_set_tile_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options) + * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_set_tile_options$descriptor() { - return mln_map_set_tile_options.DESC; + public static FunctionDescriptor mln_map_lat_lng_bounds_for_camera_unwrapped$descriptor() { + return mln_map_lat_lng_bounds_for_camera_unwrapped.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options) + * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static MethodHandle mln_map_set_tile_options$handle() { - return mln_map_set_tile_options.HANDLE; + public static MethodHandle mln_map_lat_lng_bounds_for_camera_unwrapped$handle() { + return mln_map_lat_lng_bounds_for_camera_unwrapped.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options) + * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_tile_options$address() { - return mln_map_set_tile_options.ADDR; + public static MemorySegment mln_map_lat_lng_bounds_for_camera_unwrapped$address() { + return mln_map_lat_lng_bounds_for_camera_unwrapped.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_tile_options(mln_map map, const mln_map_tile_options *options) + * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, const mln_completion *completion) * } */ - public static int mln_map_set_tile_options(long map, MemorySegment options) { - var mh$ = mln_map_set_tile_options.HANDLE; + public static int mln_map_lat_lng_bounds_for_camera_unwrapped(long map, MemorySegment camera, MemorySegment completion) { + var mh$ = mln_map_lat_lng_bounds_for_camera_unwrapped.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_tile_options", map, options); + traceDowncall("mln_map_lat_lng_bounds_for_camera_unwrapped", map, camera, completion); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(map, camera, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7072,14 +7028,15 @@ public static int mln_map_set_tile_options(long map, MemorySegment options) { } } - private static class mln_map_get_camera { + private static class mln_map_set_bounds { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_camera"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_bounds"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7087,45 +7044,45 @@ private static class mln_map_get_camera { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_camera(mln_map map, mln_camera_options *out_camera) + * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_camera$descriptor() { - return mln_map_get_camera.DESC; + public static FunctionDescriptor mln_map_set_bounds$descriptor() { + return mln_map_set_bounds.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_camera(mln_map map, mln_camera_options *out_camera) + * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_camera$handle() { - return mln_map_get_camera.HANDLE; + public static MethodHandle mln_map_set_bounds$handle() { + return mln_map_set_bounds.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_camera(mln_map map, mln_camera_options *out_camera) + * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_camera$address() { - return mln_map_get_camera.ADDR; + public static MemorySegment mln_map_set_bounds$address() { + return mln_map_set_bounds.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_camera(mln_map map, mln_camera_options *out_camera) + * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options, const mln_completion *completion) * } */ - public static int mln_map_get_camera(long map, MemorySegment out_camera) { - var mh$ = mln_map_get_camera.HANDLE; + public static int mln_map_set_bounds(long map, MemorySegment options, MemorySegment completion) { + var mh$ = mln_map_set_bounds.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_camera", map, out_camera); + traceDowncall("mln_map_set_bounds", map, options, completion); } - return (int)mh$.invokeExact(map, out_camera); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7133,14 +7090,15 @@ public static int mln_map_get_camera(long map, MemorySegment out_camera) { } } - private static class mln_map_jump_to { + private static class mln_map_set_free_camera_options { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_jump_to"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_free_camera_options"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7148,45 +7106,45 @@ private static class mln_map_jump_to { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_jump_to(mln_map map, const mln_camera_options *camera) + * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_jump_to$descriptor() { - return mln_map_jump_to.DESC; + public static FunctionDescriptor mln_map_set_free_camera_options$descriptor() { + return mln_map_set_free_camera_options.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_jump_to(mln_map map, const mln_camera_options *camera) + * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_jump_to$handle() { - return mln_map_jump_to.HANDLE; + public static MethodHandle mln_map_set_free_camera_options$handle() { + return mln_map_set_free_camera_options.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_jump_to(mln_map map, const mln_camera_options *camera) + * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_jump_to$address() { - return mln_map_jump_to.ADDR; + public static MemorySegment mln_map_set_free_camera_options$address() { + return mln_map_set_free_camera_options.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_jump_to(mln_map map, const mln_camera_options *camera) + * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options, const mln_completion *completion) * } */ - public static int mln_map_jump_to(long map, MemorySegment camera) { - var mh$ = mln_map_jump_to.HANDLE; + public static int mln_map_set_free_camera_options(long map, MemorySegment options, MemorySegment completion) { + var mh$ = mln_map_set_free_camera_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_jump_to", map, camera); + traceDowncall("mln_map_set_free_camera_options", map, options, completion); } - return (int)mh$.invokeExact(map, camera); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7194,7 +7152,7 @@ public static int mln_map_jump_to(long map, MemorySegment camera) { } } - private static class mln_map_ease_to { + private static class mln_map_set_projection_mode { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, @@ -7202,7 +7160,7 @@ private static class mln_map_ease_to { MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_ease_to"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_projection_mode"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7210,45 +7168,45 @@ private static class mln_map_ease_to { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_ease_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_ease_to$descriptor() { - return mln_map_ease_to.DESC; + public static FunctionDescriptor mln_map_set_projection_mode$descriptor() { + return mln_map_set_projection_mode.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_ease_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode, const mln_completion *completion) * } */ - public static MethodHandle mln_map_ease_to$handle() { - return mln_map_ease_to.HANDLE; + public static MethodHandle mln_map_set_projection_mode$handle() { + return mln_map_set_projection_mode.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_ease_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode, const mln_completion *completion) * } */ - public static MemorySegment mln_map_ease_to$address() { - return mln_map_ease_to.ADDR; + public static MemorySegment mln_map_set_projection_mode$address() { + return mln_map_set_projection_mode.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_ease_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode, const mln_completion *completion) * } */ - public static int mln_map_ease_to(long map, MemorySegment camera, MemorySegment animation) { - var mh$ = mln_map_ease_to.HANDLE; + public static int mln_map_set_projection_mode(long map, MemorySegment mode, MemorySegment completion) { + var mh$ = mln_map_set_projection_mode.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_ease_to", map, camera, animation); + traceDowncall("mln_map_set_projection_mode", map, mode, completion); } - return (int)mh$.invokeExact(map, camera, animation); + return (int)mh$.invokeExact(map, mode, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7256,15 +7214,15 @@ public static int mln_map_ease_to(long map, MemorySegment camera, MemorySegment } } - private static class mln_map_fly_to { + private static class mln_map_pixel_for_lat_lng { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, + mln_lat_lng.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_fly_to"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pixel_for_lat_lng"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7272,45 +7230,45 @@ private static class mln_map_fly_to { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_fly_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_fly_to$descriptor() { - return mln_map_fly_to.DESC; + public static FunctionDescriptor mln_map_pixel_for_lat_lng$descriptor() { + return mln_map_pixel_for_lat_lng.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_fly_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, const mln_completion *completion) * } */ - public static MethodHandle mln_map_fly_to$handle() { - return mln_map_fly_to.HANDLE; + public static MethodHandle mln_map_pixel_for_lat_lng$handle() { + return mln_map_pixel_for_lat_lng.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_fly_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, const mln_completion *completion) * } */ - public static MemorySegment mln_map_fly_to$address() { - return mln_map_fly_to.ADDR; + public static MemorySegment mln_map_pixel_for_lat_lng$address() { + return mln_map_pixel_for_lat_lng.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_fly_to(mln_map map, const mln_camera_options *camera, const mln_animation_options *animation) + * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, const mln_completion *completion) * } */ - public static int mln_map_fly_to(long map, MemorySegment camera, MemorySegment animation) { - var mh$ = mln_map_fly_to.HANDLE; + public static int mln_map_pixel_for_lat_lng(long map, MemorySegment coordinate, MemorySegment completion) { + var mh$ = mln_map_pixel_for_lat_lng.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_fly_to", map, camera, animation); + traceDowncall("mln_map_pixel_for_lat_lng", map, coordinate, completion); } - return (int)mh$.invokeExact(map, camera, animation); + return (int)mh$.invokeExact(map, coordinate, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7318,15 +7276,15 @@ public static int mln_map_fly_to(long map, MemorySegment camera, MemorySegment a } } - private static class mln_map_move_by { + private static class mln_map_lat_lng_for_pixel { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE, - MapLibreNativeC.C_DOUBLE + mln_screen_point.layout(), + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_move_by"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_for_pixel"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7334,45 +7292,45 @@ private static class mln_map_move_by { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_move_by(mln_map map, double delta_x, double delta_y) + * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_move_by$descriptor() { - return mln_map_move_by.DESC; + public static FunctionDescriptor mln_map_lat_lng_for_pixel$descriptor() { + return mln_map_lat_lng_for_pixel.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_move_by(mln_map map, double delta_x, double delta_y) + * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, const mln_completion *completion) * } */ - public static MethodHandle mln_map_move_by$handle() { - return mln_map_move_by.HANDLE; + public static MethodHandle mln_map_lat_lng_for_pixel$handle() { + return mln_map_lat_lng_for_pixel.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_move_by(mln_map map, double delta_x, double delta_y) + * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, const mln_completion *completion) * } */ - public static MemorySegment mln_map_move_by$address() { - return mln_map_move_by.ADDR; + public static MemorySegment mln_map_lat_lng_for_pixel$address() { + return mln_map_lat_lng_for_pixel.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_move_by(mln_map map, double delta_x, double delta_y) + * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, const mln_completion *completion) * } */ - public static int mln_map_move_by(long map, double delta_x, double delta_y) { - var mh$ = mln_map_move_by.HANDLE; + public static int mln_map_lat_lng_for_pixel(long map, MemorySegment point, MemorySegment completion) { + var mh$ = mln_map_lat_lng_for_pixel.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_move_by", map, delta_x, delta_y); + traceDowncall("mln_map_lat_lng_for_pixel", map, point, completion); } - return (int)mh$.invokeExact(map, delta_x, delta_y); + return (int)mh$.invokeExact(map, point, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7380,16 +7338,16 @@ public static int mln_map_move_by(long map, double delta_x, double delta_y) { } } - private static class mln_map_move_by_animated { + private static class mln_map_pixels_for_lat_lngs { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE, - MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_move_by_animated"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pixels_for_lat_lngs"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7397,45 +7355,45 @@ private static class mln_map_move_by_animated { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_move_by_animated(mln_map map, double delta_x, double delta_y, const mln_animation_options *animation) + * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_move_by_animated$descriptor() { - return mln_map_move_by_animated.DESC; + public static FunctionDescriptor mln_map_pixels_for_lat_lngs$descriptor() { + return mln_map_pixels_for_lat_lngs.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_move_by_animated(mln_map map, double delta_x, double delta_y, const mln_animation_options *animation) + * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ - public static MethodHandle mln_map_move_by_animated$handle() { - return mln_map_move_by_animated.HANDLE; + public static MethodHandle mln_map_pixels_for_lat_lngs$handle() { + return mln_map_pixels_for_lat_lngs.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_move_by_animated(mln_map map, double delta_x, double delta_y, const mln_animation_options *animation) + * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ - public static MemorySegment mln_map_move_by_animated$address() { - return mln_map_move_by_animated.ADDR; + public static MemorySegment mln_map_pixels_for_lat_lngs$address() { + return mln_map_pixels_for_lat_lngs.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_move_by_animated(mln_map map, double delta_x, double delta_y, const mln_animation_options *animation) + * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ - public static int mln_map_move_by_animated(long map, double delta_x, double delta_y, MemorySegment animation) { - var mh$ = mln_map_move_by_animated.HANDLE; + public static int mln_map_pixels_for_lat_lngs(long map, MemorySegment coordinates, long coordinate_count, MemorySegment completion) { + var mh$ = mln_map_pixels_for_lat_lngs.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_move_by_animated", map, delta_x, delta_y, animation); + traceDowncall("mln_map_pixels_for_lat_lngs", map, coordinates, coordinate_count, completion); } - return (int)mh$.invokeExact(map, delta_x, delta_y, animation); + return (int)mh$.invokeExact(map, coordinates, coordinate_count, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7443,15 +7401,16 @@ public static int mln_map_move_by_animated(long map, double delta_x, double delt } } - private static class mln_map_scale_by { + private static class mln_map_lat_lngs_for_pixels { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_scale_by"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lngs_for_pixels"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7459,45 +7418,45 @@ private static class mln_map_scale_by { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_scale_by(mln_map map, double scale, const mln_screen_point *anchor) + * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_scale_by$descriptor() { - return mln_map_scale_by.DESC; + public static FunctionDescriptor mln_map_lat_lngs_for_pixels$descriptor() { + return mln_map_lat_lngs_for_pixels.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_scale_by(mln_map map, double scale, const mln_screen_point *anchor) + * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, const mln_completion *completion) * } */ - public static MethodHandle mln_map_scale_by$handle() { - return mln_map_scale_by.HANDLE; + public static MethodHandle mln_map_lat_lngs_for_pixels$handle() { + return mln_map_lat_lngs_for_pixels.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_scale_by(mln_map map, double scale, const mln_screen_point *anchor) + * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, const mln_completion *completion) * } */ - public static MemorySegment mln_map_scale_by$address() { - return mln_map_scale_by.ADDR; + public static MemorySegment mln_map_lat_lngs_for_pixels$address() { + return mln_map_lat_lngs_for_pixels.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_scale_by(mln_map map, double scale, const mln_screen_point *anchor) + * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, const mln_completion *completion) * } */ - public static int mln_map_scale_by(long map, double scale, MemorySegment anchor) { - var mh$ = mln_map_scale_by.HANDLE; + public static int mln_map_lat_lngs_for_pixels(long map, MemorySegment points, long point_count, MemorySegment completion) { + var mh$ = mln_map_lat_lngs_for_pixels.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_scale_by", map, scale, anchor); + traceDowncall("mln_map_lat_lngs_for_pixels", map, points, point_count, completion); } - return (int)mh$.invokeExact(map, scale, anchor); + return (int)mh$.invokeExact(map, points, point_count, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7505,16 +7464,11 @@ public static int mln_map_scale_by(long map, double scale, MemorySegment anchor) } } - private static class mln_map_scale_by_animated { + private static class mln_thread_last_error_message { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_scale_by_animated"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_thread_last_error_message"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7522,247 +7476,286 @@ private static class mln_map_scale_by_animated { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_scale_by_animated(mln_map map, double scale, const mln_screen_point *anchor, const mln_animation_options *animation) + * const char *mln_thread_last_error_message() * } */ - public static FunctionDescriptor mln_map_scale_by_animated$descriptor() { - return mln_map_scale_by_animated.DESC; + public static FunctionDescriptor mln_thread_last_error_message$descriptor() { + return mln_thread_last_error_message.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_scale_by_animated(mln_map map, double scale, const mln_screen_point *anchor, const mln_animation_options *animation) + * const char *mln_thread_last_error_message() * } */ - public static MethodHandle mln_map_scale_by_animated$handle() { - return mln_map_scale_by_animated.HANDLE; + public static MethodHandle mln_thread_last_error_message$handle() { + return mln_thread_last_error_message.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_scale_by_animated(mln_map map, double scale, const mln_screen_point *anchor, const mln_animation_options *animation) + * const char *mln_thread_last_error_message() * } */ - public static MemorySegment mln_map_scale_by_animated$address() { - return mln_map_scale_by_animated.ADDR; + public static MemorySegment mln_thread_last_error_message$address() { + return mln_thread_last_error_message.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_scale_by_animated(mln_map map, double scale, const mln_screen_point *anchor, const mln_animation_options *animation) + * const char *mln_thread_last_error_message() * } */ - public static int mln_map_scale_by_animated(long map, double scale, MemorySegment anchor, MemorySegment animation) { - var mh$ = mln_map_scale_by_animated.HANDLE; + public static MemorySegment mln_thread_last_error_message() { + var mh$ = mln_thread_last_error_message.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_scale_by_animated", map, scale, anchor, animation); + traceDowncall("mln_thread_last_error_message"); } - return (int)mh$.invokeExact(map, scale, anchor, animation); + return (MemorySegment)mh$.invokeExact(); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - - private static class mln_map_rotate_by { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_screen_point.layout(), - mln_screen_point.layout() - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_rotate_by"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - + private static final int MLN_LOG_SEVERITY_INFO = (int)1L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) + * enum mln_log_severity.MLN_LOG_SEVERITY_INFO = 1 * } */ - public static FunctionDescriptor mln_map_rotate_by$descriptor() { - return mln_map_rotate_by.DESC; + public static int MLN_LOG_SEVERITY_INFO() { + return MLN_LOG_SEVERITY_INFO; } - + private static final int MLN_LOG_SEVERITY_WARNING = (int)2L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) + * enum mln_log_severity.MLN_LOG_SEVERITY_WARNING = 2 * } */ - public static MethodHandle mln_map_rotate_by$handle() { - return mln_map_rotate_by.HANDLE; + public static int MLN_LOG_SEVERITY_WARNING() { + return MLN_LOG_SEVERITY_WARNING; } - + private static final int MLN_LOG_SEVERITY_ERROR = (int)3L; /** - * Address for: * {@snippet lang=c : - * mln_status mln_map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) + * enum mln_log_severity.MLN_LOG_SEVERITY_ERROR = 3 * } */ - public static MemorySegment mln_map_rotate_by$address() { - return mln_map_rotate_by.ADDR; + public static int MLN_LOG_SEVERITY_ERROR() { + return MLN_LOG_SEVERITY_ERROR; } - + private static final int MLN_LOG_SEVERITY_MASK_INFO = (int)2L; /** * {@snippet lang=c : - * mln_status mln_map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) + * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_INFO = 2 * } */ - public static int mln_map_rotate_by(long map, MemorySegment first, MemorySegment second) { - var mh$ = mln_map_rotate_by.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_rotate_by", map, first, second); - } - return (int)mh$.invokeExact(map, first, second); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_rotate_by_animated { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_screen_point.layout(), - mln_screen_point.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_rotate_by_animated"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int MLN_LOG_SEVERITY_MASK_INFO() { + return MLN_LOG_SEVERITY_MASK_INFO; } - + private static final int MLN_LOG_SEVERITY_MASK_WARNING = (int)4L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_rotate_by_animated(mln_map map, mln_screen_point first, mln_screen_point second, const mln_animation_options *animation) + * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_WARNING = 4 * } */ - public static FunctionDescriptor mln_map_rotate_by_animated$descriptor() { - return mln_map_rotate_by_animated.DESC; + public static int MLN_LOG_SEVERITY_MASK_WARNING() { + return MLN_LOG_SEVERITY_MASK_WARNING; } - + private static final int MLN_LOG_SEVERITY_MASK_ERROR = (int)8L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_rotate_by_animated(mln_map map, mln_screen_point first, mln_screen_point second, const mln_animation_options *animation) + * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_ERROR = 8 * } */ - public static MethodHandle mln_map_rotate_by_animated$handle() { - return mln_map_rotate_by_animated.HANDLE; + public static int MLN_LOG_SEVERITY_MASK_ERROR() { + return MLN_LOG_SEVERITY_MASK_ERROR; } - + private static final int MLN_LOG_SEVERITY_MASK_DEFAULT = (int)6L; /** - * Address for: * {@snippet lang=c : - * mln_status mln_map_rotate_by_animated(mln_map map, mln_screen_point first, mln_screen_point second, const mln_animation_options *animation) + * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_DEFAULT = 6 * } */ - public static MemorySegment mln_map_rotate_by_animated$address() { - return mln_map_rotate_by_animated.ADDR; + public static int MLN_LOG_SEVERITY_MASK_DEFAULT() { + return MLN_LOG_SEVERITY_MASK_DEFAULT; } - + private static final int MLN_LOG_SEVERITY_MASK_ALL = (int)14L; /** * {@snippet lang=c : - * mln_status mln_map_rotate_by_animated(mln_map map, mln_screen_point first, mln_screen_point second, const mln_animation_options *animation) + * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_ALL = 14 * } */ - public static int mln_map_rotate_by_animated(long map, MemorySegment first, MemorySegment second, MemorySegment animation) { - var mh$ = mln_map_rotate_by_animated.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_rotate_by_animated", map, first, second, animation); - } - return (int)mh$.invokeExact(map, first, second, animation); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_pitch_by { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pitch_by"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int MLN_LOG_SEVERITY_MASK_ALL() { + return MLN_LOG_SEVERITY_MASK_ALL; } - + private static final int MLN_LOG_EVENT_GENERAL = (int)0L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_pitch_by(mln_map map, double pitch) + * enum mln_log_event.MLN_LOG_EVENT_GENERAL = 0 * } */ - public static FunctionDescriptor mln_map_pitch_by$descriptor() { - return mln_map_pitch_by.DESC; + public static int MLN_LOG_EVENT_GENERAL() { + return MLN_LOG_EVENT_GENERAL; } - + private static final int MLN_LOG_EVENT_SETUP = (int)1L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_pitch_by(mln_map map, double pitch) + * enum mln_log_event.MLN_LOG_EVENT_SETUP = 1 * } */ - public static MethodHandle mln_map_pitch_by$handle() { - return mln_map_pitch_by.HANDLE; + public static int MLN_LOG_EVENT_SETUP() { + return MLN_LOG_EVENT_SETUP; } - + private static final int MLN_LOG_EVENT_SHADER = (int)2L; /** - * Address for: * {@snippet lang=c : - * mln_status mln_map_pitch_by(mln_map map, double pitch) + * enum mln_log_event.MLN_LOG_EVENT_SHADER = 2 * } */ - public static MemorySegment mln_map_pitch_by$address() { - return mln_map_pitch_by.ADDR; + public static int MLN_LOG_EVENT_SHADER() { + return MLN_LOG_EVENT_SHADER; } - + private static final int MLN_LOG_EVENT_PARSE_STYLE = (int)3L; /** * {@snippet lang=c : - * mln_status mln_map_pitch_by(mln_map map, double pitch) + * enum mln_log_event.MLN_LOG_EVENT_PARSE_STYLE = 3 * } */ - public static int mln_map_pitch_by(long map, double pitch) { - var mh$ = mln_map_pitch_by.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_pitch_by", map, pitch); - } - return (int)mh$.invokeExact(map, pitch); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } + public static int MLN_LOG_EVENT_PARSE_STYLE() { + return MLN_LOG_EVENT_PARSE_STYLE; + } + private static final int MLN_LOG_EVENT_PARSE_TILE = (int)4L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_PARSE_TILE = 4 + * } + */ + public static int MLN_LOG_EVENT_PARSE_TILE() { + return MLN_LOG_EVENT_PARSE_TILE; + } + private static final int MLN_LOG_EVENT_RENDER = (int)5L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_RENDER = 5 + * } + */ + public static int MLN_LOG_EVENT_RENDER() { + return MLN_LOG_EVENT_RENDER; + } + private static final int MLN_LOG_EVENT_STYLE = (int)6L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_STYLE = 6 + * } + */ + public static int MLN_LOG_EVENT_STYLE() { + return MLN_LOG_EVENT_STYLE; + } + private static final int MLN_LOG_EVENT_DATABASE = (int)7L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_DATABASE = 7 + * } + */ + public static int MLN_LOG_EVENT_DATABASE() { + return MLN_LOG_EVENT_DATABASE; + } + private static final int MLN_LOG_EVENT_HTTP_REQUEST = (int)8L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_HTTP_REQUEST = 8 + * } + */ + public static int MLN_LOG_EVENT_HTTP_REQUEST() { + return MLN_LOG_EVENT_HTTP_REQUEST; + } + private static final int MLN_LOG_EVENT_SPRITE = (int)9L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_SPRITE = 9 + * } + */ + public static int MLN_LOG_EVENT_SPRITE() { + return MLN_LOG_EVENT_SPRITE; + } + private static final int MLN_LOG_EVENT_IMAGE = (int)10L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_IMAGE = 10 + * } + */ + public static int MLN_LOG_EVENT_IMAGE() { + return MLN_LOG_EVENT_IMAGE; + } + private static final int MLN_LOG_EVENT_OPENGL = (int)11L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_OPENGL = 11 + * } + */ + public static int MLN_LOG_EVENT_OPENGL() { + return MLN_LOG_EVENT_OPENGL; + } + private static final int MLN_LOG_EVENT_JNI = (int)12L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_JNI = 12 + * } + */ + public static int MLN_LOG_EVENT_JNI() { + return MLN_LOG_EVENT_JNI; + } + private static final int MLN_LOG_EVENT_ANDROID = (int)13L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_ANDROID = 13 + * } + */ + public static int MLN_LOG_EVENT_ANDROID() { + return MLN_LOG_EVENT_ANDROID; + } + private static final int MLN_LOG_EVENT_CRASH = (int)14L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_CRASH = 14 + * } + */ + public static int MLN_LOG_EVENT_CRASH() { + return MLN_LOG_EVENT_CRASH; + } + private static final int MLN_LOG_EVENT_GLYPH = (int)15L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_GLYPH = 15 + * } + */ + public static int MLN_LOG_EVENT_GLYPH() { + return MLN_LOG_EVENT_GLYPH; + } + private static final int MLN_LOG_EVENT_TIMING = (int)16L; + /** + * {@snippet lang=c : + * enum mln_log_event.MLN_LOG_EVENT_TIMING = 16 + * } + */ + public static int MLN_LOG_EVENT_TIMING() { + return MLN_LOG_EVENT_TIMING; } - private static class mln_map_pitch_by_animated { + private static class mln_log_set_callback { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pitch_by_animated"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_set_callback"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7770,45 +7763,45 @@ private static class mln_map_pitch_by_animated { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_pitch_by_animated(mln_map map, double pitch, const mln_animation_options *animation) + * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data, mln_log_callback_release release_user_data) * } */ - public static FunctionDescriptor mln_map_pitch_by_animated$descriptor() { - return mln_map_pitch_by_animated.DESC; + public static FunctionDescriptor mln_log_set_callback$descriptor() { + return mln_log_set_callback.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_pitch_by_animated(mln_map map, double pitch, const mln_animation_options *animation) + * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data, mln_log_callback_release release_user_data) * } */ - public static MethodHandle mln_map_pitch_by_animated$handle() { - return mln_map_pitch_by_animated.HANDLE; + public static MethodHandle mln_log_set_callback$handle() { + return mln_log_set_callback.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_pitch_by_animated(mln_map map, double pitch, const mln_animation_options *animation) + * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data, mln_log_callback_release release_user_data) * } */ - public static MemorySegment mln_map_pitch_by_animated$address() { - return mln_map_pitch_by_animated.ADDR; + public static MemorySegment mln_log_set_callback$address() { + return mln_log_set_callback.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_pitch_by_animated(mln_map map, double pitch, const mln_animation_options *animation) + * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data, mln_log_callback_release release_user_data) * } */ - public static int mln_map_pitch_by_animated(long map, double pitch, MemorySegment animation) { - var mh$ = mln_map_pitch_by_animated.HANDLE; + public static int mln_log_set_callback(MemorySegment callback, MemorySegment user_data, MemorySegment release_user_data) { + var mh$ = mln_log_set_callback.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_pitch_by_animated", map, pitch, animation); + traceDowncall("mln_log_set_callback", callback, user_data, release_user_data); } - return (int)mh$.invokeExact(map, pitch, animation); + return (int)mh$.invokeExact(callback, user_data, release_user_data); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7816,13 +7809,11 @@ public static int mln_map_pitch_by_animated(long map, double pitch, MemorySegmen } } - private static class mln_map_cancel_transitions { + private static class mln_log_clear_callback { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG - ); + MapLibreNativeC.C_INT ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_cancel_transitions"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_clear_callback"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7830,45 +7821,45 @@ private static class mln_map_cancel_transitions { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_cancel_transitions(mln_map map) + * mln_status mln_log_clear_callback() * } */ - public static FunctionDescriptor mln_map_cancel_transitions$descriptor() { - return mln_map_cancel_transitions.DESC; + public static FunctionDescriptor mln_log_clear_callback$descriptor() { + return mln_log_clear_callback.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_cancel_transitions(mln_map map) + * mln_status mln_log_clear_callback() * } */ - public static MethodHandle mln_map_cancel_transitions$handle() { - return mln_map_cancel_transitions.HANDLE; + public static MethodHandle mln_log_clear_callback$handle() { + return mln_log_clear_callback.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_cancel_transitions(mln_map map) + * mln_status mln_log_clear_callback() * } */ - public static MemorySegment mln_map_cancel_transitions$address() { - return mln_map_cancel_transitions.ADDR; + public static MemorySegment mln_log_clear_callback$address() { + return mln_log_clear_callback.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_cancel_transitions(mln_map map) + * mln_status mln_log_clear_callback() * } */ - public static int mln_map_cancel_transitions(long map) { - var mh$ = mln_map_cancel_transitions.HANDLE; + public static int mln_log_clear_callback() { + var mh$ = mln_log_clear_callback.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_cancel_transitions", map); + traceDowncall("mln_log_clear_callback"); } - return (int)mh$.invokeExact(map); + return (int)mh$.invokeExact(); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7876,14 +7867,13 @@ public static int mln_map_cancel_transitions(long map) { } } - private static class mln_map_set_gesture_in_progress { + private static class mln_log_set_async_severity_mask { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_BOOL + MapLibreNativeC.C_INT ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_gesture_in_progress"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_set_async_severity_mask"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7891,45 +7881,45 @@ private static class mln_map_set_gesture_in_progress { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_gesture_in_progress(mln_map map, bool in_progress) + * mln_status mln_log_set_async_severity_mask(uint32_t mask) * } */ - public static FunctionDescriptor mln_map_set_gesture_in_progress$descriptor() { - return mln_map_set_gesture_in_progress.DESC; + public static FunctionDescriptor mln_log_set_async_severity_mask$descriptor() { + return mln_log_set_async_severity_mask.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_gesture_in_progress(mln_map map, bool in_progress) + * mln_status mln_log_set_async_severity_mask(uint32_t mask) * } */ - public static MethodHandle mln_map_set_gesture_in_progress$handle() { - return mln_map_set_gesture_in_progress.HANDLE; + public static MethodHandle mln_log_set_async_severity_mask$handle() { + return mln_log_set_async_severity_mask.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_gesture_in_progress(mln_map map, bool in_progress) + * mln_status mln_log_set_async_severity_mask(uint32_t mask) * } */ - public static MemorySegment mln_map_set_gesture_in_progress$address() { - return mln_map_set_gesture_in_progress.ADDR; + public static MemorySegment mln_log_set_async_severity_mask$address() { + return mln_log_set_async_severity_mask.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_gesture_in_progress(mln_map map, bool in_progress) + * mln_status mln_log_set_async_severity_mask(uint32_t mask) * } */ - public static int mln_map_set_gesture_in_progress(long map, boolean in_progress) { - var mh$ = mln_map_set_gesture_in_progress.HANDLE; + public static int mln_log_set_async_severity_mask(int mask) { + var mh$ = mln_log_set_async_severity_mask.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_gesture_in_progress", map, in_progress); + traceDowncall("mln_log_set_async_severity_mask", mask); } - return (int)mh$.invokeExact(map, in_progress); + return (int)mh$.invokeExact(mask); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7937,14 +7927,14 @@ public static int mln_map_set_gesture_in_progress(long map, boolean in_progress) } } - private static class mln_map_is_gesture_in_progress { + private static class mln_map_projection_create { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_is_gesture_in_progress"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_create"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -7952,45 +7942,45 @@ private static class mln_map_is_gesture_in_progress { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_is_gesture_in_progress(mln_map map, bool *out_in_progress) + * mln_status mln_map_projection_create(mln_map map, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_is_gesture_in_progress$descriptor() { - return mln_map_is_gesture_in_progress.DESC; + public static FunctionDescriptor mln_map_projection_create$descriptor() { + return mln_map_projection_create.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_is_gesture_in_progress(mln_map map, bool *out_in_progress) + * mln_status mln_map_projection_create(mln_map map, const mln_completion *completion) * } */ - public static MethodHandle mln_map_is_gesture_in_progress$handle() { - return mln_map_is_gesture_in_progress.HANDLE; + public static MethodHandle mln_map_projection_create$handle() { + return mln_map_projection_create.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_is_gesture_in_progress(mln_map map, bool *out_in_progress) + * mln_status mln_map_projection_create(mln_map map, const mln_completion *completion) * } */ - public static MemorySegment mln_map_is_gesture_in_progress$address() { - return mln_map_is_gesture_in_progress.ADDR; + public static MemorySegment mln_map_projection_create$address() { + return mln_map_projection_create.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_is_gesture_in_progress(mln_map map, bool *out_in_progress) + * mln_status mln_map_projection_create(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_is_gesture_in_progress(long map, MemorySegment out_in_progress) { - var mh$ = mln_map_is_gesture_in_progress.HANDLE; + public static int mln_map_projection_create(long map, MemorySegment completion) { + var mh$ = mln_map_projection_create.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_is_gesture_in_progress", map, out_in_progress); + traceDowncall("mln_map_projection_create", map, completion); } - return (int)mh$.invokeExact(map, out_in_progress); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -7998,16 +7988,13 @@ public static int mln_map_is_gesture_in_progress(long map, MemorySegment out_in_ } } - private static class mln_map_camera_for_lat_lng_bounds { + private static class mln_map_projection_close { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_lat_lng_bounds.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_lat_lng_bounds"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_close"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8015,45 +8002,45 @@ private static class mln_map_camera_for_lat_lng_bounds { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_close(mln_map_projection projection) * } */ - public static FunctionDescriptor mln_map_camera_for_lat_lng_bounds$descriptor() { - return mln_map_camera_for_lat_lng_bounds.DESC; + public static FunctionDescriptor mln_map_projection_close$descriptor() { + return mln_map_projection_close.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_close(mln_map_projection projection) * } */ - public static MethodHandle mln_map_camera_for_lat_lng_bounds$handle() { - return mln_map_camera_for_lat_lng_bounds.HANDLE; + public static MethodHandle mln_map_projection_close$handle() { + return mln_map_projection_close.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_close(mln_map_projection projection) * } */ - public static MemorySegment mln_map_camera_for_lat_lng_bounds$address() { - return mln_map_camera_for_lat_lng_bounds.ADDR; + public static MemorySegment mln_map_projection_close$address() { + return mln_map_projection_close.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lng_bounds(mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_close(mln_map_projection projection) * } */ - public static int mln_map_camera_for_lat_lng_bounds(long map, MemorySegment bounds, MemorySegment fit_options, MemorySegment out_camera) { - var mh$ = mln_map_camera_for_lat_lng_bounds.HANDLE; + public static int mln_map_projection_close(long projection) { + var mh$ = mln_map_projection_close.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_camera_for_lat_lng_bounds", map, bounds, fit_options, out_camera); + traceDowncall("mln_map_projection_close", projection); } - return (int)mh$.invokeExact(map, bounds, fit_options, out_camera); + return (int)mh$.invokeExact(projection); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8061,17 +8048,14 @@ public static int mln_map_camera_for_lat_lng_bounds(long map, MemorySegment boun } } - private static class mln_map_camera_for_lat_lngs { + private static class mln_map_projection_get_camera { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_lat_lngs"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_get_camera"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8079,45 +8063,45 @@ private static class mln_map_camera_for_lat_lngs { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) * } */ - public static FunctionDescriptor mln_map_camera_for_lat_lngs$descriptor() { - return mln_map_camera_for_lat_lngs.DESC; + public static FunctionDescriptor mln_map_projection_get_camera$descriptor() { + return mln_map_projection_get_camera.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) * } */ - public static MethodHandle mln_map_camera_for_lat_lngs$handle() { - return mln_map_camera_for_lat_lngs.HANDLE; + public static MethodHandle mln_map_projection_get_camera$handle() { + return mln_map_projection_get_camera.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) * } */ - public static MemorySegment mln_map_camera_for_lat_lngs$address() { - return mln_map_camera_for_lat_lngs.ADDR; + public static MemorySegment mln_map_projection_get_camera$address() { + return mln_map_projection_get_camera.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_camera_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) * } */ - public static int mln_map_camera_for_lat_lngs(long map, MemorySegment coordinates, long coordinate_count, MemorySegment fit_options, MemorySegment out_camera) { - var mh$ = mln_map_camera_for_lat_lngs.HANDLE; + public static int mln_map_projection_get_camera(long projection, MemorySegment out_camera) { + var mh$ = mln_map_projection_get_camera.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_camera_for_lat_lngs", map, coordinates, coordinate_count, fit_options, out_camera); + traceDowncall("mln_map_projection_get_camera", projection, out_camera); } - return (int)mh$.invokeExact(map, coordinates, coordinate_count, fit_options, out_camera); + return (int)mh$.invokeExact(projection, out_camera); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8125,16 +8109,14 @@ public static int mln_map_camera_for_lat_lngs(long map, MemorySegment coordinate } } - private static class mln_map_camera_for_geometry { + private static class mln_map_projection_set_camera { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_camera_for_geometry"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_camera"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8142,45 +8124,45 @@ private static class mln_map_camera_for_geometry { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) * } */ - public static FunctionDescriptor mln_map_camera_for_geometry$descriptor() { - return mln_map_camera_for_geometry.DESC; + public static FunctionDescriptor mln_map_projection_set_camera$descriptor() { + return mln_map_projection_set_camera.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) * } */ - public static MethodHandle mln_map_camera_for_geometry$handle() { - return mln_map_camera_for_geometry.HANDLE; + public static MethodHandle mln_map_projection_set_camera$handle() { + return mln_map_projection_set_camera.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) * } */ - public static MemorySegment mln_map_camera_for_geometry$address() { - return mln_map_camera_for_geometry.ADDR; + public static MemorySegment mln_map_projection_set_camera$address() { + return mln_map_projection_set_camera.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_camera_for_geometry(mln_map map, mln_buffer_view geometry, const mln_camera_fit_options *fit_options, mln_camera_options *out_camera) + * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) * } */ - public static int mln_map_camera_for_geometry(long map, MemorySegment geometry, MemorySegment fit_options, MemorySegment out_camera) { - var mh$ = mln_map_camera_for_geometry.HANDLE; + public static int mln_map_projection_set_camera(long projection, MemorySegment camera) { + var mh$ = mln_map_projection_set_camera.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_camera_for_geometry", map, geometry, fit_options, out_camera); + traceDowncall("mln_map_projection_set_camera", projection, camera); } - return (int)mh$.invokeExact(map, geometry, fit_options, out_camera); + return (int)mh$.invokeExact(projection, camera); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8188,15 +8170,16 @@ public static int mln_map_camera_for_geometry(long map, MemorySegment geometry, } } - private static class mln_map_lat_lng_bounds_for_camera { + private static class mln_map_projection_set_visible_coordinates { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER + MapLibreNativeC.C_LONG, + mln_edge_insets.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_bounds_for_camera"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_visible_coordinates"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8204,45 +8187,45 @@ private static class mln_map_lat_lng_bounds_for_camera { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) * } */ - public static FunctionDescriptor mln_map_lat_lng_bounds_for_camera$descriptor() { - return mln_map_lat_lng_bounds_for_camera.DESC; + public static FunctionDescriptor mln_map_projection_set_visible_coordinates$descriptor() { + return mln_map_projection_set_visible_coordinates.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) * } */ - public static MethodHandle mln_map_lat_lng_bounds_for_camera$handle() { - return mln_map_lat_lng_bounds_for_camera.HANDLE; + public static MethodHandle mln_map_projection_set_visible_coordinates$handle() { + return mln_map_projection_set_visible_coordinates.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) * } */ - public static MemorySegment mln_map_lat_lng_bounds_for_camera$address() { - return mln_map_lat_lng_bounds_for_camera.ADDR; + public static MemorySegment mln_map_projection_set_visible_coordinates$address() { + return mln_map_projection_set_visible_coordinates.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) * } */ - public static int mln_map_lat_lng_bounds_for_camera(long map, MemorySegment camera, MemorySegment out_bounds) { - var mh$ = mln_map_lat_lng_bounds_for_camera.HANDLE; + public static int mln_map_projection_set_visible_coordinates(long projection, MemorySegment coordinates, long coordinate_count, MemorySegment padding) { + var mh$ = mln_map_projection_set_visible_coordinates.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_lat_lng_bounds_for_camera", map, camera, out_bounds); + traceDowncall("mln_map_projection_set_visible_coordinates", projection, coordinates, coordinate_count, padding); } - return (int)mh$.invokeExact(map, camera, out_bounds); + return (int)mh$.invokeExact(projection, coordinates, coordinate_count, padding); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8250,15 +8233,15 @@ public static int mln_map_lat_lng_bounds_for_camera(long map, MemorySegment came } } - private static class mln_map_lat_lng_bounds_for_camera_unwrapped { + private static class mln_map_projection_set_visible_geometry { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER + mln_buffer_view.layout(), + mln_edge_insets.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_bounds_for_camera_unwrapped"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_visible_geometry"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8266,45 +8249,45 @@ private static class mln_map_lat_lng_bounds_for_camera_unwrapped { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) * } */ - public static FunctionDescriptor mln_map_lat_lng_bounds_for_camera_unwrapped$descriptor() { - return mln_map_lat_lng_bounds_for_camera_unwrapped.DESC; + public static FunctionDescriptor mln_map_projection_set_visible_geometry$descriptor() { + return mln_map_projection_set_visible_geometry.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) * } */ - public static MethodHandle mln_map_lat_lng_bounds_for_camera_unwrapped$handle() { - return mln_map_lat_lng_bounds_for_camera_unwrapped.HANDLE; + public static MethodHandle mln_map_projection_set_visible_geometry$handle() { + return mln_map_projection_set_visible_geometry.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) * } */ - public static MemorySegment mln_map_lat_lng_bounds_for_camera_unwrapped$address() { - return mln_map_lat_lng_bounds_for_camera_unwrapped.ADDR; + public static MemorySegment mln_map_projection_set_visible_geometry$address() { + return mln_map_projection_set_visible_geometry.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_lat_lng_bounds_for_camera_unwrapped(mln_map map, const mln_camera_options *camera, mln_lat_lng_bounds *out_bounds) + * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) * } */ - public static int mln_map_lat_lng_bounds_for_camera_unwrapped(long map, MemorySegment camera, MemorySegment out_bounds) { - var mh$ = mln_map_lat_lng_bounds_for_camera_unwrapped.HANDLE; + public static int mln_map_projection_set_visible_geometry(long projection, MemorySegment geometry, MemorySegment padding) { + var mh$ = mln_map_projection_set_visible_geometry.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_lat_lng_bounds_for_camera_unwrapped", map, camera, out_bounds); + traceDowncall("mln_map_projection_set_visible_geometry", projection, geometry, padding); } - return (int)mh$.invokeExact(map, camera, out_bounds); + return (int)mh$.invokeExact(projection, geometry, padding); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8312,14 +8295,15 @@ public static int mln_map_lat_lng_bounds_for_camera_unwrapped(long map, MemorySe } } - private static class mln_map_get_bounds { + private static class mln_map_projection_pixel_for_lat_lng { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + mln_lat_lng.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_bounds"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_pixel_for_lat_lng"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8327,45 +8311,45 @@ private static class mln_map_get_bounds { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_bounds(mln_map map, mln_bound_options *out_options) + * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) * } */ - public static FunctionDescriptor mln_map_get_bounds$descriptor() { - return mln_map_get_bounds.DESC; + public static FunctionDescriptor mln_map_projection_pixel_for_lat_lng$descriptor() { + return mln_map_projection_pixel_for_lat_lng.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_bounds(mln_map map, mln_bound_options *out_options) + * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) * } */ - public static MethodHandle mln_map_get_bounds$handle() { - return mln_map_get_bounds.HANDLE; + public static MethodHandle mln_map_projection_pixel_for_lat_lng$handle() { + return mln_map_projection_pixel_for_lat_lng.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_bounds(mln_map map, mln_bound_options *out_options) + * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) * } */ - public static MemorySegment mln_map_get_bounds$address() { - return mln_map_get_bounds.ADDR; + public static MemorySegment mln_map_projection_pixel_for_lat_lng$address() { + return mln_map_projection_pixel_for_lat_lng.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_bounds(mln_map map, mln_bound_options *out_options) + * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) * } */ - public static int mln_map_get_bounds(long map, MemorySegment out_options) { - var mh$ = mln_map_get_bounds.HANDLE; + public static int mln_map_projection_pixel_for_lat_lng(long projection, MemorySegment coordinate, MemorySegment out_point) { + var mh$ = mln_map_projection_pixel_for_lat_lng.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_bounds", map, out_options); + traceDowncall("mln_map_projection_pixel_for_lat_lng", projection, coordinate, out_point); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(projection, coordinate, out_point); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8373,14 +8357,15 @@ public static int mln_map_get_bounds(long map, MemorySegment out_options) { } } - private static class mln_map_set_bounds { + private static class mln_map_projection_lat_lng_for_pixel { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + mln_screen_point.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_bounds"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_lat_lng_for_pixel"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8388,45 +8373,45 @@ private static class mln_map_set_bounds { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options) + * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) * } */ - public static FunctionDescriptor mln_map_set_bounds$descriptor() { - return mln_map_set_bounds.DESC; + public static FunctionDescriptor mln_map_projection_lat_lng_for_pixel$descriptor() { + return mln_map_projection_lat_lng_for_pixel.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options) + * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) * } */ - public static MethodHandle mln_map_set_bounds$handle() { - return mln_map_set_bounds.HANDLE; + public static MethodHandle mln_map_projection_lat_lng_for_pixel$handle() { + return mln_map_projection_lat_lng_for_pixel.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options) + * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) * } */ - public static MemorySegment mln_map_set_bounds$address() { - return mln_map_set_bounds.ADDR; + public static MemorySegment mln_map_projection_lat_lng_for_pixel$address() { + return mln_map_projection_lat_lng_for_pixel.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_bounds(mln_map map, const mln_bound_options *options) + * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) * } */ - public static int mln_map_set_bounds(long map, MemorySegment options) { - var mh$ = mln_map_set_bounds.HANDLE; + public static int mln_map_projection_lat_lng_for_pixel(long projection, MemorySegment point, MemorySegment out_coordinate) { + var mh$ = mln_map_projection_lat_lng_for_pixel.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_bounds", map, options); + traceDowncall("mln_map_projection_lat_lng_for_pixel", projection, point, out_coordinate); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(projection, point, out_coordinate); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8434,14 +8419,14 @@ public static int mln_map_set_bounds(long map, MemorySegment options) { } } - private static class mln_map_get_free_camera_options { + private static class mln_projected_meters_for_lat_lng { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, + mln_lat_lng.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_free_camera_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_projected_meters_for_lat_lng"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8449,45 +8434,45 @@ private static class mln_map_get_free_camera_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_free_camera_options(mln_map map, mln_free_camera_options *out_options) + * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) * } */ - public static FunctionDescriptor mln_map_get_free_camera_options$descriptor() { - return mln_map_get_free_camera_options.DESC; + public static FunctionDescriptor mln_projected_meters_for_lat_lng$descriptor() { + return mln_projected_meters_for_lat_lng.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_free_camera_options(mln_map map, mln_free_camera_options *out_options) + * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) * } */ - public static MethodHandle mln_map_get_free_camera_options$handle() { - return mln_map_get_free_camera_options.HANDLE; + public static MethodHandle mln_projected_meters_for_lat_lng$handle() { + return mln_projected_meters_for_lat_lng.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_free_camera_options(mln_map map, mln_free_camera_options *out_options) + * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) * } */ - public static MemorySegment mln_map_get_free_camera_options$address() { - return mln_map_get_free_camera_options.ADDR; + public static MemorySegment mln_projected_meters_for_lat_lng$address() { + return mln_projected_meters_for_lat_lng.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_free_camera_options(mln_map map, mln_free_camera_options *out_options) + * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) * } */ - public static int mln_map_get_free_camera_options(long map, MemorySegment out_options) { - var mh$ = mln_map_get_free_camera_options.HANDLE; + public static int mln_projected_meters_for_lat_lng(MemorySegment coordinate, MemorySegment out_meters) { + var mh$ = mln_projected_meters_for_lat_lng.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_free_camera_options", map, out_options); + traceDowncall("mln_projected_meters_for_lat_lng", coordinate, out_meters); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(coordinate, out_meters); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8495,14 +8480,14 @@ public static int mln_map_get_free_camera_options(long map, MemorySegment out_op } } - private static class mln_map_set_free_camera_options { + private static class mln_lat_lng_for_projected_meters { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, + mln_projected_meters.layout(), MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_free_camera_options"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_lat_lng_for_projected_meters"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8510,121 +8495,129 @@ private static class mln_map_set_free_camera_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options) + * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) * } */ - public static FunctionDescriptor mln_map_set_free_camera_options$descriptor() { - return mln_map_set_free_camera_options.DESC; + public static FunctionDescriptor mln_lat_lng_for_projected_meters$descriptor() { + return mln_lat_lng_for_projected_meters.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options) + * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) * } */ - public static MethodHandle mln_map_set_free_camera_options$handle() { - return mln_map_set_free_camera_options.HANDLE; + public static MethodHandle mln_lat_lng_for_projected_meters$handle() { + return mln_lat_lng_for_projected_meters.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options) + * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) * } */ - public static MemorySegment mln_map_set_free_camera_options$address() { - return mln_map_set_free_camera_options.ADDR; + public static MemorySegment mln_lat_lng_for_projected_meters$address() { + return mln_lat_lng_for_projected_meters.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_free_camera_options(mln_map map, const mln_free_camera_options *options) + * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) * } */ - public static int mln_map_set_free_camera_options(long map, MemorySegment options) { - var mh$ = mln_map_set_free_camera_options.HANDLE; + public static int mln_lat_lng_for_projected_meters(MemorySegment meters, MemorySegment out_coordinate) { + var mh$ = mln_lat_lng_for_projected_meters.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_free_camera_options", map, options); + traceDowncall("mln_lat_lng_for_projected_meters", meters, out_coordinate); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(meters, out_coordinate); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - - private static class mln_map_get_projection_mode { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_projection_mode"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT = (int)1L; + /** + * {@snippet lang=c : + * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT = 1 + * } + */ + public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT() { + return MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT; } - + private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX = (int)2L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_projection_mode(mln_map map, mln_projection_mode *out_mode) + * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX = 2 * } */ - public static FunctionDescriptor mln_map_get_projection_mode$descriptor() { - return mln_map_get_projection_mode.DESC; + public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX() { + return MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX; } - + private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING = (int)3L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_projection_mode(mln_map map, mln_projection_mode *out_mode) + * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING = 3 * } */ - public static MethodHandle mln_map_get_projection_mode$handle() { - return mln_map_get_projection_mode.HANDLE; + public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING() { + return MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING; } - + private static final int MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS = (int)1L; /** - * Address for: * {@snippet lang=c : - * mln_status mln_map_get_projection_mode(mln_map map, mln_projection_mode *out_mode) + * enum mln_rendered_feature_query_option_field.MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS = 1 * } */ - public static MemorySegment mln_map_get_projection_mode$address() { - return mln_map_get_projection_mode.ADDR; + public static int MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS() { + return MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS; } - + private static final int MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS = (int)1L; /** * {@snippet lang=c : - * mln_status mln_map_get_projection_mode(mln_map map, mln_projection_mode *out_mode) + * enum mln_source_feature_query_option_field.MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS = 1 * } */ - public static int mln_map_get_projection_mode(long map, MemorySegment out_mode) { - var mh$ = mln_map_get_projection_mode.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_projection_mode", map, out_mode); - } - return (int)mh$.invokeExact(map, out_mode); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } + public static int MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS() { + return MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS; + } + private static final int MLN_QUERIED_FEATURE_SOURCE_ID = (int)1L; + /** + * {@snippet lang=c : + * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_ID = 1 + * } + */ + public static int MLN_QUERIED_FEATURE_SOURCE_ID() { + return MLN_QUERIED_FEATURE_SOURCE_ID; + } + private static final int MLN_QUERIED_FEATURE_SOURCE_LAYER_ID = (int)2L; + /** + * {@snippet lang=c : + * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_LAYER_ID = 2 + * } + */ + public static int MLN_QUERIED_FEATURE_SOURCE_LAYER_ID() { + return MLN_QUERIED_FEATURE_SOURCE_LAYER_ID; + } + private static final int MLN_QUERIED_FEATURE_STATE = (int)4L; + /** + * {@snippet lang=c : + * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE = 4 + * } + */ + public static int MLN_QUERIED_FEATURE_STATE() { + return MLN_QUERIED_FEATURE_STATE; } - private static class mln_map_set_projection_mode { + private static class mln_rendered_feature_query_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); + mln_rendered_feature_query_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_projection_mode"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_feature_query_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8632,45 +8625,45 @@ private static class mln_map_set_projection_mode { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode) + * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() * } */ - public static FunctionDescriptor mln_map_set_projection_mode$descriptor() { - return mln_map_set_projection_mode.DESC; + public static FunctionDescriptor mln_rendered_feature_query_options_default$descriptor() { + return mln_rendered_feature_query_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode) + * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() * } */ - public static MethodHandle mln_map_set_projection_mode$handle() { - return mln_map_set_projection_mode.HANDLE; + public static MethodHandle mln_rendered_feature_query_options_default$handle() { + return mln_rendered_feature_query_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode) + * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() * } */ - public static MemorySegment mln_map_set_projection_mode$address() { - return mln_map_set_projection_mode.ADDR; + public static MemorySegment mln_rendered_feature_query_options_default$address() { + return mln_rendered_feature_query_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_projection_mode(mln_map map, const mln_projection_mode *mode) + * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() * } */ - public static int mln_map_set_projection_mode(long map, MemorySegment mode) { - var mh$ = mln_map_set_projection_mode.HANDLE; + public static MemorySegment mln_rendered_feature_query_options_default(SegmentAllocator allocator) { + var mh$ = mln_rendered_feature_query_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_projection_mode", map, mode); + traceDowncall("mln_rendered_feature_query_options_default", allocator); } - return (int)mh$.invokeExact(map, mode); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8678,15 +8671,11 @@ public static int mln_map_set_projection_mode(long map, MemorySegment mode) { } } - private static class mln_map_pixel_for_lat_lng { + private static class mln_source_feature_query_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_lat_lng.layout(), - MapLibreNativeC.C_POINTER - ); + mln_source_feature_query_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pixel_for_lat_lng"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_source_feature_query_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8694,45 +8683,45 @@ private static class mln_map_pixel_for_lat_lng { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_source_feature_query_options mln_source_feature_query_options_default() * } */ - public static FunctionDescriptor mln_map_pixel_for_lat_lng$descriptor() { - return mln_map_pixel_for_lat_lng.DESC; + public static FunctionDescriptor mln_source_feature_query_options_default$descriptor() { + return mln_source_feature_query_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_source_feature_query_options mln_source_feature_query_options_default() * } */ - public static MethodHandle mln_map_pixel_for_lat_lng$handle() { - return mln_map_pixel_for_lat_lng.HANDLE; + public static MethodHandle mln_source_feature_query_options_default$handle() { + return mln_source_feature_query_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_source_feature_query_options mln_source_feature_query_options_default() * } */ - public static MemorySegment mln_map_pixel_for_lat_lng$address() { - return mln_map_pixel_for_lat_lng.ADDR; + public static MemorySegment mln_source_feature_query_options_default$address() { + return mln_source_feature_query_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_pixel_for_lat_lng(mln_map map, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_source_feature_query_options mln_source_feature_query_options_default() * } */ - public static int mln_map_pixel_for_lat_lng(long map, MemorySegment coordinate, MemorySegment out_point) { - var mh$ = mln_map_pixel_for_lat_lng.HANDLE; + public static MemorySegment mln_source_feature_query_options_default(SegmentAllocator allocator) { + var mh$ = mln_source_feature_query_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_pixel_for_lat_lng", map, coordinate, out_point); + traceDowncall("mln_source_feature_query_options_default", allocator); } - return (int)mh$.invokeExact(map, coordinate, out_point); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8740,15 +8729,13 @@ public static int mln_map_pixel_for_lat_lng(long map, MemorySegment coordinate, } } - private static class mln_map_lat_lng_for_pixel { + private static class mln_rendered_query_geometry_point { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_screen_point.layout(), - MapLibreNativeC.C_POINTER + mln_rendered_query_geometry.layout(), + mln_screen_point.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lng_for_pixel"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_point"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8756,45 +8743,45 @@ private static class mln_map_lat_lng_for_pixel { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) * } */ - public static FunctionDescriptor mln_map_lat_lng_for_pixel$descriptor() { - return mln_map_lat_lng_for_pixel.DESC; + public static FunctionDescriptor mln_rendered_query_geometry_point$descriptor() { + return mln_rendered_query_geometry_point.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) * } */ - public static MethodHandle mln_map_lat_lng_for_pixel$handle() { - return mln_map_lat_lng_for_pixel.HANDLE; + public static MethodHandle mln_rendered_query_geometry_point$handle() { + return mln_rendered_query_geometry_point.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) * } */ - public static MemorySegment mln_map_lat_lng_for_pixel$address() { - return mln_map_lat_lng_for_pixel.ADDR; + public static MemorySegment mln_rendered_query_geometry_point$address() { + return mln_rendered_query_geometry_point.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_lat_lng_for_pixel(mln_map map, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) * } */ - public static int mln_map_lat_lng_for_pixel(long map, MemorySegment point, MemorySegment out_coordinate) { - var mh$ = mln_map_lat_lng_for_pixel.HANDLE; + public static MemorySegment mln_rendered_query_geometry_point(SegmentAllocator allocator, MemorySegment point) { + var mh$ = mln_rendered_query_geometry_point.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_lat_lng_for_pixel", map, point, out_coordinate); + traceDowncall("mln_rendered_query_geometry_point", allocator, point); } - return (int)mh$.invokeExact(map, point, out_coordinate); + return (MemorySegment)mh$.invokeExact(allocator, point); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8802,16 +8789,13 @@ public static int mln_map_lat_lng_for_pixel(long map, MemorySegment point, Memor } } - private static class mln_map_pixels_for_lat_lngs { + private static class mln_rendered_query_geometry_box { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER + mln_rendered_query_geometry.layout(), + mln_screen_box.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_pixels_for_lat_lngs"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_box"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8819,45 +8803,45 @@ private static class mln_map_pixels_for_lat_lngs { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, mln_screen_point *out_points) + * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) * } */ - public static FunctionDescriptor mln_map_pixels_for_lat_lngs$descriptor() { - return mln_map_pixels_for_lat_lngs.DESC; + public static FunctionDescriptor mln_rendered_query_geometry_box$descriptor() { + return mln_rendered_query_geometry_box.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, mln_screen_point *out_points) + * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) * } */ - public static MethodHandle mln_map_pixels_for_lat_lngs$handle() { - return mln_map_pixels_for_lat_lngs.HANDLE; + public static MethodHandle mln_rendered_query_geometry_box$handle() { + return mln_rendered_query_geometry_box.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, mln_screen_point *out_points) + * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) * } */ - public static MemorySegment mln_map_pixels_for_lat_lngs$address() { - return mln_map_pixels_for_lat_lngs.ADDR; + public static MemorySegment mln_rendered_query_geometry_box$address() { + return mln_rendered_query_geometry_box.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_pixels_for_lat_lngs(mln_map map, const mln_lat_lng *coordinates, size_t coordinate_count, mln_screen_point *out_points) + * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) * } */ - public static int mln_map_pixels_for_lat_lngs(long map, MemorySegment coordinates, long coordinate_count, MemorySegment out_points) { - var mh$ = mln_map_pixels_for_lat_lngs.HANDLE; + public static MemorySegment mln_rendered_query_geometry_box(SegmentAllocator allocator, MemorySegment box) { + var mh$ = mln_rendered_query_geometry_box.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_pixels_for_lat_lngs", map, coordinates, coordinate_count, out_points); + traceDowncall("mln_rendered_query_geometry_box", allocator, box); } - return (int)mh$.invokeExact(map, coordinates, coordinate_count, out_points); + return (MemorySegment)mh$.invokeExact(allocator, box); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8865,16 +8849,14 @@ public static int mln_map_pixels_for_lat_lngs(long map, MemorySegment coordinate } } - private static class mln_map_lat_lngs_for_pixels { + private static class mln_rendered_query_geometry_line_string { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, + mln_rendered_query_geometry.layout(), MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_lat_lngs_for_pixels"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_line_string"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8882,45 +8864,45 @@ private static class mln_map_lat_lngs_for_pixels { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, mln_lat_lng *out_coordinates) + * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) * } */ - public static FunctionDescriptor mln_map_lat_lngs_for_pixels$descriptor() { - return mln_map_lat_lngs_for_pixels.DESC; + public static FunctionDescriptor mln_rendered_query_geometry_line_string$descriptor() { + return mln_rendered_query_geometry_line_string.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, mln_lat_lng *out_coordinates) + * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) * } */ - public static MethodHandle mln_map_lat_lngs_for_pixels$handle() { - return mln_map_lat_lngs_for_pixels.HANDLE; + public static MethodHandle mln_rendered_query_geometry_line_string$handle() { + return mln_rendered_query_geometry_line_string.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, mln_lat_lng *out_coordinates) + * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) * } */ - public static MemorySegment mln_map_lat_lngs_for_pixels$address() { - return mln_map_lat_lngs_for_pixels.ADDR; + public static MemorySegment mln_rendered_query_geometry_line_string$address() { + return mln_rendered_query_geometry_line_string.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_lat_lngs_for_pixels(mln_map map, const mln_screen_point *points, size_t point_count, mln_lat_lng *out_coordinates) + * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) * } */ - public static int mln_map_lat_lngs_for_pixels(long map, MemorySegment points, long point_count, MemorySegment out_coordinates) { - var mh$ = mln_map_lat_lngs_for_pixels.HANDLE; + public static MemorySegment mln_rendered_query_geometry_line_string(SegmentAllocator allocator, MemorySegment points, long point_count) { + var mh$ = mln_rendered_query_geometry_line_string.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_lat_lngs_for_pixels", map, points, point_count, out_coordinates); + traceDowncall("mln_rendered_query_geometry_line_string", allocator, points, point_count); } - return (int)mh$.invokeExact(map, points, point_count, out_coordinates); + return (MemorySegment)mh$.invokeExact(allocator, points, point_count); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -8928,11 +8910,11 @@ public static int mln_map_lat_lngs_for_pixels(long map, MemorySegment points, lo } } - private static class mln_thread_last_error_message { + private static class mln_queried_feature_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_POINTER ); + mln_queried_feature.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_thread_last_error_message"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -8940,510 +8922,168 @@ private static class mln_thread_last_error_message { /** * Function descriptor for: * {@snippet lang=c : - * const char *mln_thread_last_error_message() + * mln_queried_feature mln_queried_feature_default() * } */ - public static FunctionDescriptor mln_thread_last_error_message$descriptor() { - return mln_thread_last_error_message.DESC; + public static FunctionDescriptor mln_queried_feature_default$descriptor() { + return mln_queried_feature_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * const char *mln_thread_last_error_message() + * mln_queried_feature mln_queried_feature_default() * } */ - public static MethodHandle mln_thread_last_error_message$handle() { - return mln_thread_last_error_message.HANDLE; + public static MethodHandle mln_queried_feature_default$handle() { + return mln_queried_feature_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * const char *mln_thread_last_error_message() + * mln_queried_feature mln_queried_feature_default() * } */ - public static MemorySegment mln_thread_last_error_message$address() { - return mln_thread_last_error_message.ADDR; + public static MemorySegment mln_queried_feature_default$address() { + return mln_queried_feature_default.ADDR; } /** * {@snippet lang=c : - * const char *mln_thread_last_error_message() + * mln_queried_feature mln_queried_feature_default() * } */ - public static MemorySegment mln_thread_last_error_message() { - var mh$ = mln_thread_last_error_message.HANDLE; + public static MemorySegment mln_queried_feature_default(SegmentAllocator allocator) { + var mh$ = mln_queried_feature_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_thread_last_error_message"); + traceDowncall("mln_queried_feature_default", allocator); } - return (MemorySegment)mh$.invokeExact(); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - private static final int MLN_LOG_SEVERITY_INFO = (int)1L; + + private static class mln_queried_feature_list_count { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_count"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + /** + * Function descriptor for: * {@snippet lang=c : - * enum mln_log_severity.MLN_LOG_SEVERITY_INFO = 1 + * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) * } */ - public static int MLN_LOG_SEVERITY_INFO() { - return MLN_LOG_SEVERITY_INFO; + public static FunctionDescriptor mln_queried_feature_list_count$descriptor() { + return mln_queried_feature_list_count.DESC; } - private static final int MLN_LOG_SEVERITY_WARNING = (int)2L; + /** + * Downcall method handle for: * {@snippet lang=c : - * enum mln_log_severity.MLN_LOG_SEVERITY_WARNING = 2 + * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) * } */ - public static int MLN_LOG_SEVERITY_WARNING() { - return MLN_LOG_SEVERITY_WARNING; + public static MethodHandle mln_queried_feature_list_count$handle() { + return mln_queried_feature_list_count.HANDLE; } - private static final int MLN_LOG_SEVERITY_ERROR = (int)3L; + /** + * Address for: * {@snippet lang=c : - * enum mln_log_severity.MLN_LOG_SEVERITY_ERROR = 3 + * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) * } */ - public static int MLN_LOG_SEVERITY_ERROR() { - return MLN_LOG_SEVERITY_ERROR; + public static MemorySegment mln_queried_feature_list_count$address() { + return mln_queried_feature_list_count.ADDR; } - private static final int MLN_LOG_SEVERITY_MASK_INFO = (int)2L; + /** * {@snippet lang=c : - * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_INFO = 2 + * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) * } */ - public static int MLN_LOG_SEVERITY_MASK_INFO() { - return MLN_LOG_SEVERITY_MASK_INFO; + public static int mln_queried_feature_list_count(long list, MemorySegment out_count) { + var mh$ = mln_queried_feature_list_count.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("mln_queried_feature_list_count", list, out_count); + } + return (int)mh$.invokeExact(list, out_count); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } } - private static final int MLN_LOG_SEVERITY_MASK_WARNING = (int)4L; + + private static class mln_queried_feature_list_get { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_get"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + /** + * Function descriptor for: * {@snippet lang=c : - * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_WARNING = 4 + * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) * } */ - public static int MLN_LOG_SEVERITY_MASK_WARNING() { - return MLN_LOG_SEVERITY_MASK_WARNING; + public static FunctionDescriptor mln_queried_feature_list_get$descriptor() { + return mln_queried_feature_list_get.DESC; } - private static final int MLN_LOG_SEVERITY_MASK_ERROR = (int)8L; + /** + * Downcall method handle for: * {@snippet lang=c : - * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_ERROR = 8 + * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) * } */ - public static int MLN_LOG_SEVERITY_MASK_ERROR() { - return MLN_LOG_SEVERITY_MASK_ERROR; - } - private static final int MLN_LOG_SEVERITY_MASK_DEFAULT = (int)6L; - /** - * {@snippet lang=c : - * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_DEFAULT = 6 - * } - */ - public static int MLN_LOG_SEVERITY_MASK_DEFAULT() { - return MLN_LOG_SEVERITY_MASK_DEFAULT; - } - private static final int MLN_LOG_SEVERITY_MASK_ALL = (int)14L; - /** - * {@snippet lang=c : - * enum mln_log_severity_mask.MLN_LOG_SEVERITY_MASK_ALL = 14 - * } - */ - public static int MLN_LOG_SEVERITY_MASK_ALL() { - return MLN_LOG_SEVERITY_MASK_ALL; - } - private static final int MLN_LOG_EVENT_GENERAL = (int)0L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_GENERAL = 0 - * } - */ - public static int MLN_LOG_EVENT_GENERAL() { - return MLN_LOG_EVENT_GENERAL; - } - private static final int MLN_LOG_EVENT_SETUP = (int)1L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_SETUP = 1 - * } - */ - public static int MLN_LOG_EVENT_SETUP() { - return MLN_LOG_EVENT_SETUP; - } - private static final int MLN_LOG_EVENT_SHADER = (int)2L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_SHADER = 2 - * } - */ - public static int MLN_LOG_EVENT_SHADER() { - return MLN_LOG_EVENT_SHADER; - } - private static final int MLN_LOG_EVENT_PARSE_STYLE = (int)3L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_PARSE_STYLE = 3 - * } - */ - public static int MLN_LOG_EVENT_PARSE_STYLE() { - return MLN_LOG_EVENT_PARSE_STYLE; - } - private static final int MLN_LOG_EVENT_PARSE_TILE = (int)4L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_PARSE_TILE = 4 - * } - */ - public static int MLN_LOG_EVENT_PARSE_TILE() { - return MLN_LOG_EVENT_PARSE_TILE; - } - private static final int MLN_LOG_EVENT_RENDER = (int)5L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_RENDER = 5 - * } - */ - public static int MLN_LOG_EVENT_RENDER() { - return MLN_LOG_EVENT_RENDER; - } - private static final int MLN_LOG_EVENT_STYLE = (int)6L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_STYLE = 6 - * } - */ - public static int MLN_LOG_EVENT_STYLE() { - return MLN_LOG_EVENT_STYLE; - } - private static final int MLN_LOG_EVENT_DATABASE = (int)7L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_DATABASE = 7 - * } - */ - public static int MLN_LOG_EVENT_DATABASE() { - return MLN_LOG_EVENT_DATABASE; - } - private static final int MLN_LOG_EVENT_HTTP_REQUEST = (int)8L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_HTTP_REQUEST = 8 - * } - */ - public static int MLN_LOG_EVENT_HTTP_REQUEST() { - return MLN_LOG_EVENT_HTTP_REQUEST; - } - private static final int MLN_LOG_EVENT_SPRITE = (int)9L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_SPRITE = 9 - * } - */ - public static int MLN_LOG_EVENT_SPRITE() { - return MLN_LOG_EVENT_SPRITE; - } - private static final int MLN_LOG_EVENT_IMAGE = (int)10L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_IMAGE = 10 - * } - */ - public static int MLN_LOG_EVENT_IMAGE() { - return MLN_LOG_EVENT_IMAGE; - } - private static final int MLN_LOG_EVENT_OPENGL = (int)11L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_OPENGL = 11 - * } - */ - public static int MLN_LOG_EVENT_OPENGL() { - return MLN_LOG_EVENT_OPENGL; - } - private static final int MLN_LOG_EVENT_JNI = (int)12L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_JNI = 12 - * } - */ - public static int MLN_LOG_EVENT_JNI() { - return MLN_LOG_EVENT_JNI; - } - private static final int MLN_LOG_EVENT_ANDROID = (int)13L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_ANDROID = 13 - * } - */ - public static int MLN_LOG_EVENT_ANDROID() { - return MLN_LOG_EVENT_ANDROID; - } - private static final int MLN_LOG_EVENT_CRASH = (int)14L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_CRASH = 14 - * } - */ - public static int MLN_LOG_EVENT_CRASH() { - return MLN_LOG_EVENT_CRASH; - } - private static final int MLN_LOG_EVENT_GLYPH = (int)15L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_GLYPH = 15 - * } - */ - public static int MLN_LOG_EVENT_GLYPH() { - return MLN_LOG_EVENT_GLYPH; - } - private static final int MLN_LOG_EVENT_TIMING = (int)16L; - /** - * {@snippet lang=c : - * enum mln_log_event.MLN_LOG_EVENT_TIMING = 16 - * } - */ - public static int MLN_LOG_EVENT_TIMING() { - return MLN_LOG_EVENT_TIMING; - } - - private static class mln_log_set_callback { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_set_callback"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data) - * } - */ - public static FunctionDescriptor mln_log_set_callback$descriptor() { - return mln_log_set_callback.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data) - * } - */ - public static MethodHandle mln_log_set_callback$handle() { - return mln_log_set_callback.HANDLE; + public static MethodHandle mln_queried_feature_list_get$handle() { + return mln_queried_feature_list_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data) - * } - */ - public static MemorySegment mln_log_set_callback$address() { - return mln_log_set_callback.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_log_set_callback(mln_log_callback callback, void *user_data) - * } - */ - public static int mln_log_set_callback(MemorySegment callback, MemorySegment user_data) { - var mh$ = mln_log_set_callback.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_log_set_callback", callback, user_data); - } - return (int)mh$.invokeExact(callback, user_data); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_log_clear_callback { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_clear_callback"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_log_clear_callback() - * } - */ - public static FunctionDescriptor mln_log_clear_callback$descriptor() { - return mln_log_clear_callback.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_log_clear_callback() - * } - */ - public static MethodHandle mln_log_clear_callback$handle() { - return mln_log_clear_callback.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_log_clear_callback() - * } - */ - public static MemorySegment mln_log_clear_callback$address() { - return mln_log_clear_callback.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_log_clear_callback() - * } - */ - public static int mln_log_clear_callback() { - var mh$ = mln_log_clear_callback.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_log_clear_callback"); - } - return (int)mh$.invokeExact(); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_log_set_async_severity_mask { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_INT - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_log_set_async_severity_mask"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_log_set_async_severity_mask(uint32_t mask) - * } - */ - public static FunctionDescriptor mln_log_set_async_severity_mask$descriptor() { - return mln_log_set_async_severity_mask.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_log_set_async_severity_mask(uint32_t mask) - * } - */ - public static MethodHandle mln_log_set_async_severity_mask$handle() { - return mln_log_set_async_severity_mask.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_log_set_async_severity_mask(uint32_t mask) - * } - */ - public static MemorySegment mln_log_set_async_severity_mask$address() { - return mln_log_set_async_severity_mask.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_log_set_async_severity_mask(uint32_t mask) - * } - */ - public static int mln_log_set_async_severity_mask(int mask) { - var mh$ = mln_log_set_async_severity_mask.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_log_set_async_severity_mask", mask); - } - return (int)mh$.invokeExact(mask); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_projection_create { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_create"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_projection_create(mln_map map, mln_map_projection *out_projection) - * } - */ - public static FunctionDescriptor mln_map_projection_create$descriptor() { - return mln_map_projection_create.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_projection_create(mln_map map, mln_map_projection *out_projection) - * } - */ - public static MethodHandle mln_map_projection_create$handle() { - return mln_map_projection_create.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_projection_create(mln_map map, mln_map_projection *out_projection) + * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) * } */ - public static MemorySegment mln_map_projection_create$address() { - return mln_map_projection_create.ADDR; + public static MemorySegment mln_queried_feature_list_get$address() { + return mln_queried_feature_list_get.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_create(mln_map map, mln_map_projection *out_projection) + * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) * } */ - public static int mln_map_projection_create(long map, MemorySegment out_projection) { - var mh$ = mln_map_projection_create.HANDLE; + public static int mln_queried_feature_list_get(long list, long index, MemorySegment out_feature) { + var mh$ = mln_queried_feature_list_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_create", map, out_projection); + traceDowncall("mln_queried_feature_list_get", list, index, out_feature); } - return (int)mh$.invokeExact(map, out_projection); + return (int)mh$.invokeExact(list, index, out_feature); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9451,74 +9091,12 @@ public static int mln_map_projection_create(long map, MemorySegment out_projecti } } - private static class mln_map_projection_destroy { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, + private static class mln_queried_feature_list_destroy { + public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_destroy"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_projection_destroy(mln_map_projection projection) - * } - */ - public static FunctionDescriptor mln_map_projection_destroy$descriptor() { - return mln_map_projection_destroy.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_projection_destroy(mln_map_projection projection) - * } - */ - public static MethodHandle mln_map_projection_destroy$handle() { - return mln_map_projection_destroy.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_projection_destroy(mln_map_projection projection) - * } - */ - public static MemorySegment mln_map_projection_destroy$address() { - return mln_map_projection_destroy.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_projection_destroy(mln_map_projection projection) - * } - */ - public static int mln_map_projection_destroy(long projection) { - var mh$ = mln_map_projection_destroy.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_destroy", projection); - } - return (int)mh$.invokeExact(projection); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_projection_get_camera { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_get_camera"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_destroy"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9526,45 +9104,45 @@ private static class mln_map_projection_get_camera { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) + * void mln_queried_feature_list_destroy(mln_queried_feature_list list) * } */ - public static FunctionDescriptor mln_map_projection_get_camera$descriptor() { - return mln_map_projection_get_camera.DESC; + public static FunctionDescriptor mln_queried_feature_list_destroy$descriptor() { + return mln_queried_feature_list_destroy.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) + * void mln_queried_feature_list_destroy(mln_queried_feature_list list) * } */ - public static MethodHandle mln_map_projection_get_camera$handle() { - return mln_map_projection_get_camera.HANDLE; + public static MethodHandle mln_queried_feature_list_destroy$handle() { + return mln_queried_feature_list_destroy.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) + * void mln_queried_feature_list_destroy(mln_queried_feature_list list) * } */ - public static MemorySegment mln_map_projection_get_camera$address() { - return mln_map_projection_get_camera.ADDR; + public static MemorySegment mln_queried_feature_list_destroy$address() { + return mln_queried_feature_list_destroy.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_get_camera(mln_map_projection projection, mln_camera_options *out_camera) + * void mln_queried_feature_list_destroy(mln_queried_feature_list list) * } */ - public static int mln_map_projection_get_camera(long projection, MemorySegment out_camera) { - var mh$ = mln_map_projection_get_camera.HANDLE; + public static void mln_queried_feature_list_destroy(long list) { + var mh$ = mln_queried_feature_list_destroy.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_get_camera", projection, out_camera); + traceDowncall("mln_queried_feature_list_destroy", list); } - return (int)mh$.invokeExact(projection, out_camera); + mh$.invokeExact(list); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9572,14 +9150,16 @@ public static int mln_map_projection_get_camera(long projection, MemorySegment o } } - private static class mln_map_projection_set_camera { + private static class mln_render_session_query_rendered_features { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_camera"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_rendered_features"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9587,45 +9167,45 @@ private static class mln_map_projection_set_camera { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) + * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_projection_set_camera$descriptor() { - return mln_map_projection_set_camera.DESC; + public static FunctionDescriptor mln_render_session_query_rendered_features$descriptor() { + return mln_render_session_query_rendered_features.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) + * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_projection_set_camera$handle() { - return mln_map_projection_set_camera.HANDLE; + public static MethodHandle mln_render_session_query_rendered_features$handle() { + return mln_render_session_query_rendered_features.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) + * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_projection_set_camera$address() { - return mln_map_projection_set_camera.ADDR; + public static MemorySegment mln_render_session_query_rendered_features$address() { + return mln_render_session_query_rendered_features.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_set_camera(mln_map_projection projection, const mln_camera_options *camera) + * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, const mln_completion *completion) * } */ - public static int mln_map_projection_set_camera(long projection, MemorySegment camera) { - var mh$ = mln_map_projection_set_camera.HANDLE; + public static int mln_render_session_query_rendered_features(long session, MemorySegment geometry, MemorySegment options, MemorySegment completion) { + var mh$ = mln_render_session_query_rendered_features.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_set_camera", projection, camera); + traceDowncall("mln_render_session_query_rendered_features", session, geometry, options, completion); } - return (int)mh$.invokeExact(projection, camera); + return (int)mh$.invokeExact(session, geometry, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9633,16 +9213,16 @@ public static int mln_map_projection_set_camera(long projection, MemorySegment c } } - private static class mln_map_projection_set_visible_coordinates { + private static class mln_render_session_query_source_features { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - mln_edge_insets.layout() + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_visible_coordinates"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_source_features"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9650,45 +9230,45 @@ private static class mln_map_projection_set_visible_coordinates { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) + * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_projection_set_visible_coordinates$descriptor() { - return mln_map_projection_set_visible_coordinates.DESC; + public static FunctionDescriptor mln_render_session_query_source_features$descriptor() { + return mln_render_session_query_source_features.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) + * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, const mln_completion *completion) * } */ - public static MethodHandle mln_map_projection_set_visible_coordinates$handle() { - return mln_map_projection_set_visible_coordinates.HANDLE; + public static MethodHandle mln_render_session_query_source_features$handle() { + return mln_render_session_query_source_features.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) + * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, const mln_completion *completion) * } */ - public static MemorySegment mln_map_projection_set_visible_coordinates$address() { - return mln_map_projection_set_visible_coordinates.ADDR; + public static MemorySegment mln_render_session_query_source_features$address() { + return mln_render_session_query_source_features.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_coordinates(mln_map_projection projection, const mln_lat_lng *coordinates, size_t coordinate_count, mln_edge_insets padding) + * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, const mln_completion *completion) * } */ - public static int mln_map_projection_set_visible_coordinates(long projection, MemorySegment coordinates, long coordinate_count, MemorySegment padding) { - var mh$ = mln_map_projection_set_visible_coordinates.HANDLE; + public static int mln_render_session_query_source_features(long session, MemorySegment source_id, MemorySegment options, MemorySegment completion) { + var mh$ = mln_render_session_query_source_features.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_set_visible_coordinates", projection, coordinates, coordinate_count, padding); + traceDowncall("mln_render_session_query_source_features", session, source_id, options, completion); } - return (int)mh$.invokeExact(projection, coordinates, coordinate_count, padding); + return (int)mh$.invokeExact(session, source_id, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9696,15 +9276,19 @@ public static int mln_map_projection_set_visible_coordinates(long projection, Me } } - private static class mln_map_projection_set_visible_geometry { + private static class mln_render_session_query_feature_extensions { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_edge_insets.layout() + mln_buffer_view.layout(), + mln_buffer_view.layout(), + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_set_visible_geometry"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_feature_extensions"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9712,61 +9296,156 @@ private static class mln_map_projection_set_visible_geometry { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) + * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_projection_set_visible_geometry$descriptor() { - return mln_map_projection_set_visible_geometry.DESC; + public static FunctionDescriptor mln_render_session_query_feature_extensions$descriptor() { + return mln_render_session_query_feature_extensions.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) + * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, const mln_completion *completion) * } */ - public static MethodHandle mln_map_projection_set_visible_geometry$handle() { - return mln_map_projection_set_visible_geometry.HANDLE; + public static MethodHandle mln_render_session_query_feature_extensions$handle() { + return mln_render_session_query_feature_extensions.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) + * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, const mln_completion *completion) * } */ - public static MemorySegment mln_map_projection_set_visible_geometry$address() { - return mln_map_projection_set_visible_geometry.ADDR; + public static MemorySegment mln_render_session_query_feature_extensions$address() { + return mln_render_session_query_feature_extensions.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_set_visible_geometry(mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding) + * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, const mln_completion *completion) * } */ - public static int mln_map_projection_set_visible_geometry(long projection, MemorySegment geometry, MemorySegment padding) { - var mh$ = mln_map_projection_set_visible_geometry.HANDLE; + public static int mln_render_session_query_feature_extensions(long session, MemorySegment source_id, MemorySegment feature, MemorySegment extension, MemorySegment extension_field, MemorySegment arguments, MemorySegment completion) { + var mh$ = mln_render_session_query_feature_extensions.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_set_visible_geometry", projection, geometry, padding); + traceDowncall("mln_render_session_query_feature_extensions", session, source_id, feature, extension, extension_field, arguments, completion); } - return (int)mh$.invokeExact(projection, geometry, padding); + return (int)mh$.invokeExact(session, source_id, feature, extension, extension_field, arguments, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } + private static final int MLN_RENDER_DRIVER_CORE_WORKER = (int)1L; + /** + * {@snippet lang=c : + * enum mln_render_driver_kind.MLN_RENDER_DRIVER_CORE_WORKER = 1 + * } + */ + public static int MLN_RENDER_DRIVER_CORE_WORKER() { + return MLN_RENDER_DRIVER_CORE_WORKER; + } + private static final int MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD = (int)2L; + /** + * {@snippet lang=c : + * enum mln_render_driver_kind.MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD = 2 + * } + */ + public static int MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD() { + return MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; + } + private static final int MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION = (int)1L; + /** + * {@snippet lang=c : + * enum mln_render_session_capability_flag.MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION = 1 + * } + */ + public static int MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION() { + return MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION; + } + private static final int MLN_RENDER_SESSION_CAPABILITY_READBACK = (int)2L; + /** + * {@snippet lang=c : + * enum mln_render_session_capability_flag.MLN_RENDER_SESSION_CAPABILITY_READBACK = 2 + * } + */ + public static int MLN_RENDER_SESSION_CAPABILITY_READBACK() { + return MLN_RENDER_SESSION_CAPABILITY_READBACK; + } + private static final int MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC = (int)4L; + /** + * {@snippet lang=c : + * enum mln_render_session_capability_flag.MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC = 4 + * } + */ + public static int MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC() { + return MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC; + } + private static final int MLN_RENDER_SESSION_CAPABILITY_PRESENTATION = (int)8L; + /** + * {@snippet lang=c : + * enum mln_render_session_capability_flag.MLN_RENDER_SESSION_CAPABILITY_PRESENTATION = 8 + * } + */ + public static int MLN_RENDER_SESSION_CAPABILITY_PRESENTATION() { + return MLN_RENDER_SESSION_CAPABILITY_PRESENTATION; + } + private static final int MLN_GPU_SYNC_CPU_COMPLETE = (int)0L; + /** + * {@snippet lang=c : + * enum mln_gpu_sync_kind.MLN_GPU_SYNC_CPU_COMPLETE = 0 + * } + */ + public static int MLN_GPU_SYNC_CPU_COMPLETE() { + return MLN_GPU_SYNC_CPU_COMPLETE; + } + private static final int MLN_GPU_SYNC_METAL_SHARED_EVENT = (int)1L; + /** + * {@snippet lang=c : + * enum mln_gpu_sync_kind.MLN_GPU_SYNC_METAL_SHARED_EVENT = 1 + * } + */ + public static int MLN_GPU_SYNC_METAL_SHARED_EVENT() { + return MLN_GPU_SYNC_METAL_SHARED_EVENT; + } + private static final int MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE = (int)2L; + /** + * {@snippet lang=c : + * enum mln_gpu_sync_kind.MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE = 2 + * } + */ + public static int MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE() { + return MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE; + } + private static final int MLN_GPU_SYNC_OPENGL_FENCE = (int)3L; + /** + * {@snippet lang=c : + * enum mln_gpu_sync_kind.MLN_GPU_SYNC_OPENGL_FENCE = 3 + * } + */ + public static int MLN_GPU_SYNC_OPENGL_FENCE() { + return MLN_GPU_SYNC_OPENGL_FENCE; + } + private static final int MLN_GPU_SYNC_WEBGPU_TOKEN = (int)4L; + /** + * {@snippet lang=c : + * enum mln_gpu_sync_kind.MLN_GPU_SYNC_WEBGPU_TOKEN = 4 + * } + */ + public static int MLN_GPU_SYNC_WEBGPU_TOKEN() { + return MLN_GPU_SYNC_WEBGPU_TOKEN; + } - private static class mln_map_projection_pixel_for_lat_lng { + private static class mln_gpu_sync_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_lat_lng.layout(), - MapLibreNativeC.C_POINTER - ); + mln_gpu_sync.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_pixel_for_lat_lng"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_gpu_sync_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9774,61 +9453,183 @@ private static class mln_map_projection_pixel_for_lat_lng { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_gpu_sync mln_gpu_sync_default() * } */ - public static FunctionDescriptor mln_map_projection_pixel_for_lat_lng$descriptor() { - return mln_map_projection_pixel_for_lat_lng.DESC; + public static FunctionDescriptor mln_gpu_sync_default$descriptor() { + return mln_gpu_sync_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_gpu_sync mln_gpu_sync_default() * } */ - public static MethodHandle mln_map_projection_pixel_for_lat_lng$handle() { - return mln_map_projection_pixel_for_lat_lng.HANDLE; + public static MethodHandle mln_gpu_sync_default$handle() { + return mln_gpu_sync_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_gpu_sync mln_gpu_sync_default() * } */ - public static MemorySegment mln_map_projection_pixel_for_lat_lng$address() { - return mln_map_projection_pixel_for_lat_lng.ADDR; + public static MemorySegment mln_gpu_sync_default$address() { + return mln_gpu_sync_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_pixel_for_lat_lng(mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point *out_point) + * mln_gpu_sync mln_gpu_sync_default() * } */ - public static int mln_map_projection_pixel_for_lat_lng(long projection, MemorySegment coordinate, MemorySegment out_point) { - var mh$ = mln_map_projection_pixel_for_lat_lng.HANDLE; + public static MemorySegment mln_gpu_sync_default(SegmentAllocator allocator) { + var mh$ = mln_gpu_sync_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_pixel_for_lat_lng", projection, coordinate, out_point); + traceDowncall("mln_gpu_sync_default", allocator); } - return (int)mh$.invokeExact(projection, coordinate, out_point); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } + private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL = (int)1L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL = 1 + * } + */ + public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL() { + return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL; + } + private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL = (int)2L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL = 2 + * } + */ + public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL() { + return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL; + } + private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL = (int)4L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL = 4 + * } + */ + public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL() { + return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL; + } + private static final int MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED = (int)0L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED = 0 + * } + */ + public static int MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED() { + return MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED; + } + private static final int MLN_OPENGL_CONTEXT_PLATFORM_WGL = (int)1L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WGL = 1 + * } + */ + public static int MLN_OPENGL_CONTEXT_PLATFORM_WGL() { + return MLN_OPENGL_CONTEXT_PLATFORM_WGL; + } + private static final int MLN_OPENGL_CONTEXT_PLATFORM_EGL = (int)2L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_EGL = 2 + * } + */ + public static int MLN_OPENGL_CONTEXT_PLATFORM_EGL() { + return MLN_OPENGL_CONTEXT_PLATFORM_EGL; + } + private static final int MLN_OPENGL_CONTEXT_PLATFORM_WEBGL = (int)3L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WEBGL = 3 + * } + */ + public static int MLN_OPENGL_CONTEXT_PLATFORM_WEBGL() { + return MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + } + private static final int MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED = (int)0L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_ownership.MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED = 0 + * } + */ + public static int MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED() { + return MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED; + } + private static final int MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED = (int)1L; + /** + * {@snippet lang=c : + * enum mln_opengl_context_ownership.MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED = 1 + * } + */ + public static int MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED() { + return MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED; + } + private static final int MLN_OPENGL_CLIENT_API_UNSPECIFIED = (int)0L; + /** + * {@snippet lang=c : + * enum mln_opengl_client_api.MLN_OPENGL_CLIENT_API_UNSPECIFIED = 0 + * } + */ + public static int MLN_OPENGL_CLIENT_API_UNSPECIFIED() { + return MLN_OPENGL_CLIENT_API_UNSPECIFIED; + } + private static final int MLN_OPENGL_CLIENT_API_GL = (int)1L; + /** + * {@snippet lang=c : + * enum mln_opengl_client_api.MLN_OPENGL_CLIENT_API_GL = 1 + * } + */ + public static int MLN_OPENGL_CLIENT_API_GL() { + return MLN_OPENGL_CLIENT_API_GL; + } + private static final int MLN_OPENGL_CLIENT_API_GLES = (int)2L; + /** + * {@snippet lang=c : + * enum mln_opengl_client_api.MLN_OPENGL_CLIENT_API_GLES = 2 + * } + */ + public static int MLN_OPENGL_CLIENT_API_GLES() { + return MLN_OPENGL_CLIENT_API_GLES; + } + private static final int MLN_WEBGL_CONTEXT_EXISTING = (int)0L; + /** + * {@snippet lang=c : + * enum mln_webgl_context_kind.MLN_WEBGL_CONTEXT_EXISTING = 0 + * } + */ + public static int MLN_WEBGL_CONTEXT_EXISTING() { + return MLN_WEBGL_CONTEXT_EXISTING; + } + private static final int MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS = (int)1L; + /** + * {@snippet lang=c : + * enum mln_webgl_context_kind.MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS = 1 + * } + */ + public static int MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS() { + return MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS; + } - private static class mln_map_projection_lat_lng_for_pixel { + private static class mln_render_session_attach_options_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_screen_point.layout(), - MapLibreNativeC.C_POINTER - ); + mln_render_session_attach_options.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_projection_lat_lng_for_pixel"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_attach_options_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9836,45 +9637,45 @@ private static class mln_map_projection_lat_lng_for_pixel { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_render_session_attach_options mln_render_session_attach_options_default() * } */ - public static FunctionDescriptor mln_map_projection_lat_lng_for_pixel$descriptor() { - return mln_map_projection_lat_lng_for_pixel.DESC; + public static FunctionDescriptor mln_render_session_attach_options_default$descriptor() { + return mln_render_session_attach_options_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_render_session_attach_options mln_render_session_attach_options_default() * } */ - public static MethodHandle mln_map_projection_lat_lng_for_pixel$handle() { - return mln_map_projection_lat_lng_for_pixel.HANDLE; + public static MethodHandle mln_render_session_attach_options_default$handle() { + return mln_render_session_attach_options_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_render_session_attach_options mln_render_session_attach_options_default() * } */ - public static MemorySegment mln_map_projection_lat_lng_for_pixel$address() { - return mln_map_projection_lat_lng_for_pixel.ADDR; + public static MemorySegment mln_render_session_attach_options_default$address() { + return mln_render_session_attach_options_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_projection_lat_lng_for_pixel(mln_map_projection projection, mln_screen_point point, mln_lat_lng *out_coordinate) + * mln_render_session_attach_options mln_render_session_attach_options_default() * } */ - public static int mln_map_projection_lat_lng_for_pixel(long projection, MemorySegment point, MemorySegment out_coordinate) { - var mh$ = mln_map_projection_lat_lng_for_pixel.HANDLE; + public static MemorySegment mln_render_session_attach_options_default(SegmentAllocator allocator) { + var mh$ = mln_render_session_attach_options_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_projection_lat_lng_for_pixel", projection, point, out_coordinate); + traceDowncall("mln_render_session_attach_options_default", allocator); } - return (int)mh$.invokeExact(projection, point, out_coordinate); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9882,14 +9683,15 @@ public static int mln_map_projection_lat_lng_for_pixel(long projection, MemorySe } } - private static class mln_projected_meters_for_lat_lng { + private static class mln_render_target_extent_physical_size { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - mln_lat_lng.layout(), + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_projected_meters_for_lat_lng"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_target_extent_physical_size"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9897,45 +9699,45 @@ private static class mln_projected_meters_for_lat_lng { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) + * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) * } */ - public static FunctionDescriptor mln_projected_meters_for_lat_lng$descriptor() { - return mln_projected_meters_for_lat_lng.DESC; + public static FunctionDescriptor mln_render_target_extent_physical_size$descriptor() { + return mln_render_target_extent_physical_size.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) + * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) * } */ - public static MethodHandle mln_projected_meters_for_lat_lng$handle() { - return mln_projected_meters_for_lat_lng.HANDLE; + public static MethodHandle mln_render_target_extent_physical_size$handle() { + return mln_render_target_extent_physical_size.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) + * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) * } */ - public static MemorySegment mln_projected_meters_for_lat_lng$address() { - return mln_projected_meters_for_lat_lng.ADDR; + public static MemorySegment mln_render_target_extent_physical_size$address() { + return mln_render_target_extent_physical_size.ADDR; } /** * {@snippet lang=c : - * mln_status mln_projected_meters_for_lat_lng(mln_lat_lng coordinate, mln_projected_meters *out_meters) + * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) * } */ - public static int mln_projected_meters_for_lat_lng(MemorySegment coordinate, MemorySegment out_meters) { - var mh$ = mln_projected_meters_for_lat_lng.HANDLE; + public static int mln_render_target_extent_physical_size(MemorySegment extent, MemorySegment out_width, MemorySegment out_height) { + var mh$ = mln_render_target_extent_physical_size.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_projected_meters_for_lat_lng", coordinate, out_meters); + traceDowncall("mln_render_target_extent_physical_size", extent, out_width, out_height); } - return (int)mh$.invokeExact(coordinate, out_meters); + return (int)mh$.invokeExact(extent, out_width, out_height); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -9943,14 +9745,11 @@ public static int mln_projected_meters_for_lat_lng(MemorySegment coordinate, Mem } } - private static class mln_lat_lng_for_projected_meters { + private static class mln_opengl_supported_context_provider_mask { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - mln_projected_meters.layout(), - MapLibreNativeC.C_POINTER - ); + MapLibreNativeC.C_INT ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_lat_lng_for_projected_meters"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_supported_context_provider_mask"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -9958,247 +9757,201 @@ private static class mln_lat_lng_for_projected_meters { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) + * uint32_t mln_opengl_supported_context_provider_mask() * } */ - public static FunctionDescriptor mln_lat_lng_for_projected_meters$descriptor() { - return mln_lat_lng_for_projected_meters.DESC; + public static FunctionDescriptor mln_opengl_supported_context_provider_mask$descriptor() { + return mln_opengl_supported_context_provider_mask.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) + * uint32_t mln_opengl_supported_context_provider_mask() * } */ - public static MethodHandle mln_lat_lng_for_projected_meters$handle() { - return mln_lat_lng_for_projected_meters.HANDLE; + public static MethodHandle mln_opengl_supported_context_provider_mask$handle() { + return mln_opengl_supported_context_provider_mask.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) + * uint32_t mln_opengl_supported_context_provider_mask() * } */ - public static MemorySegment mln_lat_lng_for_projected_meters$address() { - return mln_lat_lng_for_projected_meters.ADDR; + public static MemorySegment mln_opengl_supported_context_provider_mask$address() { + return mln_opengl_supported_context_provider_mask.ADDR; } /** * {@snippet lang=c : - * mln_status mln_lat_lng_for_projected_meters(mln_projected_meters meters, mln_lat_lng *out_coordinate) + * uint32_t mln_opengl_supported_context_provider_mask() * } */ - public static int mln_lat_lng_for_projected_meters(MemorySegment meters, MemorySegment out_coordinate) { - var mh$ = mln_lat_lng_for_projected_meters.HANDLE; + public static int mln_opengl_supported_context_provider_mask() { + var mh$ = mln_opengl_supported_context_provider_mask.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_lat_lng_for_projected_meters", meters, out_coordinate); + traceDowncall("mln_opengl_supported_context_provider_mask"); } - return (int)mh$.invokeExact(meters, out_coordinate); + return (int)mh$.invokeExact(); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT = (int)1L; + private static final int MLN_RENDER_RESULT_RENDERED = (int)0L; /** * {@snippet lang=c : - * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT = 1 + * enum mln_render_result.MLN_RENDER_RESULT_RENDERED = 0 * } */ - public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT() { - return MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT; + public static int MLN_RENDER_RESULT_RENDERED() { + return MLN_RENDER_RESULT_RENDERED; } - private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX = (int)2L; + private static final int MLN_RENDER_RESULT_NO_UPDATE = (int)1L; /** * {@snippet lang=c : - * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX = 2 + * enum mln_render_result.MLN_RENDER_RESULT_NO_UPDATE = 1 * } */ - public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX() { - return MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX; + public static int MLN_RENDER_RESULT_NO_UPDATE() { + return MLN_RENDER_RESULT_NO_UPDATE; } - private static final int MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING = (int)3L; + private static final int MLN_RENDER_RESULT_SIZE_PENDING = (int)2L; /** * {@snippet lang=c : - * enum mln_rendered_query_geometry_type.MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING = 3 + * enum mln_render_result.MLN_RENDER_RESULT_SIZE_PENDING = 2 * } */ - public static int MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING() { - return MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING; + public static int MLN_RENDER_RESULT_SIZE_PENDING() { + return MLN_RENDER_RESULT_SIZE_PENDING; } - private static final int MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS = (int)1L; + private static final int MLN_RENDER_RESULT_TARGET_NOT_READY = (int)3L; /** * {@snippet lang=c : - * enum mln_rendered_feature_query_option_field.MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS = 1 + * enum mln_render_result.MLN_RENDER_RESULT_TARGET_NOT_READY = 3 * } */ - public static int MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS() { - return MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS; + public static int MLN_RENDER_RESULT_TARGET_NOT_READY() { + return MLN_RENDER_RESULT_TARGET_NOT_READY; } - private static final int MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS = (int)1L; + private static final int MLN_RENDER_RESULT_SUPERSEDED = (int)4L; /** * {@snippet lang=c : - * enum mln_source_feature_query_option_field.MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS = 1 + * enum mln_render_result.MLN_RENDER_RESULT_SUPERSEDED = 4 * } */ - public static int MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS() { - return MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS; + public static int MLN_RENDER_RESULT_SUPERSEDED() { + return MLN_RENDER_RESULT_SUPERSEDED; } - private static final int MLN_QUERIED_FEATURE_SOURCE_ID = (int)1L; + private static final int MLN_RENDER_RESULT_DEADLINE_MISSED = (int)5L; /** * {@snippet lang=c : - * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_ID = 1 + * enum mln_render_result.MLN_RENDER_RESULT_DEADLINE_MISSED = 5 * } */ - public static int MLN_QUERIED_FEATURE_SOURCE_ID() { - return MLN_QUERIED_FEATURE_SOURCE_ID; + public static int MLN_RENDER_RESULT_DEADLINE_MISSED() { + return MLN_RENDER_RESULT_DEADLINE_MISSED; } - private static final int MLN_QUERIED_FEATURE_SOURCE_LAYER_ID = (int)2L; + private static final int MLN_RENDER_SESSION_STATE_ATTACHING = (int)1L; /** * {@snippet lang=c : - * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_SOURCE_LAYER_ID = 2 + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_ATTACHING = 1 * } */ - public static int MLN_QUERIED_FEATURE_SOURCE_LAYER_ID() { - return MLN_QUERIED_FEATURE_SOURCE_LAYER_ID; + public static int MLN_RENDER_SESSION_STATE_ATTACHING() { + return MLN_RENDER_SESSION_STATE_ATTACHING; } - private static final int MLN_QUERIED_FEATURE_STATE = (int)4L; + private static final int MLN_RENDER_SESSION_STATE_ATTACHED = (int)2L; /** * {@snippet lang=c : - * enum mln_queried_feature_field.MLN_QUERIED_FEATURE_STATE = 4 + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_ATTACHED = 2 * } */ - public static int MLN_QUERIED_FEATURE_STATE() { - return MLN_QUERIED_FEATURE_STATE; - } - - private static class mln_rendered_feature_query_options_default { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_rendered_feature_query_options.layout() ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_feature_query_options_default"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int MLN_RENDER_SESSION_STATE_ATTACHED() { + return MLN_RENDER_SESSION_STATE_ATTACHED; } - + private static final int MLN_RENDER_SESSION_STATE_DETACHING = (int)3L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_DETACHING = 3 * } */ - public static FunctionDescriptor mln_rendered_feature_query_options_default$descriptor() { - return mln_rendered_feature_query_options_default.DESC; + public static int MLN_RENDER_SESSION_STATE_DETACHING() { + return MLN_RENDER_SESSION_STATE_DETACHING; } - + private static final int MLN_RENDER_SESSION_STATE_DETACHED = (int)4L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_DETACHED = 4 * } */ - public static MethodHandle mln_rendered_feature_query_options_default$handle() { - return mln_rendered_feature_query_options_default.HANDLE; + public static int MLN_RENDER_SESSION_STATE_DETACHED() { + return MLN_RENDER_SESSION_STATE_DETACHED; } - + private static final int MLN_RENDER_SESSION_STATE_TARGET_LOST = (int)5L; /** - * Address for: * {@snippet lang=c : - * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_TARGET_LOST = 5 * } */ - public static MemorySegment mln_rendered_feature_query_options_default$address() { - return mln_rendered_feature_query_options_default.ADDR; + public static int MLN_RENDER_SESSION_STATE_TARGET_LOST() { + return MLN_RENDER_SESSION_STATE_TARGET_LOST; } - + private static final int MLN_RENDER_SESSION_STATE_ABANDONED = (int)6L; /** * {@snippet lang=c : - * mln_rendered_feature_query_options mln_rendered_feature_query_options_default() + * enum mln_render_session_state.MLN_RENDER_SESSION_STATE_ABANDONED = 6 * } */ - public static MemorySegment mln_rendered_feature_query_options_default(SegmentAllocator allocator) { - var mh$ = mln_rendered_feature_query_options_default.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_rendered_feature_query_options_default", allocator); - } - return (MemorySegment)mh$.invokeExact(allocator); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_source_feature_query_options_default { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_source_feature_query_options.layout() ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_source_feature_query_options_default"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int MLN_RENDER_SESSION_STATE_ABANDONED() { + return MLN_RENDER_SESSION_STATE_ABANDONED; } - + private static final int MLN_FRAME_DEMAND_IF_NEEDED = (int)1L; /** - * Function descriptor for: * {@snippet lang=c : - * mln_source_feature_query_options mln_source_feature_query_options_default() + * enum mln_frame_demand_flag.MLN_FRAME_DEMAND_IF_NEEDED = 1 * } */ - public static FunctionDescriptor mln_source_feature_query_options_default$descriptor() { - return mln_source_feature_query_options_default.DESC; + public static int MLN_FRAME_DEMAND_IF_NEEDED() { + return MLN_FRAME_DEMAND_IF_NEEDED; } - + private static final int MLN_FRAME_DEMAND_PRESENT = (int)2L; /** - * Downcall method handle for: * {@snippet lang=c : - * mln_source_feature_query_options mln_source_feature_query_options_default() + * enum mln_frame_demand_flag.MLN_FRAME_DEMAND_PRESENT = 2 * } */ - public static MethodHandle mln_source_feature_query_options_default$handle() { - return mln_source_feature_query_options_default.HANDLE; + public static int MLN_FRAME_DEMAND_PRESENT() { + return MLN_FRAME_DEMAND_PRESENT; } - + private static final int MLN_RENDER_ABANDON_DISPOSITION_CLEAN = (int)0L; /** - * Address for: * {@snippet lang=c : - * mln_source_feature_query_options mln_source_feature_query_options_default() + * enum mln_render_abandon_disposition.MLN_RENDER_ABANDON_DISPOSITION_CLEAN = 0 * } */ - public static MemorySegment mln_source_feature_query_options_default$address() { - return mln_source_feature_query_options_default.ADDR; + public static int MLN_RENDER_ABANDON_DISPOSITION_CLEAN() { + return MLN_RENDER_ABANDON_DISPOSITION_CLEAN; } - + private static final int MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED = (int)1L; /** * {@snippet lang=c : - * mln_source_feature_query_options mln_source_feature_query_options_default() + * enum mln_render_abandon_disposition.MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED = 1 * } */ - public static MemorySegment mln_source_feature_query_options_default(SegmentAllocator allocator) { - var mh$ = mln_source_feature_query_options_default.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_source_feature_query_options_default", allocator); - } - return (MemorySegment)mh$.invokeExact(allocator); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } + public static int MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED() { + return MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED; } - private static class mln_rendered_query_geometry_point { + private static class mln_frame_demand_default { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_rendered_query_geometry.layout(), - mln_screen_point.layout() - ); + mln_frame_demand.layout() ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_point"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_frame_demand_default"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10206,45 +9959,45 @@ private static class mln_rendered_query_geometry_point { /** * Function descriptor for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) + * mln_frame_demand mln_frame_demand_default() * } */ - public static FunctionDescriptor mln_rendered_query_geometry_point$descriptor() { - return mln_rendered_query_geometry_point.DESC; + public static FunctionDescriptor mln_frame_demand_default$descriptor() { + return mln_frame_demand_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) + * mln_frame_demand mln_frame_demand_default() * } */ - public static MethodHandle mln_rendered_query_geometry_point$handle() { - return mln_rendered_query_geometry_point.HANDLE; + public static MethodHandle mln_frame_demand_default$handle() { + return mln_frame_demand_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) + * mln_frame_demand mln_frame_demand_default() * } */ - public static MemorySegment mln_rendered_query_geometry_point$address() { - return mln_rendered_query_geometry_point.ADDR; + public static MemorySegment mln_frame_demand_default$address() { + return mln_frame_demand_default.ADDR; } /** * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_point(mln_screen_point point) + * mln_frame_demand mln_frame_demand_default() * } */ - public static MemorySegment mln_rendered_query_geometry_point(SegmentAllocator allocator, MemorySegment point) { - var mh$ = mln_rendered_query_geometry_point.HANDLE; + public static MemorySegment mln_frame_demand_default(SegmentAllocator allocator) { + var mh$ = mln_frame_demand_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_rendered_query_geometry_point", allocator, point); + traceDowncall("mln_frame_demand_default", allocator); } - return (MemorySegment)mh$.invokeExact(allocator, point); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10252,13 +10005,14 @@ public static MemorySegment mln_rendered_query_geometry_point(SegmentAllocator a } } - private static class mln_rendered_query_geometry_box { + private static class mln_render_session_get_capabilities { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_rendered_query_geometry.layout(), - mln_screen_box.layout() + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_box"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_get_capabilities"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10266,45 +10020,45 @@ private static class mln_rendered_query_geometry_box { /** * Function descriptor for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) + * mln_status mln_render_session_get_capabilities(mln_render_session session, mln_render_session_capabilities *out_capabilities) * } */ - public static FunctionDescriptor mln_rendered_query_geometry_box$descriptor() { - return mln_rendered_query_geometry_box.DESC; + public static FunctionDescriptor mln_render_session_get_capabilities$descriptor() { + return mln_render_session_get_capabilities.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) + * mln_status mln_render_session_get_capabilities(mln_render_session session, mln_render_session_capabilities *out_capabilities) * } */ - public static MethodHandle mln_rendered_query_geometry_box$handle() { - return mln_rendered_query_geometry_box.HANDLE; + public static MethodHandle mln_render_session_get_capabilities$handle() { + return mln_render_session_get_capabilities.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) + * mln_status mln_render_session_get_capabilities(mln_render_session session, mln_render_session_capabilities *out_capabilities) * } */ - public static MemorySegment mln_rendered_query_geometry_box$address() { - return mln_rendered_query_geometry_box.ADDR; + public static MemorySegment mln_render_session_get_capabilities$address() { + return mln_render_session_get_capabilities.ADDR; } /** * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_box(mln_screen_box box) + * mln_status mln_render_session_get_capabilities(mln_render_session session, mln_render_session_capabilities *out_capabilities) * } */ - public static MemorySegment mln_rendered_query_geometry_box(SegmentAllocator allocator, MemorySegment box) { - var mh$ = mln_rendered_query_geometry_box.HANDLE; + public static int mln_render_session_get_capabilities(long session, MemorySegment out_capabilities) { + var mh$ = mln_render_session_get_capabilities.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_rendered_query_geometry_box", allocator, box); + traceDowncall("mln_render_session_get_capabilities", session, out_capabilities); } - return (MemorySegment)mh$.invokeExact(allocator, box); + return (int)mh$.invokeExact(session, out_capabilities); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10312,14 +10066,14 @@ public static MemorySegment mln_rendered_query_geometry_box(SegmentAllocator all } } - private static class mln_rendered_query_geometry_line_string { + private static class mln_render_session_get_snapshot { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_rendered_query_geometry.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_rendered_query_geometry_line_string"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_get_snapshot"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10327,45 +10081,45 @@ private static class mln_rendered_query_geometry_line_string { /** * Function descriptor for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) + * mln_status mln_render_session_get_snapshot(mln_render_session session, mln_render_session_snapshot *out_snapshot) * } */ - public static FunctionDescriptor mln_rendered_query_geometry_line_string$descriptor() { - return mln_rendered_query_geometry_line_string.DESC; + public static FunctionDescriptor mln_render_session_get_snapshot$descriptor() { + return mln_render_session_get_snapshot.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) + * mln_status mln_render_session_get_snapshot(mln_render_session session, mln_render_session_snapshot *out_snapshot) * } */ - public static MethodHandle mln_rendered_query_geometry_line_string$handle() { - return mln_rendered_query_geometry_line_string.HANDLE; + public static MethodHandle mln_render_session_get_snapshot$handle() { + return mln_render_session_get_snapshot.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) + * mln_status mln_render_session_get_snapshot(mln_render_session session, mln_render_session_snapshot *out_snapshot) * } */ - public static MemorySegment mln_rendered_query_geometry_line_string$address() { - return mln_rendered_query_geometry_line_string.ADDR; + public static MemorySegment mln_render_session_get_snapshot$address() { + return mln_render_session_get_snapshot.ADDR; } /** * {@snippet lang=c : - * mln_rendered_query_geometry mln_rendered_query_geometry_line_string(const mln_screen_point *points, size_t point_count) + * mln_status mln_render_session_get_snapshot(mln_render_session session, mln_render_session_snapshot *out_snapshot) * } */ - public static MemorySegment mln_rendered_query_geometry_line_string(SegmentAllocator allocator, MemorySegment points, long point_count) { - var mh$ = mln_rendered_query_geometry_line_string.HANDLE; + public static int mln_render_session_get_snapshot(long session, MemorySegment out_snapshot) { + var mh$ = mln_render_session_get_snapshot.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_rendered_query_geometry_line_string", allocator, points, point_count); + traceDowncall("mln_render_session_get_snapshot", session, out_snapshot); } - return (MemorySegment)mh$.invokeExact(allocator, points, point_count); + return (int)mh$.invokeExact(session, out_snapshot); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10373,11 +10127,14 @@ public static MemorySegment mln_rendered_query_geometry_line_string(SegmentAlloc } } - private static class mln_queried_feature_default { + private static class mln_render_session_request_frame { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - mln_queried_feature.layout() ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_default"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_request_frame"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10385,45 +10142,45 @@ private static class mln_queried_feature_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_queried_feature mln_queried_feature_default() + * mln_status mln_render_session_request_frame(mln_render_session session, const mln_frame_demand *demand) * } */ - public static FunctionDescriptor mln_queried_feature_default$descriptor() { - return mln_queried_feature_default.DESC; + public static FunctionDescriptor mln_render_session_request_frame$descriptor() { + return mln_render_session_request_frame.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_queried_feature mln_queried_feature_default() + * mln_status mln_render_session_request_frame(mln_render_session session, const mln_frame_demand *demand) * } */ - public static MethodHandle mln_queried_feature_default$handle() { - return mln_queried_feature_default.HANDLE; + public static MethodHandle mln_render_session_request_frame$handle() { + return mln_render_session_request_frame.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_queried_feature mln_queried_feature_default() + * mln_status mln_render_session_request_frame(mln_render_session session, const mln_frame_demand *demand) * } */ - public static MemorySegment mln_queried_feature_default$address() { - return mln_queried_feature_default.ADDR; + public static MemorySegment mln_render_session_request_frame$address() { + return mln_render_session_request_frame.ADDR; } /** * {@snippet lang=c : - * mln_queried_feature mln_queried_feature_default() + * mln_status mln_render_session_request_frame(mln_render_session session, const mln_frame_demand *demand) * } */ - public static MemorySegment mln_queried_feature_default(SegmentAllocator allocator) { - var mh$ = mln_queried_feature_default.HANDLE; + public static int mln_render_session_request_frame(long session, MemorySegment demand) { + var mh$ = mln_render_session_request_frame.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_queried_feature_default", allocator); + traceDowncall("mln_render_session_request_frame", session, demand); } - return (MemorySegment)mh$.invokeExact(allocator); + return (int)mh$.invokeExact(session, demand); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10431,14 +10188,14 @@ public static MemorySegment mln_queried_feature_default(SegmentAllocator allocat } } - private static class mln_queried_feature_list_count { + private static class mln_render_session_drain_frame_results { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_count"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_drain_frame_results"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10446,45 +10203,45 @@ private static class mln_queried_feature_list_count { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) + * mln_status mln_render_session_drain_frame_results(mln_render_session session, mln_render_frame_batch *out_batch) * } */ - public static FunctionDescriptor mln_queried_feature_list_count$descriptor() { - return mln_queried_feature_list_count.DESC; + public static FunctionDescriptor mln_render_session_drain_frame_results$descriptor() { + return mln_render_session_drain_frame_results.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) + * mln_status mln_render_session_drain_frame_results(mln_render_session session, mln_render_frame_batch *out_batch) * } */ - public static MethodHandle mln_queried_feature_list_count$handle() { - return mln_queried_feature_list_count.HANDLE; + public static MethodHandle mln_render_session_drain_frame_results$handle() { + return mln_render_session_drain_frame_results.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) + * mln_status mln_render_session_drain_frame_results(mln_render_session session, mln_render_frame_batch *out_batch) * } */ - public static MemorySegment mln_queried_feature_list_count$address() { - return mln_queried_feature_list_count.ADDR; + public static MemorySegment mln_render_session_drain_frame_results$address() { + return mln_render_session_drain_frame_results.ADDR; } /** * {@snippet lang=c : - * mln_status mln_queried_feature_list_count(mln_queried_feature_list list, size_t *out_count) + * mln_status mln_render_session_drain_frame_results(mln_render_session session, mln_render_frame_batch *out_batch) * } */ - public static int mln_queried_feature_list_count(long list, MemorySegment out_count) { - var mh$ = mln_queried_feature_list_count.HANDLE; + public static int mln_render_session_drain_frame_results(long session, MemorySegment out_batch) { + var mh$ = mln_render_session_drain_frame_results.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_queried_feature_list_count", list, out_count); + traceDowncall("mln_render_session_drain_frame_results", session, out_batch); } - return (int)mh$.invokeExact(list, out_count); + return (int)mh$.invokeExact(session, out_batch); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10492,15 +10249,14 @@ public static int mln_queried_feature_list_count(long list, MemorySegment out_co } } - private static class mln_queried_feature_list_get { + private static class mln_render_frame_batch_count { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_get"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_frame_batch_count"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10508,45 +10264,45 @@ private static class mln_queried_feature_list_get { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) + * mln_status mln_render_frame_batch_count(mln_render_frame_batch batch, size_t *out_count) * } */ - public static FunctionDescriptor mln_queried_feature_list_get$descriptor() { - return mln_queried_feature_list_get.DESC; + public static FunctionDescriptor mln_render_frame_batch_count$descriptor() { + return mln_render_frame_batch_count.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) + * mln_status mln_render_frame_batch_count(mln_render_frame_batch batch, size_t *out_count) * } */ - public static MethodHandle mln_queried_feature_list_get$handle() { - return mln_queried_feature_list_get.HANDLE; + public static MethodHandle mln_render_frame_batch_count$handle() { + return mln_render_frame_batch_count.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) + * mln_status mln_render_frame_batch_count(mln_render_frame_batch batch, size_t *out_count) * } */ - public static MemorySegment mln_queried_feature_list_get$address() { - return mln_queried_feature_list_get.ADDR; + public static MemorySegment mln_render_frame_batch_count$address() { + return mln_render_frame_batch_count.ADDR; } /** * {@snippet lang=c : - * mln_status mln_queried_feature_list_get(mln_queried_feature_list list, size_t index, mln_queried_feature *out_feature) + * mln_status mln_render_frame_batch_count(mln_render_frame_batch batch, size_t *out_count) * } */ - public static int mln_queried_feature_list_get(long list, long index, MemorySegment out_feature) { - var mh$ = mln_queried_feature_list_get.HANDLE; + public static int mln_render_frame_batch_count(long batch, MemorySegment out_count) { + var mh$ = mln_render_frame_batch_count.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_queried_feature_list_get", list, index, out_feature); + traceDowncall("mln_render_frame_batch_count", batch, out_count); } - return (int)mh$.invokeExact(list, index, out_feature); + return (int)mh$.invokeExact(batch, out_count); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10554,12 +10310,15 @@ public static int mln_queried_feature_list_get(long list, long index, MemorySegm } } - private static class mln_queried_feature_list_destroy { - public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - MapLibreNativeC.C_LONG + private static class mln_render_frame_batch_get { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_queried_feature_list_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_frame_batch_get"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10567,45 +10326,45 @@ private static class mln_queried_feature_list_destroy { /** * Function descriptor for: * {@snippet lang=c : - * void mln_queried_feature_list_destroy(mln_queried_feature_list list) + * mln_status mln_render_frame_batch_get(mln_render_frame_batch batch, size_t index, mln_render_frame_result *out_result) * } */ - public static FunctionDescriptor mln_queried_feature_list_destroy$descriptor() { - return mln_queried_feature_list_destroy.DESC; + public static FunctionDescriptor mln_render_frame_batch_get$descriptor() { + return mln_render_frame_batch_get.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * void mln_queried_feature_list_destroy(mln_queried_feature_list list) + * mln_status mln_render_frame_batch_get(mln_render_frame_batch batch, size_t index, mln_render_frame_result *out_result) * } */ - public static MethodHandle mln_queried_feature_list_destroy$handle() { - return mln_queried_feature_list_destroy.HANDLE; + public static MethodHandle mln_render_frame_batch_get$handle() { + return mln_render_frame_batch_get.HANDLE; } /** * Address for: * {@snippet lang=c : - * void mln_queried_feature_list_destroy(mln_queried_feature_list list) + * mln_status mln_render_frame_batch_get(mln_render_frame_batch batch, size_t index, mln_render_frame_result *out_result) * } */ - public static MemorySegment mln_queried_feature_list_destroy$address() { - return mln_queried_feature_list_destroy.ADDR; + public static MemorySegment mln_render_frame_batch_get$address() { + return mln_render_frame_batch_get.ADDR; } /** * {@snippet lang=c : - * void mln_queried_feature_list_destroy(mln_queried_feature_list list) + * mln_status mln_render_frame_batch_get(mln_render_frame_batch batch, size_t index, mln_render_frame_result *out_result) * } */ - public static void mln_queried_feature_list_destroy(long list) { - var mh$ = mln_queried_feature_list_destroy.HANDLE; + public static int mln_render_frame_batch_get(long batch, long index, MemorySegment out_result) { + var mh$ = mln_render_frame_batch_get.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_queried_feature_list_destroy", list); + traceDowncall("mln_render_frame_batch_get", batch, index, out_result); } - mh$.invokeExact(list); + return (int)mh$.invokeExact(batch, index, out_result); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10613,16 +10372,12 @@ public static void mln_queried_feature_list_destroy(long list) { } } - private static class mln_render_session_query_rendered_features { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER + private static class mln_render_frame_batch_release { + public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_LONG ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_rendered_features"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_frame_batch_release"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10630,45 +10385,45 @@ private static class mln_render_session_query_rendered_features { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, mln_queried_feature_list *out_result) + * void mln_render_frame_batch_release(mln_render_frame_batch batch) * } */ - public static FunctionDescriptor mln_render_session_query_rendered_features$descriptor() { - return mln_render_session_query_rendered_features.DESC; + public static FunctionDescriptor mln_render_frame_batch_release$descriptor() { + return mln_render_frame_batch_release.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, mln_queried_feature_list *out_result) + * void mln_render_frame_batch_release(mln_render_frame_batch batch) * } */ - public static MethodHandle mln_render_session_query_rendered_features$handle() { - return mln_render_session_query_rendered_features.HANDLE; + public static MethodHandle mln_render_frame_batch_release$handle() { + return mln_render_frame_batch_release.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, mln_queried_feature_list *out_result) + * void mln_render_frame_batch_release(mln_render_frame_batch batch) * } */ - public static MemorySegment mln_render_session_query_rendered_features$address() { - return mln_render_session_query_rendered_features.ADDR; + public static MemorySegment mln_render_frame_batch_release$address() { + return mln_render_frame_batch_release.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_query_rendered_features(mln_render_session session, const mln_rendered_query_geometry *geometry, const mln_rendered_feature_query_options *options, mln_queried_feature_list *out_result) + * void mln_render_frame_batch_release(mln_render_frame_batch batch) * } */ - public static int mln_render_session_query_rendered_features(long session, MemorySegment geometry, MemorySegment options, MemorySegment out_result) { - var mh$ = mln_render_session_query_rendered_features.HANDLE; + public static void mln_render_frame_batch_release(long batch) { + var mh$ = mln_render_frame_batch_release.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_query_rendered_features", session, geometry, options, out_result); + traceDowncall("mln_render_frame_batch_release", batch); } - return (int)mh$.invokeExact(session, geometry, options, out_result); + mh$.invokeExact(batch); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10676,16 +10431,14 @@ public static int mln_render_session_query_rendered_features(long session, Memor } } - private static class mln_render_session_query_source_features { + private static class mln_render_session_acquire_frame { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_source_features"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_acquire_frame"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10693,45 +10446,45 @@ private static class mln_render_session_query_source_features { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, mln_queried_feature_list *out_result) + * mln_status mln_render_session_acquire_frame(mln_render_session session, mln_acquired_frame *out_frame) * } */ - public static FunctionDescriptor mln_render_session_query_source_features$descriptor() { - return mln_render_session_query_source_features.DESC; + public static FunctionDescriptor mln_render_session_acquire_frame$descriptor() { + return mln_render_session_acquire_frame.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, mln_queried_feature_list *out_result) + * mln_status mln_render_session_acquire_frame(mln_render_session session, mln_acquired_frame *out_frame) * } */ - public static MethodHandle mln_render_session_query_source_features$handle() { - return mln_render_session_query_source_features.HANDLE; + public static MethodHandle mln_render_session_acquire_frame$handle() { + return mln_render_session_acquire_frame.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, mln_queried_feature_list *out_result) + * mln_status mln_render_session_acquire_frame(mln_render_session session, mln_acquired_frame *out_frame) * } */ - public static MemorySegment mln_render_session_query_source_features$address() { - return mln_render_session_query_source_features.ADDR; + public static MemorySegment mln_render_session_acquire_frame$address() { + return mln_render_session_acquire_frame.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_query_source_features(mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options *options, mln_queried_feature_list *out_result) + * mln_status mln_render_session_acquire_frame(mln_render_session session, mln_acquired_frame *out_frame) * } */ - public static int mln_render_session_query_source_features(long session, MemorySegment source_id, MemorySegment options, MemorySegment out_result) { - var mh$ = mln_render_session_query_source_features.HANDLE; + public static int mln_render_session_acquire_frame(long session, MemorySegment out_frame) { + var mh$ = mln_render_session_acquire_frame.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_query_source_features", session, source_id, options, out_result); + traceDowncall("mln_render_session_acquire_frame", session, out_frame); } - return (int)mh$.invokeExact(session, source_id, options, out_result); + return (int)mh$.invokeExact(session, out_frame); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10739,19 +10492,14 @@ public static int mln_render_session_query_source_features(long session, MemoryS } } - private static class mln_render_session_query_feature_extensions { + private static class mln_acquired_frame_get_result { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - mln_buffer_view.layout(), - mln_buffer_view.layout(), - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_query_feature_extensions"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_result"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10759,45 +10507,45 @@ private static class mln_render_session_query_feature_extensions { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, mln_buffer *out_result) + * mln_status mln_acquired_frame_get_result(mln_acquired_frame frame, mln_render_frame_result *out_result) * } */ - public static FunctionDescriptor mln_render_session_query_feature_extensions$descriptor() { - return mln_render_session_query_feature_extensions.DESC; + public static FunctionDescriptor mln_acquired_frame_get_result$descriptor() { + return mln_acquired_frame_get_result.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, mln_buffer *out_result) + * mln_status mln_acquired_frame_get_result(mln_acquired_frame frame, mln_render_frame_result *out_result) * } */ - public static MethodHandle mln_render_session_query_feature_extensions$handle() { - return mln_render_session_query_feature_extensions.HANDLE; + public static MethodHandle mln_acquired_frame_get_result$handle() { + return mln_acquired_frame_get_result.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, mln_buffer *out_result) + * mln_status mln_acquired_frame_get_result(mln_acquired_frame frame, mln_render_frame_result *out_result) * } */ - public static MemorySegment mln_render_session_query_feature_extensions$address() { - return mln_render_session_query_feature_extensions.ADDR; + public static MemorySegment mln_acquired_frame_get_result$address() { + return mln_acquired_frame_get_result.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_query_feature_extensions(mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view *arguments, mln_buffer *out_result) + * mln_status mln_acquired_frame_get_result(mln_acquired_frame frame, mln_render_frame_result *out_result) * } */ - public static int mln_render_session_query_feature_extensions(long session, MemorySegment source_id, MemorySegment feature, MemorySegment extension, MemorySegment extension_field, MemorySegment arguments, MemorySegment out_result) { - var mh$ = mln_render_session_query_feature_extensions.HANDLE; + public static int mln_acquired_frame_get_result(long frame, MemorySegment out_result) { + var mh$ = mln_acquired_frame_get_result.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_query_feature_extensions", session, source_id, feature, extension, extension_field, arguments, out_result); + traceDowncall("mln_acquired_frame_get_result", frame, out_result); } - return (int)mh$.invokeExact(session, source_id, feature, extension, extension_field, arguments, out_result); + return (int)mh$.invokeExact(frame, out_result); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10805,16 +10553,14 @@ public static int mln_render_session_query_feature_extensions(long session, Memo } } - private static class mln_render_session_resize { + private static class mln_acquired_frame_get_producer_sync { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_INT, - MapLibreNativeC.C_INT, - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_resize"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_producer_sync"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10822,45 +10568,45 @@ private static class mln_render_session_resize { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_resize(mln_render_session session, uint32_t width, uint32_t height, double scale_factor) + * mln_status mln_acquired_frame_get_producer_sync(mln_acquired_frame frame, mln_gpu_sync *out_sync) * } */ - public static FunctionDescriptor mln_render_session_resize$descriptor() { - return mln_render_session_resize.DESC; + public static FunctionDescriptor mln_acquired_frame_get_producer_sync$descriptor() { + return mln_acquired_frame_get_producer_sync.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_resize(mln_render_session session, uint32_t width, uint32_t height, double scale_factor) + * mln_status mln_acquired_frame_get_producer_sync(mln_acquired_frame frame, mln_gpu_sync *out_sync) * } */ - public static MethodHandle mln_render_session_resize$handle() { - return mln_render_session_resize.HANDLE; + public static MethodHandle mln_acquired_frame_get_producer_sync$handle() { + return mln_acquired_frame_get_producer_sync.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_resize(mln_render_session session, uint32_t width, uint32_t height, double scale_factor) + * mln_status mln_acquired_frame_get_producer_sync(mln_acquired_frame frame, mln_gpu_sync *out_sync) * } */ - public static MemorySegment mln_render_session_resize$address() { - return mln_render_session_resize.ADDR; + public static MemorySegment mln_acquired_frame_get_producer_sync$address() { + return mln_acquired_frame_get_producer_sync.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_resize(mln_render_session session, uint32_t width, uint32_t height, double scale_factor) + * mln_status mln_acquired_frame_get_producer_sync(mln_acquired_frame frame, mln_gpu_sync *out_sync) * } */ - public static int mln_render_session_resize(long session, int width, int height, double scale_factor) { - var mh$ = mln_render_session_resize.HANDLE; + public static int mln_acquired_frame_get_producer_sync(long frame, MemorySegment out_sync) { + var mh$ = mln_acquired_frame_get_producer_sync.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_resize", session, width, height, scale_factor); + traceDowncall("mln_acquired_frame_get_producer_sync", frame, out_sync); } - return (int)mh$.invokeExact(session, width, height, scale_factor); + return (int)mh$.invokeExact(frame, out_sync); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10868,15 +10614,14 @@ public static int mln_render_session_resize(long session, int width, int height, } } - private static class mln_render_session_render_update { + private static class mln_acquired_frame_release { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_render_update"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_release"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10884,45 +10629,45 @@ private static class mln_render_session_render_update { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_render_update(mln_render_session session, mln_render_result *out_result, bool *out_needs_repaint) + * mln_status mln_acquired_frame_release(mln_acquired_frame *frame, const mln_gpu_sync *consumer_completion) * } */ - public static FunctionDescriptor mln_render_session_render_update$descriptor() { - return mln_render_session_render_update.DESC; + public static FunctionDescriptor mln_acquired_frame_release$descriptor() { + return mln_acquired_frame_release.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_render_update(mln_render_session session, mln_render_result *out_result, bool *out_needs_repaint) + * mln_status mln_acquired_frame_release(mln_acquired_frame *frame, const mln_gpu_sync *consumer_completion) * } */ - public static MethodHandle mln_render_session_render_update$handle() { - return mln_render_session_render_update.HANDLE; + public static MethodHandle mln_acquired_frame_release$handle() { + return mln_acquired_frame_release.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_render_update(mln_render_session session, mln_render_result *out_result, bool *out_needs_repaint) + * mln_status mln_acquired_frame_release(mln_acquired_frame *frame, const mln_gpu_sync *consumer_completion) * } */ - public static MemorySegment mln_render_session_render_update$address() { - return mln_render_session_render_update.ADDR; + public static MemorySegment mln_acquired_frame_release$address() { + return mln_acquired_frame_release.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_render_update(mln_render_session session, mln_render_result *out_result, bool *out_needs_repaint) + * mln_status mln_acquired_frame_release(mln_acquired_frame *frame, const mln_gpu_sync *consumer_completion) * } */ - public static int mln_render_session_render_update(long session, MemorySegment out_result, MemorySegment out_needs_repaint) { - var mh$ = mln_render_session_render_update.HANDLE; + public static int mln_acquired_frame_release(MemorySegment frame, MemorySegment consumer_completion) { + var mh$ = mln_acquired_frame_release.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_render_update", session, out_result, out_needs_repaint); + traceDowncall("mln_acquired_frame_release", frame, consumer_completion); } - return (int)mh$.invokeExact(session, out_result, out_needs_repaint); + return (int)mh$.invokeExact(frame, consumer_completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10930,13 +10675,15 @@ public static int mln_render_session_render_update(long session, MemorySegment o } } - private static class mln_render_session_detach { + private static class mln_render_session_resize { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_detach"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_resize"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -10944,45 +10691,45 @@ private static class mln_render_session_detach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_detach(mln_render_session session) + * mln_status mln_render_session_resize(mln_render_session session, const mln_render_target_extent *extent, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_render_session_detach$descriptor() { - return mln_render_session_detach.DESC; + public static FunctionDescriptor mln_render_session_resize$descriptor() { + return mln_render_session_resize.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_detach(mln_render_session session) + * mln_status mln_render_session_resize(mln_render_session session, const mln_render_target_extent *extent, const mln_completion *completion) * } */ - public static MethodHandle mln_render_session_detach$handle() { - return mln_render_session_detach.HANDLE; + public static MethodHandle mln_render_session_resize$handle() { + return mln_render_session_resize.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_detach(mln_render_session session) + * mln_status mln_render_session_resize(mln_render_session session, const mln_render_target_extent *extent, const mln_completion *completion) * } */ - public static MemorySegment mln_render_session_detach$address() { - return mln_render_session_detach.ADDR; + public static MemorySegment mln_render_session_resize$address() { + return mln_render_session_resize.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_detach(mln_render_session session) + * mln_status mln_render_session_resize(mln_render_session session, const mln_render_target_extent *extent, const mln_completion *completion) * } */ - public static int mln_render_session_detach(long session) { - var mh$ = mln_render_session_detach.HANDLE; + public static int mln_render_session_resize(long session, MemorySegment extent, MemorySegment completion) { + var mh$ = mln_render_session_resize.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_detach", session); + traceDowncall("mln_render_session_resize", session, extent, completion); } - return (int)mh$.invokeExact(session); + return (int)mh$.invokeExact(session, extent, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -10990,13 +10737,14 @@ public static int mln_render_session_detach(long session) { } } - private static class mln_render_session_destroy { + private static class mln_render_session_barrier { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_destroy"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_barrier"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -11004,45 +10752,45 @@ private static class mln_render_session_destroy { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_destroy(mln_render_session session) + * mln_status mln_render_session_barrier(mln_render_session session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_render_session_destroy$descriptor() { - return mln_render_session_destroy.DESC; + public static FunctionDescriptor mln_render_session_barrier$descriptor() { + return mln_render_session_barrier.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_destroy(mln_render_session session) + * mln_status mln_render_session_barrier(mln_render_session session, const mln_completion *completion) * } */ - public static MethodHandle mln_render_session_destroy$handle() { - return mln_render_session_destroy.HANDLE; + public static MethodHandle mln_render_session_barrier$handle() { + return mln_render_session_barrier.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_destroy(mln_render_session session) + * mln_status mln_render_session_barrier(mln_render_session session, const mln_completion *completion) * } */ - public static MemorySegment mln_render_session_destroy$address() { - return mln_render_session_destroy.ADDR; + public static MemorySegment mln_render_session_barrier$address() { + return mln_render_session_barrier.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_session_destroy(mln_render_session session) + * mln_status mln_render_session_barrier(mln_render_session session, const mln_completion *completion) * } */ - public static int mln_render_session_destroy(long session) { - var mh$ = mln_render_session_destroy.HANDLE; + public static int mln_render_session_barrier(long session, MemorySegment completion) { + var mh$ = mln_render_session_barrier.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_destroy", session); + traceDowncall("mln_render_session_barrier", session, completion); } - return (int)mh$.invokeExact(session); + return (int)mh$.invokeExact(session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -11053,7 +10801,8 @@ public static int mln_render_session_destroy(long session) { private static class mln_render_session_reduce_memory_use { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_reduce_memory_use"); @@ -11064,7 +10813,7 @@ private static class mln_render_session_reduce_memory_use { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_reduce_memory_use(mln_render_session session) + * mln_status mln_render_session_reduce_memory_use(mln_render_session session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_render_session_reduce_memory_use$descriptor() { @@ -11074,7 +10823,7 @@ private static class mln_render_session_reduce_memory_use { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_reduce_memory_use(mln_render_session session) + * mln_status mln_render_session_reduce_memory_use(mln_render_session session, const mln_completion *completion) * } */ public static MethodHandle mln_render_session_reduce_memory_use$handle() { @@ -11084,7 +10833,7 @@ private static class mln_render_session_reduce_memory_use { /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_reduce_memory_use(mln_render_session session) + * mln_status mln_render_session_reduce_memory_use(mln_render_session session, const mln_completion *completion) * } */ public static MemorySegment mln_render_session_reduce_memory_use$address() { @@ -11093,16 +10842,16 @@ private static class mln_render_session_reduce_memory_use { /** * {@snippet lang=c : - * mln_status mln_render_session_reduce_memory_use(mln_render_session session) + * mln_status mln_render_session_reduce_memory_use(mln_render_session session, const mln_completion *completion) * } */ - public static int mln_render_session_reduce_memory_use(long session) { + public static int mln_render_session_reduce_memory_use(long session, MemorySegment completion) { var mh$ = mln_render_session_reduce_memory_use.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_reduce_memory_use", session); + traceDowncall("mln_render_session_reduce_memory_use", session, completion); } - return (int)mh$.invokeExact(session); + return (int)mh$.invokeExact(session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -11113,7 +10862,8 @@ public static int mln_render_session_reduce_memory_use(long session) { private static class mln_render_session_clear_data { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_clear_data"); @@ -11124,7 +10874,7 @@ private static class mln_render_session_clear_data { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_clear_data(mln_render_session session) + * mln_status mln_render_session_clear_data(mln_render_session session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_render_session_clear_data$descriptor() { @@ -11134,7 +10884,7 @@ private static class mln_render_session_clear_data { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_clear_data(mln_render_session session) + * mln_status mln_render_session_clear_data(mln_render_session session, const mln_completion *completion) * } */ public static MethodHandle mln_render_session_clear_data$handle() { @@ -11144,7 +10894,7 @@ private static class mln_render_session_clear_data { /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_clear_data(mln_render_session session) + * mln_status mln_render_session_clear_data(mln_render_session session, const mln_completion *completion) * } */ public static MemorySegment mln_render_session_clear_data$address() { @@ -11153,16 +10903,16 @@ private static class mln_render_session_clear_data { /** * {@snippet lang=c : - * mln_status mln_render_session_clear_data(mln_render_session session) + * mln_status mln_render_session_clear_data(mln_render_session session, const mln_completion *completion) * } */ - public static int mln_render_session_clear_data(long session) { + public static int mln_render_session_clear_data(long session, MemorySegment completion) { var mh$ = mln_render_session_clear_data.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_clear_data", session); + traceDowncall("mln_render_session_clear_data", session, completion); } - return (int)mh$.invokeExact(session); + return (int)mh$.invokeExact(session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -11173,7 +10923,8 @@ public static int mln_render_session_clear_data(long session) { private static class mln_render_session_dump_debug_logs { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_dump_debug_logs"); @@ -11184,7 +10935,7 @@ private static class mln_render_session_dump_debug_logs { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_session_dump_debug_logs(mln_render_session session) + * mln_status mln_render_session_dump_debug_logs(mln_render_session session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_render_session_dump_debug_logs$descriptor() { @@ -11194,7 +10945,7 @@ private static class mln_render_session_dump_debug_logs { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_session_dump_debug_logs(mln_render_session session) + * mln_status mln_render_session_dump_debug_logs(mln_render_session session, const mln_completion *completion) * } */ public static MethodHandle mln_render_session_dump_debug_logs$handle() { @@ -11204,7 +10955,7 @@ private static class mln_render_session_dump_debug_logs { /** * Address for: * {@snippet lang=c : - * mln_status mln_render_session_dump_debug_logs(mln_render_session session) + * mln_status mln_render_session_dump_debug_logs(mln_render_session session, const mln_completion *completion) * } */ public static MemorySegment mln_render_session_dump_debug_logs$address() { @@ -11213,95 +10964,154 @@ private static class mln_render_session_dump_debug_logs { /** * {@snippet lang=c : - * mln_status mln_render_session_dump_debug_logs(mln_render_session session) + * mln_status mln_render_session_dump_debug_logs(mln_render_session session, const mln_completion *completion) * } */ - public static int mln_render_session_dump_debug_logs(long session) { + public static int mln_render_session_dump_debug_logs(long session, MemorySegment completion) { var mh$ = mln_render_session_dump_debug_logs.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_session_dump_debug_logs", session); + traceDowncall("mln_render_session_dump_debug_logs", session, completion); } - return (int)mh$.invokeExact(session); + return (int)mh$.invokeExact(session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL = (int)1L; + + private static class mln_render_session_service_driver_work { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_service_driver_work"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + /** + * Function descriptor for: * {@snippet lang=c : - * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL = 1 + * mln_status mln_render_session_service_driver_work(mln_render_session session, size_t max_work, size_t *out_serviced) * } */ - public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL() { - return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WGL; + public static FunctionDescriptor mln_render_session_service_driver_work$descriptor() { + return mln_render_session_service_driver_work.DESC; } - private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL = (int)2L; + /** + * Downcall method handle for: * {@snippet lang=c : - * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL = 2 + * mln_status mln_render_session_service_driver_work(mln_render_session session, size_t max_work, size_t *out_serviced) * } */ - public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL() { - return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_EGL; + public static MethodHandle mln_render_session_service_driver_work$handle() { + return mln_render_session_service_driver_work.HANDLE; } - private static final int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL = (int)4L; + /** + * Address for: * {@snippet lang=c : - * enum mln_opengl_context_provider_flag.MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL = 4 + * mln_status mln_render_session_service_driver_work(mln_render_session session, size_t max_work, size_t *out_serviced) * } */ - public static int MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL() { - return MLN_OPENGL_CONTEXT_PROVIDER_FLAG_WEBGL; + public static MemorySegment mln_render_session_service_driver_work$address() { + return mln_render_session_service_driver_work.ADDR; } - private static final int MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED = (int)0L; + /** * {@snippet lang=c : - * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED = 0 + * mln_status mln_render_session_service_driver_work(mln_render_session session, size_t max_work, size_t *out_serviced) * } */ - public static int MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED() { - return MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED; + public static int mln_render_session_service_driver_work(long session, long max_work, MemorySegment out_serviced) { + var mh$ = mln_render_session_service_driver_work.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("mln_render_session_service_driver_work", session, max_work, out_serviced); + } + return (int)mh$.invokeExact(session, max_work, out_serviced); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } } - private static final int MLN_OPENGL_CONTEXT_PLATFORM_WGL = (int)1L; + + private static class mln_render_session_detach { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_detach"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + /** + * Function descriptor for: * {@snippet lang=c : - * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WGL = 1 + * mln_status mln_render_session_detach(mln_render_session session, const mln_completion *completion) * } */ - public static int MLN_OPENGL_CONTEXT_PLATFORM_WGL() { - return MLN_OPENGL_CONTEXT_PLATFORM_WGL; + public static FunctionDescriptor mln_render_session_detach$descriptor() { + return mln_render_session_detach.DESC; } - private static final int MLN_OPENGL_CONTEXT_PLATFORM_EGL = (int)2L; + /** + * Downcall method handle for: * {@snippet lang=c : - * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_EGL = 2 + * mln_status mln_render_session_detach(mln_render_session session, const mln_completion *completion) * } */ - public static int MLN_OPENGL_CONTEXT_PLATFORM_EGL() { - return MLN_OPENGL_CONTEXT_PLATFORM_EGL; + public static MethodHandle mln_render_session_detach$handle() { + return mln_render_session_detach.HANDLE; } - private static final int MLN_OPENGL_CONTEXT_PLATFORM_WEBGL = (int)3L; + /** + * Address for: * {@snippet lang=c : - * enum mln_opengl_context_platform.MLN_OPENGL_CONTEXT_PLATFORM_WEBGL = 3 + * mln_status mln_render_session_detach(mln_render_session session, const mln_completion *completion) * } */ - public static int MLN_OPENGL_CONTEXT_PLATFORM_WEBGL() { - return MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + public static MemorySegment mln_render_session_detach$address() { + return mln_render_session_detach.ADDR; + } + + /** + * {@snippet lang=c : + * mln_status mln_render_session_detach(mln_render_session session, const mln_completion *completion) + * } + */ + public static int mln_render_session_detach(long session, MemorySegment completion) { + var mh$ = mln_render_session_detach.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("mln_render_session_detach", session, completion); + } + return (int)mh$.invokeExact(session, completion); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } } - private static class mln_render_target_extent_physical_size { + private static class mln_render_session_abandon { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_target_extent_physical_size"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_abandon"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -11309,45 +11119,45 @@ private static class mln_render_target_extent_physical_size { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) + * mln_status mln_render_session_abandon(mln_render_session session, mln_render_abandon_result *out_result) * } */ - public static FunctionDescriptor mln_render_target_extent_physical_size$descriptor() { - return mln_render_target_extent_physical_size.DESC; + public static FunctionDescriptor mln_render_session_abandon$descriptor() { + return mln_render_session_abandon.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) + * mln_status mln_render_session_abandon(mln_render_session session, mln_render_abandon_result *out_result) * } */ - public static MethodHandle mln_render_target_extent_physical_size$handle() { - return mln_render_target_extent_physical_size.HANDLE; + public static MethodHandle mln_render_session_abandon$handle() { + return mln_render_session_abandon.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) + * mln_status mln_render_session_abandon(mln_render_session session, mln_render_abandon_result *out_result) * } */ - public static MemorySegment mln_render_target_extent_physical_size$address() { - return mln_render_target_extent_physical_size.ADDR; + public static MemorySegment mln_render_session_abandon$address() { + return mln_render_session_abandon.ADDR; } /** * {@snippet lang=c : - * mln_status mln_render_target_extent_physical_size(const mln_render_target_extent *extent, uint32_t *out_width, uint32_t *out_height) + * mln_status mln_render_session_abandon(mln_render_session session, mln_render_abandon_result *out_result) * } */ - public static int mln_render_target_extent_physical_size(MemorySegment extent, MemorySegment out_width, MemorySegment out_height) { - var mh$ = mln_render_target_extent_physical_size.HANDLE; + public static int mln_render_session_abandon(long session, MemorySegment out_result) { + var mh$ = mln_render_session_abandon.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_render_target_extent_physical_size", extent, out_width, out_height); + traceDowncall("mln_render_session_abandon", session, out_result); } - return (int)mh$.invokeExact(extent, out_width, out_height); + return (int)mh$.invokeExact(session, out_result); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -11355,11 +11165,13 @@ public static int mln_render_target_extent_physical_size(MemorySegment extent, M } } - private static class mln_opengl_supported_context_provider_mask { + private static class mln_render_session_destroy { public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT ); + MapLibreNativeC.C_INT, + MapLibreNativeC.C_LONG + ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_supported_context_provider_mask"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_render_session_destroy"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -11367,51 +11179,63 @@ private static class mln_opengl_supported_context_provider_mask { /** * Function descriptor for: * {@snippet lang=c : - * uint32_t mln_opengl_supported_context_provider_mask() + * mln_status mln_render_session_destroy(mln_render_session session) * } */ - public static FunctionDescriptor mln_opengl_supported_context_provider_mask$descriptor() { - return mln_opengl_supported_context_provider_mask.DESC; + public static FunctionDescriptor mln_render_session_destroy$descriptor() { + return mln_render_session_destroy.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * uint32_t mln_opengl_supported_context_provider_mask() + * mln_status mln_render_session_destroy(mln_render_session session) * } */ - public static MethodHandle mln_opengl_supported_context_provider_mask$handle() { - return mln_opengl_supported_context_provider_mask.HANDLE; + public static MethodHandle mln_render_session_destroy$handle() { + return mln_render_session_destroy.HANDLE; } /** * Address for: * {@snippet lang=c : - * uint32_t mln_opengl_supported_context_provider_mask() + * mln_status mln_render_session_destroy(mln_render_session session) * } */ - public static MemorySegment mln_opengl_supported_context_provider_mask$address() { - return mln_opengl_supported_context_provider_mask.ADDR; + public static MemorySegment mln_render_session_destroy$address() { + return mln_render_session_destroy.ADDR; } /** * {@snippet lang=c : - * uint32_t mln_opengl_supported_context_provider_mask() + * mln_status mln_render_session_destroy(mln_render_session session) * } */ - public static int mln_opengl_supported_context_provider_mask() { - var mh$ = mln_opengl_supported_context_provider_mask.HANDLE; + public static int mln_render_session_destroy(long session) { + var mh$ = mln_render_session_destroy.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_supported_context_provider_mask"); + traceDowncall("mln_render_session_destroy", session); } - return (int)mh$.invokeExact(); + return (int)mh$.invokeExact(session); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } + /** + * {@snippet lang=c : + * typedef uint64_t mln_style_id_list + * } + */ + public static final OfLong mln_style_id_list = MapLibreNativeC.C_LONG; + /** + * {@snippet lang=c : + * typedef uint64_t mln_style_string_list + * } + */ + public static final OfLong mln_style_string_list = MapLibreNativeC.C_LONG; private static final int MLN_STYLE_SOURCE_TYPE_UNKNOWN = (int)0L; /** * {@snippet lang=c : @@ -11997,6 +11821,15 @@ public static int MLN_STYLE_TRANSITION_OPTION_DURATION() { public static int MLN_STYLE_TRANSITION_OPTION_DELAY() { return MLN_STYLE_TRANSITION_OPTION_DELAY; } + private static final int MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS = (int)4L; + /** + * {@snippet lang=c : + * enum mln_style_transition_option_field.MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS = 4 + * } + */ + public static int MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS() { + return MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; + } private static final int MLN_LOCATION_INDICATOR_IMAGE_KIND_TOP = (int)0L; /** * {@snippet lang=c : @@ -12858,7 +12691,8 @@ private static class mln_map_add_style_source_json { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_style_source_json"); @@ -12869,7 +12703,7 @@ private static class mln_map_add_style_source_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json) + * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_style_source_json$descriptor() { @@ -12879,7 +12713,7 @@ private static class mln_map_add_style_source_json { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json) + * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_style_source_json$handle() { @@ -12889,7 +12723,7 @@ private static class mln_map_add_style_source_json { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json) + * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_style_source_json$address() { @@ -12898,16 +12732,16 @@ private static class mln_map_add_style_source_json { /** * {@snippet lang=c : - * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json) + * mln_status mln_map_add_style_source_json(mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, const mln_completion *completion) * } */ - public static int mln_map_add_style_source_json(long map, MemorySegment source_id, MemorySegment source_json) { + public static int mln_map_add_style_source_json(long map, MemorySegment source_id, MemorySegment source_json, MemorySegment completion) { var mh$ = mln_map_add_style_source_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_style_source_json", map, source_id, source_json); + traceDowncall("mln_map_add_style_source_json", map, source_id, source_json, completion); } - return (int)mh$.invokeExact(map, source_id, source_json); + return (int)mh$.invokeExact(map, source_id, source_json, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -12931,7 +12765,7 @@ private static class mln_map_remove_style_source { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, bool *out_removed) + * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_remove_style_source$descriptor() { @@ -12941,7 +12775,7 @@ private static class mln_map_remove_style_source { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, bool *out_removed) + * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_remove_style_source$handle() { @@ -12951,7 +12785,7 @@ private static class mln_map_remove_style_source { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, bool *out_removed) + * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_remove_style_source$address() { @@ -12960,141 +12794,16 @@ private static class mln_map_remove_style_source { /** * {@snippet lang=c : - * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, bool *out_removed) + * mln_status mln_map_remove_style_source(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_remove_style_source(long map, MemorySegment source_id, MemorySegment out_removed) { + public static int mln_map_remove_style_source(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_remove_style_source.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_remove_style_source", map, source_id, out_removed); - } - return (int)mh$.invokeExact(map, source_id, out_removed); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_style_source_exists { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_style_source_exists"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_style_source_exists(mln_map map, mln_buffer_view source_id, bool *out_exists) - * } - */ - public static FunctionDescriptor mln_map_style_source_exists$descriptor() { - return mln_map_style_source_exists.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_style_source_exists(mln_map map, mln_buffer_view source_id, bool *out_exists) - * } - */ - public static MethodHandle mln_map_style_source_exists$handle() { - return mln_map_style_source_exists.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_style_source_exists(mln_map map, mln_buffer_view source_id, bool *out_exists) - * } - */ - public static MemorySegment mln_map_style_source_exists$address() { - return mln_map_style_source_exists.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_style_source_exists(mln_map map, mln_buffer_view source_id, bool *out_exists) - * } - */ - public static int mln_map_style_source_exists(long map, MemorySegment source_id, MemorySegment out_exists) { - var mh$ = mln_map_style_source_exists.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_style_source_exists", map, source_id, out_exists); - } - return (int)mh$.invokeExact(map, source_id, out_exists); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_get_style_source_type { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_style_source_type"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_get_style_source_type(mln_map map, mln_buffer_view source_id, uint32_t *out_source_type, bool *out_found) - * } - */ - public static FunctionDescriptor mln_map_get_style_source_type$descriptor() { - return mln_map_get_style_source_type.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_get_style_source_type(mln_map map, mln_buffer_view source_id, uint32_t *out_source_type, bool *out_found) - * } - */ - public static MethodHandle mln_map_get_style_source_type$handle() { - return mln_map_get_style_source_type.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_get_style_source_type(mln_map map, mln_buffer_view source_id, uint32_t *out_source_type, bool *out_found) - * } - */ - public static MemorySegment mln_map_get_style_source_type$address() { - return mln_map_get_style_source_type.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_get_style_source_type(mln_map map, mln_buffer_view source_id, uint32_t *out_source_type, bool *out_found) - * } - */ - public static int mln_map_get_style_source_type(long map, MemorySegment source_id, MemorySegment out_source_type, MemorySegment out_found) { - var mh$ = mln_map_get_style_source_type.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_source_type", map, source_id, out_source_type, out_found); + traceDowncall("mln_map_remove_style_source", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_source_type, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13107,7 +12816,6 @@ private static class mln_map_get_style_source_info { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13119,7 +12827,7 @@ private static class mln_map_get_style_source_info { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, mln_style_source_info *out_info, bool *out_found) + * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_source_info$descriptor() { @@ -13129,7 +12837,7 @@ private static class mln_map_get_style_source_info { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, mln_style_source_info *out_info, bool *out_found) + * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_source_info$handle() { @@ -13139,7 +12847,7 @@ private static class mln_map_get_style_source_info { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, mln_style_source_info *out_info, bool *out_found) + * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_source_info$address() { @@ -13148,16 +12856,16 @@ private static class mln_map_get_style_source_info { /** * {@snippet lang=c : - * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, mln_style_source_info *out_info, bool *out_found) + * mln_status mln_map_get_style_source_info(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_get_style_source_info(long map, MemorySegment source_id, MemorySegment out_info, MemorySegment out_found) { + public static int mln_map_get_style_source_info(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_get_style_source_info.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_source_info", map, source_id, out_info, out_found); + traceDowncall("mln_map_get_style_source_info", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_info, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13170,9 +12878,6 @@ private static class mln_map_copy_style_source_attribution { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13184,7 +12889,7 @@ private static class mln_map_copy_style_source_attribution { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, char *out_attribution, size_t attribution_capacity, size_t *out_attribution_size, bool *out_found) + * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_style_source_attribution$descriptor() { @@ -13194,7 +12899,7 @@ private static class mln_map_copy_style_source_attribution { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, char *out_attribution, size_t attribution_capacity, size_t *out_attribution_size, bool *out_found) + * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_style_source_attribution$handle() { @@ -13204,7 +12909,7 @@ private static class mln_map_copy_style_source_attribution { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, char *out_attribution, size_t attribution_capacity, size_t *out_attribution_size, bool *out_found) + * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_style_source_attribution$address() { @@ -13213,16 +12918,16 @@ private static class mln_map_copy_style_source_attribution { /** * {@snippet lang=c : - * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, char *out_attribution, size_t attribution_capacity, size_t *out_attribution_size, bool *out_found) + * mln_status mln_map_copy_style_source_attribution(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_copy_style_source_attribution(long map, MemorySegment source_id, MemorySegment out_attribution, long attribution_capacity, MemorySegment out_attribution_size, MemorySegment out_found) { + public static int mln_map_copy_style_source_attribution(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_copy_style_source_attribution.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_style_source_attribution", map, source_id, out_attribution, attribution_capacity, out_attribution_size, out_found); + traceDowncall("mln_map_copy_style_source_attribution", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_attribution, attribution_capacity, out_attribution_size, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13235,9 +12940,6 @@ private static class mln_map_copy_style_source_url { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13249,7 +12951,7 @@ private static class mln_map_copy_style_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, char *out_url, size_t url_capacity, size_t *out_url_size, bool *out_found) + * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_style_source_url$descriptor() { @@ -13259,7 +12961,7 @@ private static class mln_map_copy_style_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, char *out_url, size_t url_capacity, size_t *out_url_size, bool *out_found) + * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_style_source_url$handle() { @@ -13269,7 +12971,7 @@ private static class mln_map_copy_style_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, char *out_url, size_t url_capacity, size_t *out_url_size, bool *out_found) + * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_style_source_url$address() { @@ -13278,16 +12980,16 @@ private static class mln_map_copy_style_source_url { /** * {@snippet lang=c : - * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, char *out_url, size_t url_capacity, size_t *out_url_size, bool *out_found) + * mln_status mln_map_copy_style_source_url(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_copy_style_source_url(long map, MemorySegment source_id, MemorySegment out_url, long url_capacity, MemorySegment out_url_size, MemorySegment out_found) { + public static int mln_map_copy_style_source_url(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_copy_style_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_style_source_url", map, source_id, out_url, url_capacity, out_url_size, out_found); + traceDowncall("mln_map_copy_style_source_url", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_url, url_capacity, out_url_size, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13300,7 +13002,6 @@ private static class mln_map_get_style_source_tile_urls { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13312,7 +13013,7 @@ private static class mln_map_get_style_source_tile_urls { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, mln_style_string_list *out_tile_urls, bool *out_found) + * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_source_tile_urls$descriptor() { @@ -13322,7 +13023,7 @@ private static class mln_map_get_style_source_tile_urls { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, mln_style_string_list *out_tile_urls, bool *out_found) + * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_source_tile_urls$handle() { @@ -13332,7 +13033,7 @@ private static class mln_map_get_style_source_tile_urls { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, mln_style_string_list *out_tile_urls, bool *out_found) + * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_source_tile_urls$address() { @@ -13341,16 +13042,16 @@ private static class mln_map_get_style_source_tile_urls { /** * {@snippet lang=c : - * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, mln_style_string_list *out_tile_urls, bool *out_found) + * mln_status mln_map_get_style_source_tile_urls(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_get_style_source_tile_urls(long map, MemorySegment source_id, MemorySegment out_tile_urls, MemorySegment out_found) { + public static int mln_map_get_style_source_tile_urls(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_get_style_source_tile_urls.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_source_tile_urls", map, source_id, out_tile_urls, out_found); + traceDowncall("mln_map_get_style_source_tile_urls", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_tile_urls, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13373,7 +13074,7 @@ private static class mln_map_list_style_source_ids { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_list_style_source_ids(mln_map map, mln_style_id_list *out_source_ids) + * mln_status mln_map_list_style_source_ids(mln_map map, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_list_style_source_ids$descriptor() { @@ -13383,7 +13084,7 @@ private static class mln_map_list_style_source_ids { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_list_style_source_ids(mln_map map, mln_style_id_list *out_source_ids) + * mln_status mln_map_list_style_source_ids(mln_map map, const mln_completion *completion) * } */ public static MethodHandle mln_map_list_style_source_ids$handle() { @@ -13393,7 +13094,7 @@ private static class mln_map_list_style_source_ids { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_list_style_source_ids(mln_map map, mln_style_id_list *out_source_ids) + * mln_status mln_map_list_style_source_ids(mln_map map, const mln_completion *completion) * } */ public static MemorySegment mln_map_list_style_source_ids$address() { @@ -13402,16 +13103,16 @@ private static class mln_map_list_style_source_ids { /** * {@snippet lang=c : - * mln_status mln_map_list_style_source_ids(mln_map map, mln_style_id_list *out_source_ids) + * mln_status mln_map_list_style_source_ids(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_list_style_source_ids(long map, MemorySegment out_source_ids) { + public static int mln_map_list_style_source_ids(long map, MemorySegment completion) { var mh$ = mln_map_list_style_source_ids.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_list_style_source_ids", map, out_source_ids); + traceDowncall("mln_map_list_style_source_ids", map, completion); } - return (int)mh$.invokeExact(map, out_source_ids); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13425,6 +13126,7 @@ private static class mln_map_add_geojson_source_url { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13436,7 +13138,7 @@ private static class mln_map_add_geojson_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options) + * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_geojson_source_url$descriptor() { @@ -13446,7 +13148,7 @@ private static class mln_map_add_geojson_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options) + * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_geojson_source_url$handle() { @@ -13456,7 +13158,7 @@ private static class mln_map_add_geojson_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options) + * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_geojson_source_url$address() { @@ -13465,16 +13167,16 @@ private static class mln_map_add_geojson_source_url { /** * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options) + * mln_status mln_map_add_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_geojson_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_geojson_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options) { + public static int mln_map_add_geojson_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_geojson_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_geojson_source_url", map, source_id, url, options); + traceDowncall("mln_map_add_geojson_source_url", map, source_id, url, options, completion); } - return (int)mh$.invokeExact(map, source_id, url, options); + return (int)mh$.invokeExact(map, source_id, url, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13608,7 +13310,8 @@ private static class mln_map_add_geojson_source_data { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_geojson_source_data"); @@ -13619,7 +13322,7 @@ private static class mln_map_add_geojson_source_data { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_geojson_source_data$descriptor() { @@ -13629,7 +13332,7 @@ private static class mln_map_add_geojson_source_data { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_geojson_source_data$handle() { @@ -13639,7 +13342,7 @@ private static class mln_map_add_geojson_source_data { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_geojson_source_data$address() { @@ -13648,16 +13351,16 @@ private static class mln_map_add_geojson_source_data { /** * {@snippet lang=c : - * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_add_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ - public static int mln_map_add_geojson_source_data(long map, MemorySegment source_id, long data) { + public static int mln_map_add_geojson_source_data(long map, MemorySegment source_id, long data, MemorySegment completion) { var mh$ = mln_map_add_geojson_source_data.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_geojson_source_data", map, source_id, data); + traceDowncall("mln_map_add_geojson_source_data", map, source_id, data, completion); } - return (int)mh$.invokeExact(map, source_id, data); + return (int)mh$.invokeExact(map, source_id, data, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13670,7 +13373,8 @@ private static class mln_map_set_geojson_source_url { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_geojson_source_url"); @@ -13681,7 +13385,7 @@ private static class mln_map_set_geojson_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_geojson_source_url$descriptor() { @@ -13691,7 +13395,7 @@ private static class mln_map_set_geojson_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_geojson_source_url$handle() { @@ -13701,7 +13405,7 @@ private static class mln_map_set_geojson_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_geojson_source_url$address() { @@ -13710,16 +13414,16 @@ private static class mln_map_set_geojson_source_url { /** * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_geojson_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ - public static int mln_map_set_geojson_source_url(long map, MemorySegment source_id, MemorySegment url) { + public static int mln_map_set_geojson_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment completion) { var mh$ = mln_map_set_geojson_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_geojson_source_url", map, source_id, url); + traceDowncall("mln_map_set_geojson_source_url", map, source_id, url, completion); } - return (int)mh$.invokeExact(map, source_id, url); + return (int)mh$.invokeExact(map, source_id, url, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13732,7 +13436,8 @@ private static class mln_map_set_geojson_source_data { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_geojson_source_data"); @@ -13743,7 +13448,7 @@ private static class mln_map_set_geojson_source_data { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_geojson_source_data$descriptor() { @@ -13753,7 +13458,7 @@ private static class mln_map_set_geojson_source_data { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_geojson_source_data$handle() { @@ -13763,7 +13468,7 @@ private static class mln_map_set_geojson_source_data { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_geojson_source_data$address() { @@ -13772,16 +13477,16 @@ private static class mln_map_set_geojson_source_data { /** * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data) + * mln_status mln_map_set_geojson_source_data(mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, const mln_completion *completion) * } */ - public static int mln_map_set_geojson_source_data(long map, MemorySegment source_id, long data) { + public static int mln_map_set_geojson_source_data(long map, MemorySegment source_id, long data, MemorySegment completion) { var mh$ = mln_map_set_geojson_source_data.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_geojson_source_data", map, source_id, data); + traceDowncall("mln_map_set_geojson_source_data", map, source_id, data, completion); } - return (int)mh$.invokeExact(map, source_id, data); + return (int)mh$.invokeExact(map, source_id, data, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13794,7 +13499,8 @@ private static class mln_map_set_geojson_source_synchronous_tiling { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_BOOL + MapLibreNativeC.C_BOOL, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_geojson_source_synchronous_tiling"); @@ -13805,7 +13511,7 @@ private static class mln_map_set_geojson_source_synchronous_tiling { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled) + * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_geojson_source_synchronous_tiling$descriptor() { @@ -13815,7 +13521,7 @@ private static class mln_map_set_geojson_source_synchronous_tiling { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled) + * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_geojson_source_synchronous_tiling$handle() { @@ -13825,7 +13531,7 @@ private static class mln_map_set_geojson_source_synchronous_tiling { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled) + * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_geojson_source_synchronous_tiling$address() { @@ -13834,16 +13540,16 @@ private static class mln_map_set_geojson_source_synchronous_tiling { /** * {@snippet lang=c : - * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled) + * mln_status mln_map_set_geojson_source_synchronous_tiling(mln_map map, mln_buffer_view source_id, bool enabled, const mln_completion *completion) * } */ - public static int mln_map_set_geojson_source_synchronous_tiling(long map, MemorySegment source_id, boolean enabled) { + public static int mln_map_set_geojson_source_synchronous_tiling(long map, MemorySegment source_id, boolean enabled, MemorySegment completion) { var mh$ = mln_map_set_geojson_source_synchronous_tiling.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_geojson_source_synchronous_tiling", map, source_id, enabled); + traceDowncall("mln_map_set_geojson_source_synchronous_tiling", map, source_id, enabled, completion); } - return (int)mh$.invokeExact(map, source_id, enabled); + return (int)mh$.invokeExact(map, source_id, enabled, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13857,6 +13563,7 @@ private static class mln_map_add_vector_source_url { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13868,7 +13575,7 @@ private static class mln_map_add_vector_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_vector_source_url$descriptor() { @@ -13878,7 +13585,7 @@ private static class mln_map_add_vector_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_vector_source_url$handle() { @@ -13888,7 +13595,7 @@ private static class mln_map_add_vector_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_vector_source_url$address() { @@ -13897,16 +13604,16 @@ private static class mln_map_add_vector_source_url { /** * {@snippet lang=c : - * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_vector_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options) { + public static int mln_map_add_vector_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_vector_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_vector_source_url", map, source_id, url, options); + traceDowncall("mln_map_add_vector_source_url", map, source_id, url, options, completion); } - return (int)mh$.invokeExact(map, source_id, url, options); + return (int)mh$.invokeExact(map, source_id, url, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13921,6 +13628,7 @@ private static class mln_map_add_vector_source_tiles { mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13932,7 +13640,7 @@ private static class mln_map_add_vector_source_tiles { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_vector_source_tiles$descriptor() { @@ -13942,7 +13650,7 @@ private static class mln_map_add_vector_source_tiles { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_vector_source_tiles$handle() { @@ -13952,7 +13660,7 @@ private static class mln_map_add_vector_source_tiles { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_vector_source_tiles$address() { @@ -13961,16 +13669,16 @@ private static class mln_map_add_vector_source_tiles { /** * {@snippet lang=c : - * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_vector_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_vector_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options) { + public static int mln_map_add_vector_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_vector_source_tiles.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_vector_source_tiles", map, source_id, tiles, tile_count, options); + traceDowncall("mln_map_add_vector_source_tiles", map, source_id, tiles, tile_count, options, completion); } - return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options); + return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -13984,6 +13692,7 @@ private static class mln_map_add_raster_source_url { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -13995,7 +13704,7 @@ private static class mln_map_add_raster_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_raster_source_url$descriptor() { @@ -14005,7 +13714,7 @@ private static class mln_map_add_raster_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_raster_source_url$handle() { @@ -14015,7 +13724,7 @@ private static class mln_map_add_raster_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_raster_source_url$address() { @@ -14024,16 +13733,16 @@ private static class mln_map_add_raster_source_url { /** * {@snippet lang=c : - * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_raster_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options) { + public static int mln_map_add_raster_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_raster_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_raster_source_url", map, source_id, url, options); + traceDowncall("mln_map_add_raster_source_url", map, source_id, url, options, completion); } - return (int)mh$.invokeExact(map, source_id, url, options); + return (int)mh$.invokeExact(map, source_id, url, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14048,6 +13757,7 @@ private static class mln_map_add_raster_source_tiles { mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14059,7 +13769,7 @@ private static class mln_map_add_raster_source_tiles { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_raster_source_tiles$descriptor() { @@ -14069,7 +13779,7 @@ private static class mln_map_add_raster_source_tiles { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_raster_source_tiles$handle() { @@ -14079,7 +13789,7 @@ private static class mln_map_add_raster_source_tiles { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_raster_source_tiles$address() { @@ -14088,16 +13798,16 @@ private static class mln_map_add_raster_source_tiles { /** * {@snippet lang=c : - * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_raster_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options) { + public static int mln_map_add_raster_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_raster_source_tiles.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_raster_source_tiles", map, source_id, tiles, tile_count, options); + traceDowncall("mln_map_add_raster_source_tiles", map, source_id, tiles, tile_count, options, completion); } - return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options); + return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14111,6 +13821,7 @@ private static class mln_map_add_raster_dem_source_url { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14122,7 +13833,7 @@ private static class mln_map_add_raster_dem_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_raster_dem_source_url$descriptor() { @@ -14132,7 +13843,7 @@ private static class mln_map_add_raster_dem_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_raster_dem_source_url$handle() { @@ -14142,7 +13853,7 @@ private static class mln_map_add_raster_dem_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_raster_dem_source_url$address() { @@ -14151,16 +13862,16 @@ private static class mln_map_add_raster_dem_source_url { /** * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_raster_dem_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options) { + public static int mln_map_add_raster_dem_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_raster_dem_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_raster_dem_source_url", map, source_id, url, options); + traceDowncall("mln_map_add_raster_dem_source_url", map, source_id, url, options, completion); } - return (int)mh$.invokeExact(map, source_id, url, options); + return (int)mh$.invokeExact(map, source_id, url, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14175,6 +13886,7 @@ private static class mln_map_add_raster_dem_source_tiles { mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14186,7 +13898,7 @@ private static class mln_map_add_raster_dem_source_tiles { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_raster_dem_source_tiles$descriptor() { @@ -14196,7 +13908,7 @@ private static class mln_map_add_raster_dem_source_tiles { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_raster_dem_source_tiles$handle() { @@ -14206,7 +13918,7 @@ private static class mln_map_add_raster_dem_source_tiles { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_raster_dem_source_tiles$address() { @@ -14215,16 +13927,16 @@ private static class mln_map_add_raster_dem_source_tiles { /** * {@snippet lang=c : - * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options) + * mln_status mln_map_add_raster_dem_source_tiles(mln_map map, mln_buffer_view source_id, const mln_buffer_view *tiles, size_t tile_count, const mln_style_tile_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_raster_dem_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options) { + public static int mln_map_add_raster_dem_source_tiles(long map, MemorySegment source_id, MemorySegment tiles, long tile_count, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_raster_dem_source_tiles.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_raster_dem_source_tiles", map, source_id, tiles, tile_count, options); + traceDowncall("mln_map_add_raster_dem_source_tiles", map, source_id, tiles, tile_count, options, completion); } - return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options); + return (int)mh$.invokeExact(map, source_id, tiles, tile_count, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14237,6 +13949,7 @@ private static class mln_map_add_custom_geometry_source { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14248,7 +13961,7 @@ private static class mln_map_add_custom_geometry_source { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options) + * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_custom_geometry_source$descriptor() { @@ -14258,7 +13971,7 @@ private static class mln_map_add_custom_geometry_source { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options) + * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_custom_geometry_source$handle() { @@ -14268,7 +13981,7 @@ private static class mln_map_add_custom_geometry_source { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options) + * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_custom_geometry_source$address() { @@ -14277,16 +13990,16 @@ private static class mln_map_add_custom_geometry_source { /** * {@snippet lang=c : - * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options) + * mln_status mln_map_add_custom_geometry_source(mln_map map, mln_buffer_view source_id, const mln_custom_geometry_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_custom_geometry_source(long map, MemorySegment source_id, MemorySegment options) { + public static int mln_map_add_custom_geometry_source(long map, MemorySegment source_id, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_custom_geometry_source.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_custom_geometry_source", map, source_id, options); + traceDowncall("mln_map_add_custom_geometry_source", map, source_id, options, completion); } - return (int)mh$.invokeExact(map, source_id, options); + return (int)mh$.invokeExact(map, source_id, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14300,7 +14013,8 @@ private static class mln_map_set_custom_geometry_source_tile_data { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_canonical_tile_id.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_custom_geometry_source_tile_data"); @@ -14311,7 +14025,7 @@ private static class mln_map_set_custom_geometry_source_tile_data { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_custom_geometry_source_tile_data$descriptor() { @@ -14321,7 +14035,7 @@ private static class mln_map_set_custom_geometry_source_tile_data { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_custom_geometry_source_tile_data$handle() { @@ -14331,7 +14045,7 @@ private static class mln_map_set_custom_geometry_source_tile_data { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_custom_geometry_source_tile_data$address() { @@ -14340,16 +14054,16 @@ private static class mln_map_set_custom_geometry_source_tile_data { /** * {@snippet lang=c : - * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_geometry_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ - public static int mln_map_set_custom_geometry_source_tile_data(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment data) { + public static int mln_map_set_custom_geometry_source_tile_data(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment data, MemorySegment completion) { var mh$ = mln_map_set_custom_geometry_source_tile_data.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_custom_geometry_source_tile_data", map, source_id, tile_id, data); + traceDowncall("mln_map_set_custom_geometry_source_tile_data", map, source_id, tile_id, data, completion); } - return (int)mh$.invokeExact(map, source_id, tile_id, data); + return (int)mh$.invokeExact(map, source_id, tile_id, data, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14362,7 +14076,8 @@ private static class mln_map_invalidate_custom_geometry_source_tile { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_canonical_tile_id.layout() + mln_canonical_tile_id.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_invalidate_custom_geometry_source_tile"); @@ -14373,7 +14088,7 @@ private static class mln_map_invalidate_custom_geometry_source_tile { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_invalidate_custom_geometry_source_tile$descriptor() { @@ -14383,7 +14098,7 @@ private static class mln_map_invalidate_custom_geometry_source_tile { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_invalidate_custom_geometry_source_tile$handle() { @@ -14393,7 +14108,7 @@ private static class mln_map_invalidate_custom_geometry_source_tile { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_invalidate_custom_geometry_source_tile$address() { @@ -14402,16 +14117,16 @@ private static class mln_map_invalidate_custom_geometry_source_tile { /** * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_geometry_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ - public static int mln_map_invalidate_custom_geometry_source_tile(long map, MemorySegment source_id, MemorySegment tile_id) { + public static int mln_map_invalidate_custom_geometry_source_tile(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment completion) { var mh$ = mln_map_invalidate_custom_geometry_source_tile.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_invalidate_custom_geometry_source_tile", map, source_id, tile_id); + traceDowncall("mln_map_invalidate_custom_geometry_source_tile", map, source_id, tile_id, completion); } - return (int)mh$.invokeExact(map, source_id, tile_id); + return (int)mh$.invokeExact(map, source_id, tile_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14424,7 +14139,8 @@ private static class mln_map_invalidate_custom_geometry_source_region { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_lat_lng_bounds.layout() + mln_lat_lng_bounds.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_invalidate_custom_geometry_source_region"); @@ -14435,7 +14151,7 @@ private static class mln_map_invalidate_custom_geometry_source_region { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds) + * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_invalidate_custom_geometry_source_region$descriptor() { @@ -14445,7 +14161,7 @@ private static class mln_map_invalidate_custom_geometry_source_region { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds) + * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, const mln_completion *completion) * } */ public static MethodHandle mln_map_invalidate_custom_geometry_source_region$handle() { @@ -14455,7 +14171,7 @@ private static class mln_map_invalidate_custom_geometry_source_region { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds) + * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, const mln_completion *completion) * } */ public static MemorySegment mln_map_invalidate_custom_geometry_source_region$address() { @@ -14464,16 +14180,16 @@ private static class mln_map_invalidate_custom_geometry_source_region { /** * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds) + * mln_status mln_map_invalidate_custom_geometry_source_region(mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, const mln_completion *completion) * } */ - public static int mln_map_invalidate_custom_geometry_source_region(long map, MemorySegment source_id, MemorySegment bounds) { + public static int mln_map_invalidate_custom_geometry_source_region(long map, MemorySegment source_id, MemorySegment bounds, MemorySegment completion) { var mh$ = mln_map_invalidate_custom_geometry_source_region.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_invalidate_custom_geometry_source_region", map, source_id, bounds); + traceDowncall("mln_map_invalidate_custom_geometry_source_region", map, source_id, bounds, completion); } - return (int)mh$.invokeExact(map, source_id, bounds); + return (int)mh$.invokeExact(map, source_id, bounds, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14486,6 +14202,7 @@ private static class mln_map_add_custom_mvt_vector_source { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14497,7 +14214,7 @@ private static class mln_map_add_custom_mvt_vector_source { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options) + * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_custom_mvt_vector_source$descriptor() { @@ -14507,7 +14224,7 @@ private static class mln_map_add_custom_mvt_vector_source { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options) + * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_custom_mvt_vector_source$handle() { @@ -14517,7 +14234,7 @@ private static class mln_map_add_custom_mvt_vector_source { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options) + * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_custom_mvt_vector_source$address() { @@ -14526,16 +14243,16 @@ private static class mln_map_add_custom_mvt_vector_source { /** * {@snippet lang=c : - * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options) + * mln_status mln_map_add_custom_mvt_vector_source(mln_map map, mln_buffer_view source_id, const mln_custom_mvt_vector_source_options *options, const mln_completion *completion) * } */ - public static int mln_map_add_custom_mvt_vector_source(long map, MemorySegment source_id, MemorySegment options) { + public static int mln_map_add_custom_mvt_vector_source(long map, MemorySegment source_id, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_add_custom_mvt_vector_source.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_custom_mvt_vector_source", map, source_id, options); + traceDowncall("mln_map_add_custom_mvt_vector_source", map, source_id, options, completion); } - return (int)mh$.invokeExact(map, source_id, options); + return (int)mh$.invokeExact(map, source_id, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14549,7 +14266,8 @@ private static class mln_map_set_custom_mvt_vector_source_tile_data { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_canonical_tile_id.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_custom_mvt_vector_source_tile_data"); @@ -14560,7 +14278,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_data { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_custom_mvt_vector_source_tile_data$descriptor() { @@ -14570,7 +14288,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_data { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_custom_mvt_vector_source_tile_data$handle() { @@ -14580,7 +14298,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_data { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_custom_mvt_vector_source_tile_data$address() { @@ -14589,16 +14307,16 @@ private static class mln_map_set_custom_mvt_vector_source_tile_data { /** * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data) + * mln_status mln_map_set_custom_mvt_vector_source_tile_data(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view data, const mln_completion *completion) * } */ - public static int mln_map_set_custom_mvt_vector_source_tile_data(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment data) { + public static int mln_map_set_custom_mvt_vector_source_tile_data(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment data, MemorySegment completion) { var mh$ = mln_map_set_custom_mvt_vector_source_tile_data.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_custom_mvt_vector_source_tile_data", map, source_id, tile_id, data); + traceDowncall("mln_map_set_custom_mvt_vector_source_tile_data", map, source_id, tile_id, data, completion); } - return (int)mh$.invokeExact(map, source_id, tile_id, data); + return (int)mh$.invokeExact(map, source_id, tile_id, data, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14612,7 +14330,8 @@ private static class mln_map_set_custom_mvt_vector_source_tile_error { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_canonical_tile_id.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_custom_mvt_vector_source_tile_error"); @@ -14623,7 +14342,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_error { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message) + * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_custom_mvt_vector_source_tile_error$descriptor() { @@ -14633,7 +14352,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_error { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message) + * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_custom_mvt_vector_source_tile_error$handle() { @@ -14643,7 +14362,7 @@ private static class mln_map_set_custom_mvt_vector_source_tile_error { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message) + * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_custom_mvt_vector_source_tile_error$address() { @@ -14652,16 +14371,16 @@ private static class mln_map_set_custom_mvt_vector_source_tile_error { /** * {@snippet lang=c : - * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message) + * mln_status mln_map_set_custom_mvt_vector_source_tile_error(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, mln_buffer_view message, const mln_completion *completion) * } */ - public static int mln_map_set_custom_mvt_vector_source_tile_error(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment message) { + public static int mln_map_set_custom_mvt_vector_source_tile_error(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment message, MemorySegment completion) { var mh$ = mln_map_set_custom_mvt_vector_source_tile_error.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_custom_mvt_vector_source_tile_error", map, source_id, tile_id, message); + traceDowncall("mln_map_set_custom_mvt_vector_source_tile_error", map, source_id, tile_id, message, completion); } - return (int)mh$.invokeExact(map, source_id, tile_id, message); + return (int)mh$.invokeExact(map, source_id, tile_id, message, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14674,7 +14393,8 @@ private static class mln_map_invalidate_custom_mvt_vector_source_tile { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_canonical_tile_id.layout() + mln_canonical_tile_id.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_invalidate_custom_mvt_vector_source_tile"); @@ -14685,7 +14405,7 @@ private static class mln_map_invalidate_custom_mvt_vector_source_tile { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_invalidate_custom_mvt_vector_source_tile$descriptor() { @@ -14695,7 +14415,7 @@ private static class mln_map_invalidate_custom_mvt_vector_source_tile { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_invalidate_custom_mvt_vector_source_tile$handle() { @@ -14705,7 +14425,7 @@ private static class mln_map_invalidate_custom_mvt_vector_source_tile { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_invalidate_custom_mvt_vector_source_tile$address() { @@ -14714,16 +14434,16 @@ private static class mln_map_invalidate_custom_mvt_vector_source_tile { /** * {@snippet lang=c : - * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id) + * mln_status mln_map_invalidate_custom_mvt_vector_source_tile(mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, const mln_completion *completion) * } */ - public static int mln_map_invalidate_custom_mvt_vector_source_tile(long map, MemorySegment source_id, MemorySegment tile_id) { + public static int mln_map_invalidate_custom_mvt_vector_source_tile(long map, MemorySegment source_id, MemorySegment tile_id, MemorySegment completion) { var mh$ = mln_map_invalidate_custom_mvt_vector_source_tile.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_invalidate_custom_mvt_vector_source_tile", map, source_id, tile_id); + traceDowncall("mln_map_invalidate_custom_mvt_vector_source_tile", map, source_id, tile_id, completion); } - return (int)mh$.invokeExact(map, source_id, tile_id); + return (int)mh$.invokeExact(map, source_id, tile_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14737,6 +14457,7 @@ private static class mln_map_set_style_image { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14748,7 +14469,7 @@ private static class mln_map_set_style_image { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options) + * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_style_image$descriptor() { @@ -14758,7 +14479,7 @@ private static class mln_map_set_style_image { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options) + * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_style_image$handle() { @@ -14768,7 +14489,7 @@ private static class mln_map_set_style_image { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options) + * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_style_image$address() { @@ -14777,16 +14498,16 @@ private static class mln_map_set_style_image { /** * {@snippet lang=c : - * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options) + * mln_status mln_map_set_style_image(mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image *image, const mln_style_image_options *options, const mln_completion *completion) * } */ - public static int mln_map_set_style_image(long map, MemorySegment image_id, MemorySegment image, MemorySegment options) { + public static int mln_map_set_style_image(long map, MemorySegment image_id, MemorySegment image, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_set_style_image.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_image", map, image_id, image, options); + traceDowncall("mln_map_set_style_image", map, image_id, image, options, completion); } - return (int)mh$.invokeExact(map, image_id, image, options); + return (int)mh$.invokeExact(map, image_id, image, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14810,7 +14531,7 @@ private static class mln_map_remove_style_image { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, bool *out_removed) + * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_remove_style_image$descriptor() { @@ -14820,7 +14541,7 @@ private static class mln_map_remove_style_image { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, bool *out_removed) + * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_remove_style_image$handle() { @@ -14830,7 +14551,7 @@ private static class mln_map_remove_style_image { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, bool *out_removed) + * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_remove_style_image$address() { @@ -14839,78 +14560,16 @@ private static class mln_map_remove_style_image { /** * {@snippet lang=c : - * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, bool *out_removed) + * mln_status mln_map_remove_style_image(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ - public static int mln_map_remove_style_image(long map, MemorySegment image_id, MemorySegment out_removed) { + public static int mln_map_remove_style_image(long map, MemorySegment image_id, MemorySegment completion) { var mh$ = mln_map_remove_style_image.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_remove_style_image", map, image_id, out_removed); - } - return (int)mh$.invokeExact(map, image_id, out_removed); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_style_image_exists { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_style_image_exists"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_style_image_exists(mln_map map, mln_buffer_view image_id, bool *out_exists) - * } - */ - public static FunctionDescriptor mln_map_style_image_exists$descriptor() { - return mln_map_style_image_exists.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_style_image_exists(mln_map map, mln_buffer_view image_id, bool *out_exists) - * } - */ - public static MethodHandle mln_map_style_image_exists$handle() { - return mln_map_style_image_exists.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_style_image_exists(mln_map map, mln_buffer_view image_id, bool *out_exists) - * } - */ - public static MemorySegment mln_map_style_image_exists$address() { - return mln_map_style_image_exists.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_style_image_exists(mln_map map, mln_buffer_view image_id, bool *out_exists) - * } - */ - public static int mln_map_style_image_exists(long map, MemorySegment image_id, MemorySegment out_exists) { - var mh$ = mln_map_style_image_exists.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_style_image_exists", map, image_id, out_exists); + traceDowncall("mln_map_remove_style_image", map, image_id, completion); } - return (int)mh$.invokeExact(map, image_id, out_exists); + return (int)mh$.invokeExact(map, image_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14923,7 +14582,6 @@ private static class mln_map_get_style_image_info { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -14935,7 +14593,7 @@ private static class mln_map_get_style_image_info { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, mln_style_image_info *out_info, bool *out_found) + * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_image_info$descriptor() { @@ -14945,7 +14603,7 @@ private static class mln_map_get_style_image_info { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, mln_style_image_info *out_info, bool *out_found) + * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_image_info$handle() { @@ -14955,7 +14613,7 @@ private static class mln_map_get_style_image_info { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, mln_style_image_info *out_info, bool *out_found) + * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_image_info$address() { @@ -14964,16 +14622,16 @@ private static class mln_map_get_style_image_info { /** * {@snippet lang=c : - * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, mln_style_image_info *out_info, bool *out_found) + * mln_status mln_map_get_style_image_info(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ - public static int mln_map_get_style_image_info(long map, MemorySegment image_id, MemorySegment out_info, MemorySegment out_found) { + public static int mln_map_get_style_image_info(long map, MemorySegment image_id, MemorySegment completion) { var mh$ = mln_map_get_style_image_info.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_image_info", map, image_id, out_info, out_found); + traceDowncall("mln_map_get_style_image_info", map, image_id, completion); } - return (int)mh$.invokeExact(map, image_id, out_info, out_found); + return (int)mh$.invokeExact(map, image_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -14986,9 +14644,6 @@ private static class mln_map_copy_style_image_premultiplied_rgba8 { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -15000,7 +14655,7 @@ private static class mln_map_copy_style_image_premultiplied_rgba8 { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, uint8_t *out_pixels, size_t pixel_capacity, size_t *out_byte_length, bool *out_found) + * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_style_image_premultiplied_rgba8$descriptor() { @@ -15010,7 +14665,7 @@ private static class mln_map_copy_style_image_premultiplied_rgba8 { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, uint8_t *out_pixels, size_t pixel_capacity, size_t *out_byte_length, bool *out_found) + * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_style_image_premultiplied_rgba8$handle() { @@ -15020,7 +14675,7 @@ private static class mln_map_copy_style_image_premultiplied_rgba8 { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, uint8_t *out_pixels, size_t pixel_capacity, size_t *out_byte_length, bool *out_found) + * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_style_image_premultiplied_rgba8$address() { @@ -15029,16 +14684,16 @@ private static class mln_map_copy_style_image_premultiplied_rgba8 { /** * {@snippet lang=c : - * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, uint8_t *out_pixels, size_t pixel_capacity, size_t *out_byte_length, bool *out_found) + * mln_status mln_map_copy_style_image_premultiplied_rgba8(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ - public static int mln_map_copy_style_image_premultiplied_rgba8(long map, MemorySegment image_id, MemorySegment out_pixels, long pixel_capacity, MemorySegment out_byte_length, MemorySegment out_found) { + public static int mln_map_copy_style_image_premultiplied_rgba8(long map, MemorySegment image_id, MemorySegment completion) { var mh$ = mln_map_copy_style_image_premultiplied_rgba8.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_style_image_premultiplied_rgba8", map, image_id, out_pixels, pixel_capacity, out_byte_length, out_found); + traceDowncall("mln_map_copy_style_image_premultiplied_rgba8", map, image_id, completion); } - return (int)mh$.invokeExact(map, image_id, out_pixels, pixel_capacity, out_byte_length, out_found); + return (int)mh$.invokeExact(map, image_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15051,12 +14706,6 @@ private static class mln_map_copy_style_image_stretches { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -15068,7 +14717,7 @@ private static class mln_map_copy_style_image_stretches { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, mln_image_stretch *out_stretch_x, size_t stretch_x_capacity, size_t *out_stretch_x_count, mln_image_stretch *out_stretch_y, size_t stretch_y_capacity, size_t *out_stretch_y_count, bool *out_found) + * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_style_image_stretches$descriptor() { @@ -15078,7 +14727,7 @@ private static class mln_map_copy_style_image_stretches { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, mln_image_stretch *out_stretch_x, size_t stretch_x_capacity, size_t *out_stretch_x_count, mln_image_stretch *out_stretch_y, size_t stretch_y_capacity, size_t *out_stretch_y_count, bool *out_found) + * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_style_image_stretches$handle() { @@ -15088,7 +14737,7 @@ private static class mln_map_copy_style_image_stretches { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, mln_image_stretch *out_stretch_x, size_t stretch_x_capacity, size_t *out_stretch_x_count, mln_image_stretch *out_stretch_y, size_t stretch_y_capacity, size_t *out_stretch_y_count, bool *out_found) + * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_style_image_stretches$address() { @@ -15097,16 +14746,16 @@ private static class mln_map_copy_style_image_stretches { /** * {@snippet lang=c : - * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, mln_image_stretch *out_stretch_x, size_t stretch_x_capacity, size_t *out_stretch_x_count, mln_image_stretch *out_stretch_y, size_t stretch_y_capacity, size_t *out_stretch_y_count, bool *out_found) + * mln_status mln_map_copy_style_image_stretches(mln_map map, mln_buffer_view image_id, const mln_completion *completion) * } */ - public static int mln_map_copy_style_image_stretches(long map, MemorySegment image_id, MemorySegment out_stretch_x, long stretch_x_capacity, MemorySegment out_stretch_x_count, MemorySegment out_stretch_y, long stretch_y_capacity, MemorySegment out_stretch_y_count, MemorySegment out_found) { + public static int mln_map_copy_style_image_stretches(long map, MemorySegment image_id, MemorySegment completion) { var mh$ = mln_map_copy_style_image_stretches.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_style_image_stretches", map, image_id, out_stretch_x, stretch_x_capacity, out_stretch_x_count, out_stretch_y, stretch_y_capacity, out_stretch_y_count, out_found); + traceDowncall("mln_map_copy_style_image_stretches", map, image_id, completion); } - return (int)mh$.invokeExact(map, image_id, out_stretch_x, stretch_x_capacity, out_stretch_x_count, out_stretch_y, stretch_y_capacity, out_stretch_y_count, out_found); + return (int)mh$.invokeExact(map, image_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15121,7 +14770,8 @@ private static class mln_map_add_image_source_url { mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, MapLibreNativeC.C_LONG, - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_image_source_url"); @@ -15132,7 +14782,7 @@ private static class mln_map_add_image_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url) + * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_image_source_url$descriptor() { @@ -15142,7 +14792,7 @@ private static class mln_map_add_image_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url) + * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_image_source_url$handle() { @@ -15152,7 +14802,7 @@ private static class mln_map_add_image_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url) + * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_image_source_url$address() { @@ -15161,16 +14811,16 @@ private static class mln_map_add_image_source_url { /** * {@snippet lang=c : - * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url) + * mln_status mln_map_add_image_source_url(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, mln_buffer_view url, const mln_completion *completion) * } */ - public static int mln_map_add_image_source_url(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count, MemorySegment url) { + public static int mln_map_add_image_source_url(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count, MemorySegment url, MemorySegment completion) { var mh$ = mln_map_add_image_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_image_source_url", map, source_id, coordinates, coordinate_count, url); + traceDowncall("mln_map_add_image_source_url", map, source_id, coordinates, coordinate_count, url, completion); } - return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count, url); + return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count, url, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15185,6 +14835,7 @@ private static class mln_map_add_image_source_image { mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -15196,7 +14847,7 @@ private static class mln_map_add_image_source_image { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_image_source_image$descriptor() { @@ -15206,7 +14857,7 @@ private static class mln_map_add_image_source_image { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_image_source_image$handle() { @@ -15216,7 +14867,7 @@ private static class mln_map_add_image_source_image { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_image_source_image$address() { @@ -15225,16 +14876,16 @@ private static class mln_map_add_image_source_image { /** * {@snippet lang=c : - * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_add_image_source_image(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ - public static int mln_map_add_image_source_image(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count, MemorySegment image) { + public static int mln_map_add_image_source_image(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count, MemorySegment image, MemorySegment completion) { var mh$ = mln_map_add_image_source_image.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_image_source_image", map, source_id, coordinates, coordinate_count, image); + traceDowncall("mln_map_add_image_source_image", map, source_id, coordinates, coordinate_count, image, completion); } - return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count, image); + return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count, image, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15247,7 +14898,8 @@ private static class mln_map_set_image_source_url { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_image_source_url"); @@ -15258,7 +14910,7 @@ private static class mln_map_set_image_source_url { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_image_source_url$descriptor() { @@ -15268,7 +14920,7 @@ private static class mln_map_set_image_source_url { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_image_source_url$handle() { @@ -15278,7 +14930,7 @@ private static class mln_map_set_image_source_url { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_image_source_url$address() { @@ -15287,16 +14939,16 @@ private static class mln_map_set_image_source_url { /** * {@snippet lang=c : - * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url) + * mln_status mln_map_set_image_source_url(mln_map map, mln_buffer_view source_id, mln_buffer_view url, const mln_completion *completion) * } */ - public static int mln_map_set_image_source_url(long map, MemorySegment source_id, MemorySegment url) { + public static int mln_map_set_image_source_url(long map, MemorySegment source_id, MemorySegment url, MemorySegment completion) { var mh$ = mln_map_set_image_source_url.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_image_source_url", map, source_id, url); + traceDowncall("mln_map_set_image_source_url", map, source_id, url, completion); } - return (int)mh$.invokeExact(map, source_id, url); + return (int)mh$.invokeExact(map, source_id, url, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15309,6 +14961,7 @@ private static class mln_map_set_image_source_image { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -15320,7 +14973,7 @@ private static class mln_map_set_image_source_image { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_image_source_image$descriptor() { @@ -15330,7 +14983,7 @@ private static class mln_map_set_image_source_image { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_image_source_image$handle() { @@ -15340,7 +14993,7 @@ private static class mln_map_set_image_source_image { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_image_source_image$address() { @@ -15349,16 +15002,16 @@ private static class mln_map_set_image_source_image { /** * {@snippet lang=c : - * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image) + * mln_status mln_map_set_image_source_image(mln_map map, mln_buffer_view source_id, const mln_premultiplied_rgba8_image *image, const mln_completion *completion) * } */ - public static int mln_map_set_image_source_image(long map, MemorySegment source_id, MemorySegment image) { + public static int mln_map_set_image_source_image(long map, MemorySegment source_id, MemorySegment image, MemorySegment completion) { var mh$ = mln_map_set_image_source_image.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_image_source_image", map, source_id, image); + traceDowncall("mln_map_set_image_source_image", map, source_id, image, completion); } - return (int)mh$.invokeExact(map, source_id, image); + return (int)mh$.invokeExact(map, source_id, image, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15372,7 +15025,8 @@ private static class mln_map_set_image_source_coordinates { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG + MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_image_source_coordinates"); @@ -15383,7 +15037,7 @@ private static class mln_map_set_image_source_coordinates { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count) + * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_image_source_coordinates$descriptor() { @@ -15393,7 +15047,7 @@ private static class mln_map_set_image_source_coordinates { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count) + * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_image_source_coordinates$handle() { @@ -15403,7 +15057,7 @@ private static class mln_map_set_image_source_coordinates { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count) + * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_image_source_coordinates$address() { @@ -15412,16 +15066,16 @@ private static class mln_map_set_image_source_coordinates { /** * {@snippet lang=c : - * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count) + * mln_status mln_map_set_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_lat_lng *coordinates, size_t coordinate_count, const mln_completion *completion) * } */ - public static int mln_map_set_image_source_coordinates(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count) { + public static int mln_map_set_image_source_coordinates(long map, MemorySegment source_id, MemorySegment coordinates, long coordinate_count, MemorySegment completion) { var mh$ = mln_map_set_image_source_coordinates.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_image_source_coordinates", map, source_id, coordinates, coordinate_count); + traceDowncall("mln_map_set_image_source_coordinates", map, source_id, coordinates, coordinate_count, completion); } - return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count); + return (int)mh$.invokeExact(map, source_id, coordinates, coordinate_count, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15434,9 +15088,6 @@ private static class mln_map_get_image_source_coordinates { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -15448,7 +15099,7 @@ private static class mln_map_get_image_source_coordinates { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, mln_lat_lng *out_coordinates, size_t coordinate_capacity, size_t *out_coordinate_count, bool *out_found) + * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_image_source_coordinates$descriptor() { @@ -15458,7 +15109,7 @@ private static class mln_map_get_image_source_coordinates { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, mln_lat_lng *out_coordinates, size_t coordinate_capacity, size_t *out_coordinate_count, bool *out_found) + * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_image_source_coordinates$handle() { @@ -15468,7 +15119,7 @@ private static class mln_map_get_image_source_coordinates { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, mln_lat_lng *out_coordinates, size_t coordinate_capacity, size_t *out_coordinate_count, bool *out_found) + * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_image_source_coordinates$address() { @@ -15477,16 +15128,16 @@ private static class mln_map_get_image_source_coordinates { /** * {@snippet lang=c : - * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, mln_lat_lng *out_coordinates, size_t coordinate_capacity, size_t *out_coordinate_count, bool *out_found) + * mln_status mln_map_get_image_source_coordinates(mln_map map, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_get_image_source_coordinates(long map, MemorySegment source_id, MemorySegment out_coordinates, long coordinate_capacity, MemorySegment out_coordinate_count, MemorySegment out_found) { + public static int mln_map_get_image_source_coordinates(long map, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_get_image_source_coordinates.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_image_source_coordinates", map, source_id, out_coordinates, coordinate_capacity, out_coordinate_count, out_found); + traceDowncall("mln_map_get_image_source_coordinates", map, source_id, completion); } - return (int)mh$.invokeExact(map, source_id, out_coordinates, coordinate_capacity, out_coordinate_count, out_found); + return (int)mh$.invokeExact(map, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15500,7 +15151,8 @@ private static class mln_map_add_hillshade_layer { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_hillshade_layer"); @@ -15511,7 +15163,7 @@ private static class mln_map_add_hillshade_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_hillshade_layer$descriptor() { @@ -15521,7 +15173,7 @@ private static class mln_map_add_hillshade_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_hillshade_layer$handle() { @@ -15531,7 +15183,7 @@ private static class mln_map_add_hillshade_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_hillshade_layer$address() { @@ -15540,16 +15192,16 @@ private static class mln_map_add_hillshade_layer { /** * {@snippet lang=c : - * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_hillshade_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ - public static int mln_map_add_hillshade_layer(long map, MemorySegment layer_id, MemorySegment source_id, MemorySegment before_layer_id) { + public static int mln_map_add_hillshade_layer(long map, MemorySegment layer_id, MemorySegment source_id, MemorySegment before_layer_id, MemorySegment completion) { var mh$ = mln_map_add_hillshade_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_hillshade_layer", map, layer_id, source_id, before_layer_id); + traceDowncall("mln_map_add_hillshade_layer", map, layer_id, source_id, before_layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, source_id, before_layer_id); + return (int)mh$.invokeExact(map, layer_id, source_id, before_layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15563,7 +15215,8 @@ private static class mln_map_add_color_relief_layer { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_color_relief_layer"); @@ -15574,7 +15227,7 @@ private static class mln_map_add_color_relief_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_color_relief_layer$descriptor() { @@ -15584,7 +15237,7 @@ private static class mln_map_add_color_relief_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_color_relief_layer$handle() { @@ -15594,7 +15247,7 @@ private static class mln_map_add_color_relief_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_color_relief_layer$address() { @@ -15603,16 +15256,16 @@ private static class mln_map_add_color_relief_layer { /** * {@snippet lang=c : - * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_color_relief_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ - public static int mln_map_add_color_relief_layer(long map, MemorySegment layer_id, MemorySegment source_id, MemorySegment before_layer_id) { + public static int mln_map_add_color_relief_layer(long map, MemorySegment layer_id, MemorySegment source_id, MemorySegment before_layer_id, MemorySegment completion) { var mh$ = mln_map_add_color_relief_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_color_relief_layer", map, layer_id, source_id, before_layer_id); + traceDowncall("mln_map_add_color_relief_layer", map, layer_id, source_id, before_layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, source_id, before_layer_id); + return (int)mh$.invokeExact(map, layer_id, source_id, before_layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15625,7 +15278,8 @@ private static class mln_map_add_location_indicator_layer { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_location_indicator_layer"); @@ -15636,7 +15290,7 @@ private static class mln_map_add_location_indicator_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_location_indicator_layer$descriptor() { @@ -15646,7 +15300,7 @@ private static class mln_map_add_location_indicator_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_location_indicator_layer$handle() { @@ -15656,7 +15310,7 @@ private static class mln_map_add_location_indicator_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_location_indicator_layer$address() { @@ -15665,16 +15319,16 @@ private static class mln_map_add_location_indicator_layer { /** * {@snippet lang=c : - * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_add_location_indicator_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ - public static int mln_map_add_location_indicator_layer(long map, MemorySegment layer_id, MemorySegment before_layer_id) { + public static int mln_map_add_location_indicator_layer(long map, MemorySegment layer_id, MemorySegment before_layer_id, MemorySegment completion) { var mh$ = mln_map_add_location_indicator_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_location_indicator_layer", map, layer_id, before_layer_id); + traceDowncall("mln_map_add_location_indicator_layer", map, layer_id, before_layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, before_layer_id); + return (int)mh$.invokeExact(map, layer_id, before_layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15688,7 +15342,8 @@ private static class mln_map_set_location_indicator_location { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_lat_lng.layout(), - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_location_indicator_location"); @@ -15699,7 +15354,7 @@ private static class mln_map_set_location_indicator_location { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude) + * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_location_indicator_location$descriptor() { @@ -15709,7 +15364,7 @@ private static class mln_map_set_location_indicator_location { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude) + * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_location_indicator_location$handle() { @@ -15719,7 +15374,7 @@ private static class mln_map_set_location_indicator_location { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude) + * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_location_indicator_location$address() { @@ -15728,16 +15383,16 @@ private static class mln_map_set_location_indicator_location { /** * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude) + * mln_status mln_map_set_location_indicator_location(mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude, const mln_completion *completion) * } */ - public static int mln_map_set_location_indicator_location(long map, MemorySegment layer_id, MemorySegment coordinate, double altitude) { + public static int mln_map_set_location_indicator_location(long map, MemorySegment layer_id, MemorySegment coordinate, double altitude, MemorySegment completion) { var mh$ = mln_map_set_location_indicator_location.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_location_indicator_location", map, layer_id, coordinate, altitude); + traceDowncall("mln_map_set_location_indicator_location", map, layer_id, coordinate, altitude, completion); } - return (int)mh$.invokeExact(map, layer_id, coordinate, altitude); + return (int)mh$.invokeExact(map, layer_id, coordinate, altitude, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15750,7 +15405,8 @@ private static class mln_map_set_location_indicator_bearing { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_location_indicator_bearing"); @@ -15761,7 +15417,7 @@ private static class mln_map_set_location_indicator_bearing { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing) + * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_location_indicator_bearing$descriptor() { @@ -15771,7 +15427,7 @@ private static class mln_map_set_location_indicator_bearing { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing) + * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_location_indicator_bearing$handle() { @@ -15781,7 +15437,7 @@ private static class mln_map_set_location_indicator_bearing { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing) + * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_location_indicator_bearing$address() { @@ -15790,16 +15446,16 @@ private static class mln_map_set_location_indicator_bearing { /** * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing) + * mln_status mln_map_set_location_indicator_bearing(mln_map map, mln_buffer_view layer_id, double bearing, const mln_completion *completion) * } */ - public static int mln_map_set_location_indicator_bearing(long map, MemorySegment layer_id, double bearing) { + public static int mln_map_set_location_indicator_bearing(long map, MemorySegment layer_id, double bearing, MemorySegment completion) { var mh$ = mln_map_set_location_indicator_bearing.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_location_indicator_bearing", map, layer_id, bearing); + traceDowncall("mln_map_set_location_indicator_bearing", map, layer_id, bearing, completion); } - return (int)mh$.invokeExact(map, layer_id, bearing); + return (int)mh$.invokeExact(map, layer_id, bearing, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15812,7 +15468,8 @@ private static class mln_map_set_location_indicator_accuracy_radius { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_location_indicator_accuracy_radius"); @@ -15823,7 +15480,7 @@ private static class mln_map_set_location_indicator_accuracy_radius { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius) + * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_location_indicator_accuracy_radius$descriptor() { @@ -15833,7 +15490,7 @@ private static class mln_map_set_location_indicator_accuracy_radius { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius) + * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_location_indicator_accuracy_radius$handle() { @@ -15843,7 +15500,7 @@ private static class mln_map_set_location_indicator_accuracy_radius { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius) + * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_location_indicator_accuracy_radius$address() { @@ -15852,16 +15509,16 @@ private static class mln_map_set_location_indicator_accuracy_radius { /** * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius) + * mln_status mln_map_set_location_indicator_accuracy_radius(mln_map map, mln_buffer_view layer_id, double radius, const mln_completion *completion) * } */ - public static int mln_map_set_location_indicator_accuracy_radius(long map, MemorySegment layer_id, double radius) { + public static int mln_map_set_location_indicator_accuracy_radius(long map, MemorySegment layer_id, double radius, MemorySegment completion) { var mh$ = mln_map_set_location_indicator_accuracy_radius.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_location_indicator_accuracy_radius", map, layer_id, radius); + traceDowncall("mln_map_set_location_indicator_accuracy_radius", map, layer_id, radius, completion); } - return (int)mh$.invokeExact(map, layer_id, radius); + return (int)mh$.invokeExact(map, layer_id, radius, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15875,7 +15532,8 @@ private static class mln_map_set_location_indicator_image_name { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), MapLibreNativeC.C_INT, - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_location_indicator_image_name"); @@ -15886,7 +15544,7 @@ private static class mln_map_set_location_indicator_image_name { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id) + * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_location_indicator_image_name$descriptor() { @@ -15896,7 +15554,7 @@ private static class mln_map_set_location_indicator_image_name { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id) + * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_location_indicator_image_name$handle() { @@ -15906,7 +15564,7 @@ private static class mln_map_set_location_indicator_image_name { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id) + * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_location_indicator_image_name$address() { @@ -15915,16 +15573,16 @@ private static class mln_map_set_location_indicator_image_name { /** * {@snippet lang=c : - * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id) + * mln_status mln_map_set_location_indicator_image_name(mln_map map, mln_buffer_view layer_id, uint32_t image_kind, mln_buffer_view image_id, const mln_completion *completion) * } */ - public static int mln_map_set_location_indicator_image_name(long map, MemorySegment layer_id, int image_kind, MemorySegment image_id) { + public static int mln_map_set_location_indicator_image_name(long map, MemorySegment layer_id, int image_kind, MemorySegment image_id, MemorySegment completion) { var mh$ = mln_map_set_location_indicator_image_name.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_location_indicator_image_name", map, layer_id, image_kind, image_id); + traceDowncall("mln_map_set_location_indicator_image_name", map, layer_id, image_kind, image_id, completion); } - return (int)mh$.invokeExact(map, layer_id, image_kind, image_id); + return (int)mh$.invokeExact(map, layer_id, image_kind, image_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -15937,7 +15595,8 @@ private static class mln_map_add_style_layer_json { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_add_style_layer_json"); @@ -15948,7 +15607,7 @@ private static class mln_map_add_style_layer_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id) + * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_add_style_layer_json$descriptor() { @@ -15958,7 +15617,7 @@ private static class mln_map_add_style_layer_json { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id) + * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_add_style_layer_json$handle() { @@ -15968,7 +15627,7 @@ private static class mln_map_add_style_layer_json { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id) + * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_add_style_layer_json$address() { @@ -15977,16 +15636,16 @@ private static class mln_map_add_style_layer_json { /** * {@snippet lang=c : - * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id) + * mln_status mln_map_add_style_layer_json(mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ - public static int mln_map_add_style_layer_json(long map, MemorySegment layer_json, MemorySegment before_layer_id) { + public static int mln_map_add_style_layer_json(long map, MemorySegment layer_json, MemorySegment before_layer_id, MemorySegment completion) { var mh$ = mln_map_add_style_layer_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_add_style_layer_json", map, layer_json, before_layer_id); + traceDowncall("mln_map_add_style_layer_json", map, layer_json, before_layer_id, completion); } - return (int)mh$.invokeExact(map, layer_json, before_layer_id); + return (int)mh$.invokeExact(map, layer_json, before_layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16010,7 +15669,7 @@ private static class mln_map_remove_style_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, bool *out_removed) + * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_remove_style_layer$descriptor() { @@ -16020,7 +15679,7 @@ private static class mln_map_remove_style_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, bool *out_removed) + * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_remove_style_layer$handle() { @@ -16030,7 +15689,7 @@ private static class mln_map_remove_style_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, bool *out_removed) + * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_remove_style_layer$address() { @@ -16039,78 +15698,16 @@ private static class mln_map_remove_style_layer { /** * {@snippet lang=c : - * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, bool *out_removed) + * mln_status mln_map_remove_style_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_remove_style_layer(long map, MemorySegment layer_id, MemorySegment out_removed) { + public static int mln_map_remove_style_layer(long map, MemorySegment layer_id, MemorySegment completion) { var mh$ = mln_map_remove_style_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_remove_style_layer", map, layer_id, out_removed); - } - return (int)mh$.invokeExact(map, layer_id, out_removed); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_style_layer_exists { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_style_layer_exists"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_style_layer_exists(mln_map map, mln_buffer_view layer_id, bool *out_exists) - * } - */ - public static FunctionDescriptor mln_map_style_layer_exists$descriptor() { - return mln_map_style_layer_exists.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_style_layer_exists(mln_map map, mln_buffer_view layer_id, bool *out_exists) - * } - */ - public static MethodHandle mln_map_style_layer_exists$handle() { - return mln_map_style_layer_exists.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_style_layer_exists(mln_map map, mln_buffer_view layer_id, bool *out_exists) - * } - */ - public static MemorySegment mln_map_style_layer_exists$address() { - return mln_map_style_layer_exists.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_style_layer_exists(mln_map map, mln_buffer_view layer_id, bool *out_exists) - * } - */ - public static int mln_map_style_layer_exists(long map, MemorySegment layer_id, MemorySegment out_exists) { - var mh$ = mln_map_style_layer_exists.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_style_layer_exists", map, layer_id, out_exists); + traceDowncall("mln_map_remove_style_layer", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_exists); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16118,16 +15715,15 @@ public static int mln_map_style_layer_exists(long map, MemorySegment layer_id, M } } - private static class mln_map_get_style_layer_type { + private static class mln_map_get_style_layer_info { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_style_layer_type"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_style_layer_info"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -16135,45 +15731,45 @@ private static class mln_map_get_style_layer_type { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_type(mln_map map, mln_buffer_view layer_id, mln_buffer_view *out_layer_type, bool *out_found) + * mln_status mln_map_get_style_layer_info(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_style_layer_type$descriptor() { - return mln_map_get_style_layer_type.DESC; + public static FunctionDescriptor mln_map_get_style_layer_info$descriptor() { + return mln_map_get_style_layer_info.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_type(mln_map map, mln_buffer_view layer_id, mln_buffer_view *out_layer_type, bool *out_found) + * mln_status mln_map_get_style_layer_info(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_style_layer_type$handle() { - return mln_map_get_style_layer_type.HANDLE; + public static MethodHandle mln_map_get_style_layer_info$handle() { + return mln_map_get_style_layer_info.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_type(mln_map map, mln_buffer_view layer_id, mln_buffer_view *out_layer_type, bool *out_found) + * mln_status mln_map_get_style_layer_info(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_style_layer_type$address() { - return mln_map_get_style_layer_type.ADDR; + public static MemorySegment mln_map_get_style_layer_info$address() { + return mln_map_get_style_layer_info.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_style_layer_type(mln_map map, mln_buffer_view layer_id, mln_buffer_view *out_layer_type, bool *out_found) + * mln_status mln_map_get_style_layer_info(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_get_style_layer_type(long map, MemorySegment layer_id, MemorySegment out_layer_type, MemorySegment out_found) { - var mh$ = mln_map_get_style_layer_type.HANDLE; + public static int mln_map_get_style_layer_info(long map, MemorySegment layer_id, MemorySegment completion) { + var mh$ = mln_map_get_style_layer_info.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_layer_type", map, layer_id, out_layer_type, out_found); + traceDowncall("mln_map_get_style_layer_info", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_layer_type, out_found); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16196,7 +15792,7 @@ private static class mln_map_list_style_layer_ids { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_list_style_layer_ids(mln_map map, mln_style_id_list *out_layer_ids) + * mln_status mln_map_list_style_layer_ids(mln_map map, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_list_style_layer_ids$descriptor() { @@ -16206,7 +15802,7 @@ private static class mln_map_list_style_layer_ids { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_list_style_layer_ids(mln_map map, mln_style_id_list *out_layer_ids) + * mln_status mln_map_list_style_layer_ids(mln_map map, const mln_completion *completion) * } */ public static MethodHandle mln_map_list_style_layer_ids$handle() { @@ -16216,7 +15812,7 @@ private static class mln_map_list_style_layer_ids { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_list_style_layer_ids(mln_map map, mln_style_id_list *out_layer_ids) + * mln_status mln_map_list_style_layer_ids(mln_map map, const mln_completion *completion) * } */ public static MemorySegment mln_map_list_style_layer_ids$address() { @@ -16225,16 +15821,16 @@ private static class mln_map_list_style_layer_ids { /** * {@snippet lang=c : - * mln_status mln_map_list_style_layer_ids(mln_map map, mln_style_id_list *out_layer_ids) + * mln_status mln_map_list_style_layer_ids(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_list_style_layer_ids(long map, MemorySegment out_layer_ids) { + public static int mln_map_list_style_layer_ids(long map, MemorySegment completion) { var mh$ = mln_map_list_style_layer_ids.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_list_style_layer_ids", map, out_layer_ids); + traceDowncall("mln_map_list_style_layer_ids", map, completion); } - return (int)mh$.invokeExact(map, out_layer_ids); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16247,7 +15843,8 @@ private static class mln_map_move_style_layer { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_move_style_layer"); @@ -16258,7 +15855,7 @@ private static class mln_map_move_style_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_move_style_layer$descriptor() { @@ -16268,7 +15865,7 @@ private static class mln_map_move_style_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_move_style_layer$handle() { @@ -16278,7 +15875,7 @@ private static class mln_map_move_style_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_move_style_layer$address() { @@ -16287,16 +15884,16 @@ private static class mln_map_move_style_layer { /** * {@snippet lang=c : - * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id) + * mln_status mln_map_move_style_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, const mln_completion *completion) * } */ - public static int mln_map_move_style_layer(long map, MemorySegment layer_id, MemorySegment before_layer_id) { + public static int mln_map_move_style_layer(long map, MemorySegment layer_id, MemorySegment before_layer_id, MemorySegment completion) { var mh$ = mln_map_move_style_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_move_style_layer", map, layer_id, before_layer_id); + traceDowncall("mln_map_move_style_layer", map, layer_id, before_layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, before_layer_id); + return (int)mh$.invokeExact(map, layer_id, before_layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16309,7 +15906,6 @@ private static class mln_map_get_style_layer_json { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -16321,7 +15917,7 @@ private static class mln_map_get_style_layer_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, mln_buffer *out_layer, bool *out_found) + * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_layer_json$descriptor() { @@ -16331,7 +15927,7 @@ private static class mln_map_get_style_layer_json { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, mln_buffer *out_layer, bool *out_found) + * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_layer_json$handle() { @@ -16341,7 +15937,7 @@ private static class mln_map_get_style_layer_json { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, mln_buffer *out_layer, bool *out_found) + * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_layer_json$address() { @@ -16350,16 +15946,16 @@ private static class mln_map_get_style_layer_json { /** * {@snippet lang=c : - * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, mln_buffer *out_layer, bool *out_found) + * mln_status mln_map_get_style_layer_json(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_get_style_layer_json(long map, MemorySegment layer_id, MemorySegment out_layer, MemorySegment out_found) { + public static int mln_map_get_style_layer_json(long map, MemorySegment layer_id, MemorySegment completion) { var mh$ = mln_map_get_style_layer_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_layer_json", map, layer_id, out_layer, out_found); + traceDowncall("mln_map_get_style_layer_json", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_layer, out_found); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16371,7 +15967,8 @@ private static class mln_map_set_style_light_json { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_light_json"); @@ -16382,7 +15979,7 @@ private static class mln_map_set_style_light_json { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json) + * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_style_light_json$descriptor() { @@ -16392,7 +15989,7 @@ private static class mln_map_set_style_light_json { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json) + * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_style_light_json$handle() { @@ -16402,7 +15999,7 @@ private static class mln_map_set_style_light_json { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json) + * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_style_light_json$address() { @@ -16411,16 +16008,16 @@ private static class mln_map_set_style_light_json { /** * {@snippet lang=c : - * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json) + * mln_status mln_map_set_style_light_json(mln_map map, mln_buffer_view light_json, const mln_completion *completion) * } */ - public static int mln_map_set_style_light_json(long map, MemorySegment light_json) { + public static int mln_map_set_style_light_json(long map, MemorySegment light_json, MemorySegment completion) { var mh$ = mln_map_set_style_light_json.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_light_json", map, light_json); + traceDowncall("mln_map_set_style_light_json", map, light_json, completion); } - return (int)mh$.invokeExact(map, light_json); + return (int)mh$.invokeExact(map, light_json, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16433,7 +16030,8 @@ private static class mln_map_set_style_light_property { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_style_light_property"); @@ -16444,7 +16042,7 @@ private static class mln_map_set_style_light_property { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_style_light_property$descriptor() { @@ -16454,7 +16052,7 @@ private static class mln_map_set_style_light_property { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_style_light_property$handle() { @@ -16464,7 +16062,7 @@ private static class mln_map_set_style_light_property { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_style_light_property$address() { @@ -16473,16 +16071,16 @@ private static class mln_map_set_style_light_property { /** * {@snippet lang=c : - * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ - public static int mln_map_set_style_light_property(long map, MemorySegment property_name, MemorySegment value) { + public static int mln_map_set_style_light_property(long map, MemorySegment property_name, MemorySegment value, MemorySegment completion) { var mh$ = mln_map_set_style_light_property.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_light_property", map, property_name, value); + traceDowncall("mln_map_set_style_light_property", map, property_name, value, completion); } - return (int)mh$.invokeExact(map, property_name, value); + return (int)mh$.invokeExact(map, property_name, value, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16506,7 +16104,7 @@ private static class mln_map_get_style_light_property { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_light_property$descriptor() { @@ -16516,7 +16114,7 @@ private static class mln_map_get_style_light_property { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_light_property$handle() { @@ -16526,7 +16124,7 @@ private static class mln_map_get_style_light_property { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_light_property$address() { @@ -16535,16 +16133,16 @@ private static class mln_map_get_style_light_property { /** * {@snippet lang=c : - * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_style_light_property(mln_map map, mln_buffer_view property_name, const mln_completion *completion) * } */ - public static int mln_map_get_style_light_property(long map, MemorySegment property_name, MemorySegment out_value) { + public static int mln_map_get_style_light_property(long map, MemorySegment property_name, MemorySegment completion) { var mh$ = mln_map_get_style_light_property.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_light_property", map, property_name, out_value); + traceDowncall("mln_map_get_style_light_property", map, property_name, completion); } - return (int)mh$.invokeExact(map, property_name, out_value); + return (int)mh$.invokeExact(map, property_name, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16556,6 +16154,7 @@ private static class mln_map_set_style_transition_options { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -16567,7 +16166,7 @@ private static class mln_map_set_style_transition_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options) + * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_style_transition_options$descriptor() { @@ -16577,7 +16176,7 @@ private static class mln_map_set_style_transition_options { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options) + * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_style_transition_options$handle() { @@ -16587,7 +16186,7 @@ private static class mln_map_set_style_transition_options { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options) + * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_style_transition_options$address() { @@ -16596,16 +16195,16 @@ private static class mln_map_set_style_transition_options { /** * {@snippet lang=c : - * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options) + * mln_status mln_map_set_style_transition_options(mln_map map, const mln_style_transition_options *options, const mln_completion *completion) * } */ - public static int mln_map_set_style_transition_options(long map, MemorySegment options) { + public static int mln_map_set_style_transition_options(long map, MemorySegment options, MemorySegment completion) { var mh$ = mln_map_set_style_transition_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_style_transition_options", map, options); + traceDowncall("mln_map_set_style_transition_options", map, options, completion); } - return (int)mh$.invokeExact(map, options); + return (int)mh$.invokeExact(map, options, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16628,7 +16227,7 @@ private static class mln_map_get_style_transition_options { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_style_transition_options(mln_map map, mln_style_transition_options *out_options) + * mln_status mln_map_get_style_transition_options(mln_map map, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_style_transition_options$descriptor() { @@ -16638,7 +16237,7 @@ private static class mln_map_get_style_transition_options { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_style_transition_options(mln_map map, mln_style_transition_options *out_options) + * mln_status mln_map_get_style_transition_options(mln_map map, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_style_transition_options$handle() { @@ -16648,7 +16247,7 @@ private static class mln_map_get_style_transition_options { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_style_transition_options(mln_map map, mln_style_transition_options *out_options) + * mln_status mln_map_get_style_transition_options(mln_map map, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_style_transition_options$address() { @@ -16657,16 +16256,16 @@ private static class mln_map_get_style_transition_options { /** * {@snippet lang=c : - * mln_status mln_map_get_style_transition_options(mln_map map, mln_style_transition_options *out_options) + * mln_status mln_map_get_style_transition_options(mln_map map, const mln_completion *completion) * } */ - public static int mln_map_get_style_transition_options(long map, MemorySegment out_options) { + public static int mln_map_get_style_transition_options(long map, MemorySegment completion) { var mh$ = mln_map_get_style_transition_options.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_style_transition_options", map, out_options); + traceDowncall("mln_map_get_style_transition_options", map, completion); } - return (int)mh$.invokeExact(map, out_options); + return (int)mh$.invokeExact(map, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16680,7 +16279,8 @@ private static class mln_map_set_layer_property { MapLibreNativeC.C_LONG, mln_buffer_view.layout(), mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_property"); @@ -16691,7 +16291,7 @@ private static class mln_map_set_layer_property { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_property$descriptor() { @@ -16701,7 +16301,7 @@ private static class mln_map_set_layer_property { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_property$handle() { @@ -16711,7 +16311,7 @@ private static class mln_map_set_layer_property { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_layer_property$address() { @@ -16720,16 +16320,16 @@ private static class mln_map_set_layer_property { /** * {@snippet lang=c : - * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value) + * mln_status mln_map_set_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer_view value, const mln_completion *completion) * } */ - public static int mln_map_set_layer_property(long map, MemorySegment layer_id, MemorySegment property_name, MemorySegment value) { + public static int mln_map_set_layer_property(long map, MemorySegment layer_id, MemorySegment property_name, MemorySegment value, MemorySegment completion) { var mh$ = mln_map_set_layer_property.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_property", map, layer_id, property_name, value); + traceDowncall("mln_map_set_layer_property", map, layer_id, property_name, value, completion); } - return (int)mh$.invokeExact(map, layer_id, property_name, value); + return (int)mh$.invokeExact(map, layer_id, property_name, value, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16754,7 +16354,7 @@ private static class mln_map_get_layer_property { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_layer_property$descriptor() { @@ -16764,7 +16364,7 @@ private static class mln_map_get_layer_property { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_layer_property$handle() { @@ -16774,7 +16374,7 @@ private static class mln_map_get_layer_property { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_layer_property$address() { @@ -16783,16 +16383,16 @@ private static class mln_map_get_layer_property { /** * {@snippet lang=c : - * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, mln_buffer *out_value) + * mln_status mln_map_get_layer_property(mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, const mln_completion *completion) * } */ - public static int mln_map_get_layer_property(long map, MemorySegment layer_id, MemorySegment property_name, MemorySegment out_value) { + public static int mln_map_get_layer_property(long map, MemorySegment layer_id, MemorySegment property_name, MemorySegment completion) { var mh$ = mln_map_get_layer_property.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_layer_property", map, layer_id, property_name, out_value); + traceDowncall("mln_map_get_layer_property", map, layer_id, property_name, completion); } - return (int)mh$.invokeExact(map, layer_id, property_name, out_value); + return (int)mh$.invokeExact(map, layer_id, property_name, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16805,6 +16405,7 @@ private static class mln_map_set_layer_filter { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -16816,7 +16417,7 @@ private static class mln_map_set_layer_filter { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter) + * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_filter$descriptor() { @@ -16826,7 +16427,7 @@ private static class mln_map_set_layer_filter { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter) + * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_filter$handle() { @@ -16836,7 +16437,7 @@ private static class mln_map_set_layer_filter { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter) + * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_layer_filter$address() { @@ -16845,16 +16446,16 @@ private static class mln_map_set_layer_filter { /** * {@snippet lang=c : - * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter) + * mln_status mln_map_set_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_buffer_view *filter, const mln_completion *completion) * } */ - public static int mln_map_set_layer_filter(long map, MemorySegment layer_id, MemorySegment filter) { + public static int mln_map_set_layer_filter(long map, MemorySegment layer_id, MemorySegment filter, MemorySegment completion) { var mh$ = mln_map_set_layer_filter.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_filter", map, layer_id, filter); + traceDowncall("mln_map_set_layer_filter", map, layer_id, filter, completion); } - return (int)mh$.invokeExact(map, layer_id, filter); + return (int)mh$.invokeExact(map, layer_id, filter, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16878,7 +16479,7 @@ private static class mln_map_get_layer_filter { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, mln_buffer *out_filter) + * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_get_layer_filter$descriptor() { @@ -16888,7 +16489,7 @@ private static class mln_map_get_layer_filter { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, mln_buffer *out_filter) + * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_get_layer_filter$handle() { @@ -16898,7 +16499,7 @@ private static class mln_map_get_layer_filter { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, mln_buffer *out_filter) + * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_get_layer_filter$address() { @@ -16907,16 +16508,16 @@ private static class mln_map_get_layer_filter { /** * {@snippet lang=c : - * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, mln_buffer *out_filter) + * mln_status mln_map_get_layer_filter(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_get_layer_filter(long map, MemorySegment layer_id, MemorySegment out_filter) { + public static int mln_map_get_layer_filter(long map, MemorySegment layer_id, MemorySegment completion) { var mh$ = mln_map_get_layer_filter.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_layer_filter", map, layer_id, out_filter); + traceDowncall("mln_map_get_layer_filter", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_filter); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16929,7 +16530,8 @@ private static class mln_map_set_layer_source_layer { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_source_layer"); @@ -16940,7 +16542,7 @@ private static class mln_map_set_layer_source_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer) + * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_source_layer$descriptor() { @@ -16950,7 +16552,7 @@ private static class mln_map_set_layer_source_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer) + * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_source_layer$handle() { @@ -16960,7 +16562,7 @@ private static class mln_map_set_layer_source_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer) + * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_layer_source_layer$address() { @@ -16969,16 +16571,16 @@ private static class mln_map_set_layer_source_layer { /** * {@snippet lang=c : - * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer) + * mln_status mln_map_set_layer_source_layer(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, const mln_completion *completion) * } */ - public static int mln_map_set_layer_source_layer(long map, MemorySegment layer_id, MemorySegment source_layer) { + public static int mln_map_set_layer_source_layer(long map, MemorySegment layer_id, MemorySegment source_layer, MemorySegment completion) { var mh$ = mln_map_set_layer_source_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_source_layer", map, layer_id, source_layer); + traceDowncall("mln_map_set_layer_source_layer", map, layer_id, source_layer, completion); } - return (int)mh$.invokeExact(map, layer_id, source_layer); + return (int)mh$.invokeExact(map, layer_id, source_layer, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -16991,8 +16593,6 @@ private static class mln_map_copy_layer_source_layer { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); @@ -17004,7 +16604,7 @@ private static class mln_map_copy_layer_source_layer { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, char *out_source_layer, size_t source_layer_capacity, size_t *out_source_layer_size) + * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_layer_source_layer$descriptor() { @@ -17014,7 +16614,7 @@ private static class mln_map_copy_layer_source_layer { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, char *out_source_layer, size_t source_layer_capacity, size_t *out_source_layer_size) + * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_layer_source_layer$handle() { @@ -17024,7 +16624,7 @@ private static class mln_map_copy_layer_source_layer { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, char *out_source_layer, size_t source_layer_capacity, size_t *out_source_layer_size) + * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_layer_source_layer$address() { @@ -17033,16 +16633,16 @@ private static class mln_map_copy_layer_source_layer { /** * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, char *out_source_layer, size_t source_layer_capacity, size_t *out_source_layer_size) + * mln_status mln_map_copy_layer_source_layer(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_copy_layer_source_layer(long map, MemorySegment layer_id, MemorySegment out_source_layer, long source_layer_capacity, MemorySegment out_source_layer_size) { + public static int mln_map_copy_layer_source_layer(long map, MemorySegment layer_id, MemorySegment completion) { var mh$ = mln_map_copy_layer_source_layer.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_layer_source_layer", map, layer_id, out_source_layer, source_layer_capacity, out_source_layer_size); + traceDowncall("mln_map_copy_layer_source_layer", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_source_layer, source_layer_capacity, out_source_layer_size); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17055,7 +16655,8 @@ private static class mln_map_set_layer_source_id { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - mln_buffer_view.layout() + mln_buffer_view.layout(), + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_source_id"); @@ -17066,7 +16667,7 @@ private static class mln_map_set_layer_source_id { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id) + * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_source_id$descriptor() { @@ -17076,7 +16677,7 @@ private static class mln_map_set_layer_source_id { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id) + * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_source_id$handle() { @@ -17086,7 +16687,7 @@ private static class mln_map_set_layer_source_id { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id) + * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_layer_source_id$address() { @@ -17095,16 +16696,16 @@ private static class mln_map_set_layer_source_id { /** * {@snippet lang=c : - * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id) + * mln_status mln_map_set_layer_source_id(mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, const mln_completion *completion) * } */ - public static int mln_map_set_layer_source_id(long map, MemorySegment layer_id, MemorySegment source_id) { + public static int mln_map_set_layer_source_id(long map, MemorySegment layer_id, MemorySegment source_id, MemorySegment completion) { var mh$ = mln_map_set_layer_source_id.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_source_id", map, layer_id, source_id); + traceDowncall("mln_map_set_layer_source_id", map, layer_id, source_id, completion); } - return (int)mh$.invokeExact(map, layer_id, source_id); + return (int)mh$.invokeExact(map, layer_id, source_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17117,8 +16718,6 @@ private static class mln_map_copy_layer_source_id { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); @@ -17130,7 +16729,7 @@ private static class mln_map_copy_layer_source_id { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, char *out_source_id, size_t source_id_capacity, size_t *out_source_id_size) + * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_copy_layer_source_id$descriptor() { @@ -17140,7 +16739,7 @@ private static class mln_map_copy_layer_source_id { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, char *out_source_id, size_t source_id_capacity, size_t *out_source_id_size) + * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MethodHandle mln_map_copy_layer_source_id$handle() { @@ -17150,7 +16749,7 @@ private static class mln_map_copy_layer_source_id { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, char *out_source_id, size_t source_id_capacity, size_t *out_source_id_size) + * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ public static MemorySegment mln_map_copy_layer_source_id$address() { @@ -17159,16 +16758,16 @@ private static class mln_map_copy_layer_source_id { /** * {@snippet lang=c : - * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, char *out_source_id, size_t source_id_capacity, size_t *out_source_id_size) + * mln_status mln_map_copy_layer_source_id(mln_map map, mln_buffer_view layer_id, const mln_completion *completion) * } */ - public static int mln_map_copy_layer_source_id(long map, MemorySegment layer_id, MemorySegment out_source_id, long source_id_capacity, MemorySegment out_source_id_size) { + public static int mln_map_copy_layer_source_id(long map, MemorySegment layer_id, MemorySegment completion) { var mh$ = mln_map_copy_layer_source_id.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_copy_layer_source_id", map, layer_id, out_source_id, source_id_capacity, out_source_id_size); + traceDowncall("mln_map_copy_layer_source_id", map, layer_id, completion); } - return (int)mh$.invokeExact(map, layer_id, out_source_id, source_id_capacity, out_source_id_size); + return (int)mh$.invokeExact(map, layer_id, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17181,7 +16780,8 @@ private static class mln_map_set_layer_min_zoom { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_min_zoom"); @@ -17192,7 +16792,7 @@ private static class mln_map_set_layer_min_zoom { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom) + * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_min_zoom$descriptor() { @@ -17202,7 +16802,7 @@ private static class mln_map_set_layer_min_zoom { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom) + * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_min_zoom$handle() { @@ -17212,7 +16812,7 @@ private static class mln_map_set_layer_min_zoom { /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom) + * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom, const mln_completion *completion) * } */ public static MemorySegment mln_map_set_layer_min_zoom$address() { @@ -17221,78 +16821,16 @@ private static class mln_map_set_layer_min_zoom { /** * {@snippet lang=c : - * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom) + * mln_status mln_map_set_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double min_zoom, const mln_completion *completion) * } */ - public static int mln_map_set_layer_min_zoom(long map, MemorySegment layer_id, double min_zoom) { + public static int mln_map_set_layer_min_zoom(long map, MemorySegment layer_id, double min_zoom, MemorySegment completion) { var mh$ = mln_map_set_layer_min_zoom.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_min_zoom", map, layer_id, min_zoom); - } - return (int)mh$.invokeExact(map, layer_id, min_zoom); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_get_layer_min_zoom { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_layer_min_zoom"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double *out_min_zoom) - * } - */ - public static FunctionDescriptor mln_map_get_layer_min_zoom$descriptor() { - return mln_map_get_layer_min_zoom.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double *out_min_zoom) - * } - */ - public static MethodHandle mln_map_get_layer_min_zoom$handle() { - return mln_map_get_layer_min_zoom.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double *out_min_zoom) - * } - */ - public static MemorySegment mln_map_get_layer_min_zoom$address() { - return mln_map_get_layer_min_zoom.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_get_layer_min_zoom(mln_map map, mln_buffer_view layer_id, double *out_min_zoom) - * } - */ - public static int mln_map_get_layer_min_zoom(long map, MemorySegment layer_id, MemorySegment out_min_zoom) { - var mh$ = mln_map_get_layer_min_zoom.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_layer_min_zoom", map, layer_id, out_min_zoom); + traceDowncall("mln_map_set_layer_min_zoom", map, layer_id, min_zoom, completion); } - return (int)mh$.invokeExact(map, layer_id, out_min_zoom); + return (int)mh$.invokeExact(map, layer_id, min_zoom, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17305,7 +16843,8 @@ private static class mln_map_set_layer_max_zoom { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), - MapLibreNativeC.C_DOUBLE + MapLibreNativeC.C_DOUBLE, + MapLibreNativeC.C_POINTER ); public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_max_zoom"); @@ -17316,7 +16855,7 @@ private static class mln_map_set_layer_max_zoom { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom) + * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom, const mln_completion *completion) * } */ public static FunctionDescriptor mln_map_set_layer_max_zoom$descriptor() { @@ -17326,159 +16865,35 @@ private static class mln_map_set_layer_max_zoom { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom) + * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom, const mln_completion *completion) * } */ public static MethodHandle mln_map_set_layer_max_zoom$handle() { - return mln_map_set_layer_max_zoom.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom) - * } - */ - public static MemorySegment mln_map_set_layer_max_zoom$address() { - return mln_map_set_layer_max_zoom.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom) - * } - */ - public static int mln_map_set_layer_max_zoom(long map, MemorySegment layer_id, double max_zoom) { - var mh$ = mln_map_set_layer_max_zoom.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_max_zoom", map, layer_id, max_zoom); - } - return (int)mh$.invokeExact(map, layer_id, max_zoom); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_get_layer_max_zoom { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_layer_max_zoom"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double *out_max_zoom) - * } - */ - public static FunctionDescriptor mln_map_get_layer_max_zoom$descriptor() { - return mln_map_get_layer_max_zoom.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double *out_max_zoom) - * } - */ - public static MethodHandle mln_map_get_layer_max_zoom$handle() { - return mln_map_get_layer_max_zoom.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_map_get_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double *out_max_zoom) - * } - */ - public static MemorySegment mln_map_get_layer_max_zoom$address() { - return mln_map_get_layer_max_zoom.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_map_get_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double *out_max_zoom) - * } - */ - public static int mln_map_get_layer_max_zoom(long map, MemorySegment layer_id, MemorySegment out_max_zoom) { - var mh$ = mln_map_get_layer_max_zoom.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_layer_max_zoom", map, layer_id, out_max_zoom); - } - return (int)mh$.invokeExact(map, layer_id, out_max_zoom); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_map_set_layer_visibility { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - mln_buffer_view.layout(), - MapLibreNativeC.C_INT - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_visibility"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility) - * } - */ - public static FunctionDescriptor mln_map_set_layer_visibility$descriptor() { - return mln_map_set_layer_visibility.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility) - * } - */ - public static MethodHandle mln_map_set_layer_visibility$handle() { - return mln_map_set_layer_visibility.HANDLE; + return mln_map_set_layer_max_zoom.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility) + * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom, const mln_completion *completion) * } */ - public static MemorySegment mln_map_set_layer_visibility$address() { - return mln_map_set_layer_visibility.ADDR; + public static MemorySegment mln_map_set_layer_max_zoom$address() { + return mln_map_set_layer_max_zoom.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility) + * mln_status mln_map_set_layer_max_zoom(mln_map map, mln_buffer_view layer_id, double max_zoom, const mln_completion *completion) * } */ - public static int mln_map_set_layer_visibility(long map, MemorySegment layer_id, int visibility) { - var mh$ = mln_map_set_layer_visibility.HANDLE; + public static int mln_map_set_layer_max_zoom(long map, MemorySegment layer_id, double max_zoom, MemorySegment completion) { + var mh$ = mln_map_set_layer_max_zoom.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_set_layer_visibility", map, layer_id, visibility); + traceDowncall("mln_map_set_layer_max_zoom", map, layer_id, max_zoom, completion); } - return (int)mh$.invokeExact(map, layer_id, visibility); + return (int)mh$.invokeExact(map, layer_id, max_zoom, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17486,15 +16901,16 @@ public static int mln_map_set_layer_visibility(long map, MemorySegment layer_id, } } - private static class mln_map_get_layer_visibility { + private static class mln_map_set_layer_visibility { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, mln_buffer_view.layout(), + MapLibreNativeC.C_INT, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_get_layer_visibility"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_map_set_layer_visibility"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -17502,45 +16918,45 @@ private static class mln_map_get_layer_visibility { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_map_get_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t *out_visibility) + * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_map_get_layer_visibility$descriptor() { - return mln_map_get_layer_visibility.DESC; + public static FunctionDescriptor mln_map_set_layer_visibility$descriptor() { + return mln_map_set_layer_visibility.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_map_get_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t *out_visibility) + * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility, const mln_completion *completion) * } */ - public static MethodHandle mln_map_get_layer_visibility$handle() { - return mln_map_get_layer_visibility.HANDLE; + public static MethodHandle mln_map_set_layer_visibility$handle() { + return mln_map_set_layer_visibility.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_map_get_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t *out_visibility) + * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility, const mln_completion *completion) * } */ - public static MemorySegment mln_map_get_layer_visibility$address() { - return mln_map_get_layer_visibility.ADDR; + public static MemorySegment mln_map_set_layer_visibility$address() { + return mln_map_set_layer_visibility.ADDR; } /** * {@snippet lang=c : - * mln_status mln_map_get_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t *out_visibility) + * mln_status mln_map_set_layer_visibility(mln_map map, mln_buffer_view layer_id, uint32_t visibility, const mln_completion *completion) * } */ - public static int mln_map_get_layer_visibility(long map, MemorySegment layer_id, MemorySegment out_visibility) { - var mh$ = mln_map_get_layer_visibility.HANDLE; + public static int mln_map_set_layer_visibility(long map, MemorySegment layer_id, int visibility, MemorySegment completion) { + var mh$ = mln_map_set_layer_visibility.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_map_get_layer_visibility", map, layer_id, out_visibility); + traceDowncall("mln_map_set_layer_visibility", map, layer_id, visibility, completion); } - return (int)mh$.invokeExact(map, layer_id, out_visibility); + return (int)mh$.invokeExact(map, layer_id, visibility, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17785,6 +17201,8 @@ private static class mln_metal_surface_attach { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -17796,7 +17214,7 @@ private static class mln_metal_surface_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_metal_surface_attach$descriptor() { @@ -17806,7 +17224,7 @@ private static class mln_metal_surface_attach { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MethodHandle mln_metal_surface_attach$handle() { @@ -17816,7 +17234,7 @@ private static class mln_metal_surface_attach { /** * Address for: * {@snippet lang=c : - * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MemorySegment mln_metal_surface_attach$address() { @@ -17825,16 +17243,16 @@ private static class mln_metal_surface_attach { /** * {@snippet lang=c : - * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_surface_attach(mln_map map, const mln_metal_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_metal_surface_attach(long map, MemorySegment descriptor, MemorySegment out_session) { + public static int mln_metal_surface_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { var mh$ = mln_metal_surface_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_surface_attach", map, descriptor, out_session); + traceDowncall("mln_metal_surface_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17847,6 +17265,8 @@ private static class mln_vulkan_surface_attach { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -17858,7 +17278,7 @@ private static class mln_vulkan_surface_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_vulkan_surface_attach$descriptor() { @@ -17868,7 +17288,7 @@ private static class mln_vulkan_surface_attach { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MethodHandle mln_vulkan_surface_attach$handle() { @@ -17878,7 +17298,7 @@ private static class mln_vulkan_surface_attach { /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MemorySegment mln_vulkan_surface_attach$address() { @@ -17887,16 +17307,16 @@ private static class mln_vulkan_surface_attach { /** * {@snippet lang=c : - * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_vulkan_surface_attach(mln_map map, const mln_vulkan_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_vulkan_surface_attach(long map, MemorySegment descriptor, MemorySegment out_session) { + public static int mln_vulkan_surface_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { var mh$ = mln_vulkan_surface_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_surface_attach", map, descriptor, out_session); + traceDowncall("mln_vulkan_surface_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17909,6 +17329,8 @@ private static class mln_opengl_surface_attach { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -17920,7 +17342,7 @@ private static class mln_opengl_surface_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_opengl_surface_attach$descriptor() { @@ -17930,7 +17352,7 @@ private static class mln_opengl_surface_attach { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MethodHandle mln_opengl_surface_attach$handle() { @@ -17940,7 +17362,7 @@ private static class mln_opengl_surface_attach { /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MemorySegment mln_opengl_surface_attach$address() { @@ -17949,16 +17371,16 @@ private static class mln_opengl_surface_attach { /** * {@snippet lang=c : - * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_opengl_surface_attach(mln_map map, const mln_opengl_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_opengl_surface_attach(long map, MemorySegment descriptor, MemorySegment out_session) { + public static int mln_opengl_surface_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { var mh$ = mln_opengl_surface_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_surface_attach", map, descriptor, out_session); + traceDowncall("mln_opengl_surface_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -17971,6 +17393,8 @@ private static class mln_webgpu_surface_attach { MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -17982,7 +17406,7 @@ private static class mln_webgpu_surface_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static FunctionDescriptor mln_webgpu_surface_attach$descriptor() { @@ -17992,7 +17416,7 @@ private static class mln_webgpu_surface_attach { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MethodHandle mln_webgpu_surface_attach$handle() { @@ -18002,7 +17426,7 @@ private static class mln_webgpu_surface_attach { /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ public static MemorySegment mln_webgpu_surface_attach$address() { @@ -18011,16 +17435,16 @@ private static class mln_webgpu_surface_attach { /** * {@snippet lang=c : - * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_webgpu_surface_attach(mln_map map, const mln_webgpu_surface_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_webgpu_surface_attach(long map, MemorySegment descriptor, MemorySegment out_session) { + public static int mln_webgpu_surface_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { var mh$ = mln_webgpu_surface_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_surface_attach", map, descriptor, out_session); + traceDowncall("mln_webgpu_surface_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -18032,6 +17456,7 @@ private static class mln_metal_surface_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -18043,7 +17468,7 @@ private static class mln_metal_surface_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor) + * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static FunctionDescriptor mln_metal_surface_set_target$descriptor() { @@ -18053,7 +17478,7 @@ private static class mln_metal_surface_set_target { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor) + * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MethodHandle mln_metal_surface_set_target$handle() { @@ -18063,7 +17488,7 @@ private static class mln_metal_surface_set_target { /** * Address for: * {@snippet lang=c : - * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor) + * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MemorySegment mln_metal_surface_set_target$address() { @@ -18072,16 +17497,16 @@ private static class mln_metal_surface_set_target { /** * {@snippet lang=c : - * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor) + * mln_status mln_metal_surface_set_target(mln_render_session session, const mln_metal_surface_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_metal_surface_set_target(long session, MemorySegment descriptor) { + public static int mln_metal_surface_set_target(long session, MemorySegment descriptor, MemorySegment completion) { var mh$ = mln_metal_surface_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_surface_set_target", session, descriptor); + traceDowncall("mln_metal_surface_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -18093,6 +17518,7 @@ private static class mln_vulkan_surface_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -18104,7 +17530,7 @@ private static class mln_vulkan_surface_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor) + * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static FunctionDescriptor mln_vulkan_surface_set_target$descriptor() { @@ -18114,7 +17540,7 @@ private static class mln_vulkan_surface_set_target { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor) + * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MethodHandle mln_vulkan_surface_set_target$handle() { @@ -18124,7 +17550,7 @@ private static class mln_vulkan_surface_set_target { /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor) + * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MemorySegment mln_vulkan_surface_set_target$address() { @@ -18133,16 +17559,16 @@ private static class mln_vulkan_surface_set_target { /** * {@snippet lang=c : - * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor) + * mln_status mln_vulkan_surface_set_target(mln_render_session session, const mln_vulkan_surface_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_vulkan_surface_set_target(long session, MemorySegment descriptor) { + public static int mln_vulkan_surface_set_target(long session, MemorySegment descriptor, MemorySegment completion) { var mh$ = mln_vulkan_surface_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_surface_set_target", session, descriptor); + traceDowncall("mln_vulkan_surface_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -18154,6 +17580,7 @@ private static class mln_opengl_surface_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -18165,7 +17592,7 @@ private static class mln_opengl_surface_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor) + * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static FunctionDescriptor mln_opengl_surface_set_target$descriptor() { @@ -18175,7 +17602,7 @@ private static class mln_opengl_surface_set_target { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor) + * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MethodHandle mln_opengl_surface_set_target$handle() { @@ -18185,7 +17612,7 @@ private static class mln_opengl_surface_set_target { /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor) + * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MemorySegment mln_opengl_surface_set_target$address() { @@ -18194,16 +17621,16 @@ private static class mln_opengl_surface_set_target { /** * {@snippet lang=c : - * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor) + * mln_status mln_opengl_surface_set_target(mln_render_session session, const mln_opengl_surface_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_opengl_surface_set_target(long session, MemorySegment descriptor) { + public static int mln_opengl_surface_set_target(long session, MemorySegment descriptor, MemorySegment completion) { var mh$ = mln_opengl_surface_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_surface_set_target", session, descriptor); + traceDowncall("mln_opengl_surface_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -18215,6 +17642,7 @@ private static class mln_webgpu_surface_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); @@ -18226,7 +17654,7 @@ private static class mln_webgpu_surface_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor) + * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static FunctionDescriptor mln_webgpu_surface_set_target$descriptor() { @@ -18236,7 +17664,7 @@ private static class mln_webgpu_surface_set_target { /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor) + * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MethodHandle mln_webgpu_surface_set_target$handle() { @@ -18246,7 +17674,7 @@ private static class mln_webgpu_surface_set_target { /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor) + * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor, const mln_completion *completion) * } */ public static MemorySegment mln_webgpu_surface_set_target$address() { @@ -18255,16 +17683,16 @@ private static class mln_webgpu_surface_set_target { /** * {@snippet lang=c : - * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor) + * mln_status mln_webgpu_surface_set_target(mln_render_session session, const mln_webgpu_surface_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_webgpu_surface_set_target(long session, MemorySegment descriptor) { + public static int mln_webgpu_surface_set_target(long session, MemorySegment descriptor, MemorySegment completion) { var mh$ = mln_webgpu_surface_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_surface_set_target", session, descriptor); + traceDowncall("mln_webgpu_surface_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -18748,293 +18176,45 @@ private static class mln_texture_image_info_default { /** * Function descriptor for: * {@snippet lang=c : - * mln_texture_image_info mln_texture_image_info_default() - * } - */ - public static FunctionDescriptor mln_texture_image_info_default$descriptor() { - return mln_texture_image_info_default.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_texture_image_info mln_texture_image_info_default() - * } - */ - public static MethodHandle mln_texture_image_info_default$handle() { - return mln_texture_image_info_default.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_texture_image_info mln_texture_image_info_default() - * } - */ - public static MemorySegment mln_texture_image_info_default$address() { - return mln_texture_image_info_default.ADDR; - } - - /** - * {@snippet lang=c : - * mln_texture_image_info mln_texture_image_info_default() - * } - */ - public static MemorySegment mln_texture_image_info_default(SegmentAllocator allocator) { - var mh$ = mln_texture_image_info_default.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_texture_image_info_default", allocator); - } - return (MemorySegment)mh$.invokeExact(allocator); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_metal_owned_texture_attach { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_owned_texture_attach"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static FunctionDescriptor mln_metal_owned_texture_attach$descriptor() { - return mln_metal_owned_texture_attach.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MethodHandle mln_metal_owned_texture_attach$handle() { - return mln_metal_owned_texture_attach.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MemorySegment mln_metal_owned_texture_attach$address() { - return mln_metal_owned_texture_attach.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static int mln_metal_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_metal_owned_texture_attach.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_owned_texture_attach", map, descriptor, out_session); - } - return (int)mh$.invokeExact(map, descriptor, out_session); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_metal_borrowed_texture_attach { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_borrowed_texture_attach"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static FunctionDescriptor mln_metal_borrowed_texture_attach$descriptor() { - return mln_metal_borrowed_texture_attach.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MethodHandle mln_metal_borrowed_texture_attach$handle() { - return mln_metal_borrowed_texture_attach.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MemorySegment mln_metal_borrowed_texture_attach$address() { - return mln_metal_borrowed_texture_attach.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static int mln_metal_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_metal_borrowed_texture_attach.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_borrowed_texture_attach", map, descriptor, out_session); - } - return (int)mh$.invokeExact(map, descriptor, out_session); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_vulkan_owned_texture_attach { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_owned_texture_attach"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static FunctionDescriptor mln_vulkan_owned_texture_attach$descriptor() { - return mln_vulkan_owned_texture_attach.DESC; - } - - /** - * Downcall method handle for: - * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MethodHandle mln_vulkan_owned_texture_attach$handle() { - return mln_vulkan_owned_texture_attach.HANDLE; - } - - /** - * Address for: - * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static MemorySegment mln_vulkan_owned_texture_attach$address() { - return mln_vulkan_owned_texture_attach.ADDR; - } - - /** - * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, mln_render_session *out_session) - * } - */ - public static int mln_vulkan_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_vulkan_owned_texture_attach.HANDLE; - try { - if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_owned_texture_attach", map, descriptor, out_session); - } - return (int)mh$.invokeExact(map, descriptor, out_session); - } catch (Error | RuntimeException ex) { - throw ex; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - - private static class mln_vulkan_borrowed_texture_attach { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - MapLibreNativeC.C_INT, - MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_POINTER - ); - - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_borrowed_texture_attach"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - - /** - * Function descriptor for: - * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_texture_image_info mln_texture_image_info_default() * } */ - public static FunctionDescriptor mln_vulkan_borrowed_texture_attach$descriptor() { - return mln_vulkan_borrowed_texture_attach.DESC; + public static FunctionDescriptor mln_texture_image_info_default$descriptor() { + return mln_texture_image_info_default.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_texture_image_info mln_texture_image_info_default() * } */ - public static MethodHandle mln_vulkan_borrowed_texture_attach$handle() { - return mln_vulkan_borrowed_texture_attach.HANDLE; + public static MethodHandle mln_texture_image_info_default$handle() { + return mln_texture_image_info_default.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_texture_image_info mln_texture_image_info_default() * } */ - public static MemorySegment mln_vulkan_borrowed_texture_attach$address() { - return mln_vulkan_borrowed_texture_attach.ADDR; + public static MemorySegment mln_texture_image_info_default$address() { + return mln_texture_image_info_default.ADDR; } /** * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_texture_image_info mln_texture_image_info_default() * } */ - public static int mln_vulkan_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_vulkan_borrowed_texture_attach.HANDLE; + public static MemorySegment mln_texture_image_info_default(SegmentAllocator allocator) { + var mh$ = mln_texture_image_info_default.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_borrowed_texture_attach", map, descriptor, out_session); + traceDowncall("mln_texture_image_info_default", allocator); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (MemorySegment)mh$.invokeExact(allocator); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19042,15 +18222,17 @@ public static int mln_vulkan_borrowed_texture_attach(long map, MemorySegment des } } - private static class mln_opengl_owned_texture_attach { + private static class mln_metal_owned_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_owned_texture_attach"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_owned_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19058,45 +18240,45 @@ private static class mln_opengl_owned_texture_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_opengl_owned_texture_attach$descriptor() { - return mln_opengl_owned_texture_attach.DESC; + public static FunctionDescriptor mln_metal_owned_texture_attach$descriptor() { + return mln_metal_owned_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_opengl_owned_texture_attach$handle() { - return mln_opengl_owned_texture_attach.HANDLE; + public static MethodHandle mln_metal_owned_texture_attach$handle() { + return mln_metal_owned_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_opengl_owned_texture_attach$address() { - return mln_opengl_owned_texture_attach.ADDR; + public static MemorySegment mln_metal_owned_texture_attach$address() { + return mln_metal_owned_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_owned_texture_attach(mln_map map, const mln_metal_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_opengl_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_opengl_owned_texture_attach.HANDLE; + public static int mln_metal_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_metal_owned_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_owned_texture_attach", map, descriptor, out_session); + traceDowncall("mln_metal_owned_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19104,15 +18286,17 @@ public static int mln_opengl_owned_texture_attach(long map, MemorySegment descri } } - private static class mln_opengl_borrowed_texture_attach { + private static class mln_metal_borrowed_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_borrowed_texture_attach"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_borrowed_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19120,45 +18304,45 @@ private static class mln_opengl_borrowed_texture_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_opengl_borrowed_texture_attach$descriptor() { - return mln_opengl_borrowed_texture_attach.DESC; + public static FunctionDescriptor mln_metal_borrowed_texture_attach$descriptor() { + return mln_metal_borrowed_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_opengl_borrowed_texture_attach$handle() { - return mln_opengl_borrowed_texture_attach.HANDLE; + public static MethodHandle mln_metal_borrowed_texture_attach$handle() { + return mln_metal_borrowed_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_opengl_borrowed_texture_attach$address() { - return mln_opengl_borrowed_texture_attach.ADDR; + public static MemorySegment mln_metal_borrowed_texture_attach$address() { + return mln_metal_borrowed_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_metal_borrowed_texture_attach(mln_map map, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_opengl_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_opengl_borrowed_texture_attach.HANDLE; + public static int mln_metal_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_metal_borrowed_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_borrowed_texture_attach", map, descriptor, out_session); + traceDowncall("mln_metal_borrowed_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19166,14 +18350,17 @@ public static int mln_opengl_borrowed_texture_attach(long map, MemorySegment des } } - private static class mln_metal_borrowed_texture_set_target { + private static class mln_vulkan_owned_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_borrowed_texture_set_target"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_owned_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19181,45 +18368,45 @@ private static class mln_metal_borrowed_texture_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_metal_borrowed_texture_set_target$descriptor() { - return mln_metal_borrowed_texture_set_target.DESC; + public static FunctionDescriptor mln_vulkan_owned_texture_attach$descriptor() { + return mln_vulkan_owned_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_metal_borrowed_texture_set_target$handle() { - return mln_metal_borrowed_texture_set_target.HANDLE; + public static MethodHandle mln_vulkan_owned_texture_attach$handle() { + return mln_vulkan_owned_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_metal_borrowed_texture_set_target$address() { - return mln_metal_borrowed_texture_set_target.ADDR; + public static MemorySegment mln_vulkan_owned_texture_attach$address() { + return mln_vulkan_owned_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_owned_texture_attach(mln_map map, const mln_vulkan_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_metal_borrowed_texture_set_target(long session, MemorySegment descriptor) { - var mh$ = mln_metal_borrowed_texture_set_target.HANDLE; + public static int mln_vulkan_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_vulkan_owned_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_borrowed_texture_set_target", session, descriptor); + traceDowncall("mln_vulkan_owned_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19227,14 +18414,17 @@ public static int mln_metal_borrowed_texture_set_target(long session, MemorySegm } } - private static class mln_vulkan_borrowed_texture_set_target { + private static class mln_vulkan_borrowed_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_borrowed_texture_set_target"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_borrowed_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19242,45 +18432,45 @@ private static class mln_vulkan_borrowed_texture_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_vulkan_borrowed_texture_set_target$descriptor() { - return mln_vulkan_borrowed_texture_set_target.DESC; + public static FunctionDescriptor mln_vulkan_borrowed_texture_attach$descriptor() { + return mln_vulkan_borrowed_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_vulkan_borrowed_texture_set_target$handle() { - return mln_vulkan_borrowed_texture_set_target.HANDLE; + public static MethodHandle mln_vulkan_borrowed_texture_attach$handle() { + return mln_vulkan_borrowed_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_vulkan_borrowed_texture_set_target$address() { - return mln_vulkan_borrowed_texture_set_target.ADDR; + public static MemorySegment mln_vulkan_borrowed_texture_attach$address() { + return mln_vulkan_borrowed_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor) + * mln_status mln_vulkan_borrowed_texture_attach(mln_map map, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_vulkan_borrowed_texture_set_target(long session, MemorySegment descriptor) { - var mh$ = mln_vulkan_borrowed_texture_set_target.HANDLE; + public static int mln_vulkan_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_vulkan_borrowed_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_borrowed_texture_set_target", session, descriptor); + traceDowncall("mln_vulkan_borrowed_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19288,14 +18478,17 @@ public static int mln_vulkan_borrowed_texture_set_target(long session, MemorySeg } } - private static class mln_opengl_borrowed_texture_set_target { + private static class mln_opengl_owned_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_borrowed_texture_set_target"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_owned_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19303,45 +18496,45 @@ private static class mln_opengl_borrowed_texture_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor) + * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_opengl_borrowed_texture_set_target$descriptor() { - return mln_opengl_borrowed_texture_set_target.DESC; + public static FunctionDescriptor mln_opengl_owned_texture_attach$descriptor() { + return mln_opengl_owned_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor) + * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_opengl_borrowed_texture_set_target$handle() { - return mln_opengl_borrowed_texture_set_target.HANDLE; + public static MethodHandle mln_opengl_owned_texture_attach$handle() { + return mln_opengl_owned_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor) + * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_opengl_borrowed_texture_set_target$address() { - return mln_opengl_borrowed_texture_set_target.ADDR; + public static MemorySegment mln_opengl_owned_texture_attach$address() { + return mln_opengl_owned_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor) + * mln_status mln_opengl_owned_texture_attach(mln_map map, const mln_opengl_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_opengl_borrowed_texture_set_target(long session, MemorySegment descriptor) { - var mh$ = mln_opengl_borrowed_texture_set_target.HANDLE; + public static int mln_opengl_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_opengl_owned_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_borrowed_texture_set_target", session, descriptor); + traceDowncall("mln_opengl_owned_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19349,16 +18542,17 @@ public static int mln_opengl_borrowed_texture_set_target(long session, MemorySeg } } - private static class mln_texture_read_premultiplied_rgba8 { + private static class mln_opengl_borrowed_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER, - MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_texture_read_premultiplied_rgba8"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_borrowed_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19366,45 +18560,45 @@ private static class mln_texture_read_premultiplied_rgba8 { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, uint8_t *out_data, size_t out_data_capacity, mln_texture_image_info *out_info) + * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_texture_read_premultiplied_rgba8$descriptor() { - return mln_texture_read_premultiplied_rgba8.DESC; + public static FunctionDescriptor mln_opengl_borrowed_texture_attach$descriptor() { + return mln_opengl_borrowed_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, uint8_t *out_data, size_t out_data_capacity, mln_texture_image_info *out_info) + * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_texture_read_premultiplied_rgba8$handle() { - return mln_texture_read_premultiplied_rgba8.HANDLE; + public static MethodHandle mln_opengl_borrowed_texture_attach$handle() { + return mln_opengl_borrowed_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, uint8_t *out_data, size_t out_data_capacity, mln_texture_image_info *out_info) + * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_texture_read_premultiplied_rgba8$address() { - return mln_texture_read_premultiplied_rgba8.ADDR; + public static MemorySegment mln_opengl_borrowed_texture_attach$address() { + return mln_opengl_borrowed_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, uint8_t *out_data, size_t out_data_capacity, mln_texture_image_info *out_info) + * mln_status mln_opengl_borrowed_texture_attach(mln_map map, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_texture_read_premultiplied_rgba8(long session, MemorySegment out_data, long out_data_capacity, MemorySegment out_info) { - var mh$ = mln_texture_read_premultiplied_rgba8.HANDLE; + public static int mln_opengl_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_opengl_borrowed_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_texture_read_premultiplied_rgba8", session, out_data, out_data_capacity, out_info); + traceDowncall("mln_opengl_borrowed_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, out_data, out_data_capacity, out_info); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19412,14 +18606,17 @@ public static int mln_texture_read_premultiplied_rgba8(long session, MemorySegme } } - private static class mln_metal_owned_texture_acquire_frame { + private static class mln_webgpu_owned_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_owned_texture_acquire_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_owned_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19427,45 +18624,45 @@ private static class mln_metal_owned_texture_acquire_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_acquire_frame(mln_render_session session, mln_metal_owned_texture_frame *out_frame) + * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_metal_owned_texture_acquire_frame$descriptor() { - return mln_metal_owned_texture_acquire_frame.DESC; + public static FunctionDescriptor mln_webgpu_owned_texture_attach$descriptor() { + return mln_webgpu_owned_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_acquire_frame(mln_render_session session, mln_metal_owned_texture_frame *out_frame) + * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_metal_owned_texture_acquire_frame$handle() { - return mln_metal_owned_texture_acquire_frame.HANDLE; + public static MethodHandle mln_webgpu_owned_texture_attach$handle() { + return mln_webgpu_owned_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_acquire_frame(mln_render_session session, mln_metal_owned_texture_frame *out_frame) + * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_metal_owned_texture_acquire_frame$address() { - return mln_metal_owned_texture_acquire_frame.ADDR; + public static MemorySegment mln_webgpu_owned_texture_attach$address() { + return mln_webgpu_owned_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_metal_owned_texture_acquire_frame(mln_render_session session, mln_metal_owned_texture_frame *out_frame) + * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_metal_owned_texture_acquire_frame(long session, MemorySegment out_frame) { - var mh$ = mln_metal_owned_texture_acquire_frame.HANDLE; + public static int mln_webgpu_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_webgpu_owned_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_owned_texture_acquire_frame", session, out_frame); + traceDowncall("mln_webgpu_owned_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, out_frame); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19473,14 +18670,17 @@ public static int mln_metal_owned_texture_acquire_frame(long session, MemorySegm } } - private static class mln_metal_owned_texture_release_frame { + private static class mln_webgpu_borrowed_texture_attach { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_owned_texture_release_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_borrowed_texture_attach"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19488,45 +18688,45 @@ private static class mln_metal_owned_texture_release_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_release_frame(mln_render_session session, const mln_metal_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_metal_owned_texture_release_frame$descriptor() { - return mln_metal_owned_texture_release_frame.DESC; + public static FunctionDescriptor mln_webgpu_borrowed_texture_attach$descriptor() { + return mln_webgpu_borrowed_texture_attach.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_release_frame(mln_render_session session, const mln_metal_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MethodHandle mln_metal_owned_texture_release_frame$handle() { - return mln_metal_owned_texture_release_frame.HANDLE; + public static MethodHandle mln_webgpu_borrowed_texture_attach$handle() { + return mln_webgpu_borrowed_texture_attach.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_metal_owned_texture_release_frame(mln_render_session session, const mln_metal_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static MemorySegment mln_metal_owned_texture_release_frame$address() { - return mln_metal_owned_texture_release_frame.ADDR; + public static MemorySegment mln_webgpu_borrowed_texture_attach$address() { + return mln_webgpu_borrowed_texture_attach.ADDR; } /** * {@snippet lang=c : - * mln_status mln_metal_owned_texture_release_frame(mln_render_session session, const mln_metal_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_render_session_attach_options *options, mln_render_session *out_session, const mln_completion *completion) * } */ - public static int mln_metal_owned_texture_release_frame(long session, MemorySegment frame) { - var mh$ = mln_metal_owned_texture_release_frame.HANDLE; + public static int mln_webgpu_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment options, MemorySegment out_session, MemorySegment completion) { + var mh$ = mln_webgpu_borrowed_texture_attach.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_metal_owned_texture_release_frame", session, frame); + traceDowncall("mln_webgpu_borrowed_texture_attach", map, descriptor, options, out_session, completion); } - return (int)mh$.invokeExact(session, frame); + return (int)mh$.invokeExact(map, descriptor, options, out_session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19534,14 +18734,15 @@ public static int mln_metal_owned_texture_release_frame(long session, MemorySegm } } - private static class mln_vulkan_owned_texture_acquire_frame { + private static class mln_metal_borrowed_texture_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_owned_texture_acquire_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_metal_borrowed_texture_set_target"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19549,45 +18750,45 @@ private static class mln_vulkan_owned_texture_acquire_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_acquire_frame(mln_render_session session, mln_vulkan_owned_texture_frame *out_frame) + * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_vulkan_owned_texture_acquire_frame$descriptor() { - return mln_vulkan_owned_texture_acquire_frame.DESC; + public static FunctionDescriptor mln_metal_borrowed_texture_set_target$descriptor() { + return mln_metal_borrowed_texture_set_target.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_acquire_frame(mln_render_session session, mln_vulkan_owned_texture_frame *out_frame) + * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MethodHandle mln_vulkan_owned_texture_acquire_frame$handle() { - return mln_vulkan_owned_texture_acquire_frame.HANDLE; + public static MethodHandle mln_metal_borrowed_texture_set_target$handle() { + return mln_metal_borrowed_texture_set_target.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_acquire_frame(mln_render_session session, mln_vulkan_owned_texture_frame *out_frame) + * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MemorySegment mln_vulkan_owned_texture_acquire_frame$address() { - return mln_vulkan_owned_texture_acquire_frame.ADDR; + public static MemorySegment mln_metal_borrowed_texture_set_target$address() { + return mln_metal_borrowed_texture_set_target.ADDR; } /** * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_acquire_frame(mln_render_session session, mln_vulkan_owned_texture_frame *out_frame) + * mln_status mln_metal_borrowed_texture_set_target(mln_render_session session, const mln_metal_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_vulkan_owned_texture_acquire_frame(long session, MemorySegment out_frame) { - var mh$ = mln_vulkan_owned_texture_acquire_frame.HANDLE; + public static int mln_metal_borrowed_texture_set_target(long session, MemorySegment descriptor, MemorySegment completion) { + var mh$ = mln_metal_borrowed_texture_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_owned_texture_acquire_frame", session, out_frame); + traceDowncall("mln_metal_borrowed_texture_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, out_frame); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19595,14 +18796,15 @@ public static int mln_vulkan_owned_texture_acquire_frame(long session, MemorySeg } } - private static class mln_vulkan_owned_texture_release_frame { + private static class mln_vulkan_borrowed_texture_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_owned_texture_release_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_vulkan_borrowed_texture_set_target"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19610,45 +18812,45 @@ private static class mln_vulkan_owned_texture_release_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_release_frame(mln_render_session session, const mln_vulkan_owned_texture_frame *frame) + * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_vulkan_owned_texture_release_frame$descriptor() { - return mln_vulkan_owned_texture_release_frame.DESC; + public static FunctionDescriptor mln_vulkan_borrowed_texture_set_target$descriptor() { + return mln_vulkan_borrowed_texture_set_target.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_release_frame(mln_render_session session, const mln_vulkan_owned_texture_frame *frame) + * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MethodHandle mln_vulkan_owned_texture_release_frame$handle() { - return mln_vulkan_owned_texture_release_frame.HANDLE; + public static MethodHandle mln_vulkan_borrowed_texture_set_target$handle() { + return mln_vulkan_borrowed_texture_set_target.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_release_frame(mln_render_session session, const mln_vulkan_owned_texture_frame *frame) + * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MemorySegment mln_vulkan_owned_texture_release_frame$address() { - return mln_vulkan_owned_texture_release_frame.ADDR; + public static MemorySegment mln_vulkan_borrowed_texture_set_target$address() { + return mln_vulkan_borrowed_texture_set_target.ADDR; } /** * {@snippet lang=c : - * mln_status mln_vulkan_owned_texture_release_frame(mln_render_session session, const mln_vulkan_owned_texture_frame *frame) + * mln_status mln_vulkan_borrowed_texture_set_target(mln_render_session session, const mln_vulkan_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_vulkan_owned_texture_release_frame(long session, MemorySegment frame) { - var mh$ = mln_vulkan_owned_texture_release_frame.HANDLE; + public static int mln_vulkan_borrowed_texture_set_target(long session, MemorySegment descriptor, MemorySegment completion) { + var mh$ = mln_vulkan_borrowed_texture_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_vulkan_owned_texture_release_frame", session, frame); + traceDowncall("mln_vulkan_borrowed_texture_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, frame); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19656,14 +18858,15 @@ public static int mln_vulkan_owned_texture_release_frame(long session, MemorySeg } } - private static class mln_opengl_owned_texture_acquire_frame { + private static class mln_opengl_borrowed_texture_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_owned_texture_acquire_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_borrowed_texture_set_target"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19671,45 +18874,45 @@ private static class mln_opengl_owned_texture_acquire_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_acquire_frame(mln_render_session session, mln_opengl_owned_texture_frame *out_frame) + * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_opengl_owned_texture_acquire_frame$descriptor() { - return mln_opengl_owned_texture_acquire_frame.DESC; + public static FunctionDescriptor mln_opengl_borrowed_texture_set_target$descriptor() { + return mln_opengl_borrowed_texture_set_target.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_acquire_frame(mln_render_session session, mln_opengl_owned_texture_frame *out_frame) + * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MethodHandle mln_opengl_owned_texture_acquire_frame$handle() { - return mln_opengl_owned_texture_acquire_frame.HANDLE; + public static MethodHandle mln_opengl_borrowed_texture_set_target$handle() { + return mln_opengl_borrowed_texture_set_target.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_acquire_frame(mln_render_session session, mln_opengl_owned_texture_frame *out_frame) + * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MemorySegment mln_opengl_owned_texture_acquire_frame$address() { - return mln_opengl_owned_texture_acquire_frame.ADDR; + public static MemorySegment mln_opengl_borrowed_texture_set_target$address() { + return mln_opengl_borrowed_texture_set_target.ADDR; } /** * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_acquire_frame(mln_render_session session, mln_opengl_owned_texture_frame *out_frame) + * mln_status mln_opengl_borrowed_texture_set_target(mln_render_session session, const mln_opengl_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_opengl_owned_texture_acquire_frame(long session, MemorySegment out_frame) { - var mh$ = mln_opengl_owned_texture_acquire_frame.HANDLE; + public static int mln_opengl_borrowed_texture_set_target(long session, MemorySegment descriptor, MemorySegment completion) { + var mh$ = mln_opengl_borrowed_texture_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_owned_texture_acquire_frame", session, out_frame); + traceDowncall("mln_opengl_borrowed_texture_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, out_frame); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19717,14 +18920,15 @@ public static int mln_opengl_owned_texture_acquire_frame(long session, MemorySeg } } - private static class mln_opengl_owned_texture_release_frame { + private static class mln_webgpu_borrowed_texture_set_target { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, + MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_opengl_owned_texture_release_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_borrowed_texture_set_target"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19732,45 +18936,45 @@ private static class mln_opengl_owned_texture_release_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_release_frame(mln_render_session session, const mln_opengl_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_opengl_owned_texture_release_frame$descriptor() { - return mln_opengl_owned_texture_release_frame.DESC; + public static FunctionDescriptor mln_webgpu_borrowed_texture_set_target$descriptor() { + return mln_webgpu_borrowed_texture_set_target.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_release_frame(mln_render_session session, const mln_opengl_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MethodHandle mln_opengl_owned_texture_release_frame$handle() { - return mln_opengl_owned_texture_release_frame.HANDLE; + public static MethodHandle mln_webgpu_borrowed_texture_set_target$handle() { + return mln_webgpu_borrowed_texture_set_target.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_release_frame(mln_render_session session, const mln_opengl_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static MemorySegment mln_opengl_owned_texture_release_frame$address() { - return mln_opengl_owned_texture_release_frame.ADDR; + public static MemorySegment mln_webgpu_borrowed_texture_set_target$address() { + return mln_webgpu_borrowed_texture_set_target.ADDR; } /** * {@snippet lang=c : - * mln_status mln_opengl_owned_texture_release_frame(mln_render_session session, const mln_opengl_owned_texture_frame *frame) + * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor, const mln_completion *completion) * } */ - public static int mln_opengl_owned_texture_release_frame(long session, MemorySegment frame) { - var mh$ = mln_opengl_owned_texture_release_frame.HANDLE; + public static int mln_webgpu_borrowed_texture_set_target(long session, MemorySegment descriptor, MemorySegment completion) { + var mh$ = mln_webgpu_borrowed_texture_set_target.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_opengl_owned_texture_release_frame", session, frame); + traceDowncall("mln_webgpu_borrowed_texture_set_target", session, descriptor, completion); } - return (int)mh$.invokeExact(session, frame); + return (int)mh$.invokeExact(session, descriptor, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19778,15 +18982,14 @@ public static int mln_opengl_owned_texture_release_frame(long session, MemorySeg } } - private static class mln_webgpu_owned_texture_attach { + private static class mln_texture_read_premultiplied_rgba8 { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_owned_texture_attach"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_texture_read_premultiplied_rgba8"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19794,45 +18997,45 @@ private static class mln_webgpu_owned_texture_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, const mln_completion *completion) * } */ - public static FunctionDescriptor mln_webgpu_owned_texture_attach$descriptor() { - return mln_webgpu_owned_texture_attach.DESC; + public static FunctionDescriptor mln_texture_read_premultiplied_rgba8$descriptor() { + return mln_texture_read_premultiplied_rgba8.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, const mln_completion *completion) * } */ - public static MethodHandle mln_webgpu_owned_texture_attach$handle() { - return mln_webgpu_owned_texture_attach.HANDLE; + public static MethodHandle mln_texture_read_premultiplied_rgba8$handle() { + return mln_texture_read_premultiplied_rgba8.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, const mln_completion *completion) * } */ - public static MemorySegment mln_webgpu_owned_texture_attach$address() { - return mln_webgpu_owned_texture_attach.ADDR; + public static MemorySegment mln_texture_read_premultiplied_rgba8$address() { + return mln_texture_read_premultiplied_rgba8.ADDR; } /** * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_attach(mln_map map, const mln_webgpu_owned_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_texture_read_premultiplied_rgba8(mln_render_session session, const mln_completion *completion) * } */ - public static int mln_webgpu_owned_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_webgpu_owned_texture_attach.HANDLE; + public static int mln_texture_read_premultiplied_rgba8(long session, MemorySegment completion) { + var mh$ = mln_texture_read_premultiplied_rgba8.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_owned_texture_attach", map, descriptor, out_session); + traceDowncall("mln_texture_read_premultiplied_rgba8", session, completion); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(session, completion); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19840,15 +19043,14 @@ public static int mln_webgpu_owned_texture_attach(long map, MemorySegment descri } } - private static class mln_webgpu_borrowed_texture_attach { + private static class mln_acquired_frame_get_metal_texture { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, - MapLibreNativeC.C_POINTER, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_borrowed_texture_attach"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_metal_texture"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19856,45 +19058,45 @@ private static class mln_webgpu_borrowed_texture_attach { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_acquired_frame_get_metal_texture(mln_acquired_frame frame, mln_metal_owned_texture_frame *out_frame) * } */ - public static FunctionDescriptor mln_webgpu_borrowed_texture_attach$descriptor() { - return mln_webgpu_borrowed_texture_attach.DESC; + public static FunctionDescriptor mln_acquired_frame_get_metal_texture$descriptor() { + return mln_acquired_frame_get_metal_texture.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_acquired_frame_get_metal_texture(mln_acquired_frame frame, mln_metal_owned_texture_frame *out_frame) * } */ - public static MethodHandle mln_webgpu_borrowed_texture_attach$handle() { - return mln_webgpu_borrowed_texture_attach.HANDLE; + public static MethodHandle mln_acquired_frame_get_metal_texture$handle() { + return mln_acquired_frame_get_metal_texture.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_acquired_frame_get_metal_texture(mln_acquired_frame frame, mln_metal_owned_texture_frame *out_frame) * } */ - public static MemorySegment mln_webgpu_borrowed_texture_attach$address() { - return mln_webgpu_borrowed_texture_attach.ADDR; + public static MemorySegment mln_acquired_frame_get_metal_texture$address() { + return mln_acquired_frame_get_metal_texture.ADDR; } /** * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_attach(mln_map map, const mln_webgpu_borrowed_texture_descriptor *descriptor, mln_render_session *out_session) + * mln_status mln_acquired_frame_get_metal_texture(mln_acquired_frame frame, mln_metal_owned_texture_frame *out_frame) * } */ - public static int mln_webgpu_borrowed_texture_attach(long map, MemorySegment descriptor, MemorySegment out_session) { - var mh$ = mln_webgpu_borrowed_texture_attach.HANDLE; + public static int mln_acquired_frame_get_metal_texture(long frame, MemorySegment out_frame) { + var mh$ = mln_acquired_frame_get_metal_texture.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_borrowed_texture_attach", map, descriptor, out_session); + traceDowncall("mln_acquired_frame_get_metal_texture", frame, out_frame); } - return (int)mh$.invokeExact(map, descriptor, out_session); + return (int)mh$.invokeExact(frame, out_frame); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19902,14 +19104,14 @@ public static int mln_webgpu_borrowed_texture_attach(long map, MemorySegment des } } - private static class mln_webgpu_borrowed_texture_set_target { + private static class mln_acquired_frame_get_vulkan_texture { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_borrowed_texture_set_target"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_vulkan_texture"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19917,45 +19119,45 @@ private static class mln_webgpu_borrowed_texture_set_target { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor) + * mln_status mln_acquired_frame_get_vulkan_texture(mln_acquired_frame frame, mln_vulkan_owned_texture_frame *out_frame) * } */ - public static FunctionDescriptor mln_webgpu_borrowed_texture_set_target$descriptor() { - return mln_webgpu_borrowed_texture_set_target.DESC; + public static FunctionDescriptor mln_acquired_frame_get_vulkan_texture$descriptor() { + return mln_acquired_frame_get_vulkan_texture.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor) + * mln_status mln_acquired_frame_get_vulkan_texture(mln_acquired_frame frame, mln_vulkan_owned_texture_frame *out_frame) * } */ - public static MethodHandle mln_webgpu_borrowed_texture_set_target$handle() { - return mln_webgpu_borrowed_texture_set_target.HANDLE; + public static MethodHandle mln_acquired_frame_get_vulkan_texture$handle() { + return mln_acquired_frame_get_vulkan_texture.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor) + * mln_status mln_acquired_frame_get_vulkan_texture(mln_acquired_frame frame, mln_vulkan_owned_texture_frame *out_frame) * } */ - public static MemorySegment mln_webgpu_borrowed_texture_set_target$address() { - return mln_webgpu_borrowed_texture_set_target.ADDR; + public static MemorySegment mln_acquired_frame_get_vulkan_texture$address() { + return mln_acquired_frame_get_vulkan_texture.ADDR; } /** * {@snippet lang=c : - * mln_status mln_webgpu_borrowed_texture_set_target(mln_render_session session, const mln_webgpu_borrowed_texture_descriptor *descriptor) + * mln_status mln_acquired_frame_get_vulkan_texture(mln_acquired_frame frame, mln_vulkan_owned_texture_frame *out_frame) * } */ - public static int mln_webgpu_borrowed_texture_set_target(long session, MemorySegment descriptor) { - var mh$ = mln_webgpu_borrowed_texture_set_target.HANDLE; + public static int mln_acquired_frame_get_vulkan_texture(long frame, MemorySegment out_frame) { + var mh$ = mln_acquired_frame_get_vulkan_texture.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_borrowed_texture_set_target", session, descriptor); + traceDowncall("mln_acquired_frame_get_vulkan_texture", frame, out_frame); } - return (int)mh$.invokeExact(session, descriptor); + return (int)mh$.invokeExact(frame, out_frame); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -19963,14 +19165,14 @@ public static int mln_webgpu_borrowed_texture_set_target(long session, MemorySeg } } - private static class mln_webgpu_owned_texture_acquire_frame { + private static class mln_acquired_frame_get_opengl_texture { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_owned_texture_acquire_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_opengl_texture"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -19978,45 +19180,45 @@ private static class mln_webgpu_owned_texture_acquire_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_acquire_frame(mln_render_session session, mln_webgpu_owned_texture_frame *out_frame) + * mln_status mln_acquired_frame_get_opengl_texture(mln_acquired_frame frame, mln_opengl_owned_texture_frame *out_frame) * } */ - public static FunctionDescriptor mln_webgpu_owned_texture_acquire_frame$descriptor() { - return mln_webgpu_owned_texture_acquire_frame.DESC; + public static FunctionDescriptor mln_acquired_frame_get_opengl_texture$descriptor() { + return mln_acquired_frame_get_opengl_texture.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_acquire_frame(mln_render_session session, mln_webgpu_owned_texture_frame *out_frame) + * mln_status mln_acquired_frame_get_opengl_texture(mln_acquired_frame frame, mln_opengl_owned_texture_frame *out_frame) * } */ - public static MethodHandle mln_webgpu_owned_texture_acquire_frame$handle() { - return mln_webgpu_owned_texture_acquire_frame.HANDLE; + public static MethodHandle mln_acquired_frame_get_opengl_texture$handle() { + return mln_acquired_frame_get_opengl_texture.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_acquire_frame(mln_render_session session, mln_webgpu_owned_texture_frame *out_frame) + * mln_status mln_acquired_frame_get_opengl_texture(mln_acquired_frame frame, mln_opengl_owned_texture_frame *out_frame) * } */ - public static MemorySegment mln_webgpu_owned_texture_acquire_frame$address() { - return mln_webgpu_owned_texture_acquire_frame.ADDR; + public static MemorySegment mln_acquired_frame_get_opengl_texture$address() { + return mln_acquired_frame_get_opengl_texture.ADDR; } /** * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_acquire_frame(mln_render_session session, mln_webgpu_owned_texture_frame *out_frame) + * mln_status mln_acquired_frame_get_opengl_texture(mln_acquired_frame frame, mln_opengl_owned_texture_frame *out_frame) * } */ - public static int mln_webgpu_owned_texture_acquire_frame(long session, MemorySegment out_frame) { - var mh$ = mln_webgpu_owned_texture_acquire_frame.HANDLE; + public static int mln_acquired_frame_get_opengl_texture(long frame, MemorySegment out_frame) { + var mh$ = mln_acquired_frame_get_opengl_texture.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_owned_texture_acquire_frame", session, out_frame); + traceDowncall("mln_acquired_frame_get_opengl_texture", frame, out_frame); } - return (int)mh$.invokeExact(session, out_frame); + return (int)mh$.invokeExact(frame, out_frame); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { @@ -20024,14 +19226,14 @@ public static int mln_webgpu_owned_texture_acquire_frame(long session, MemorySeg } } - private static class mln_webgpu_owned_texture_release_frame { + private static class mln_acquired_frame_get_webgpu_texture { public static final FunctionDescriptor DESC = FunctionDescriptor.of( MapLibreNativeC.C_INT, MapLibreNativeC.C_LONG, MapLibreNativeC.C_POINTER ); - public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_webgpu_owned_texture_release_frame"); + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("mln_acquired_frame_get_webgpu_texture"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } @@ -20039,49 +19241,58 @@ private static class mln_webgpu_owned_texture_release_frame { /** * Function descriptor for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_release_frame(mln_render_session session, const mln_webgpu_owned_texture_frame *frame) + * mln_status mln_acquired_frame_get_webgpu_texture(mln_acquired_frame frame, mln_webgpu_owned_texture_frame *out_frame) * } */ - public static FunctionDescriptor mln_webgpu_owned_texture_release_frame$descriptor() { - return mln_webgpu_owned_texture_release_frame.DESC; + public static FunctionDescriptor mln_acquired_frame_get_webgpu_texture$descriptor() { + return mln_acquired_frame_get_webgpu_texture.DESC; } /** * Downcall method handle for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_release_frame(mln_render_session session, const mln_webgpu_owned_texture_frame *frame) + * mln_status mln_acquired_frame_get_webgpu_texture(mln_acquired_frame frame, mln_webgpu_owned_texture_frame *out_frame) * } */ - public static MethodHandle mln_webgpu_owned_texture_release_frame$handle() { - return mln_webgpu_owned_texture_release_frame.HANDLE; + public static MethodHandle mln_acquired_frame_get_webgpu_texture$handle() { + return mln_acquired_frame_get_webgpu_texture.HANDLE; } /** * Address for: * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_release_frame(mln_render_session session, const mln_webgpu_owned_texture_frame *frame) + * mln_status mln_acquired_frame_get_webgpu_texture(mln_acquired_frame frame, mln_webgpu_owned_texture_frame *out_frame) * } */ - public static MemorySegment mln_webgpu_owned_texture_release_frame$address() { - return mln_webgpu_owned_texture_release_frame.ADDR; + public static MemorySegment mln_acquired_frame_get_webgpu_texture$address() { + return mln_acquired_frame_get_webgpu_texture.ADDR; } /** * {@snippet lang=c : - * mln_status mln_webgpu_owned_texture_release_frame(mln_render_session session, const mln_webgpu_owned_texture_frame *frame) + * mln_status mln_acquired_frame_get_webgpu_texture(mln_acquired_frame frame, mln_webgpu_owned_texture_frame *out_frame) * } */ - public static int mln_webgpu_owned_texture_release_frame(long session, MemorySegment frame) { - var mh$ = mln_webgpu_owned_texture_release_frame.HANDLE; + public static int mln_acquired_frame_get_webgpu_texture(long frame, MemorySegment out_frame) { + var mh$ = mln_acquired_frame_get_webgpu_texture.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("mln_webgpu_owned_texture_release_frame", session, frame); + traceDowncall("mln_acquired_frame_get_webgpu_texture", frame, out_frame); } - return (int)mh$.invokeExact(session, frame); + return (int)mh$.invokeExact(frame, out_frame); } catch (Error | RuntimeException ex) { throw ex; } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } + private static final long MLN_HANDLE_NULL = 0L; + /** + * {@snippet lang=c : + * #define MLN_HANDLE_NULL 0 + * } + */ + public static long MLN_HANDLE_NULL() { + return MLN_HANDLE_NULL; + } } diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_delta.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_delta.java new file mode 100644 index 000000000..dd7ea68c7 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_delta.java @@ -0,0 +1,403 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_camera_delta { + * uint32_t size; + * uint32_t kind; + * mln_screen_point offset; + * double amount; + * bool has_anchor; + * mln_screen_point anchor; + * mln_animation_options animation; + * } + * } + */ +public class mln_camera_delta { + + mln_camera_delta() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("kind"), + mln_screen_point.layout().withName("offset"), + MapLibreNativeC.C_DOUBLE.withName("amount"), + MapLibreNativeC.C_BOOL.withName("has_anchor"), + MemoryLayout.paddingLayout(7), + mln_screen_point.layout().withName("anchor"), + mln_animation_options.layout().withName("animation") + ).withName("mln_camera_delta"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt kind$LAYOUT = (OfInt)$LAYOUT.select(groupElement("kind")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final OfInt kind$layout() { + return kind$LAYOUT; + } + + private static final long kind$OFFSET = $LAYOUT.byteOffset(groupElement("kind")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final long kind$offset() { + return kind$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static int kind(MemorySegment struct) { + return struct.get(kind$LAYOUT, kind$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static void kind(MemorySegment struct, int fieldValue) { + struct.set(kind$LAYOUT, kind$OFFSET, fieldValue); + } + + private static final GroupLayout offset$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("offset")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_screen_point offset + * } + */ + public static final GroupLayout offset$layout() { + return offset$LAYOUT; + } + + private static final long offset$OFFSET = $LAYOUT.byteOffset(groupElement("offset")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_screen_point offset + * } + */ + public static final long offset$offset() { + return offset$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_screen_point offset + * } + */ + public static MemorySegment offset(MemorySegment struct) { + return struct.asSlice(offset$OFFSET, offset$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_screen_point offset + * } + */ + public static void offset(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, offset$OFFSET, offset$LAYOUT.byteSize()); + } + + private static final OfDouble amount$LAYOUT = (OfDouble)$LAYOUT.select(groupElement("amount")); + + /** + * Layout for field: + * {@snippet lang=c : + * double amount + * } + */ + public static final OfDouble amount$layout() { + return amount$LAYOUT; + } + + private static final long amount$OFFSET = $LAYOUT.byteOffset(groupElement("amount")); + + /** + * Offset for field: + * {@snippet lang=c : + * double amount + * } + */ + public static final long amount$offset() { + return amount$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * double amount + * } + */ + public static double amount(MemorySegment struct) { + return struct.get(amount$LAYOUT, amount$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * double amount + * } + */ + public static void amount(MemorySegment struct, double fieldValue) { + struct.set(amount$LAYOUT, amount$OFFSET, fieldValue); + } + + private static final OfBoolean has_anchor$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("has_anchor")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool has_anchor + * } + */ + public static final OfBoolean has_anchor$layout() { + return has_anchor$LAYOUT; + } + + private static final long has_anchor$OFFSET = $LAYOUT.byteOffset(groupElement("has_anchor")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool has_anchor + * } + */ + public static final long has_anchor$offset() { + return has_anchor$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool has_anchor + * } + */ + public static boolean has_anchor(MemorySegment struct) { + return struct.get(has_anchor$LAYOUT, has_anchor$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool has_anchor + * } + */ + public static void has_anchor(MemorySegment struct, boolean fieldValue) { + struct.set(has_anchor$LAYOUT, has_anchor$OFFSET, fieldValue); + } + + private static final GroupLayout anchor$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("anchor")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_screen_point anchor + * } + */ + public static final GroupLayout anchor$layout() { + return anchor$LAYOUT; + } + + private static final long anchor$OFFSET = $LAYOUT.byteOffset(groupElement("anchor")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_screen_point anchor + * } + */ + public static final long anchor$offset() { + return anchor$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_screen_point anchor + * } + */ + public static MemorySegment anchor(MemorySegment struct) { + return struct.asSlice(anchor$OFFSET, anchor$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_screen_point anchor + * } + */ + public static void anchor(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, anchor$OFFSET, anchor$LAYOUT.byteSize()); + } + + private static final GroupLayout animation$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("animation")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static final GroupLayout animation$layout() { + return animation$LAYOUT; + } + + private static final long animation$OFFSET = $LAYOUT.byteOffset(groupElement("animation")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static final long animation$offset() { + return animation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static MemorySegment animation(MemorySegment struct) { + return struct.asSlice(animation$OFFSET, animation$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static void animation(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, animation$OFFSET, animation$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_query_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_query_result.java new file mode 100644 index 000000000..497bfef14 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_query_result.java @@ -0,0 +1,264 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_camera_query_result { + * uint32_t size; + * uint32_t reserved; + * uint64_t generation; + * mln_camera_options camera; + * } + * } + */ +public class mln_camera_query_result { + + mln_camera_query_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + MapLibreNativeC.C_LONG.withName("generation"), + mln_camera_options.layout().withName("camera") + ).withName("mln_camera_query_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final OfLong generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final OfLong generation$layout() { + return generation$LAYOUT; + } + + private static final long generation$OFFSET = $LAYOUT.byteOffset(groupElement("generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final long generation$offset() { + return generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static long generation(MemorySegment struct) { + return struct.get(generation$LAYOUT, generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static void generation(MemorySegment struct, long fieldValue) { + struct.set(generation$LAYOUT, generation$OFFSET, fieldValue); + } + + private static final GroupLayout camera$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("camera")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final GroupLayout camera$layout() { + return camera$LAYOUT; + } + + private static final long camera$OFFSET = $LAYOUT.byteOffset(groupElement("camera")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final long camera$offset() { + return camera$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static MemorySegment camera(MemorySegment struct) { + return struct.asSlice(camera$OFFSET, camera$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static void camera(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, camera$OFFSET, camera$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_update.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_update.java new file mode 100644 index 000000000..133513b17 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_camera_update.java @@ -0,0 +1,356 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_camera_update { + * uint32_t size; + * uint32_t mode; + * mln_camera_options camera; + * mln_animation_options animation; + * uint32_t gesture_phase; + * uint32_t reserved; + * } + * } + */ +public class mln_camera_update { + + mln_camera_update() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("mode"), + mln_camera_options.layout().withName("camera"), + mln_animation_options.layout().withName("animation"), + MapLibreNativeC.C_INT.withName("gesture_phase"), + MapLibreNativeC.C_INT.withName("reserved") + ).withName("mln_camera_update"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt mode$LAYOUT = (OfInt)$LAYOUT.select(groupElement("mode")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t mode + * } + */ + public static final OfInt mode$layout() { + return mode$LAYOUT; + } + + private static final long mode$OFFSET = $LAYOUT.byteOffset(groupElement("mode")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t mode + * } + */ + public static final long mode$offset() { + return mode$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t mode + * } + */ + public static int mode(MemorySegment struct) { + return struct.get(mode$LAYOUT, mode$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t mode + * } + */ + public static void mode(MemorySegment struct, int fieldValue) { + struct.set(mode$LAYOUT, mode$OFFSET, fieldValue); + } + + private static final GroupLayout camera$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("camera")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final GroupLayout camera$layout() { + return camera$LAYOUT; + } + + private static final long camera$OFFSET = $LAYOUT.byteOffset(groupElement("camera")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final long camera$offset() { + return camera$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static MemorySegment camera(MemorySegment struct) { + return struct.asSlice(camera$OFFSET, camera$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static void camera(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, camera$OFFSET, camera$LAYOUT.byteSize()); + } + + private static final GroupLayout animation$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("animation")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static final GroupLayout animation$layout() { + return animation$LAYOUT; + } + + private static final long animation$OFFSET = $LAYOUT.byteOffset(groupElement("animation")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static final long animation$offset() { + return animation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static MemorySegment animation(MemorySegment struct) { + return struct.asSlice(animation$OFFSET, animation$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_animation_options animation + * } + */ + public static void animation(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, animation$OFFSET, animation$LAYOUT.byteSize()); + } + + private static final OfInt gesture_phase$LAYOUT = (OfInt)$LAYOUT.select(groupElement("gesture_phase")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t gesture_phase + * } + */ + public static final OfInt gesture_phase$layout() { + return gesture_phase$LAYOUT; + } + + private static final long gesture_phase$OFFSET = $LAYOUT.byteOffset(groupElement("gesture_phase")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t gesture_phase + * } + */ + public static final long gesture_phase$offset() { + return gesture_phase$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t gesture_phase + * } + */ + public static int gesture_phase(MemorySegment struct) { + return struct.get(gesture_phase$LAYOUT, gesture_phase$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t gesture_phase + * } + */ + public static void gesture_phase(MemorySegment struct, int fieldValue) { + struct.set(gesture_phase$LAYOUT, gesture_phase$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion.java new file mode 100644 index 000000000..fb4591c0a --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion.java @@ -0,0 +1,265 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_completion { + * uint32_t size; + * mln_completion_callback callback; + * void *user_data; + * mln_completion_release release_user_data; + * } + * } + */ +public class mln_completion { + + mln_completion() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MemoryLayout.paddingLayout(4), + MapLibreNativeC.C_POINTER.withName("callback"), + MapLibreNativeC.C_POINTER.withName("user_data"), + MapLibreNativeC.C_POINTER.withName("release_user_data") + ).withName("mln_completion"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final AddressLayout callback$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("callback")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_completion_callback callback + * } + */ + public static final AddressLayout callback$layout() { + return callback$LAYOUT; + } + + private static final long callback$OFFSET = $LAYOUT.byteOffset(groupElement("callback")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_completion_callback callback + * } + */ + public static final long callback$offset() { + return callback$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_completion_callback callback + * } + */ + public static MemorySegment callback(MemorySegment struct) { + return struct.get(callback$LAYOUT, callback$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_completion_callback callback + * } + */ + public static void callback(MemorySegment struct, MemorySegment fieldValue) { + struct.set(callback$LAYOUT, callback$OFFSET, fieldValue); + } + + private static final AddressLayout user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static final AddressLayout user_data$layout() { + return user_data$LAYOUT; + } + + private static final long user_data$OFFSET = $LAYOUT.byteOffset(groupElement("user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static final long user_data$offset() { + return user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static MemorySegment user_data(MemorySegment struct) { + return struct.get(user_data$LAYOUT, user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static void user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(user_data$LAYOUT, user_data$OFFSET, fieldValue); + } + + private static final AddressLayout release_user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("release_user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_completion_release release_user_data + * } + */ + public static final AddressLayout release_user_data$layout() { + return release_user_data$LAYOUT; + } + + private static final long release_user_data$OFFSET = $LAYOUT.byteOffset(groupElement("release_user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_completion_release release_user_data + * } + */ + public static final long release_user_data$offset() { + return release_user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_completion_release release_user_data + * } + */ + public static MemorySegment release_user_data(MemorySegment struct) { + return struct.get(release_user_data$LAYOUT, release_user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_completion_release release_user_data + * } + */ + public static void release_user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(release_user_data$LAYOUT, release_user_data$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_callback.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_callback.java new file mode 100644 index 000000000..c1a1bbbf5 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_callback.java @@ -0,0 +1,69 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_completion_callback)(void *, const mln_completion_result *) + * } + */ +public final class mln_completion_callback { + + private mln_completion_callback() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data, MemorySegment result); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER, + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_completion_callback.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_completion_callback.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data, MemorySegment result) { + try { + DOWN$MH.invokeExact(funcPtr, user_data, result); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_release.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_release.java new file mode 100644 index 000000000..aa6566f4c --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_release.java @@ -0,0 +1,68 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_completion_release)(void *) + * } + */ +public final class mln_completion_release { + + private mln_completion_release() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_completion_release.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_completion_release.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data) { + try { + DOWN$MH.invokeExact(funcPtr, user_data); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_result.java new file mode 100644 index 000000000..53006ba17 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_completion_result.java @@ -0,0 +1,448 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_completion_result { + * uint32_t size; + * mln_status status; + * uint32_t disposition; + * uint32_t reserved; + * uint64_t generation; + * mln_buffer_view diagnostic; + * const void *value; + * size_t value_count; + * } + * } + */ +public class mln_completion_result { + + mln_completion_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("status"), + MapLibreNativeC.C_INT.withName("disposition"), + MapLibreNativeC.C_INT.withName("reserved"), + MapLibreNativeC.C_LONG.withName("generation"), + mln_buffer_view.layout().withName("diagnostic"), + MapLibreNativeC.C_POINTER.withName("value"), + MapLibreNativeC.C_LONG.withName("value_count") + ).withName("mln_completion_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt status$LAYOUT = (OfInt)$LAYOUT.select(groupElement("status")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_status status + * } + */ + public static final OfInt status$layout() { + return status$LAYOUT; + } + + private static final long status$OFFSET = $LAYOUT.byteOffset(groupElement("status")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_status status + * } + */ + public static final long status$offset() { + return status$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_status status + * } + */ + public static int status(MemorySegment struct) { + return struct.get(status$LAYOUT, status$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_status status + * } + */ + public static void status(MemorySegment struct, int fieldValue) { + struct.set(status$LAYOUT, status$OFFSET, fieldValue); + } + + private static final OfInt disposition$LAYOUT = (OfInt)$LAYOUT.select(groupElement("disposition")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final OfInt disposition$layout() { + return disposition$LAYOUT; + } + + private static final long disposition$OFFSET = $LAYOUT.byteOffset(groupElement("disposition")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final long disposition$offset() { + return disposition$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static int disposition(MemorySegment struct) { + return struct.get(disposition$LAYOUT, disposition$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static void disposition(MemorySegment struct, int fieldValue) { + struct.set(disposition$LAYOUT, disposition$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final OfLong generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final OfLong generation$layout() { + return generation$LAYOUT; + } + + private static final long generation$OFFSET = $LAYOUT.byteOffset(groupElement("generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final long generation$offset() { + return generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static long generation(MemorySegment struct) { + return struct.get(generation$LAYOUT, generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static void generation(MemorySegment struct, long fieldValue) { + struct.set(generation$LAYOUT, generation$OFFSET, fieldValue); + } + + private static final GroupLayout diagnostic$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("diagnostic")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view diagnostic + * } + */ + public static final GroupLayout diagnostic$layout() { + return diagnostic$LAYOUT; + } + + private static final long diagnostic$OFFSET = $LAYOUT.byteOffset(groupElement("diagnostic")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view diagnostic + * } + */ + public static final long diagnostic$offset() { + return diagnostic$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view diagnostic + * } + */ + public static MemorySegment diagnostic(MemorySegment struct) { + return struct.asSlice(diagnostic$OFFSET, diagnostic$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view diagnostic + * } + */ + public static void diagnostic(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, diagnostic$OFFSET, diagnostic$LAYOUT.byteSize()); + } + + private static final AddressLayout value$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("value")); + + /** + * Layout for field: + * {@snippet lang=c : + * const void *value + * } + */ + public static final AddressLayout value$layout() { + return value$LAYOUT; + } + + private static final long value$OFFSET = $LAYOUT.byteOffset(groupElement("value")); + + /** + * Offset for field: + * {@snippet lang=c : + * const void *value + * } + */ + public static final long value$offset() { + return value$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const void *value + * } + */ + public static MemorySegment value(MemorySegment struct) { + return struct.get(value$LAYOUT, value$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const void *value + * } + */ + public static void value(MemorySegment struct, MemorySegment fieldValue) { + struct.set(value$LAYOUT, value$OFFSET, fieldValue); + } + + private static final OfLong value_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("value_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t value_count + * } + */ + public static final OfLong value_count$layout() { + return value_count$LAYOUT; + } + + private static final long value_count$OFFSET = $LAYOUT.byteOffset(groupElement("value_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t value_count + * } + */ + public static final long value_count$offset() { + return value_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t value_count + * } + */ + public static long value_count(MemorySegment struct) { + return struct.get(value_count$LAYOUT, value_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t value_count + * } + */ + public static void value_count(MemorySegment struct, long fieldValue) { + struct.set(value_count$LAYOUT, value_count$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_frame_demand.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_frame_demand.java new file mode 100644 index 000000000..9ecc0a5b6 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_frame_demand.java @@ -0,0 +1,310 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_frame_demand { + * uint32_t size; + * uint32_t flags; + * uint64_t token; + * uint64_t coalescing_boundary; + * uint64_t timeout_ns; + * } + * } + */ +public class mln_frame_demand { + + mln_frame_demand() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("flags"), + MapLibreNativeC.C_LONG.withName("token"), + MapLibreNativeC.C_LONG.withName("coalescing_boundary"), + MapLibreNativeC.C_LONG.withName("timeout_ns") + ).withName("mln_frame_demand"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt flags$LAYOUT = (OfInt)$LAYOUT.select(groupElement("flags")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static final OfInt flags$layout() { + return flags$LAYOUT; + } + + private static final long flags$OFFSET = $LAYOUT.byteOffset(groupElement("flags")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static final long flags$offset() { + return flags$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static int flags(MemorySegment struct) { + return struct.get(flags$LAYOUT, flags$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static void flags(MemorySegment struct, int fieldValue) { + struct.set(flags$LAYOUT, flags$OFFSET, fieldValue); + } + + private static final OfLong token$LAYOUT = (OfLong)$LAYOUT.select(groupElement("token")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static final OfLong token$layout() { + return token$LAYOUT; + } + + private static final long token$OFFSET = $LAYOUT.byteOffset(groupElement("token")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static final long token$offset() { + return token$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static long token(MemorySegment struct) { + return struct.get(token$LAYOUT, token$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static void token(MemorySegment struct, long fieldValue) { + struct.set(token$LAYOUT, token$OFFSET, fieldValue); + } + + private static final OfLong coalescing_boundary$LAYOUT = (OfLong)$LAYOUT.select(groupElement("coalescing_boundary")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t coalescing_boundary + * } + */ + public static final OfLong coalescing_boundary$layout() { + return coalescing_boundary$LAYOUT; + } + + private static final long coalescing_boundary$OFFSET = $LAYOUT.byteOffset(groupElement("coalescing_boundary")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t coalescing_boundary + * } + */ + public static final long coalescing_boundary$offset() { + return coalescing_boundary$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t coalescing_boundary + * } + */ + public static long coalescing_boundary(MemorySegment struct) { + return struct.get(coalescing_boundary$LAYOUT, coalescing_boundary$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t coalescing_boundary + * } + */ + public static void coalescing_boundary(MemorySegment struct, long fieldValue) { + struct.set(coalescing_boundary$LAYOUT, coalescing_boundary$OFFSET, fieldValue); + } + + private static final OfLong timeout_ns$LAYOUT = (OfLong)$LAYOUT.select(groupElement("timeout_ns")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t timeout_ns + * } + */ + public static final OfLong timeout_ns$layout() { + return timeout_ns$LAYOUT; + } + + private static final long timeout_ns$OFFSET = $LAYOUT.byteOffset(groupElement("timeout_ns")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t timeout_ns + * } + */ + public static final long timeout_ns$offset() { + return timeout_ns$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t timeout_ns + * } + */ + public static long timeout_ns(MemorySegment struct) { + return struct.get(timeout_ns$LAYOUT, timeout_ns$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t timeout_ns + * } + */ + public static void timeout_ns(MemorySegment struct, long fieldValue) { + struct.set(timeout_ns$LAYOUT, timeout_ns$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_gpu_sync.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_gpu_sync.java new file mode 100644 index 000000000..7816a21de --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_gpu_sync.java @@ -0,0 +1,264 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_gpu_sync { + * uint32_t size; + * uint32_t kind; + * void *object; + * uint64_t value; + * } + * } + */ +public class mln_gpu_sync { + + mln_gpu_sync() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("kind"), + MapLibreNativeC.C_POINTER.withName("object"), + MapLibreNativeC.C_LONG.withName("value") + ).withName("mln_gpu_sync"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt kind$LAYOUT = (OfInt)$LAYOUT.select(groupElement("kind")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final OfInt kind$layout() { + return kind$LAYOUT; + } + + private static final long kind$OFFSET = $LAYOUT.byteOffset(groupElement("kind")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final long kind$offset() { + return kind$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static int kind(MemorySegment struct) { + return struct.get(kind$LAYOUT, kind$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static void kind(MemorySegment struct, int fieldValue) { + struct.set(kind$LAYOUT, kind$OFFSET, fieldValue); + } + + private static final AddressLayout object$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("object")); + + /** + * Layout for field: + * {@snippet lang=c : + * void *object + * } + */ + public static final AddressLayout object$layout() { + return object$LAYOUT; + } + + private static final long object$OFFSET = $LAYOUT.byteOffset(groupElement("object")); + + /** + * Offset for field: + * {@snippet lang=c : + * void *object + * } + */ + public static final long object$offset() { + return object$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * void *object + * } + */ + public static MemorySegment object(MemorySegment struct) { + return struct.get(object$LAYOUT, object$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * void *object + * } + */ + public static void object(MemorySegment struct, MemorySegment fieldValue) { + struct.set(object$LAYOUT, object$OFFSET, fieldValue); + } + + private static final OfLong value$LAYOUT = (OfLong)$LAYOUT.select(groupElement("value")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t value + * } + */ + public static final OfLong value$layout() { + return value$LAYOUT; + } + + private static final long value$OFFSET = $LAYOUT.byteOffset(groupElement("value")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t value + * } + */ + public static final long value$offset() { + return value$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t value + * } + */ + public static long value(MemorySegment struct) { + return struct.get(value$LAYOUT, value$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t value + * } + */ + public static void value(MemorySegment struct, long fieldValue) { + struct.set(value$LAYOUT, value$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_http_header_transform.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_http_header_transform.java index 9c26ab10c..416cfc09e 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_http_header_transform.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_http_header_transform.java @@ -18,6 +18,7 @@ * uint32_t size; * mln_http_header_transform_callback callback; * void *user_data; + * mln_runtime_callback_release release_user_data; * } * } */ @@ -31,7 +32,8 @@ public class mln_http_header_transform { MapLibreNativeC.C_INT.withName("size"), MemoryLayout.paddingLayout(4), MapLibreNativeC.C_POINTER.withName("callback"), - MapLibreNativeC.C_POINTER.withName("user_data") + MapLibreNativeC.C_POINTER.withName("user_data"), + MapLibreNativeC.C_POINTER.withName("release_user_data") ).withName("mln_http_header_transform"); /** @@ -173,6 +175,50 @@ public static void user_data(MemorySegment struct, MemorySegment fieldValue) { struct.set(user_data$LAYOUT, user_data$OFFSET, fieldValue); } + private static final AddressLayout release_user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("release_user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final AddressLayout release_user_data$layout() { + return release_user_data$LAYOUT; + } + + private static final long release_user_data$OFFSET = $LAYOUT.byteOffset(groupElement("release_user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final long release_user_data$offset() { + return release_user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static MemorySegment release_user_data(MemorySegment struct) { + return struct.get(release_user_data$LAYOUT, release_user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static void release_user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(release_user_data$LAYOUT, release_user_data$OFFSET, fieldValue); + } + /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_log_callback_release.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_log_callback_release.java new file mode 100644 index 000000000..e2ff892b5 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_log_callback_release.java @@ -0,0 +1,68 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_log_callback_release)(void *) + * } + */ +public final class mln_log_callback_release { + + private mln_log_callback_release() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_log_callback_release.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_log_callback_release.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data) { + try { + DOWN$MH.invokeExact(funcPtr, user_data); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_logical_extent.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_logical_extent.java new file mode 100644 index 000000000..a7bc4ccf7 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_logical_extent.java @@ -0,0 +1,218 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_logical_extent { + * uint32_t width; + * uint32_t height; + * double scale_factor; + * } + * } + */ +public class mln_logical_extent { + + mln_logical_extent() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("width"), + MapLibreNativeC.C_INT.withName("height"), + MapLibreNativeC.C_DOUBLE.withName("scale_factor") + ).withName("mln_logical_extent"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt width$LAYOUT = (OfInt)$LAYOUT.select(groupElement("width")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t width + * } + */ + public static final OfInt width$layout() { + return width$LAYOUT; + } + + private static final long width$OFFSET = $LAYOUT.byteOffset(groupElement("width")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t width + * } + */ + public static final long width$offset() { + return width$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t width + * } + */ + public static int width(MemorySegment struct) { + return struct.get(width$LAYOUT, width$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t width + * } + */ + public static void width(MemorySegment struct, int fieldValue) { + struct.set(width$LAYOUT, width$OFFSET, fieldValue); + } + + private static final OfInt height$LAYOUT = (OfInt)$LAYOUT.select(groupElement("height")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t height + * } + */ + public static final OfInt height$layout() { + return height$LAYOUT; + } + + private static final long height$OFFSET = $LAYOUT.byteOffset(groupElement("height")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t height + * } + */ + public static final long height$offset() { + return height$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t height + * } + */ + public static int height(MemorySegment struct) { + return struct.get(height$LAYOUT, height$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t height + * } + */ + public static void height(MemorySegment struct, int fieldValue) { + struct.set(height$LAYOUT, height$OFFSET, fieldValue); + } + + private static final OfDouble scale_factor$LAYOUT = (OfDouble)$LAYOUT.select(groupElement("scale_factor")); + + /** + * Layout for field: + * {@snippet lang=c : + * double scale_factor + * } + */ + public static final OfDouble scale_factor$layout() { + return scale_factor$LAYOUT; + } + + private static final long scale_factor$OFFSET = $LAYOUT.byteOffset(groupElement("scale_factor")); + + /** + * Offset for field: + * {@snippet lang=c : + * double scale_factor + * } + */ + public static final long scale_factor$offset() { + return scale_factor$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * double scale_factor + * } + */ + public static double scale_factor(MemorySegment struct) { + return struct.get(scale_factor$LAYOUT, scale_factor$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * double scale_factor + * } + */ + public static void scale_factor(MemorySegment struct, double fieldValue) { + struct.set(scale_factor$LAYOUT, scale_factor$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_options.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_options.java index 307f59e17..f9b24c28e 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_options.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_options.java @@ -16,9 +16,7 @@ * {@snippet lang=c : * struct mln_map_options { * uint32_t size; - * uint32_t width; - * uint32_t height; - * double scale_factor; + * mln_logical_extent initial_extent; * uint32_t map_mode; * bool fast_pfor_enabled; * uint64_t event_mask; @@ -33,10 +31,8 @@ public class mln_map_options { private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( MapLibreNativeC.C_INT.withName("size"), - MapLibreNativeC.C_INT.withName("width"), - MapLibreNativeC.C_INT.withName("height"), MemoryLayout.paddingLayout(4), - MapLibreNativeC.C_DOUBLE.withName("scale_factor"), + mln_logical_extent.layout().withName("initial_extent"), MapLibreNativeC.C_INT.withName("map_mode"), MapLibreNativeC.C_BOOL.withName("fast_pfor_enabled"), MemoryLayout.paddingLayout(3), @@ -94,136 +90,48 @@ public static void size(MemorySegment struct, int fieldValue) { struct.set(size$LAYOUT, size$OFFSET, fieldValue); } - private static final OfInt width$LAYOUT = (OfInt)$LAYOUT.select(groupElement("width")); + private static final GroupLayout initial_extent$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("initial_extent")); /** * Layout for field: * {@snippet lang=c : - * uint32_t width + * mln_logical_extent initial_extent * } */ - public static final OfInt width$layout() { - return width$LAYOUT; + public static final GroupLayout initial_extent$layout() { + return initial_extent$LAYOUT; } - private static final long width$OFFSET = $LAYOUT.byteOffset(groupElement("width")); + private static final long initial_extent$OFFSET = $LAYOUT.byteOffset(groupElement("initial_extent")); /** * Offset for field: * {@snippet lang=c : - * uint32_t width + * mln_logical_extent initial_extent * } */ - public static final long width$offset() { - return width$OFFSET; + public static final long initial_extent$offset() { + return initial_extent$OFFSET; } /** * Getter for field: * {@snippet lang=c : - * uint32_t width + * mln_logical_extent initial_extent * } */ - public static int width(MemorySegment struct) { - return struct.get(width$LAYOUT, width$OFFSET); + public static MemorySegment initial_extent(MemorySegment struct) { + return struct.asSlice(initial_extent$OFFSET, initial_extent$LAYOUT.byteSize()); } /** * Setter for field: * {@snippet lang=c : - * uint32_t width + * mln_logical_extent initial_extent * } */ - public static void width(MemorySegment struct, int fieldValue) { - struct.set(width$LAYOUT, width$OFFSET, fieldValue); - } - - private static final OfInt height$LAYOUT = (OfInt)$LAYOUT.select(groupElement("height")); - - /** - * Layout for field: - * {@snippet lang=c : - * uint32_t height - * } - */ - public static final OfInt height$layout() { - return height$LAYOUT; - } - - private static final long height$OFFSET = $LAYOUT.byteOffset(groupElement("height")); - - /** - * Offset for field: - * {@snippet lang=c : - * uint32_t height - * } - */ - public static final long height$offset() { - return height$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * uint32_t height - * } - */ - public static int height(MemorySegment struct) { - return struct.get(height$LAYOUT, height$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * uint32_t height - * } - */ - public static void height(MemorySegment struct, int fieldValue) { - struct.set(height$LAYOUT, height$OFFSET, fieldValue); - } - - private static final OfDouble scale_factor$LAYOUT = (OfDouble)$LAYOUT.select(groupElement("scale_factor")); - - /** - * Layout for field: - * {@snippet lang=c : - * double scale_factor - * } - */ - public static final OfDouble scale_factor$layout() { - return scale_factor$LAYOUT; - } - - private static final long scale_factor$OFFSET = $LAYOUT.byteOffset(groupElement("scale_factor")); - - /** - * Offset for field: - * {@snippet lang=c : - * double scale_factor - * } - */ - public static final long scale_factor$offset() { - return scale_factor$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * double scale_factor - * } - */ - public static double scale_factor(MemorySegment struct) { - return struct.get(scale_factor$LAYOUT, scale_factor$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * double scale_factor - * } - */ - public static void scale_factor(MemorySegment struct, double fieldValue) { - struct.set(scale_factor$LAYOUT, scale_factor$OFFSET, fieldValue); + public static void initial_extent(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, initial_extent$OFFSET, initial_extent$LAYOUT.byteSize()); } private static final OfInt map_mode$LAYOUT = (OfInt)$LAYOUT.select(groupElement("map_mode")); diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_snapshot.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_snapshot.java new file mode 100644 index 000000000..d772aaef2 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_map_snapshot.java @@ -0,0 +1,817 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_map_snapshot { + * uint32_t size; + * uint32_t debug_options; + * uint64_t generation; + * mln_camera_options camera; + * mln_logical_extent logical_extent; + * mln_projection_mode projection_mode; + * mln_map_viewport_options viewport; + * bool fully_loaded; + * bool rendering_stats_view_enabled; + * bool repaint_demand; + * uint8_t reserved_flags; + * uint64_t event_mask; + * uint64_t latest_render_update_generation; + * mln_map_tile_options tile; + * mln_bound_options bounds; + * mln_free_camera_options free_camera; + * } + * } + */ +public class mln_map_snapshot { + + mln_map_snapshot() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("debug_options"), + MapLibreNativeC.C_LONG.withName("generation"), + mln_camera_options.layout().withName("camera"), + mln_logical_extent.layout().withName("logical_extent"), + mln_projection_mode.layout().withName("projection_mode"), + mln_map_viewport_options.layout().withName("viewport"), + MapLibreNativeC.C_BOOL.withName("fully_loaded"), + MapLibreNativeC.C_BOOL.withName("rendering_stats_view_enabled"), + MapLibreNativeC.C_BOOL.withName("repaint_demand"), + MapLibreNativeC.C_CHAR.withName("reserved_flags"), + MemoryLayout.paddingLayout(4), + MapLibreNativeC.C_LONG.withName("event_mask"), + MapLibreNativeC.C_LONG.withName("latest_render_update_generation"), + mln_map_tile_options.layout().withName("tile"), + mln_bound_options.layout().withName("bounds"), + mln_free_camera_options.layout().withName("free_camera") + ).withName("mln_map_snapshot"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt debug_options$LAYOUT = (OfInt)$LAYOUT.select(groupElement("debug_options")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t debug_options + * } + */ + public static final OfInt debug_options$layout() { + return debug_options$LAYOUT; + } + + private static final long debug_options$OFFSET = $LAYOUT.byteOffset(groupElement("debug_options")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t debug_options + * } + */ + public static final long debug_options$offset() { + return debug_options$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t debug_options + * } + */ + public static int debug_options(MemorySegment struct) { + return struct.get(debug_options$LAYOUT, debug_options$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t debug_options + * } + */ + public static void debug_options(MemorySegment struct, int fieldValue) { + struct.set(debug_options$LAYOUT, debug_options$OFFSET, fieldValue); + } + + private static final OfLong generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final OfLong generation$layout() { + return generation$LAYOUT; + } + + private static final long generation$OFFSET = $LAYOUT.byteOffset(groupElement("generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final long generation$offset() { + return generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static long generation(MemorySegment struct) { + return struct.get(generation$LAYOUT, generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static void generation(MemorySegment struct, long fieldValue) { + struct.set(generation$LAYOUT, generation$OFFSET, fieldValue); + } + + private static final GroupLayout camera$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("camera")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final GroupLayout camera$layout() { + return camera$LAYOUT; + } + + private static final long camera$OFFSET = $LAYOUT.byteOffset(groupElement("camera")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static final long camera$offset() { + return camera$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static MemorySegment camera(MemorySegment struct) { + return struct.asSlice(camera$OFFSET, camera$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_camera_options camera + * } + */ + public static void camera(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, camera$OFFSET, camera$LAYOUT.byteSize()); + } + + private static final GroupLayout logical_extent$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("logical_extent")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_logical_extent logical_extent + * } + */ + public static final GroupLayout logical_extent$layout() { + return logical_extent$LAYOUT; + } + + private static final long logical_extent$OFFSET = $LAYOUT.byteOffset(groupElement("logical_extent")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_logical_extent logical_extent + * } + */ + public static final long logical_extent$offset() { + return logical_extent$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_logical_extent logical_extent + * } + */ + public static MemorySegment logical_extent(MemorySegment struct) { + return struct.asSlice(logical_extent$OFFSET, logical_extent$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_logical_extent logical_extent + * } + */ + public static void logical_extent(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, logical_extent$OFFSET, logical_extent$LAYOUT.byteSize()); + } + + private static final GroupLayout projection_mode$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("projection_mode")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_projection_mode projection_mode + * } + */ + public static final GroupLayout projection_mode$layout() { + return projection_mode$LAYOUT; + } + + private static final long projection_mode$OFFSET = $LAYOUT.byteOffset(groupElement("projection_mode")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_projection_mode projection_mode + * } + */ + public static final long projection_mode$offset() { + return projection_mode$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_projection_mode projection_mode + * } + */ + public static MemorySegment projection_mode(MemorySegment struct) { + return struct.asSlice(projection_mode$OFFSET, projection_mode$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_projection_mode projection_mode + * } + */ + public static void projection_mode(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, projection_mode$OFFSET, projection_mode$LAYOUT.byteSize()); + } + + private static final GroupLayout viewport$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("viewport")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_map_viewport_options viewport + * } + */ + public static final GroupLayout viewport$layout() { + return viewport$LAYOUT; + } + + private static final long viewport$OFFSET = $LAYOUT.byteOffset(groupElement("viewport")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_map_viewport_options viewport + * } + */ + public static final long viewport$offset() { + return viewport$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_map_viewport_options viewport + * } + */ + public static MemorySegment viewport(MemorySegment struct) { + return struct.asSlice(viewport$OFFSET, viewport$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_map_viewport_options viewport + * } + */ + public static void viewport(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, viewport$OFFSET, viewport$LAYOUT.byteSize()); + } + + private static final OfBoolean fully_loaded$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("fully_loaded")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool fully_loaded + * } + */ + public static final OfBoolean fully_loaded$layout() { + return fully_loaded$LAYOUT; + } + + private static final long fully_loaded$OFFSET = $LAYOUT.byteOffset(groupElement("fully_loaded")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool fully_loaded + * } + */ + public static final long fully_loaded$offset() { + return fully_loaded$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool fully_loaded + * } + */ + public static boolean fully_loaded(MemorySegment struct) { + return struct.get(fully_loaded$LAYOUT, fully_loaded$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool fully_loaded + * } + */ + public static void fully_loaded(MemorySegment struct, boolean fieldValue) { + struct.set(fully_loaded$LAYOUT, fully_loaded$OFFSET, fieldValue); + } + + private static final OfBoolean rendering_stats_view_enabled$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("rendering_stats_view_enabled")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool rendering_stats_view_enabled + * } + */ + public static final OfBoolean rendering_stats_view_enabled$layout() { + return rendering_stats_view_enabled$LAYOUT; + } + + private static final long rendering_stats_view_enabled$OFFSET = $LAYOUT.byteOffset(groupElement("rendering_stats_view_enabled")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool rendering_stats_view_enabled + * } + */ + public static final long rendering_stats_view_enabled$offset() { + return rendering_stats_view_enabled$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool rendering_stats_view_enabled + * } + */ + public static boolean rendering_stats_view_enabled(MemorySegment struct) { + return struct.get(rendering_stats_view_enabled$LAYOUT, rendering_stats_view_enabled$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool rendering_stats_view_enabled + * } + */ + public static void rendering_stats_view_enabled(MemorySegment struct, boolean fieldValue) { + struct.set(rendering_stats_view_enabled$LAYOUT, rendering_stats_view_enabled$OFFSET, fieldValue); + } + + private static final OfBoolean repaint_demand$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("repaint_demand")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool repaint_demand + * } + */ + public static final OfBoolean repaint_demand$layout() { + return repaint_demand$LAYOUT; + } + + private static final long repaint_demand$OFFSET = $LAYOUT.byteOffset(groupElement("repaint_demand")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool repaint_demand + * } + */ + public static final long repaint_demand$offset() { + return repaint_demand$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool repaint_demand + * } + */ + public static boolean repaint_demand(MemorySegment struct) { + return struct.get(repaint_demand$LAYOUT, repaint_demand$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool repaint_demand + * } + */ + public static void repaint_demand(MemorySegment struct, boolean fieldValue) { + struct.set(repaint_demand$LAYOUT, repaint_demand$OFFSET, fieldValue); + } + + private static final OfByte reserved_flags$LAYOUT = (OfByte)$LAYOUT.select(groupElement("reserved_flags")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint8_t reserved_flags + * } + */ + public static final OfByte reserved_flags$layout() { + return reserved_flags$LAYOUT; + } + + private static final long reserved_flags$OFFSET = $LAYOUT.byteOffset(groupElement("reserved_flags")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint8_t reserved_flags + * } + */ + public static final long reserved_flags$offset() { + return reserved_flags$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint8_t reserved_flags + * } + */ + public static byte reserved_flags(MemorySegment struct) { + return struct.get(reserved_flags$LAYOUT, reserved_flags$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint8_t reserved_flags + * } + */ + public static void reserved_flags(MemorySegment struct, byte fieldValue) { + struct.set(reserved_flags$LAYOUT, reserved_flags$OFFSET, fieldValue); + } + + private static final OfLong event_mask$LAYOUT = (OfLong)$LAYOUT.select(groupElement("event_mask")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t event_mask + * } + */ + public static final OfLong event_mask$layout() { + return event_mask$LAYOUT; + } + + private static final long event_mask$OFFSET = $LAYOUT.byteOffset(groupElement("event_mask")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t event_mask + * } + */ + public static final long event_mask$offset() { + return event_mask$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t event_mask + * } + */ + public static long event_mask(MemorySegment struct) { + return struct.get(event_mask$LAYOUT, event_mask$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t event_mask + * } + */ + public static void event_mask(MemorySegment struct, long fieldValue) { + struct.set(event_mask$LAYOUT, event_mask$OFFSET, fieldValue); + } + + private static final OfLong latest_render_update_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("latest_render_update_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t latest_render_update_generation + * } + */ + public static final OfLong latest_render_update_generation$layout() { + return latest_render_update_generation$LAYOUT; + } + + private static final long latest_render_update_generation$OFFSET = $LAYOUT.byteOffset(groupElement("latest_render_update_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t latest_render_update_generation + * } + */ + public static final long latest_render_update_generation$offset() { + return latest_render_update_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t latest_render_update_generation + * } + */ + public static long latest_render_update_generation(MemorySegment struct) { + return struct.get(latest_render_update_generation$LAYOUT, latest_render_update_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t latest_render_update_generation + * } + */ + public static void latest_render_update_generation(MemorySegment struct, long fieldValue) { + struct.set(latest_render_update_generation$LAYOUT, latest_render_update_generation$OFFSET, fieldValue); + } + + private static final GroupLayout tile$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("tile")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_map_tile_options tile + * } + */ + public static final GroupLayout tile$layout() { + return tile$LAYOUT; + } + + private static final long tile$OFFSET = $LAYOUT.byteOffset(groupElement("tile")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_map_tile_options tile + * } + */ + public static final long tile$offset() { + return tile$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_map_tile_options tile + * } + */ + public static MemorySegment tile(MemorySegment struct) { + return struct.asSlice(tile$OFFSET, tile$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_map_tile_options tile + * } + */ + public static void tile(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, tile$OFFSET, tile$LAYOUT.byteSize()); + } + + private static final GroupLayout bounds$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("bounds")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_bound_options bounds + * } + */ + public static final GroupLayout bounds$layout() { + return bounds$LAYOUT; + } + + private static final long bounds$OFFSET = $LAYOUT.byteOffset(groupElement("bounds")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_bound_options bounds + * } + */ + public static final long bounds$offset() { + return bounds$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_bound_options bounds + * } + */ + public static MemorySegment bounds(MemorySegment struct) { + return struct.asSlice(bounds$OFFSET, bounds$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_bound_options bounds + * } + */ + public static void bounds(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, bounds$OFFSET, bounds$LAYOUT.byteSize()); + } + + private static final GroupLayout free_camera$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("free_camera")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_free_camera_options free_camera + * } + */ + public static final GroupLayout free_camera$layout() { + return free_camera$LAYOUT; + } + + private static final long free_camera$OFFSET = $LAYOUT.byteOffset(groupElement("free_camera")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_free_camera_options free_camera + * } + */ + public static final long free_camera$offset() { + return free_camera$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_free_camera_options free_camera + * } + */ + public static MemorySegment free_camera(MemorySegment struct) { + return struct.asSlice(free_camera$OFFSET, free_camera$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_free_camera_options free_camera + * } + */ + public static void free_camera(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, free_camera$OFFSET, free_camera$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_offline_region_definition.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_offline_region_definition.java index 60aff96d6..ae5f94c4c 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_offline_region_definition.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_offline_region_definition.java @@ -148,7 +148,7 @@ public static class data { private static final GroupLayout $LAYOUT = MemoryLayout.unionLayout( mln_offline_tile_pyramid_region_definition.layout().withName("tile_pyramid"), mln_offline_geometry_region_definition.layout().withName("geometry") - ).withName("$anon$425:3"); + ).withName("$anon$460:3"); /** * The layout of this union diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_opengl_context_descriptor.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_opengl_context_descriptor.java index 33bd0d15f..920e7737f 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_opengl_context_descriptor.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_opengl_context_descriptor.java @@ -198,7 +198,7 @@ public static class data { mln_wgl_context_descriptor.layout().withName("wgl"), mln_egl_context_descriptor.layout().withName("egl"), mln_webgl_context_descriptor.layout().withName("webgl") - ).withName("$anon$179:3"); + ).withName("$anon$287:3"); /** * The layout of this union diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_abandon_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_abandon_result.java new file mode 100644 index 000000000..685619012 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_abandon_result.java @@ -0,0 +1,264 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_render_abandon_result { + * uint32_t size; + * uint32_t disposition; + * uint32_t quarantined_resource_count; + * uint32_t reserved; + * } + * } + */ +public class mln_render_abandon_result { + + mln_render_abandon_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("disposition"), + MapLibreNativeC.C_INT.withName("quarantined_resource_count"), + MapLibreNativeC.C_INT.withName("reserved") + ).withName("mln_render_abandon_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt disposition$LAYOUT = (OfInt)$LAYOUT.select(groupElement("disposition")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final OfInt disposition$layout() { + return disposition$LAYOUT; + } + + private static final long disposition$OFFSET = $LAYOUT.byteOffset(groupElement("disposition")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final long disposition$offset() { + return disposition$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static int disposition(MemorySegment struct) { + return struct.get(disposition$LAYOUT, disposition$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static void disposition(MemorySegment struct, int fieldValue) { + struct.set(disposition$LAYOUT, disposition$OFFSET, fieldValue); + } + + private static final OfInt quarantined_resource_count$LAYOUT = (OfInt)$LAYOUT.select(groupElement("quarantined_resource_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t quarantined_resource_count + * } + */ + public static final OfInt quarantined_resource_count$layout() { + return quarantined_resource_count$LAYOUT; + } + + private static final long quarantined_resource_count$OFFSET = $LAYOUT.byteOffset(groupElement("quarantined_resource_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t quarantined_resource_count + * } + */ + public static final long quarantined_resource_count$offset() { + return quarantined_resource_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t quarantined_resource_count + * } + */ + public static int quarantined_resource_count(MemorySegment struct) { + return struct.get(quarantined_resource_count$LAYOUT, quarantined_resource_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t quarantined_resource_count + * } + */ + public static void quarantined_resource_count(MemorySegment struct, int fieldValue) { + struct.set(quarantined_resource_count$LAYOUT, quarantined_resource_count$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_frame_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_frame_result.java new file mode 100644 index 000000000..a3ba2c868 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_frame_result.java @@ -0,0 +1,403 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_render_frame_result { + * uint32_t size; + * uint32_t disposition; + * uint64_t token; + * uint64_t map_update_generation; + * uint64_t extent_generation; + * uint64_t frame_generation; + * bool needs_repaint; + * } + * } + */ +public class mln_render_frame_result { + + mln_render_frame_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("disposition"), + MapLibreNativeC.C_LONG.withName("token"), + MapLibreNativeC.C_LONG.withName("map_update_generation"), + MapLibreNativeC.C_LONG.withName("extent_generation"), + MapLibreNativeC.C_LONG.withName("frame_generation"), + MapLibreNativeC.C_BOOL.withName("needs_repaint"), + MemoryLayout.paddingLayout(7) + ).withName("mln_render_frame_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt disposition$LAYOUT = (OfInt)$LAYOUT.select(groupElement("disposition")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final OfInt disposition$layout() { + return disposition$LAYOUT; + } + + private static final long disposition$OFFSET = $LAYOUT.byteOffset(groupElement("disposition")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static final long disposition$offset() { + return disposition$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static int disposition(MemorySegment struct) { + return struct.get(disposition$LAYOUT, disposition$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t disposition + * } + */ + public static void disposition(MemorySegment struct, int fieldValue) { + struct.set(disposition$LAYOUT, disposition$OFFSET, fieldValue); + } + + private static final OfLong token$LAYOUT = (OfLong)$LAYOUT.select(groupElement("token")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static final OfLong token$layout() { + return token$LAYOUT; + } + + private static final long token$OFFSET = $LAYOUT.byteOffset(groupElement("token")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static final long token$offset() { + return token$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static long token(MemorySegment struct) { + return struct.get(token$LAYOUT, token$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t token + * } + */ + public static void token(MemorySegment struct, long fieldValue) { + struct.set(token$LAYOUT, token$OFFSET, fieldValue); + } + + private static final OfLong map_update_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("map_update_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static final OfLong map_update_generation$layout() { + return map_update_generation$LAYOUT; + } + + private static final long map_update_generation$OFFSET = $LAYOUT.byteOffset(groupElement("map_update_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static final long map_update_generation$offset() { + return map_update_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static long map_update_generation(MemorySegment struct) { + return struct.get(map_update_generation$LAYOUT, map_update_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static void map_update_generation(MemorySegment struct, long fieldValue) { + struct.set(map_update_generation$LAYOUT, map_update_generation$OFFSET, fieldValue); + } + + private static final OfLong extent_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("extent_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static final OfLong extent_generation$layout() { + return extent_generation$LAYOUT; + } + + private static final long extent_generation$OFFSET = $LAYOUT.byteOffset(groupElement("extent_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static final long extent_generation$offset() { + return extent_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static long extent_generation(MemorySegment struct) { + return struct.get(extent_generation$LAYOUT, extent_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static void extent_generation(MemorySegment struct, long fieldValue) { + struct.set(extent_generation$LAYOUT, extent_generation$OFFSET, fieldValue); + } + + private static final OfLong frame_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("frame_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static final OfLong frame_generation$layout() { + return frame_generation$LAYOUT; + } + + private static final long frame_generation$OFFSET = $LAYOUT.byteOffset(groupElement("frame_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static final long frame_generation$offset() { + return frame_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static long frame_generation(MemorySegment struct) { + return struct.get(frame_generation$LAYOUT, frame_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static void frame_generation(MemorySegment struct, long fieldValue) { + struct.set(frame_generation$LAYOUT, frame_generation$OFFSET, fieldValue); + } + + private static final OfBoolean needs_repaint$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("needs_repaint")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool needs_repaint + * } + */ + public static final OfBoolean needs_repaint$layout() { + return needs_repaint$LAYOUT; + } + + private static final long needs_repaint$OFFSET = $LAYOUT.byteOffset(groupElement("needs_repaint")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool needs_repaint + * } + */ + public static final long needs_repaint$offset() { + return needs_repaint$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool needs_repaint + * } + */ + public static boolean needs_repaint(MemorySegment struct) { + return struct.get(needs_repaint$LAYOUT, needs_repaint$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool needs_repaint + * } + */ + public static void needs_repaint(MemorySegment struct, boolean fieldValue) { + struct.set(needs_repaint$LAYOUT, needs_repaint$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_attach_options.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_attach_options.java new file mode 100644 index 000000000..c7ba165e4 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_attach_options.java @@ -0,0 +1,356 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_render_session_attach_options { + * uint32_t size; + * uint32_t driver; + * uint32_t requested_texture_ring_depth; + * uint32_t reserved; + * mln_wake frame_wake; + * mln_wake driver_work_wake; + * } + * } + */ +public class mln_render_session_attach_options { + + mln_render_session_attach_options() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("driver"), + MapLibreNativeC.C_INT.withName("requested_texture_ring_depth"), + MapLibreNativeC.C_INT.withName("reserved"), + mln_wake.layout().withName("frame_wake"), + mln_wake.layout().withName("driver_work_wake") + ).withName("mln_render_session_attach_options"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt driver$LAYOUT = (OfInt)$LAYOUT.select(groupElement("driver")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final OfInt driver$layout() { + return driver$LAYOUT; + } + + private static final long driver$OFFSET = $LAYOUT.byteOffset(groupElement("driver")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final long driver$offset() { + return driver$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static int driver(MemorySegment struct) { + return struct.get(driver$LAYOUT, driver$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static void driver(MemorySegment struct, int fieldValue) { + struct.set(driver$LAYOUT, driver$OFFSET, fieldValue); + } + + private static final OfInt requested_texture_ring_depth$LAYOUT = (OfInt)$LAYOUT.select(groupElement("requested_texture_ring_depth")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t requested_texture_ring_depth + * } + */ + public static final OfInt requested_texture_ring_depth$layout() { + return requested_texture_ring_depth$LAYOUT; + } + + private static final long requested_texture_ring_depth$OFFSET = $LAYOUT.byteOffset(groupElement("requested_texture_ring_depth")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t requested_texture_ring_depth + * } + */ + public static final long requested_texture_ring_depth$offset() { + return requested_texture_ring_depth$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t requested_texture_ring_depth + * } + */ + public static int requested_texture_ring_depth(MemorySegment struct) { + return struct.get(requested_texture_ring_depth$LAYOUT, requested_texture_ring_depth$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t requested_texture_ring_depth + * } + */ + public static void requested_texture_ring_depth(MemorySegment struct, int fieldValue) { + struct.set(requested_texture_ring_depth$LAYOUT, requested_texture_ring_depth$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final GroupLayout frame_wake$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("frame_wake")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_wake frame_wake + * } + */ + public static final GroupLayout frame_wake$layout() { + return frame_wake$LAYOUT; + } + + private static final long frame_wake$OFFSET = $LAYOUT.byteOffset(groupElement("frame_wake")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_wake frame_wake + * } + */ + public static final long frame_wake$offset() { + return frame_wake$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_wake frame_wake + * } + */ + public static MemorySegment frame_wake(MemorySegment struct) { + return struct.asSlice(frame_wake$OFFSET, frame_wake$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_wake frame_wake + * } + */ + public static void frame_wake(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, frame_wake$OFFSET, frame_wake$LAYOUT.byteSize()); + } + + private static final GroupLayout driver_work_wake$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("driver_work_wake")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_wake driver_work_wake + * } + */ + public static final GroupLayout driver_work_wake$layout() { + return driver_work_wake$LAYOUT; + } + + private static final long driver_work_wake$OFFSET = $LAYOUT.byteOffset(groupElement("driver_work_wake")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_wake driver_work_wake + * } + */ + public static final long driver_work_wake$offset() { + return driver_work_wake$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_wake driver_work_wake + * } + */ + public static MemorySegment driver_work_wake(MemorySegment struct) { + return struct.asSlice(driver_work_wake$OFFSET, driver_work_wake$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_wake driver_work_wake + * } + */ + public static void driver_work_wake(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, driver_work_wake$OFFSET, driver_work_wake$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_capabilities.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_capabilities.java new file mode 100644 index 000000000..83ab81bb9 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_capabilities.java @@ -0,0 +1,264 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_render_session_capabilities { + * uint32_t size; + * uint32_t driver; + * uint32_t texture_ring_depth; + * uint32_t flags; + * } + * } + */ +public class mln_render_session_capabilities { + + mln_render_session_capabilities() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("driver"), + MapLibreNativeC.C_INT.withName("texture_ring_depth"), + MapLibreNativeC.C_INT.withName("flags") + ).withName("mln_render_session_capabilities"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt driver$LAYOUT = (OfInt)$LAYOUT.select(groupElement("driver")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final OfInt driver$layout() { + return driver$LAYOUT; + } + + private static final long driver$OFFSET = $LAYOUT.byteOffset(groupElement("driver")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final long driver$offset() { + return driver$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static int driver(MemorySegment struct) { + return struct.get(driver$LAYOUT, driver$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static void driver(MemorySegment struct, int fieldValue) { + struct.set(driver$LAYOUT, driver$OFFSET, fieldValue); + } + + private static final OfInt texture_ring_depth$LAYOUT = (OfInt)$LAYOUT.select(groupElement("texture_ring_depth")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t texture_ring_depth + * } + */ + public static final OfInt texture_ring_depth$layout() { + return texture_ring_depth$LAYOUT; + } + + private static final long texture_ring_depth$OFFSET = $LAYOUT.byteOffset(groupElement("texture_ring_depth")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t texture_ring_depth + * } + */ + public static final long texture_ring_depth$offset() { + return texture_ring_depth$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t texture_ring_depth + * } + */ + public static int texture_ring_depth(MemorySegment struct) { + return struct.get(texture_ring_depth$LAYOUT, texture_ring_depth$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t texture_ring_depth + * } + */ + public static void texture_ring_depth(MemorySegment struct, int fieldValue) { + struct.set(texture_ring_depth$LAYOUT, texture_ring_depth$OFFSET, fieldValue); + } + + private static final OfInt flags$LAYOUT = (OfInt)$LAYOUT.select(groupElement("flags")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static final OfInt flags$layout() { + return flags$LAYOUT; + } + + private static final long flags$OFFSET = $LAYOUT.byteOffset(groupElement("flags")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static final long flags$offset() { + return flags$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static int flags(MemorySegment struct) { + return struct.get(flags$LAYOUT, flags$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t flags + * } + */ + public static void flags(MemorySegment struct, int fieldValue) { + struct.set(flags$LAYOUT, flags$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_snapshot.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_snapshot.java new file mode 100644 index 000000000..2dc6bef18 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_render_session_snapshot.java @@ -0,0 +1,771 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_render_session_snapshot { + * uint32_t size; + * uint32_t state; + * uint32_t driver; + * uint32_t latest_result; + * mln_render_target_extent extent; + * uint64_t generation; + * uint64_t map_update_generation; + * uint64_t rendered_update_generation; + * uint64_t extent_generation; + * uint64_t frame_generation; + * uint64_t latest_demand_token; + * uint32_t pending_demand_count; + * uint32_t acquired_frame_count; + * bool target_ready; + * bool pending_changes; + * } + * } + */ +public class mln_render_session_snapshot { + + mln_render_session_snapshot() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("state"), + MapLibreNativeC.C_INT.withName("driver"), + MapLibreNativeC.C_INT.withName("latest_result"), + mln_render_target_extent.layout().withName("extent"), + MapLibreNativeC.C_LONG.withName("generation"), + MapLibreNativeC.C_LONG.withName("map_update_generation"), + MapLibreNativeC.C_LONG.withName("rendered_update_generation"), + MapLibreNativeC.C_LONG.withName("extent_generation"), + MapLibreNativeC.C_LONG.withName("frame_generation"), + MapLibreNativeC.C_LONG.withName("latest_demand_token"), + MapLibreNativeC.C_INT.withName("pending_demand_count"), + MapLibreNativeC.C_INT.withName("acquired_frame_count"), + MapLibreNativeC.C_BOOL.withName("target_ready"), + MapLibreNativeC.C_BOOL.withName("pending_changes"), + MemoryLayout.paddingLayout(6) + ).withName("mln_render_session_snapshot"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt state$LAYOUT = (OfInt)$LAYOUT.select(groupElement("state")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t state + * } + */ + public static final OfInt state$layout() { + return state$LAYOUT; + } + + private static final long state$OFFSET = $LAYOUT.byteOffset(groupElement("state")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t state + * } + */ + public static final long state$offset() { + return state$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t state + * } + */ + public static int state(MemorySegment struct) { + return struct.get(state$LAYOUT, state$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t state + * } + */ + public static void state(MemorySegment struct, int fieldValue) { + struct.set(state$LAYOUT, state$OFFSET, fieldValue); + } + + private static final OfInt driver$LAYOUT = (OfInt)$LAYOUT.select(groupElement("driver")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final OfInt driver$layout() { + return driver$LAYOUT; + } + + private static final long driver$OFFSET = $LAYOUT.byteOffset(groupElement("driver")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static final long driver$offset() { + return driver$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static int driver(MemorySegment struct) { + return struct.get(driver$LAYOUT, driver$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t driver + * } + */ + public static void driver(MemorySegment struct, int fieldValue) { + struct.set(driver$LAYOUT, driver$OFFSET, fieldValue); + } + + private static final OfInt latest_result$LAYOUT = (OfInt)$LAYOUT.select(groupElement("latest_result")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t latest_result + * } + */ + public static final OfInt latest_result$layout() { + return latest_result$LAYOUT; + } + + private static final long latest_result$OFFSET = $LAYOUT.byteOffset(groupElement("latest_result")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t latest_result + * } + */ + public static final long latest_result$offset() { + return latest_result$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t latest_result + * } + */ + public static int latest_result(MemorySegment struct) { + return struct.get(latest_result$LAYOUT, latest_result$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t latest_result + * } + */ + public static void latest_result(MemorySegment struct, int fieldValue) { + struct.set(latest_result$LAYOUT, latest_result$OFFSET, fieldValue); + } + + private static final GroupLayout extent$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("extent")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_render_target_extent extent + * } + */ + public static final GroupLayout extent$layout() { + return extent$LAYOUT; + } + + private static final long extent$OFFSET = $LAYOUT.byteOffset(groupElement("extent")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_render_target_extent extent + * } + */ + public static final long extent$offset() { + return extent$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_render_target_extent extent + * } + */ + public static MemorySegment extent(MemorySegment struct) { + return struct.asSlice(extent$OFFSET, extent$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_render_target_extent extent + * } + */ + public static void extent(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, extent$OFFSET, extent$LAYOUT.byteSize()); + } + + private static final OfLong generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final OfLong generation$layout() { + return generation$LAYOUT; + } + + private static final long generation$OFFSET = $LAYOUT.byteOffset(groupElement("generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static final long generation$offset() { + return generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static long generation(MemorySegment struct) { + return struct.get(generation$LAYOUT, generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t generation + * } + */ + public static void generation(MemorySegment struct, long fieldValue) { + struct.set(generation$LAYOUT, generation$OFFSET, fieldValue); + } + + private static final OfLong map_update_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("map_update_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static final OfLong map_update_generation$layout() { + return map_update_generation$LAYOUT; + } + + private static final long map_update_generation$OFFSET = $LAYOUT.byteOffset(groupElement("map_update_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static final long map_update_generation$offset() { + return map_update_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static long map_update_generation(MemorySegment struct) { + return struct.get(map_update_generation$LAYOUT, map_update_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t map_update_generation + * } + */ + public static void map_update_generation(MemorySegment struct, long fieldValue) { + struct.set(map_update_generation$LAYOUT, map_update_generation$OFFSET, fieldValue); + } + + private static final OfLong rendered_update_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("rendered_update_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t rendered_update_generation + * } + */ + public static final OfLong rendered_update_generation$layout() { + return rendered_update_generation$LAYOUT; + } + + private static final long rendered_update_generation$OFFSET = $LAYOUT.byteOffset(groupElement("rendered_update_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t rendered_update_generation + * } + */ + public static final long rendered_update_generation$offset() { + return rendered_update_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t rendered_update_generation + * } + */ + public static long rendered_update_generation(MemorySegment struct) { + return struct.get(rendered_update_generation$LAYOUT, rendered_update_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t rendered_update_generation + * } + */ + public static void rendered_update_generation(MemorySegment struct, long fieldValue) { + struct.set(rendered_update_generation$LAYOUT, rendered_update_generation$OFFSET, fieldValue); + } + + private static final OfLong extent_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("extent_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static final OfLong extent_generation$layout() { + return extent_generation$LAYOUT; + } + + private static final long extent_generation$OFFSET = $LAYOUT.byteOffset(groupElement("extent_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static final long extent_generation$offset() { + return extent_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static long extent_generation(MemorySegment struct) { + return struct.get(extent_generation$LAYOUT, extent_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t extent_generation + * } + */ + public static void extent_generation(MemorySegment struct, long fieldValue) { + struct.set(extent_generation$LAYOUT, extent_generation$OFFSET, fieldValue); + } + + private static final OfLong frame_generation$LAYOUT = (OfLong)$LAYOUT.select(groupElement("frame_generation")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static final OfLong frame_generation$layout() { + return frame_generation$LAYOUT; + } + + private static final long frame_generation$OFFSET = $LAYOUT.byteOffset(groupElement("frame_generation")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static final long frame_generation$offset() { + return frame_generation$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static long frame_generation(MemorySegment struct) { + return struct.get(frame_generation$LAYOUT, frame_generation$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t frame_generation + * } + */ + public static void frame_generation(MemorySegment struct, long fieldValue) { + struct.set(frame_generation$LAYOUT, frame_generation$OFFSET, fieldValue); + } + + private static final OfLong latest_demand_token$LAYOUT = (OfLong)$LAYOUT.select(groupElement("latest_demand_token")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint64_t latest_demand_token + * } + */ + public static final OfLong latest_demand_token$layout() { + return latest_demand_token$LAYOUT; + } + + private static final long latest_demand_token$OFFSET = $LAYOUT.byteOffset(groupElement("latest_demand_token")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint64_t latest_demand_token + * } + */ + public static final long latest_demand_token$offset() { + return latest_demand_token$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint64_t latest_demand_token + * } + */ + public static long latest_demand_token(MemorySegment struct) { + return struct.get(latest_demand_token$LAYOUT, latest_demand_token$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint64_t latest_demand_token + * } + */ + public static void latest_demand_token(MemorySegment struct, long fieldValue) { + struct.set(latest_demand_token$LAYOUT, latest_demand_token$OFFSET, fieldValue); + } + + private static final OfInt pending_demand_count$LAYOUT = (OfInt)$LAYOUT.select(groupElement("pending_demand_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t pending_demand_count + * } + */ + public static final OfInt pending_demand_count$layout() { + return pending_demand_count$LAYOUT; + } + + private static final long pending_demand_count$OFFSET = $LAYOUT.byteOffset(groupElement("pending_demand_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t pending_demand_count + * } + */ + public static final long pending_demand_count$offset() { + return pending_demand_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t pending_demand_count + * } + */ + public static int pending_demand_count(MemorySegment struct) { + return struct.get(pending_demand_count$LAYOUT, pending_demand_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t pending_demand_count + * } + */ + public static void pending_demand_count(MemorySegment struct, int fieldValue) { + struct.set(pending_demand_count$LAYOUT, pending_demand_count$OFFSET, fieldValue); + } + + private static final OfInt acquired_frame_count$LAYOUT = (OfInt)$LAYOUT.select(groupElement("acquired_frame_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t acquired_frame_count + * } + */ + public static final OfInt acquired_frame_count$layout() { + return acquired_frame_count$LAYOUT; + } + + private static final long acquired_frame_count$OFFSET = $LAYOUT.byteOffset(groupElement("acquired_frame_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t acquired_frame_count + * } + */ + public static final long acquired_frame_count$offset() { + return acquired_frame_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t acquired_frame_count + * } + */ + public static int acquired_frame_count(MemorySegment struct) { + return struct.get(acquired_frame_count$LAYOUT, acquired_frame_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t acquired_frame_count + * } + */ + public static void acquired_frame_count(MemorySegment struct, int fieldValue) { + struct.set(acquired_frame_count$LAYOUT, acquired_frame_count$OFFSET, fieldValue); + } + + private static final OfBoolean target_ready$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("target_ready")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool target_ready + * } + */ + public static final OfBoolean target_ready$layout() { + return target_ready$LAYOUT; + } + + private static final long target_ready$OFFSET = $LAYOUT.byteOffset(groupElement("target_ready")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool target_ready + * } + */ + public static final long target_ready$offset() { + return target_ready$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool target_ready + * } + */ + public static boolean target_ready(MemorySegment struct) { + return struct.get(target_ready$LAYOUT, target_ready$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool target_ready + * } + */ + public static void target_ready(MemorySegment struct, boolean fieldValue) { + struct.set(target_ready$LAYOUT, target_ready$OFFSET, fieldValue); + } + + private static final OfBoolean pending_changes$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("pending_changes")); + + /** + * Layout for field: + * {@snippet lang=c : + * bool pending_changes + * } + */ + public static final OfBoolean pending_changes$layout() { + return pending_changes$LAYOUT; + } + + private static final long pending_changes$OFFSET = $LAYOUT.byteOffset(groupElement("pending_changes")); + + /** + * Offset for field: + * {@snippet lang=c : + * bool pending_changes + * } + */ + public static final long pending_changes$offset() { + return pending_changes$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * bool pending_changes + * } + */ + public static boolean pending_changes(MemorySegment struct) { + return struct.get(pending_changes$LAYOUT, pending_changes$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * bool pending_changes + * } + */ + public static void pending_changes(MemorySegment struct, boolean fieldValue) { + struct.set(pending_changes$LAYOUT, pending_changes$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_rendered_query_geometry.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_rendered_query_geometry.java index 584142c99..5f6db35cc 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_rendered_query_geometry.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_rendered_query_geometry.java @@ -151,7 +151,7 @@ public static class data { mln_screen_point.layout().withName("point"), mln_screen_box.layout().withName("box"), mln_screen_line_string.layout().withName("line_string") - ).withName("$anon$51:3"); + ).withName("$anon$52:3"); /** * The layout of this union diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_provider.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_provider.java index 9b5aa11ad..076c1ed24 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_provider.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_provider.java @@ -18,6 +18,7 @@ * uint32_t size; * mln_resource_provider_callback callback; * void *user_data; + * mln_runtime_callback_release release_user_data; * } * } */ @@ -31,7 +32,8 @@ public class mln_resource_provider { MapLibreNativeC.C_INT.withName("size"), MemoryLayout.paddingLayout(4), MapLibreNativeC.C_POINTER.withName("callback"), - MapLibreNativeC.C_POINTER.withName("user_data") + MapLibreNativeC.C_POINTER.withName("user_data"), + MapLibreNativeC.C_POINTER.withName("release_user_data") ).withName("mln_resource_provider"); /** @@ -173,6 +175,50 @@ public static void user_data(MemorySegment struct, MemorySegment fieldValue) { struct.set(user_data$LAYOUT, user_data$OFFSET, fieldValue); } + private static final AddressLayout release_user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("release_user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final AddressLayout release_user_data$layout() { + return release_user_data$LAYOUT; + } + + private static final long release_user_data$OFFSET = $LAYOUT.byteOffset(groupElement("release_user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final long release_user_data$offset() { + return release_user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static MemorySegment release_user_data(MemorySegment struct) { + return struct.get(release_user_data$LAYOUT, release_user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static void release_user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(release_user_data$LAYOUT, release_user_data$OFFSET, fieldValue); + } + /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_transform.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_transform.java index 0559d73f8..05b483ccc 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_transform.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_resource_transform.java @@ -18,6 +18,7 @@ * uint32_t size; * mln_resource_transform_callback callback; * void *user_data; + * mln_runtime_callback_release release_user_data; * } * } */ @@ -31,7 +32,8 @@ public class mln_resource_transform { MapLibreNativeC.C_INT.withName("size"), MemoryLayout.paddingLayout(4), MapLibreNativeC.C_POINTER.withName("callback"), - MapLibreNativeC.C_POINTER.withName("user_data") + MapLibreNativeC.C_POINTER.withName("user_data"), + MapLibreNativeC.C_POINTER.withName("release_user_data") ).withName("mln_resource_transform"); /** @@ -173,6 +175,50 @@ public static void user_data(MemorySegment struct, MemorySegment fieldValue) { struct.set(user_data$LAYOUT, user_data$OFFSET, fieldValue); } + private static final AddressLayout release_user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("release_user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final AddressLayout release_user_data$layout() { + return release_user_data$LAYOUT; + } + + private static final long release_user_data$OFFSET = $LAYOUT.byteOffset(groupElement("release_user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static final long release_user_data$offset() { + return release_user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static MemorySegment release_user_data(MemorySegment struct) { + return struct.get(release_user_data$LAYOUT, release_user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_runtime_callback_release release_user_data + * } + */ + public static void release_user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(release_user_data$LAYOUT, release_user_data$OFFSET, fieldValue); + } + /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_callback_release.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_callback_release.java new file mode 100644 index 000000000..17a835c63 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_callback_release.java @@ -0,0 +1,68 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_runtime_callback_release)(void *) + * } + */ +public final class mln_runtime_callback_release { + + private mln_runtime_callback_release() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_runtime_callback_release.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_runtime_callback_release.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data) { + try { + DOWN$MH.invokeExact(funcPtr, user_data); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event.java index 49a60ec0b..510881061 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event.java @@ -20,7 +20,7 @@ * uint64_t source; * int32_t code; * uint32_t payload_type; - * uint32_t message_offset; + * uint64_t message_offset; * uint32_t message_size; * mln_runtime_event_payload payload; * } @@ -38,8 +38,9 @@ public class mln_runtime_event { MapLibreNativeC.C_LONG.withName("source"), MapLibreNativeC.C_INT.withName("code"), MapLibreNativeC.C_INT.withName("payload_type"), - MapLibreNativeC.C_INT.withName("message_offset"), + MapLibreNativeC.C_LONG.withName("message_offset"), MapLibreNativeC.C_INT.withName("message_size"), + MemoryLayout.paddingLayout(4), mln_runtime_event_payload.layout().withName("payload") ).withName("mln_runtime_event"); @@ -270,15 +271,15 @@ public static void payload_type(MemorySegment struct, int fieldValue) { struct.set(payload_type$LAYOUT, payload_type$OFFSET, fieldValue); } - private static final OfInt message_offset$LAYOUT = (OfInt)$LAYOUT.select(groupElement("message_offset")); + private static final OfLong message_offset$LAYOUT = (OfLong)$LAYOUT.select(groupElement("message_offset")); /** * Layout for field: * {@snippet lang=c : - * uint32_t message_offset + * uint64_t message_offset * } */ - public static final OfInt message_offset$layout() { + public static final OfLong message_offset$layout() { return message_offset$LAYOUT; } @@ -287,7 +288,7 @@ public static void payload_type(MemorySegment struct, int fieldValue) { /** * Offset for field: * {@snippet lang=c : - * uint32_t message_offset + * uint64_t message_offset * } */ public static final long message_offset$offset() { @@ -297,20 +298,20 @@ public static void payload_type(MemorySegment struct, int fieldValue) { /** * Getter for field: * {@snippet lang=c : - * uint32_t message_offset + * uint64_t message_offset * } */ - public static int message_offset(MemorySegment struct) { + public static long message_offset(MemorySegment struct) { return struct.get(message_offset$LAYOUT, message_offset$OFFSET); } /** * Setter for field: * {@snippet lang=c : - * uint32_t message_offset + * uint64_t message_offset * } */ - public static void message_offset(MemorySegment struct, int fieldValue) { + public static void message_offset(MemorySegment struct, long fieldValue) { struct.set(message_offset$LAYOUT, message_offset$OFFSET, fieldValue); } diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch_view.java similarity index 86% rename from bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch.java rename to bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch_view.java index 77b8aec0f..69282e055 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_batch_view.java @@ -14,20 +14,19 @@ /** * {@snippet lang=c : - * struct mln_runtime_event_batch { + * struct mln_runtime_event_batch_view { * uint32_t size; * uint32_t event_size; * const mln_runtime_event *events; * size_t event_count; * const char *messages; * size_t messages_size; - * size_t remaining_count; * } * } */ -public class mln_runtime_event_batch { +public class mln_runtime_event_batch_view { - mln_runtime_event_batch() { + mln_runtime_event_batch_view() { // Should not be called directly } @@ -37,9 +36,8 @@ public class mln_runtime_event_batch { MapLibreNativeC.C_POINTER.withName("events"), MapLibreNativeC.C_LONG.withName("event_count"), MapLibreNativeC.C_POINTER.withName("messages"), - MapLibreNativeC.C_LONG.withName("messages_size"), - MapLibreNativeC.C_LONG.withName("remaining_count") - ).withName("mln_runtime_event_batch"); + MapLibreNativeC.C_LONG.withName("messages_size") + ).withName("mln_runtime_event_batch_view"); /** * The layout of this struct @@ -312,50 +310,6 @@ public static void messages_size(MemorySegment struct, long fieldValue) { struct.set(messages_size$LAYOUT, messages_size$OFFSET, fieldValue); } - private static final OfLong remaining_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("remaining_count")); - - /** - * Layout for field: - * {@snippet lang=c : - * size_t remaining_count - * } - */ - public static final OfLong remaining_count$layout() { - return remaining_count$LAYOUT; - } - - private static final long remaining_count$OFFSET = $LAYOUT.byteOffset(groupElement("remaining_count")); - - /** - * Offset for field: - * {@snippet lang=c : - * size_t remaining_count - * } - */ - public static final long remaining_count$offset() { - return remaining_count$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * size_t remaining_count - * } - */ - public static long remaining_count(MemorySegment struct) { - return struct.get(remaining_count$LAYOUT, remaining_count$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * size_t remaining_count - * } - */ - public static void remaining_count(MemorySegment struct, long fieldValue) { - struct.set(remaining_count$LAYOUT, remaining_count$OFFSET, fieldValue); - } - /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_offline_operation_completed.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_offline_operation_completed.java deleted file mode 100644 index d5a240c3a..000000000 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_offline_operation_completed.java +++ /dev/null @@ -1,311 +0,0 @@ -// Generated by jextract - -package org.maplibre.nativeffi.internal.c; - -import java.lang.invoke.*; -import java.lang.foreign.*; -import java.nio.ByteOrder; -import java.util.*; -import java.util.function.*; -import java.util.stream.*; - -import static java.lang.foreign.ValueLayout.*; -import static java.lang.foreign.MemoryLayout.PathElement.*; - -/** - * {@snippet lang=c : - * struct mln_runtime_event_offline_operation_completed { - * mln_offline_operation_id operation_id; - * uint32_t operation_kind; - * uint32_t result_kind; - * int32_t result_status; - * bool found; - * } - * } - */ -public class mln_runtime_event_offline_operation_completed { - - mln_runtime_event_offline_operation_completed() { - // Should not be called directly - } - - private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( - MapLibreNativeC.C_LONG.withName("operation_id"), - MapLibreNativeC.C_INT.withName("operation_kind"), - MapLibreNativeC.C_INT.withName("result_kind"), - MapLibreNativeC.C_INT.withName("result_status"), - MapLibreNativeC.C_BOOL.withName("found"), - MemoryLayout.paddingLayout(3) - ).withName("mln_runtime_event_offline_operation_completed"); - - /** - * The layout of this struct - */ - public static final GroupLayout layout() { - return $LAYOUT; - } - - private static final OfLong operation_id$LAYOUT = (OfLong)$LAYOUT.select(groupElement("operation_id")); - - /** - * Layout for field: - * {@snippet lang=c : - * mln_offline_operation_id operation_id - * } - */ - public static final OfLong operation_id$layout() { - return operation_id$LAYOUT; - } - - private static final long operation_id$OFFSET = $LAYOUT.byteOffset(groupElement("operation_id")); - - /** - * Offset for field: - * {@snippet lang=c : - * mln_offline_operation_id operation_id - * } - */ - public static final long operation_id$offset() { - return operation_id$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * mln_offline_operation_id operation_id - * } - */ - public static long operation_id(MemorySegment struct) { - return struct.get(operation_id$LAYOUT, operation_id$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * mln_offline_operation_id operation_id - * } - */ - public static void operation_id(MemorySegment struct, long fieldValue) { - struct.set(operation_id$LAYOUT, operation_id$OFFSET, fieldValue); - } - - private static final OfInt operation_kind$LAYOUT = (OfInt)$LAYOUT.select(groupElement("operation_kind")); - - /** - * Layout for field: - * {@snippet lang=c : - * uint32_t operation_kind - * } - */ - public static final OfInt operation_kind$layout() { - return operation_kind$LAYOUT; - } - - private static final long operation_kind$OFFSET = $LAYOUT.byteOffset(groupElement("operation_kind")); - - /** - * Offset for field: - * {@snippet lang=c : - * uint32_t operation_kind - * } - */ - public static final long operation_kind$offset() { - return operation_kind$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * uint32_t operation_kind - * } - */ - public static int operation_kind(MemorySegment struct) { - return struct.get(operation_kind$LAYOUT, operation_kind$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * uint32_t operation_kind - * } - */ - public static void operation_kind(MemorySegment struct, int fieldValue) { - struct.set(operation_kind$LAYOUT, operation_kind$OFFSET, fieldValue); - } - - private static final OfInt result_kind$LAYOUT = (OfInt)$LAYOUT.select(groupElement("result_kind")); - - /** - * Layout for field: - * {@snippet lang=c : - * uint32_t result_kind - * } - */ - public static final OfInt result_kind$layout() { - return result_kind$LAYOUT; - } - - private static final long result_kind$OFFSET = $LAYOUT.byteOffset(groupElement("result_kind")); - - /** - * Offset for field: - * {@snippet lang=c : - * uint32_t result_kind - * } - */ - public static final long result_kind$offset() { - return result_kind$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * uint32_t result_kind - * } - */ - public static int result_kind(MemorySegment struct) { - return struct.get(result_kind$LAYOUT, result_kind$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * uint32_t result_kind - * } - */ - public static void result_kind(MemorySegment struct, int fieldValue) { - struct.set(result_kind$LAYOUT, result_kind$OFFSET, fieldValue); - } - - private static final OfInt result_status$LAYOUT = (OfInt)$LAYOUT.select(groupElement("result_status")); - - /** - * Layout for field: - * {@snippet lang=c : - * int32_t result_status - * } - */ - public static final OfInt result_status$layout() { - return result_status$LAYOUT; - } - - private static final long result_status$OFFSET = $LAYOUT.byteOffset(groupElement("result_status")); - - /** - * Offset for field: - * {@snippet lang=c : - * int32_t result_status - * } - */ - public static final long result_status$offset() { - return result_status$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * int32_t result_status - * } - */ - public static int result_status(MemorySegment struct) { - return struct.get(result_status$LAYOUT, result_status$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * int32_t result_status - * } - */ - public static void result_status(MemorySegment struct, int fieldValue) { - struct.set(result_status$LAYOUT, result_status$OFFSET, fieldValue); - } - - private static final OfBoolean found$LAYOUT = (OfBoolean)$LAYOUT.select(groupElement("found")); - - /** - * Layout for field: - * {@snippet lang=c : - * bool found - * } - */ - public static final OfBoolean found$layout() { - return found$LAYOUT; - } - - private static final long found$OFFSET = $LAYOUT.byteOffset(groupElement("found")); - - /** - * Offset for field: - * {@snippet lang=c : - * bool found - * } - */ - public static final long found$offset() { - return found$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * bool found - * } - */ - public static boolean found(MemorySegment struct) { - return struct.get(found$LAYOUT, found$OFFSET); - } - - /** - * Setter for field: - * {@snippet lang=c : - * bool found - * } - */ - public static void found(MemorySegment struct, boolean fieldValue) { - struct.set(found$LAYOUT, found$OFFSET, fieldValue); - } - - /** - * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. - * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} - */ - public static MemorySegment asSlice(MemorySegment array, long index) { - return array.asSlice(layout().byteSize() * index); - } - - /** - * The size (in bytes) of this struct - */ - public static long sizeof() { return layout().byteSize(); } - - /** - * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} - */ - public static MemorySegment allocate(SegmentAllocator allocator) { - return allocator.allocate(layout()); - } - - /** - * Allocate an array of size {@code elementCount} using {@code allocator}. - * The returned segment has size {@code elementCount * layout().byteSize()}. - */ - public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { - return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); - } - - /** - * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). - * The returned segment has size {@code layout().byteSize()} - */ - public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { - return reinterpret(addr, 1, arena, cleanup); - } - - /** - * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). - * The returned segment has size {@code elementCount * layout().byteSize()} - */ - public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { - return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); - } -} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_payload.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_payload.java index a956dd8e0..84221f864 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_payload.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_event_payload.java @@ -21,7 +21,6 @@ * mln_runtime_event_offline_region_status offline_region_status; * mln_runtime_event_offline_region_response_error offline_region_response_error; * mln_runtime_event_offline_region_tile_count_limit offline_region_tile_count_limit; - * mln_runtime_event_offline_operation_completed offline_operation_completed; * mln_runtime_event_camera_transition_finished camera_transition_finished; * } * } @@ -39,7 +38,6 @@ public class mln_runtime_event_payload { mln_runtime_event_offline_region_status.layout().withName("offline_region_status"), mln_runtime_event_offline_region_response_error.layout().withName("offline_region_response_error"), mln_runtime_event_offline_region_tile_count_limit.layout().withName("offline_region_tile_count_limit"), - mln_runtime_event_offline_operation_completed.layout().withName("offline_operation_completed"), mln_runtime_event_camera_transition_finished.layout().withName("camera_transition_finished") ).withName("mln_runtime_event_payload"); @@ -314,50 +312,6 @@ public static void offline_region_tile_count_limit(MemorySegment union, MemorySe MemorySegment.copy(fieldValue, 0L, union, offline_region_tile_count_limit$OFFSET, offline_region_tile_count_limit$LAYOUT.byteSize()); } - private static final GroupLayout offline_operation_completed$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("offline_operation_completed")); - - /** - * Layout for field: - * {@snippet lang=c : - * mln_runtime_event_offline_operation_completed offline_operation_completed - * } - */ - public static final GroupLayout offline_operation_completed$layout() { - return offline_operation_completed$LAYOUT; - } - - private static final long offline_operation_completed$OFFSET = $LAYOUT.byteOffset(groupElement("offline_operation_completed")); - - /** - * Offset for field: - * {@snippet lang=c : - * mln_runtime_event_offline_operation_completed offline_operation_completed - * } - */ - public static final long offline_operation_completed$offset() { - return offline_operation_completed$OFFSET; - } - - /** - * Getter for field: - * {@snippet lang=c : - * mln_runtime_event_offline_operation_completed offline_operation_completed - * } - */ - public static MemorySegment offline_operation_completed(MemorySegment union) { - return union.asSlice(offline_operation_completed$OFFSET, offline_operation_completed$LAYOUT.byteSize()); - } - - /** - * Setter for field: - * {@snippet lang=c : - * mln_runtime_event_offline_operation_completed offline_operation_completed - * } - */ - public static void offline_operation_completed(MemorySegment union, MemorySegment fieldValue) { - MemorySegment.copy(fieldValue, 0L, union, offline_operation_completed$OFFSET, offline_operation_completed$LAYOUT.byteSize()); - } - private static final GroupLayout camera_transition_finished$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("camera_transition_finished")); /** diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_options.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_options.java index 600914aeb..020568174 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_options.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_runtime_options.java @@ -20,6 +20,7 @@ * const char *asset_path; * const char *cache_path; * uint64_t event_mask; + * mln_wake event_wake; * } * } */ @@ -34,7 +35,8 @@ public class mln_runtime_options { MapLibreNativeC.C_INT.withName("flags"), MapLibreNativeC.C_POINTER.withName("asset_path"), MapLibreNativeC.C_POINTER.withName("cache_path"), - MapLibreNativeC.C_LONG.withName("event_mask") + MapLibreNativeC.C_LONG.withName("event_mask"), + mln_wake.layout().withName("event_wake") ).withName("mln_runtime_options"); /** @@ -264,6 +266,50 @@ public static void event_mask(MemorySegment struct, long fieldValue) { struct.set(event_mask$LAYOUT, event_mask$OFFSET, fieldValue); } + private static final GroupLayout event_wake$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("event_wake")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_wake event_wake + * } + */ + public static final GroupLayout event_wake$layout() { + return event_wake$LAYOUT; + } + + private static final long event_wake$OFFSET = $LAYOUT.byteOffset(groupElement("event_wake")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_wake event_wake + * } + */ + public static final long event_wake$offset() { + return event_wake$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_wake event_wake + * } + */ + public static MemorySegment event_wake(MemorySegment struct) { + return struct.asSlice(event_wake$OFFSET, event_wake$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_wake event_wake + * } + */ + public static void event_wake(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, event_wake$OFFSET, event_wake$LAYOUT.byteSize()); + } + /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_result.java new file mode 100644 index 000000000..72b77c050 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_result.java @@ -0,0 +1,448 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_style_image_result { + * uint32_t size; + * uint32_t reserved; + * mln_style_image_info info; + * mln_buffer_view pixels; + * const mln_image_stretch *stretch_x; + * size_t stretch_x_count; + * const mln_image_stretch *stretch_y; + * size_t stretch_y_count; + * } + * } + */ +public class mln_style_image_result { + + mln_style_image_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + mln_style_image_info.layout().withName("info"), + mln_buffer_view.layout().withName("pixels"), + MapLibreNativeC.C_POINTER.withName("stretch_x"), + MapLibreNativeC.C_LONG.withName("stretch_x_count"), + MapLibreNativeC.C_POINTER.withName("stretch_y"), + MapLibreNativeC.C_LONG.withName("stretch_y_count") + ).withName("mln_style_image_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final GroupLayout info$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("info")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_style_image_info info + * } + */ + public static final GroupLayout info$layout() { + return info$LAYOUT; + } + + private static final long info$OFFSET = $LAYOUT.byteOffset(groupElement("info")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_style_image_info info + * } + */ + public static final long info$offset() { + return info$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_style_image_info info + * } + */ + public static MemorySegment info(MemorySegment struct) { + return struct.asSlice(info$OFFSET, info$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_style_image_info info + * } + */ + public static void info(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, info$OFFSET, info$LAYOUT.byteSize()); + } + + private static final GroupLayout pixels$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("pixels")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view pixels + * } + */ + public static final GroupLayout pixels$layout() { + return pixels$LAYOUT; + } + + private static final long pixels$OFFSET = $LAYOUT.byteOffset(groupElement("pixels")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view pixels + * } + */ + public static final long pixels$offset() { + return pixels$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view pixels + * } + */ + public static MemorySegment pixels(MemorySegment struct) { + return struct.asSlice(pixels$OFFSET, pixels$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view pixels + * } + */ + public static void pixels(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, pixels$OFFSET, pixels$LAYOUT.byteSize()); + } + + private static final AddressLayout stretch_x$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("stretch_x")); + + /** + * Layout for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static final AddressLayout stretch_x$layout() { + return stretch_x$LAYOUT; + } + + private static final long stretch_x$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_x")); + + /** + * Offset for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static final long stretch_x$offset() { + return stretch_x$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static MemorySegment stretch_x(MemorySegment struct) { + return struct.get(stretch_x$LAYOUT, stretch_x$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static void stretch_x(MemorySegment struct, MemorySegment fieldValue) { + struct.set(stretch_x$LAYOUT, stretch_x$OFFSET, fieldValue); + } + + private static final OfLong stretch_x_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("stretch_x_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static final OfLong stretch_x_count$layout() { + return stretch_x_count$LAYOUT; + } + + private static final long stretch_x_count$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_x_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static final long stretch_x_count$offset() { + return stretch_x_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static long stretch_x_count(MemorySegment struct) { + return struct.get(stretch_x_count$LAYOUT, stretch_x_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static void stretch_x_count(MemorySegment struct, long fieldValue) { + struct.set(stretch_x_count$LAYOUT, stretch_x_count$OFFSET, fieldValue); + } + + private static final AddressLayout stretch_y$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("stretch_y")); + + /** + * Layout for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static final AddressLayout stretch_y$layout() { + return stretch_y$LAYOUT; + } + + private static final long stretch_y$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_y")); + + /** + * Offset for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static final long stretch_y$offset() { + return stretch_y$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static MemorySegment stretch_y(MemorySegment struct) { + return struct.get(stretch_y$LAYOUT, stretch_y$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static void stretch_y(MemorySegment struct, MemorySegment fieldValue) { + struct.set(stretch_y$LAYOUT, stretch_y$OFFSET, fieldValue); + } + + private static final OfLong stretch_y_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("stretch_y_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static final OfLong stretch_y_count$layout() { + return stretch_y_count$LAYOUT; + } + + private static final long stretch_y_count$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_y_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static final long stretch_y_count$offset() { + return stretch_y_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static long stretch_y_count(MemorySegment struct) { + return struct.get(stretch_y_count$LAYOUT, stretch_y_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static void stretch_y_count(MemorySegment struct, long fieldValue) { + struct.set(stretch_y_count$LAYOUT, stretch_y_count$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_stretches_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_stretches_result.java new file mode 100644 index 000000000..afa7976f2 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_image_stretches_result.java @@ -0,0 +1,356 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_style_image_stretches_result { + * uint32_t size; + * uint32_t reserved; + * const mln_image_stretch *stretch_x; + * size_t stretch_x_count; + * const mln_image_stretch *stretch_y; + * size_t stretch_y_count; + * } + * } + */ +public class mln_style_image_stretches_result { + + mln_style_image_stretches_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + MapLibreNativeC.C_POINTER.withName("stretch_x"), + MapLibreNativeC.C_LONG.withName("stretch_x_count"), + MapLibreNativeC.C_POINTER.withName("stretch_y"), + MapLibreNativeC.C_LONG.withName("stretch_y_count") + ).withName("mln_style_image_stretches_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final AddressLayout stretch_x$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("stretch_x")); + + /** + * Layout for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static final AddressLayout stretch_x$layout() { + return stretch_x$LAYOUT; + } + + private static final long stretch_x$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_x")); + + /** + * Offset for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static final long stretch_x$offset() { + return stretch_x$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static MemorySegment stretch_x(MemorySegment struct) { + return struct.get(stretch_x$LAYOUT, stretch_x$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_x + * } + */ + public static void stretch_x(MemorySegment struct, MemorySegment fieldValue) { + struct.set(stretch_x$LAYOUT, stretch_x$OFFSET, fieldValue); + } + + private static final OfLong stretch_x_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("stretch_x_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static final OfLong stretch_x_count$layout() { + return stretch_x_count$LAYOUT; + } + + private static final long stretch_x_count$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_x_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static final long stretch_x_count$offset() { + return stretch_x_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static long stretch_x_count(MemorySegment struct) { + return struct.get(stretch_x_count$LAYOUT, stretch_x_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t stretch_x_count + * } + */ + public static void stretch_x_count(MemorySegment struct, long fieldValue) { + struct.set(stretch_x_count$LAYOUT, stretch_x_count$OFFSET, fieldValue); + } + + private static final AddressLayout stretch_y$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("stretch_y")); + + /** + * Layout for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static final AddressLayout stretch_y$layout() { + return stretch_y$LAYOUT; + } + + private static final long stretch_y$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_y")); + + /** + * Offset for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static final long stretch_y$offset() { + return stretch_y$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static MemorySegment stretch_y(MemorySegment struct) { + return struct.get(stretch_y$LAYOUT, stretch_y$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const mln_image_stretch *stretch_y + * } + */ + public static void stretch_y(MemorySegment struct, MemorySegment fieldValue) { + struct.set(stretch_y$LAYOUT, stretch_y$OFFSET, fieldValue); + } + + private static final OfLong stretch_y_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("stretch_y_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static final OfLong stretch_y_count$layout() { + return stretch_y_count$LAYOUT; + } + + private static final long stretch_y_count$OFFSET = $LAYOUT.byteOffset(groupElement("stretch_y_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static final long stretch_y_count$offset() { + return stretch_y_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static long stretch_y_count(MemorySegment struct) { + return struct.get(stretch_y_count$LAYOUT, stretch_y_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t stretch_y_count + * } + */ + public static void stretch_y_count(MemorySegment struct, long fieldValue) { + struct.set(stretch_y_count$LAYOUT, stretch_y_count$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_info.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_info.java new file mode 100644 index 000000000..9bdc12862 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_info.java @@ -0,0 +1,449 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_style_layer_info { + * uint32_t size; + * uint32_t fields; + * mln_buffer_view type; + * double min_zoom; + * double max_zoom; + * uint32_t visibility; + * size_t source_id_size; + * size_t source_layer_size; + * } + * } + */ +public class mln_style_layer_info { + + mln_style_layer_info() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("fields"), + mln_buffer_view.layout().withName("type"), + MapLibreNativeC.C_DOUBLE.withName("min_zoom"), + MapLibreNativeC.C_DOUBLE.withName("max_zoom"), + MapLibreNativeC.C_INT.withName("visibility"), + MemoryLayout.paddingLayout(4), + MapLibreNativeC.C_LONG.withName("source_id_size"), + MapLibreNativeC.C_LONG.withName("source_layer_size") + ).withName("mln_style_layer_info"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt fields$LAYOUT = (OfInt)$LAYOUT.select(groupElement("fields")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t fields + * } + */ + public static final OfInt fields$layout() { + return fields$LAYOUT; + } + + private static final long fields$OFFSET = $LAYOUT.byteOffset(groupElement("fields")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t fields + * } + */ + public static final long fields$offset() { + return fields$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t fields + * } + */ + public static int fields(MemorySegment struct) { + return struct.get(fields$LAYOUT, fields$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t fields + * } + */ + public static void fields(MemorySegment struct, int fieldValue) { + struct.set(fields$LAYOUT, fields$OFFSET, fieldValue); + } + + private static final GroupLayout type$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("type")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view type + * } + */ + public static final GroupLayout type$layout() { + return type$LAYOUT; + } + + private static final long type$OFFSET = $LAYOUT.byteOffset(groupElement("type")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view type + * } + */ + public static final long type$offset() { + return type$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view type + * } + */ + public static MemorySegment type(MemorySegment struct) { + return struct.asSlice(type$OFFSET, type$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view type + * } + */ + public static void type(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, type$OFFSET, type$LAYOUT.byteSize()); + } + + private static final OfDouble min_zoom$LAYOUT = (OfDouble)$LAYOUT.select(groupElement("min_zoom")); + + /** + * Layout for field: + * {@snippet lang=c : + * double min_zoom + * } + */ + public static final OfDouble min_zoom$layout() { + return min_zoom$LAYOUT; + } + + private static final long min_zoom$OFFSET = $LAYOUT.byteOffset(groupElement("min_zoom")); + + /** + * Offset for field: + * {@snippet lang=c : + * double min_zoom + * } + */ + public static final long min_zoom$offset() { + return min_zoom$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * double min_zoom + * } + */ + public static double min_zoom(MemorySegment struct) { + return struct.get(min_zoom$LAYOUT, min_zoom$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * double min_zoom + * } + */ + public static void min_zoom(MemorySegment struct, double fieldValue) { + struct.set(min_zoom$LAYOUT, min_zoom$OFFSET, fieldValue); + } + + private static final OfDouble max_zoom$LAYOUT = (OfDouble)$LAYOUT.select(groupElement("max_zoom")); + + /** + * Layout for field: + * {@snippet lang=c : + * double max_zoom + * } + */ + public static final OfDouble max_zoom$layout() { + return max_zoom$LAYOUT; + } + + private static final long max_zoom$OFFSET = $LAYOUT.byteOffset(groupElement("max_zoom")); + + /** + * Offset for field: + * {@snippet lang=c : + * double max_zoom + * } + */ + public static final long max_zoom$offset() { + return max_zoom$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * double max_zoom + * } + */ + public static double max_zoom(MemorySegment struct) { + return struct.get(max_zoom$LAYOUT, max_zoom$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * double max_zoom + * } + */ + public static void max_zoom(MemorySegment struct, double fieldValue) { + struct.set(max_zoom$LAYOUT, max_zoom$OFFSET, fieldValue); + } + + private static final OfInt visibility$LAYOUT = (OfInt)$LAYOUT.select(groupElement("visibility")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t visibility + * } + */ + public static final OfInt visibility$layout() { + return visibility$LAYOUT; + } + + private static final long visibility$OFFSET = $LAYOUT.byteOffset(groupElement("visibility")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t visibility + * } + */ + public static final long visibility$offset() { + return visibility$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t visibility + * } + */ + public static int visibility(MemorySegment struct) { + return struct.get(visibility$LAYOUT, visibility$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t visibility + * } + */ + public static void visibility(MemorySegment struct, int fieldValue) { + struct.set(visibility$LAYOUT, visibility$OFFSET, fieldValue); + } + + private static final OfLong source_id_size$LAYOUT = (OfLong)$LAYOUT.select(groupElement("source_id_size")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t source_id_size + * } + */ + public static final OfLong source_id_size$layout() { + return source_id_size$LAYOUT; + } + + private static final long source_id_size$OFFSET = $LAYOUT.byteOffset(groupElement("source_id_size")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t source_id_size + * } + */ + public static final long source_id_size$offset() { + return source_id_size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t source_id_size + * } + */ + public static long source_id_size(MemorySegment struct) { + return struct.get(source_id_size$LAYOUT, source_id_size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t source_id_size + * } + */ + public static void source_id_size(MemorySegment struct, long fieldValue) { + struct.set(source_id_size$LAYOUT, source_id_size$OFFSET, fieldValue); + } + + private static final OfLong source_layer_size$LAYOUT = (OfLong)$LAYOUT.select(groupElement("source_layer_size")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t source_layer_size + * } + */ + public static final OfLong source_layer_size$layout() { + return source_layer_size$LAYOUT; + } + + private static final long source_layer_size$OFFSET = $LAYOUT.byteOffset(groupElement("source_layer_size")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t source_layer_size + * } + */ + public static final long source_layer_size$offset() { + return source_layer_size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t source_layer_size + * } + */ + public static long source_layer_size(MemorySegment struct) { + return struct.get(source_layer_size$LAYOUT, source_layer_size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t source_layer_size + * } + */ + public static void source_layer_size(MemorySegment struct, long fieldValue) { + struct.set(source_layer_size$LAYOUT, source_layer_size$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_result.java new file mode 100644 index 000000000..726f40bd3 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_layer_result.java @@ -0,0 +1,310 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_style_layer_result { + * uint32_t size; + * uint32_t reserved; + * mln_style_layer_info info; + * mln_buffer_view source_id; + * mln_buffer_view source_layer; + * } + * } + */ +public class mln_style_layer_result { + + mln_style_layer_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + mln_style_layer_info.layout().withName("info"), + mln_buffer_view.layout().withName("source_id"), + mln_buffer_view.layout().withName("source_layer") + ).withName("mln_style_layer_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final GroupLayout info$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("info")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_style_layer_info info + * } + */ + public static final GroupLayout info$layout() { + return info$LAYOUT; + } + + private static final long info$OFFSET = $LAYOUT.byteOffset(groupElement("info")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_style_layer_info info + * } + */ + public static final long info$offset() { + return info$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_style_layer_info info + * } + */ + public static MemorySegment info(MemorySegment struct) { + return struct.asSlice(info$OFFSET, info$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_style_layer_info info + * } + */ + public static void info(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, info$OFFSET, info$LAYOUT.byteSize()); + } + + private static final GroupLayout source_id$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("source_id")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view source_id + * } + */ + public static final GroupLayout source_id$layout() { + return source_id$LAYOUT; + } + + private static final long source_id$OFFSET = $LAYOUT.byteOffset(groupElement("source_id")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view source_id + * } + */ + public static final long source_id$offset() { + return source_id$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view source_id + * } + */ + public static MemorySegment source_id(MemorySegment struct) { + return struct.asSlice(source_id$OFFSET, source_id$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view source_id + * } + */ + public static void source_id(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, source_id$OFFSET, source_id$LAYOUT.byteSize()); + } + + private static final GroupLayout source_layer$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("source_layer")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view source_layer + * } + */ + public static final GroupLayout source_layer$layout() { + return source_layer$LAYOUT; + } + + private static final long source_layer$OFFSET = $LAYOUT.byteOffset(groupElement("source_layer")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view source_layer + * } + */ + public static final long source_layer$offset() { + return source_layer$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view source_layer + * } + */ + public static MemorySegment source_layer(MemorySegment struct) { + return struct.asSlice(source_layer$OFFSET, source_layer$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view source_layer + * } + */ + public static void source_layer(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, source_layer$OFFSET, source_layer$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_source_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_source_result.java new file mode 100644 index 000000000..f12b2ab1b --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_style_source_result.java @@ -0,0 +1,402 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_style_source_result { + * uint32_t size; + * uint32_t reserved; + * mln_style_source_info info; + * mln_buffer_view attribution; + * mln_buffer_view url; + * const mln_buffer_view *tile_urls; + * size_t tile_url_count; + * } + * } + */ +public class mln_style_source_result { + + mln_style_source_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + mln_style_source_info.layout().withName("info"), + mln_buffer_view.layout().withName("attribution"), + mln_buffer_view.layout().withName("url"), + MapLibreNativeC.C_POINTER.withName("tile_urls"), + MapLibreNativeC.C_LONG.withName("tile_url_count") + ).withName("mln_style_source_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final GroupLayout info$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("info")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_style_source_info info + * } + */ + public static final GroupLayout info$layout() { + return info$LAYOUT; + } + + private static final long info$OFFSET = $LAYOUT.byteOffset(groupElement("info")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_style_source_info info + * } + */ + public static final long info$offset() { + return info$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_style_source_info info + * } + */ + public static MemorySegment info(MemorySegment struct) { + return struct.asSlice(info$OFFSET, info$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_style_source_info info + * } + */ + public static void info(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, info$OFFSET, info$LAYOUT.byteSize()); + } + + private static final GroupLayout attribution$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("attribution")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view attribution + * } + */ + public static final GroupLayout attribution$layout() { + return attribution$LAYOUT; + } + + private static final long attribution$OFFSET = $LAYOUT.byteOffset(groupElement("attribution")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view attribution + * } + */ + public static final long attribution$offset() { + return attribution$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view attribution + * } + */ + public static MemorySegment attribution(MemorySegment struct) { + return struct.asSlice(attribution$OFFSET, attribution$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view attribution + * } + */ + public static void attribution(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, attribution$OFFSET, attribution$LAYOUT.byteSize()); + } + + private static final GroupLayout url$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("url")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view url + * } + */ + public static final GroupLayout url$layout() { + return url$LAYOUT; + } + + private static final long url$OFFSET = $LAYOUT.byteOffset(groupElement("url")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view url + * } + */ + public static final long url$offset() { + return url$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view url + * } + */ + public static MemorySegment url(MemorySegment struct) { + return struct.asSlice(url$OFFSET, url$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view url + * } + */ + public static void url(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, url$OFFSET, url$LAYOUT.byteSize()); + } + + private static final AddressLayout tile_urls$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("tile_urls")); + + /** + * Layout for field: + * {@snippet lang=c : + * const mln_buffer_view *tile_urls + * } + */ + public static final AddressLayout tile_urls$layout() { + return tile_urls$LAYOUT; + } + + private static final long tile_urls$OFFSET = $LAYOUT.byteOffset(groupElement("tile_urls")); + + /** + * Offset for field: + * {@snippet lang=c : + * const mln_buffer_view *tile_urls + * } + */ + public static final long tile_urls$offset() { + return tile_urls$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * const mln_buffer_view *tile_urls + * } + */ + public static MemorySegment tile_urls(MemorySegment struct) { + return struct.get(tile_urls$LAYOUT, tile_urls$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * const mln_buffer_view *tile_urls + * } + */ + public static void tile_urls(MemorySegment struct, MemorySegment fieldValue) { + struct.set(tile_urls$LAYOUT, tile_urls$OFFSET, fieldValue); + } + + private static final OfLong tile_url_count$LAYOUT = (OfLong)$LAYOUT.select(groupElement("tile_url_count")); + + /** + * Layout for field: + * {@snippet lang=c : + * size_t tile_url_count + * } + */ + public static final OfLong tile_url_count$layout() { + return tile_url_count$LAYOUT; + } + + private static final long tile_url_count$OFFSET = $LAYOUT.byteOffset(groupElement("tile_url_count")); + + /** + * Offset for field: + * {@snippet lang=c : + * size_t tile_url_count + * } + */ + public static final long tile_url_count$offset() { + return tile_url_count$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * size_t tile_url_count + * } + */ + public static long tile_url_count(MemorySegment struct) { + return struct.get(tile_url_count$LAYOUT, tile_url_count$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * size_t tile_url_count + * } + */ + public static void tile_url_count(MemorySegment struct, long fieldValue) { + struct.set(tile_url_count$LAYOUT, tile_url_count$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_texture_readback_result.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_texture_readback_result.java new file mode 100644 index 000000000..8cfe6a139 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_texture_readback_result.java @@ -0,0 +1,264 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_texture_readback_result { + * uint32_t size; + * uint32_t reserved; + * mln_buffer_view data; + * mln_texture_image_info info; + * } + * } + */ +public class mln_texture_readback_result { + + mln_texture_readback_result() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MapLibreNativeC.C_INT.withName("reserved"), + mln_buffer_view.layout().withName("data"), + mln_texture_image_info.layout().withName("info") + ).withName("mln_texture_readback_result"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final OfInt reserved$LAYOUT = (OfInt)$LAYOUT.select(groupElement("reserved")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final OfInt reserved$layout() { + return reserved$LAYOUT; + } + + private static final long reserved$OFFSET = $LAYOUT.byteOffset(groupElement("reserved")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static final long reserved$offset() { + return reserved$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static int reserved(MemorySegment struct) { + return struct.get(reserved$LAYOUT, reserved$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t reserved + * } + */ + public static void reserved(MemorySegment struct, int fieldValue) { + struct.set(reserved$LAYOUT, reserved$OFFSET, fieldValue); + } + + private static final GroupLayout data$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view data + * } + */ + public static final GroupLayout data$layout() { + return data$LAYOUT; + } + + private static final long data$OFFSET = $LAYOUT.byteOffset(groupElement("data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view data + * } + */ + public static final long data$offset() { + return data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view data + * } + */ + public static MemorySegment data(MemorySegment struct) { + return struct.asSlice(data$OFFSET, data$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view data + * } + */ + public static void data(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, data$OFFSET, data$LAYOUT.byteSize()); + } + + private static final GroupLayout info$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("info")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_texture_image_info info + * } + */ + public static final GroupLayout info$layout() { + return info$LAYOUT; + } + + private static final long info$OFFSET = $LAYOUT.byteOffset(groupElement("info")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_texture_image_info info + * } + */ + public static final long info$offset() { + return info$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_texture_image_info info + * } + */ + public static MemorySegment info(MemorySegment struct) { + return struct.asSlice(info$OFFSET, info$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_texture_image_info info + * } + */ + public static void info(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, info$OFFSET, info$LAYOUT.byteSize()); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake.java new file mode 100644 index 000000000..437d1d8c9 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake.java @@ -0,0 +1,265 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct mln_wake { + * uint32_t size; + * mln_wake_callback callback; + * void *user_data; + * mln_wake_release release_user_data; + * } + * } + */ +public class mln_wake { + + mln_wake() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + MapLibreNativeC.C_INT.withName("size"), + MemoryLayout.paddingLayout(4), + MapLibreNativeC.C_POINTER.withName("callback"), + MapLibreNativeC.C_POINTER.withName("user_data"), + MapLibreNativeC.C_POINTER.withName("release_user_data") + ).withName("mln_wake"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfInt size$LAYOUT = (OfInt)$LAYOUT.select(groupElement("size")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final OfInt size$layout() { + return size$LAYOUT; + } + + private static final long size$OFFSET = $LAYOUT.byteOffset(groupElement("size")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static final long size$offset() { + return size$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static int size(MemorySegment struct) { + return struct.get(size$LAYOUT, size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t size + * } + */ + public static void size(MemorySegment struct, int fieldValue) { + struct.set(size$LAYOUT, size$OFFSET, fieldValue); + } + + private static final AddressLayout callback$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("callback")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_wake_callback callback + * } + */ + public static final AddressLayout callback$layout() { + return callback$LAYOUT; + } + + private static final long callback$OFFSET = $LAYOUT.byteOffset(groupElement("callback")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_wake_callback callback + * } + */ + public static final long callback$offset() { + return callback$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_wake_callback callback + * } + */ + public static MemorySegment callback(MemorySegment struct) { + return struct.get(callback$LAYOUT, callback$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_wake_callback callback + * } + */ + public static void callback(MemorySegment struct, MemorySegment fieldValue) { + struct.set(callback$LAYOUT, callback$OFFSET, fieldValue); + } + + private static final AddressLayout user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static final AddressLayout user_data$layout() { + return user_data$LAYOUT; + } + + private static final long user_data$OFFSET = $LAYOUT.byteOffset(groupElement("user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static final long user_data$offset() { + return user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static MemorySegment user_data(MemorySegment struct) { + return struct.get(user_data$LAYOUT, user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * void *user_data + * } + */ + public static void user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(user_data$LAYOUT, user_data$OFFSET, fieldValue); + } + + private static final AddressLayout release_user_data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("release_user_data")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_wake_release release_user_data + * } + */ + public static final AddressLayout release_user_data$layout() { + return release_user_data$LAYOUT; + } + + private static final long release_user_data$OFFSET = $LAYOUT.byteOffset(groupElement("release_user_data")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_wake_release release_user_data + * } + */ + public static final long release_user_data$offset() { + return release_user_data$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_wake_release release_user_data + * } + */ + public static MemorySegment release_user_data(MemorySegment struct) { + return struct.get(release_user_data$LAYOUT, release_user_data$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_wake_release release_user_data + * } + */ + public static void release_user_data(MemorySegment struct, MemorySegment fieldValue) { + struct.set(release_user_data$LAYOUT, release_user_data$OFFSET, fieldValue); + } + + /** + * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. + * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} + */ + public static MemorySegment asSlice(MemorySegment array, long index) { + return array.asSlice(layout().byteSize() * index); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + + /** + * Allocate a segment of size {@code layout().byteSize()} using {@code allocator} + */ + public static MemorySegment allocate(SegmentAllocator allocator) { + return allocator.allocate(layout()); + } + + /** + * Allocate an array of size {@code elementCount} using {@code allocator}. + * The returned segment has size {@code elementCount * layout().byteSize()}. + */ + public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) { + return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout())); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) { + return reinterpret(addr, 1, arena, cleanup); + } + + /** + * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any). + * The returned segment has size {@code elementCount * layout().byteSize()} + */ + public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) { + return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup); + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_callback.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_callback.java new file mode 100644 index 000000000..ad66a2dfd --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_callback.java @@ -0,0 +1,68 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_wake_callback)(void *) + * } + */ +public final class mln_wake_callback { + + private mln_wake_callback() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_wake_callback.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_wake_callback.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data) { + try { + DOWN$MH.invokeExact(funcPtr, user_data); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_release.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_release.java new file mode 100644 index 000000000..86dff2947 --- /dev/null +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_wake_release.java @@ -0,0 +1,68 @@ +// Generated by jextract + +package org.maplibre.nativeffi.internal.c; + +import java.lang.invoke.*; +import java.lang.foreign.*; +import java.nio.ByteOrder; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * typedef void (*mln_wake_release)(void *) + * } + */ +public final class mln_wake_release { + + private mln_wake_release() { + // Should not be called directly + } + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + void apply(MemorySegment user_data); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + MapLibreNativeC.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = MapLibreNativeC.upcallHandle(mln_wake_release.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(mln_wake_release.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static void invoke(MemorySegment funcPtr, MemorySegment user_data) { + try { + DOWN$MH.invokeExact(funcPtr, user_data); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} diff --git a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_webgl_context_descriptor.java b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_webgl_context_descriptor.java index e5ac62b5d..8b7299f75 100644 --- a/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_webgl_context_descriptor.java +++ b/bindings/kotlin/src/jvmMain/generated/org/maplibre/nativeffi/internal/c/mln_webgl_context_descriptor.java @@ -16,7 +16,9 @@ * {@snippet lang=c : * struct mln_webgl_context_descriptor { * uint32_t size; + * uint32_t kind; * int32_t context; + * mln_buffer_view canvas_selector; * } * } */ @@ -28,7 +30,10 @@ public class mln_webgl_context_descriptor { private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( MapLibreNativeC.C_INT.withName("size"), - MapLibreNativeC.C_INT.withName("context") + MapLibreNativeC.C_INT.withName("kind"), + MapLibreNativeC.C_INT.withName("context"), + MemoryLayout.paddingLayout(4), + mln_buffer_view.layout().withName("canvas_selector") ).withName("mln_webgl_context_descriptor"); /** @@ -82,6 +87,50 @@ public static void size(MemorySegment struct, int fieldValue) { struct.set(size$LAYOUT, size$OFFSET, fieldValue); } + private static final OfInt kind$LAYOUT = (OfInt)$LAYOUT.select(groupElement("kind")); + + /** + * Layout for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final OfInt kind$layout() { + return kind$LAYOUT; + } + + private static final long kind$OFFSET = $LAYOUT.byteOffset(groupElement("kind")); + + /** + * Offset for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static final long kind$offset() { + return kind$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static int kind(MemorySegment struct) { + return struct.get(kind$LAYOUT, kind$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * uint32_t kind + * } + */ + public static void kind(MemorySegment struct, int fieldValue) { + struct.set(kind$LAYOUT, kind$OFFSET, fieldValue); + } + private static final OfInt context$LAYOUT = (OfInt)$LAYOUT.select(groupElement("context")); /** @@ -126,6 +175,50 @@ public static void context(MemorySegment struct, int fieldValue) { struct.set(context$LAYOUT, context$OFFSET, fieldValue); } + private static final GroupLayout canvas_selector$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("canvas_selector")); + + /** + * Layout for field: + * {@snippet lang=c : + * mln_buffer_view canvas_selector + * } + */ + public static final GroupLayout canvas_selector$layout() { + return canvas_selector$LAYOUT; + } + + private static final long canvas_selector$OFFSET = $LAYOUT.byteOffset(groupElement("canvas_selector")); + + /** + * Offset for field: + * {@snippet lang=c : + * mln_buffer_view canvas_selector + * } + */ + public static final long canvas_selector$offset() { + return canvas_selector$OFFSET; + } + + /** + * Getter for field: + * {@snippet lang=c : + * mln_buffer_view canvas_selector + * } + */ + public static MemorySegment canvas_selector(MemorySegment struct) { + return struct.asSlice(canvas_selector$OFFSET, canvas_selector$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * mln_buffer_view canvas_selector + * } + */ + public static void canvas_selector(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, canvas_selector$OFFSET, canvas_selector$LAYOUT.byteSize()); + } + /** * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}. * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt index 40171635c..ac9bbc386 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt @@ -10,6 +10,8 @@ import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.c.mln_http_header_transform import org.maplibre.nativeffi.internal.c.mln_http_header_transform_callback import org.maplibre.nativeffi.internal.c.mln_http_header_transform_response +import org.maplibre.nativeffi.internal.c.mln_runtime_callback_release +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.loader.NativeAccess import org.maplibre.nativeffi.resource.HttpHeaderTransformCallback import org.maplibre.nativeffi.resource.HttpHeaderTransformRequest @@ -17,8 +19,8 @@ import org.maplibre.nativeffi.resource.ResourceKind internal class HttpHeaderTransformState(private val callback: HttpHeaderTransformCallback) : AutoCloseable { - private val arena = Arena.ofShared() - private val gate = CallbackGate("HTTP header transform callbacks") { arena.close() } + private val arena = Arena.ofAuto() + private val gate = CallbackGate("HTTP header transform callbacks") {} private val descriptor: MemorySegment init { @@ -43,6 +45,19 @@ internal class HttpHeaderTransformState(private val callback: HttpHeaderTransfor mln_http_header_transform.size(descriptor, mln_http_header_transform.sizeof().toInt()) mln_http_header_transform.callback(descriptor, stub) mln_http_header_transform.user_data(descriptor, MemorySegment.NULL) + val releaseMethod = + MethodHandles.lookup() + .findVirtual( + HttpHeaderTransformState::class.java, + "release", + MethodType.methodType(Void.TYPE, MemorySegment::class.java), + ) + .bindTo(this) + mln_http_header_transform.release_user_data( + descriptor, + Linker.nativeLinker() + .upcallStub(releaseMethod, mln_runtime_callback_release.descriptor(), arena), + ) } fun descriptor(): MemorySegment = descriptor @@ -89,6 +104,12 @@ internal class HttpHeaderTransformState(private val callback: HttpHeaderTransfor override fun close() = gate.close() + @Suppress("UNUSED_PARAMETER") + fun release(userData: MemorySegment) { + HandleLeakCleaner.releaseNativeCallbackRoot(this) + close() + } + private fun copyCString(address: MemorySegment): String { if (address == MemorySegment.NULL) return "" var length = 0L diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt index 0bc508942..e9dae9895 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt @@ -6,10 +6,9 @@ import java.lang.foreign.MemorySegment import java.lang.foreign.ValueLayout import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi import org.maplibre.nativeffi.internal.c.mln_log_callback +import org.maplibre.nativeffi.internal.c.mln_log_callback_release +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.loader.NativeAccess import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.log.LogCallback @@ -18,16 +17,16 @@ import org.maplibre.nativeffi.log.LogRecord import org.maplibre.nativeffi.log.LogSeverity /** Owns process-global JVM FFM logging callback state. */ -@OptIn(ExperimentalAtomicApi::class) -internal class LogCallbackState private constructor(private val callback: LogCallback) : - AutoCloseable { - private val arena = Arena.ofShared() - private val gate = CallbackGate("log callbacks") { arena.close() } - private val stub: MemorySegment +internal class LogCallbackState(private val callback: LogCallback) : AutoCloseable { + private val arena = Arena.ofAuto() + private val gate = CallbackGate("log callbacks") {} + private val callbackStub: MemorySegment + private val releaseStub: MemorySegment init { - val method = - MethodHandles.lookup() + val lookup = MethodHandles.lookup() + val callbackMethod = + lookup .findVirtual( LogCallbackState::class.java, "invoke", @@ -41,7 +40,18 @@ internal class LogCallbackState private constructor(private val callback: LogCal ), ) .bindTo(this) - stub = Linker.nativeLinker().upcallStub(method, callbackDescriptor, arena) + callbackStub = + Linker.nativeLinker().upcallStub(callbackMethod, mln_log_callback.descriptor(), arena) + val releaseMethod = + lookup + .findVirtual( + LogCallbackState::class.java, + "release", + MethodType.methodType(Void.TYPE, MemorySegment::class.java), + ) + .bindTo(this) + releaseStub = + Linker.nativeLinker().upcallStub(releaseMethod, mln_log_callback_release.descriptor(), arena) } @Suppress("UNUSED_PARAMETER") @@ -69,16 +79,18 @@ internal class LogCallbackState private constructor(private val callback: LogCal } } - fun checkCanClose() = gate.checkCanClose() - - fun isClosedForTesting(): Boolean = gate.isClosedForTesting() + @Suppress("UNUSED_PARAMETER") + fun release(userData: MemorySegment) { + HandleLeakCleaner.releaseNativeCallbackRoot(this) + close() + } override fun close() = gate.close() + fun isClosedForTesting(): Boolean = gate.isClosedForTesting() + private fun copyCString(address: MemorySegment): String { - if (address == MemorySegment.NULL) { - return "" - } + if (address == MemorySegment.NULL) return "" var length = 0L while (address.reinterpret(length + 1).get(ValueLayout.JAVA_BYTE, length) != 0.toByte()) { length++ @@ -87,76 +99,24 @@ internal class LogCallbackState private constructor(private val callback: LogCal } internal companion object { - private val updateLock = AtomicInt(0) - private val current = AtomicReference(null) - fun set(callback: LogCallback) { NativeAccess.ensureLoaded() - replace(callback) { replacement -> - Status.check(NativeAccess.setLogCallback(replacement.stub)) - } - } - - internal fun setForTesting(callback: LogCallback, register: (LogCallbackState) -> Unit = {}) { - replace(callback, register) - } - - private fun replace(callback: LogCallback, register: (LogCallbackState) -> Unit) { val replacement = LogCallbackState(callback) - var previous: LogCallbackState? = null + HandleLeakCleaner.retainNativeCallbackRoot(replacement) try { - withUpdateLock { - current.load()?.checkCanClose() - register(replacement) - previous = current.exchange(replacement) - } + Status.check(NativeAccess.setLogCallback(replacement.callbackStub, replacement.releaseStub)) } catch (error: Throwable) { - closeAndSuppress(error, replacement) + HandleLeakCleaner.releaseNativeCallbackRoot(replacement) + replacement.close() throw error } - closeQuietly(previous) } fun clear() { NativeAccess.ensureLoaded() - var previous: LogCallbackState? = null - withUpdateLock { - current.load()?.checkCanClose() - Status.check(NativeAccess.clearLogCallback()) - previous = current.exchange(null) - } - closeQuietly(previous) - } - - fun currentForTesting(): LogCallbackState? = current.load() - - internal fun clearForTesting() { - closeQuietly(current.exchange(null)) - } - - private fun closeQuietly(state: LogCallbackState?) { - try { - state?.close() - } catch (_: RuntimeException) {} - } - - private fun closeAndSuppress(error: Throwable, state: LogCallbackState?) { - try { - state?.close() - } catch (cleanup: Throwable) { - error.addSuppressed(cleanup) - } - } - - private inline fun withUpdateLock(block: () -> R): R { - while (!updateLock.compareAndSet(0, 1)) {} - try { - return block() - } finally { - updateLock.store(0) - } + Status.check(NativeAccess.clearLogCallback()) } - private val callbackDescriptor = mln_log_callback.descriptor() + fun createForTesting(callback: LogCallback): LogCallbackState = LogCallbackState(callback) } } diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt index e0bb0db18..9fd765744 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt @@ -7,6 +7,8 @@ import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType import org.maplibre.nativeffi.internal.c.mln_resource_provider import org.maplibre.nativeffi.internal.c.mln_resource_provider_callback +import org.maplibre.nativeffi.internal.c.mln_runtime_callback_release +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.NativeResourceRequest import org.maplibre.nativeffi.internal.loader.NativeAccess import org.maplibre.nativeffi.resource.ResourceProviderCallback @@ -15,8 +17,8 @@ import org.maplibre.nativeffi.resource.ResourceRequestHandle /** Owns runtime-scoped JVM FFM resource provider callback state. */ internal class ResourceProviderState(private val callback: ResourceProviderCallback) : AutoCloseable { - private val arena = Arena.ofShared() - private val gate = CallbackGate("resource provider callbacks") { arena.close() } + private val arena = Arena.ofAuto() + private val gate = CallbackGate("resource provider callbacks") {} private val stub: MemorySegment private val descriptor: MemorySegment @@ -39,6 +41,19 @@ internal class ResourceProviderState(private val callback: ResourceProviderCallb mln_resource_provider.size(descriptor, mln_resource_provider.sizeof().toInt()) mln_resource_provider.callback(descriptor, stub) mln_resource_provider.user_data(descriptor, MemorySegment.NULL) + val releaseMethod = + MethodHandles.lookup() + .findVirtual( + ResourceProviderState::class.java, + "release", + MethodType.methodType(Void.TYPE, MemorySegment::class.java), + ) + .bindTo(this) + mln_resource_provider.release_user_data( + descriptor, + Linker.nativeLinker() + .upcallStub(releaseMethod, mln_runtime_callback_release.descriptor(), arena), + ) } fun descriptor(): MemorySegment = descriptor @@ -66,6 +81,12 @@ internal class ResourceProviderState(private val callback: ResourceProviderCallb override fun close() = gate.close() + @Suppress("UNUSED_PARAMETER") + fun release(userData: MemorySegment) { + HandleLeakCleaner.releaseNativeCallbackRoot(this) + close() + } + private companion object { private const val UNKNOWN_DECISION: Int = -1 diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt index 6113d458a..a3a351c8c 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt @@ -10,6 +10,8 @@ import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.c.mln_resource_transform import org.maplibre.nativeffi.internal.c.mln_resource_transform_callback import org.maplibre.nativeffi.internal.c.mln_resource_transform_response +import org.maplibre.nativeffi.internal.c.mln_runtime_callback_release +import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.loader.NativeAccess import org.maplibre.nativeffi.resource.ResourceKind import org.maplibre.nativeffi.resource.ResourceTransformCallback @@ -18,8 +20,8 @@ import org.maplibre.nativeffi.resource.ResourceTransformRequest /** Owns runtime-scoped JVM FFM resource transform callback state. */ internal class ResourceTransformState(private val callback: ResourceTransformCallback) : AutoCloseable { - private val arena = Arena.ofShared() - private val gate = CallbackGate("resource transform callbacks") { arena.close() } + private val arena = Arena.ofAuto() + private val gate = CallbackGate("resource transform callbacks") {} private val stub: MemorySegment private val descriptor: MemorySegment @@ -43,6 +45,19 @@ internal class ResourceTransformState(private val callback: ResourceTransformCal mln_resource_transform.size(descriptor, mln_resource_transform.sizeof().toInt()) mln_resource_transform.callback(descriptor, stub) mln_resource_transform.user_data(descriptor, MemorySegment.NULL) + val releaseMethod = + MethodHandles.lookup() + .findVirtual( + ResourceTransformState::class.java, + "release", + MethodType.methodType(Void.TYPE, MemorySegment::class.java), + ) + .bindTo(this) + mln_resource_transform.release_user_data( + descriptor, + Linker.nativeLinker() + .upcallStub(releaseMethod, mln_runtime_callback_release.descriptor(), arena), + ) } fun descriptor(): MemorySegment = descriptor @@ -85,6 +100,12 @@ internal class ResourceTransformState(private val callback: ResourceTransformCal override fun close() = gate.close() + @Suppress("UNUSED_PARAMETER") + fun release(userData: MemorySegment) { + HandleLeakCleaner.releaseNativeCallbackRoot(this) + close() + } + private fun copyCString(address: MemorySegment): String { if (address == MemorySegment.NULL) { return "" diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt index d1804fda1..7d150e7e3 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleaner.kt @@ -2,13 +2,12 @@ package org.maplibre.nativeffi.internal.lifecycle import java.util.concurrent.ConcurrentHashMap import org.maplibre.nativeffi.render.OwnedTextureFrameHandleCore -import org.maplibre.nativeffi.runtime.OfflineOperationLeakReport /** - * Reports owner-thread-affine native handles that become unreachable before explicit release. + * Reports native handles that become unreachable before explicit release. * - * Runtime, map, and render-session handles are bound to their owner thread, so this hook MUST NOT - * destroy them; explicit release on the owner thread stays the only path that frees native state. + * Cleanup remains explicit so failures are observable. Render sessions also require their graphics + * thread, which an unreachable-action thread cannot provide. * * Registered actions must capture leak-report state only. Capturing the wrapper would keep it * reachable and suppress every report. @@ -41,11 +40,6 @@ internal object HandleLeakCleaner { leakReportActions.register(handle, FrameLeakAction(frameCore)) } - /** Reports [leakReport] when an offline operation becomes unreachable. */ - fun registerOfflineOperation(handle: Any, leakReport: OfflineOperationLeakReport) { - leakReportActions.register(handle, OfflineOperationLeakAction(leakReport)) - } - private class LeakReportAction(private val leakReport: HandleStateCore.LeakReport) : Runnable { override fun run() { leakReport.report() @@ -57,11 +51,4 @@ internal object HandleLeakCleaner { frameCore.reportLeak() } } - - private class OfflineOperationLeakAction(private val leakReport: OfflineOperationLeakReport) : - Runnable { - override fun run() { - leakReport.report() - } - } } diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/CompletionBridge.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/CompletionBridge.kt new file mode 100644 index 000000000..f5d8de88b --- /dev/null +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/CompletionBridge.kt @@ -0,0 +1,123 @@ +package org.maplibre.nativeffi.internal.loader + +import java.lang.foreign.Arena +import java.lang.foreign.MemorySegment +import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.error.MaplibreException +import org.maplibre.nativeffi.error.MaplibreStatus +import org.maplibre.nativeffi.internal.c.mln_buffer_view +import org.maplibre.nativeffi.internal.c.mln_completion +import org.maplibre.nativeffi.internal.c.mln_completion_callback +import org.maplibre.nativeffi.internal.c.mln_completion_release +import org.maplibre.nativeffi.internal.c.mln_completion_result +import org.maplibre.nativeffi.internal.status.Status +import org.maplibre.nativeffi.runtime.CommandCompletion +import org.maplibre.nativeffi.runtime.CommandDisposition + +/** Bridges one native completion into an eager Kotlin [Deferred]. */ +internal object CompletionBridge { + private class State(val convert: (MemorySegment) -> T, val acceptErrorStatus: Boolean) { + val result = CompletableDeferred() + val arena: Arena = Arena.ofShared() + val token: MemorySegment = arena.allocate(1) + } + + private val states = ConcurrentHashMap>() + private val callback = + mln_completion_callback.allocate( + { userData, result -> complete(userData.address(), result) }, + Arena.global(), + ) + private val release = + mln_completion_release.allocate( + { userData -> states.remove(userData.address())?.arena?.close() }, + Arena.global(), + ) + + fun submit(convert: (MemorySegment) -> T, call: (MemorySegment) -> Int): Deferred = + submitInternal(convert, false, false, call) + + private fun submitInternal( + convert: (MemorySegment) -> T, + rejectSynchronously: Boolean, + acceptErrorStatus: Boolean, + call: (MemorySegment) -> Int, + ): Deferred { + val state = State(convert, acceptErrorStatus) + states[state.token.address()] = state + try { + Arena.ofConfined().use { arena -> + val descriptor = mln_completion.allocate(arena) + mln_completion.size(descriptor, mln_completion.sizeof().toInt()) + mln_completion.callback(descriptor, callback) + mln_completion.user_data(descriptor, state.token) + mln_completion.release_user_data(descriptor, release) + Status.check(call(descriptor)) + } + } catch (failure: Throwable) { + if (states.remove(state.token.address(), state)) state.arena.close() + if (rejectSynchronously) throw failure + state.result.completeExceptionally(failure) + } + return state.result + } + + fun command(call: (MemorySegment) -> Int): Deferred = + submitInternal( + { result -> + CommandCompletion( + CommandDisposition.fromNative(mln_completion_result.disposition(result)), + mln_completion_result.generation(result).toULong(), + MaplibreStatus.fromNative(mln_completion_result.status(result)), + diagnostic(result), + ) + }, + false, + true, + call, + ) + + fun unit(call: (MemorySegment) -> Int): Deferred = submit(convert = { _ -> }, call = call) + + fun unitChecked(call: (MemorySegment) -> Int): Deferred = + submitInternal( + convert = { _ -> }, + rejectSynchronously = true, + acceptErrorStatus = false, + call = call, + ) + + @Suppress("UNCHECKED_CAST") + private fun complete(address: Long, rawResult: MemorySegment) { + val state = states[address] as? State ?: return + val result = rawResult.reinterpret(mln_completion_result.sizeof()) + try { + val status = mln_completion_result.status(result) + if (status == MaplibreStatus.OK.nativeCode || state.acceptErrorStatus) { + state.result.complete(state.convert(result)) + } else { + val message = diagnostic(result) + state.result.completeExceptionally( + MaplibreException.forStatus(MaplibreStatus.fromNative(status), status, message) + ) + } + } catch (failure: Throwable) { + state.result.completeExceptionally(failure) + } + } + + private fun diagnostic(result: MemorySegment): String { + val diagnostic = mln_completion_result.diagnostic(result) + val bytes = mln_buffer_view.data(diagnostic) + val size = mln_buffer_view.size(diagnostic) + return if (bytes == MemorySegment.NULL || size == 0L) "" + else + String( + bytes.reinterpret(size).toArray(java.lang.foreign.ValueLayout.JAVA_BYTE), + StandardCharsets.UTF_8, + ) + } +} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccess.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccess.kt index 0e9176ca7..5ca238dfb 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccess.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccess.kt @@ -1,17 +1,26 @@ package org.maplibre.nativeffi.internal.loader import java.lang.foreign.Arena +import java.lang.foreign.FunctionDescriptor +import java.lang.foreign.Linker +import java.lang.foreign.MemoryLayout import java.lang.foreign.MemorySegment +import java.lang.foreign.SymbolLookup import java.lang.foreign.ValueLayout import java.lang.invoke.MethodHandle import java.nio.charset.StandardCharsets import java.nio.file.Path import java.util.NoSuchElementException +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.camera.AnimationOptions import org.maplibre.nativeffi.camera.BoundOptions import org.maplibre.nativeffi.camera.BoundsConstraint +import org.maplibre.nativeffi.camera.CameraDelta import org.maplibre.nativeffi.camera.CameraFitOptions import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraSnapshot +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.EdgeInsets import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.camera.UnitBezier @@ -30,21 +39,29 @@ import org.maplibre.nativeffi.internal.c.MapLibreNativeC import org.maplibre.nativeffi.internal.c.mln_animation_options import org.maplibre.nativeffi.internal.c.mln_bound_options import org.maplibre.nativeffi.internal.c.mln_buffer_view +import org.maplibre.nativeffi.internal.c.mln_camera_delta import org.maplibre.nativeffi.internal.c.mln_camera_fit_options import org.maplibre.nativeffi.internal.c.mln_camera_options +import org.maplibre.nativeffi.internal.c.mln_camera_query_result +import org.maplibre.nativeffi.internal.c.mln_camera_update import org.maplibre.nativeffi.internal.c.mln_canonical_tile_id +import org.maplibre.nativeffi.internal.c.mln_completion_result import org.maplibre.nativeffi.internal.c.mln_custom_geometry_source_options import org.maplibre.nativeffi.internal.c.mln_custom_mvt_vector_source_options import org.maplibre.nativeffi.internal.c.mln_edge_insets import org.maplibre.nativeffi.internal.c.mln_egl_context_descriptor import org.maplibre.nativeffi.internal.c.mln_feature_state_selector +import org.maplibre.nativeffi.internal.c.mln_frame_demand import org.maplibre.nativeffi.internal.c.mln_free_camera_options import org.maplibre.nativeffi.internal.c.mln_geojson_source_options +import org.maplibre.nativeffi.internal.c.mln_gpu_sync import org.maplibre.nativeffi.internal.c.mln_image_content import org.maplibre.nativeffi.internal.c.mln_image_stretch import org.maplibre.nativeffi.internal.c.mln_lat_lng import org.maplibre.nativeffi.internal.c.mln_lat_lng_bounds +import org.maplibre.nativeffi.internal.c.mln_logical_extent import org.maplibre.nativeffi.internal.c.mln_map_options +import org.maplibre.nativeffi.internal.c.mln_map_snapshot import org.maplibre.nativeffi.internal.c.mln_map_tile_options import org.maplibre.nativeffi.internal.c.mln_map_viewport_options import org.maplibre.nativeffi.internal.c.mln_metal_borrowed_texture_descriptor @@ -67,6 +84,11 @@ import org.maplibre.nativeffi.internal.c.mln_projected_meters import org.maplibre.nativeffi.internal.c.mln_projection_mode import org.maplibre.nativeffi.internal.c.mln_quaternion import org.maplibre.nativeffi.internal.c.mln_queried_feature +import org.maplibre.nativeffi.internal.c.mln_render_abandon_result +import org.maplibre.nativeffi.internal.c.mln_render_frame_result +import org.maplibre.nativeffi.internal.c.mln_render_session_attach_options +import org.maplibre.nativeffi.internal.c.mln_render_session_capabilities +import org.maplibre.nativeffi.internal.c.mln_render_session_snapshot import org.maplibre.nativeffi.internal.c.mln_render_target_extent import org.maplibre.nativeffi.internal.c.mln_rendered_feature_query_options import org.maplibre.nativeffi.internal.c.mln_rendered_query_geometry @@ -74,9 +96,8 @@ import org.maplibre.nativeffi.internal.c.mln_rendering_stats import org.maplibre.nativeffi.internal.c.mln_resource_request import org.maplibre.nativeffi.internal.c.mln_resource_response import org.maplibre.nativeffi.internal.c.mln_runtime_event -import org.maplibre.nativeffi.internal.c.mln_runtime_event_batch +import org.maplibre.nativeffi.internal.c.mln_runtime_event_batch_view import org.maplibre.nativeffi.internal.c.mln_runtime_event_camera_transition_finished -import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_operation_completed import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_response_error import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_status import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_tile_count_limit @@ -90,10 +111,16 @@ import org.maplibre.nativeffi.internal.c.mln_screen_point import org.maplibre.nativeffi.internal.c.mln_source_feature_query_options import org.maplibre.nativeffi.internal.c.mln_style_image_info import org.maplibre.nativeffi.internal.c.mln_style_image_options +import org.maplibre.nativeffi.internal.c.mln_style_image_result +import org.maplibre.nativeffi.internal.c.mln_style_image_stretches_result +import org.maplibre.nativeffi.internal.c.mln_style_layer_info +import org.maplibre.nativeffi.internal.c.mln_style_layer_result import org.maplibre.nativeffi.internal.c.mln_style_source_info +import org.maplibre.nativeffi.internal.c.mln_style_source_result import org.maplibre.nativeffi.internal.c.mln_style_tile_source_options import org.maplibre.nativeffi.internal.c.mln_style_transition_options import org.maplibre.nativeffi.internal.c.mln_texture_image_info +import org.maplibre.nativeffi.internal.c.mln_texture_readback_result import org.maplibre.nativeffi.internal.c.mln_tile_id import org.maplibre.nativeffi.internal.c.mln_unit_bezier import org.maplibre.nativeffi.internal.c.mln_vec3 @@ -102,6 +129,7 @@ import org.maplibre.nativeffi.internal.c.mln_vulkan_context_descriptor import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_descriptor import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_frame import org.maplibre.nativeffi.internal.c.mln_vulkan_surface_descriptor +import org.maplibre.nativeffi.internal.c.mln_wake import org.maplibre.nativeffi.internal.c.mln_wgl_context_descriptor import org.maplibre.nativeffi.internal.lifecycle.NativeGeoJsonSourceData import org.maplibre.nativeffi.internal.lifecycle.NativeHandle @@ -116,12 +144,12 @@ import org.maplibre.nativeffi.internal.lifecycle.NativeResourceRequest import org.maplibre.nativeffi.internal.lifecycle.NativeRuntime import org.maplibre.nativeffi.internal.lifecycle.NativeStyleIdList import org.maplibre.nativeffi.internal.lifecycle.NativeStyleStringList -import org.maplibre.nativeffi.internal.lifecycle.NativeWakeSource import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.map.ConstrainMode import org.maplibre.nativeffi.map.DebugOption import org.maplibre.nativeffi.map.MapOptions import org.maplibre.nativeffi.map.MapSize +import org.maplibre.nativeffi.map.MapSnapshot import org.maplibre.nativeffi.map.NorthOrientation import org.maplibre.nativeffi.map.ProjectionModeOptions import org.maplibre.nativeffi.map.RenderingStats @@ -140,13 +168,15 @@ import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.query.SourceFeatureQueryOptions import org.maplibre.nativeffi.render.EglContextDescriptor +import org.maplibre.nativeffi.render.FrameDemand import org.maplibre.nativeffi.render.FrameScope +import org.maplibre.nativeffi.render.GpuSync +import org.maplibre.nativeffi.render.GpuSyncKind import org.maplibre.nativeffi.render.MetalBorrowedTextureDescriptor import org.maplibre.nativeffi.render.MetalContextDescriptor import org.maplibre.nativeffi.render.MetalOwnedTextureDescriptor import org.maplibre.nativeffi.render.MetalOwnedTextureFrame import org.maplibre.nativeffi.render.MetalSurfaceDescriptor -import org.maplibre.nativeffi.render.NativeBuffer import org.maplibre.nativeffi.render.NativePointer import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLContextDescriptor @@ -154,11 +184,19 @@ import org.maplibre.nativeffi.render.OpenGLOwnedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLOwnedTextureFrame import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor import org.maplibre.nativeffi.render.PremultipliedRgba8Image +import org.maplibre.nativeffi.render.RenderAbandonDisposition +import org.maplibre.nativeffi.render.RenderAbandonResult +import org.maplibre.nativeffi.render.RenderDriver +import org.maplibre.nativeffi.render.RenderFrameResult import org.maplibre.nativeffi.render.RenderMode import org.maplibre.nativeffi.render.RenderResult +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionCapabilities +import org.maplibre.nativeffi.render.RenderSessionSnapshot +import org.maplibre.nativeffi.render.RenderSessionState import org.maplibre.nativeffi.render.RenderTargetExtent -import org.maplibre.nativeffi.render.RenderUpdate import org.maplibre.nativeffi.render.TextureImageInfo +import org.maplibre.nativeffi.render.TextureReadback import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor import org.maplibre.nativeffi.render.VulkanContextDescriptor import org.maplibre.nativeffi.render.VulkanOwnedTextureDescriptor @@ -173,8 +211,7 @@ import org.maplibre.nativeffi.resource.ResourceRequest import org.maplibre.nativeffi.resource.ResourceResponse import org.maplibre.nativeffi.resource.ResourceStoragePolicy import org.maplibre.nativeffi.resource.ResourceUsage -import org.maplibre.nativeffi.runtime.OfflineOperationKind -import org.maplibre.nativeffi.runtime.OfflineOperationResultKind +import org.maplibre.nativeffi.runtime.CommandCompletion import org.maplibre.nativeffi.runtime.RuntimeEventPayload import org.maplibre.nativeffi.runtime.RuntimeOptions import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @@ -182,6 +219,7 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions import org.maplibre.nativeffi.style.GeoJsonSourceOptions import org.maplibre.nativeffi.style.ImageContent import org.maplibre.nativeffi.style.ImageStretch +import org.maplibre.nativeffi.style.LayerInfo import org.maplibre.nativeffi.style.LocationIndicatorImageKind import org.maplibre.nativeffi.style.RasterDemEncoding import org.maplibre.nativeffi.style.SourceInfo @@ -190,6 +228,7 @@ import org.maplibre.nativeffi.style.StyleImage import org.maplibre.nativeffi.style.StyleImageInfo import org.maplibre.nativeffi.style.StyleImageOptions import org.maplibre.nativeffi.style.StyleImageTextFit +import org.maplibre.nativeffi.style.StyleLayerVisibility import org.maplibre.nativeffi.style.StyleTransitionOptions import org.maplibre.nativeffi.style.TileJson import org.maplibre.nativeffi.style.TileScheme @@ -300,8 +339,8 @@ internal object NativeAccess { Status.check(statusInFunction("mln_log_set_async_severity_mask").invokeNative(mask) as Int) } - internal fun setLogCallback(callback: MemorySegment): Int = - logSetCallbackFunction().invokeNative(callback, MemorySegment.NULL) as Int + internal fun setLogCallback(callback: MemorySegment, release: MemorySegment): Int = + logSetCallbackFunction().invokeNative(callback, MemorySegment.NULL, release) as Int internal fun clearLogCallback(): Int = intFunction("mln_log_clear_callback").invokeNative() as Int @@ -344,392 +383,337 @@ internal object NativeAccess { val nativeOptions = runtimeOptions(options, arena) val outRuntime = arena.allocate(ValueLayout.JAVA_LONG) outRuntime.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(runtimeCreateFunction().invokeNative(nativeOptions, outRuntime) as Int) + Status.check(MapLibreNativeC.mln_runtime_create(nativeOptions, outRuntime)) NativeRuntime(outRuntime.get(ValueLayout.JAVA_LONG, 0)).also { runtime -> require(!runtime.isNull) { "mln_runtime_create returned the null handle" } } } - internal fun pumpRuntime(runtime: NativeRuntime, timeoutMillis: Long, budgetMillis: Long) { - Status.check(runtimePumpFunction().invokeNative(runtime, timeoutMillis, budgetMillis) as Int) - } + internal fun runtimeBarrier(runtime: NativeRuntime): Deferred = + CompletionBridge.unit { completion -> + MapLibreNativeC.mln_runtime_barrier(runtime.raw, completion) + } - internal fun acquireWakeSource(runtime: NativeRuntime): NativeWakeSource = - Arena.ofConfined().use { arena -> - val outSource = arena.allocate(ValueLayout.JAVA_LONG) - outSource.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(runtimeWakeSourceAcquireFunction().invokeNative(runtime, outSource) as Int) - NativeWakeSource(outSource.get(ValueLayout.JAVA_LONG, 0)).also { source -> - require(!source.isNull) { "mln_runtime_wake_source_acquire returned the null handle" } - } + internal fun releaseRuntime(runtime: NativeRuntime): Deferred = + CompletionBridge.unitChecked { completion -> + MapLibreNativeC.mln_runtime_release(runtime.raw, completion) } - internal fun signalWakeSource(source: NativeWakeSource) { - Status.check(wakeSourceSignalFunction().invokeNative(source) as Int) + private inline fun command( + crossinline call: (MemorySegment) -> Any? + ): Deferred = CompletionBridge.command { completion -> + when (val status = call(completion)) { + is Int -> status + else -> MaplibreStatus.OK.nativeCode + } } - internal fun destroyWakeSource(source: NativeWakeSource) { - wakeSourceDestroyFunction().invokeNative(source) + internal fun setResourceProvider( + runtime: NativeRuntime, + provider: MemorySegment, + ): Deferred = command { completion -> + runtimeSetResourceProviderFunction().invokeNative(runtime.raw, provider, completion) as Int } - internal fun destroyRuntime(runtime: NativeRuntime): Int = - runtimeStatusFunction("mln_runtime_destroy").invokeNative(runtime) as Int - - internal fun setResourceProvider(runtime: NativeRuntime, provider: MemorySegment): Int = - runtimeSetResourceProviderFunction().invokeNative(runtime, provider) as Int - - internal fun clearResourceProvider(runtime: NativeRuntime): Int = - runtimeClearResourceProviderFunction().invokeNative(runtime) as Int + internal fun clearResourceProvider(runtime: NativeRuntime): Deferred = + command { completion -> + runtimeClearResourceProviderFunction().invokeNative(runtime.raw, completion) as Int + } - internal fun startAmbientCacheOperation(runtime: NativeRuntime, operation: Int): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeAmbientCacheOperationStartFunction().invokeNative(runtime, operation, outOperationId) - as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) + internal fun runAmbientCacheOperation(runtime: NativeRuntime, operation: Int): Deferred = + CompletionBridge.unit { completion -> + runtimeAmbientCacheOperationStartFunction().invokeNative(runtime, operation, completion) + as Int } - internal fun startSetMaximumAmbientCacheSize(runtime: NativeRuntime, size: Long): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeSetMaximumAmbientCacheSizeStartFunction().invokeNative(runtime, size, outOperationId) - as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) + internal fun setMaximumAmbientCacheSize(runtime: NativeRuntime, size: Long): Deferred = + CompletionBridge.unit { completion -> + runtimeSetMaximumAmbientCacheSizeStartFunction().invokeNative(runtime, size, completion) + as Int } - internal fun startCreateOfflineRegion( + internal fun createOfflineRegion( runtime: NativeRuntime, definition: OfflineRegionDefinition, metadata: ByteArray, - ): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeOfflineRegionCreateStartFunction() - .invokeNative( - runtime, - offlineRegionDefinition(definition, arena), - nativeBytes(arena, metadata), - metadata.size.toLong(), - outOperationId, - ) as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) - } - - internal fun startOfflineRegion(runtime: NativeRuntime, regionId: Long): Long = - startRuntimeLongOperation("mln_runtime_offline_region_get_start", runtime, regionId) + ): Deferred = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + { result -> offlineRegionInfo(completionValue(result, mln_offline_region_info.sizeof())) }, + { completion -> + runtimeOfflineRegionCreateStartFunction() + .invokeNative( + runtime, + offlineRegionDefinition(definition, arena), + nativeBytes(arena, metadata), + metadata.size.toLong(), + completion, + ) as Int + }, + ) + } + + internal fun offlineRegion(runtime: NativeRuntime, regionId: Long): Deferred = + CompletionBridge.submit( + { result -> + if (mln_completion_result.value_count(result) == 0L) null + else offlineRegionInfo(completionValue(result, mln_offline_region_info.sizeof())) + }, + { completion -> + MapLibreNativeC.mln_runtime_offline_region_get(runtime.raw, regionId, completion) + }, + ) - internal fun startOfflineRegions(runtime: NativeRuntime): Long = - startRuntimeOperation("mln_runtime_offline_regions_list_start", runtime) + internal fun offlineRegions(runtime: NativeRuntime): Deferred> = + offlineRegionList(runtime) { completion -> + MapLibreNativeC.mln_runtime_offline_regions_list(runtime.raw, completion) + } - internal fun startMergeOfflineRegionsDatabase(runtime: NativeRuntime, path: String): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeAddressOperationStartFunction("mln_runtime_offline_regions_merge_database_start") - .invokeNative(runtime, cString(arena, path), outOperationId) as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) + internal fun mergeOfflineRegionsDatabase( + runtime: NativeRuntime, + path: String, + ): Deferred> = + Arena.ofConfined().use { arena -> + offlineRegionList(runtime) { completion -> + MapLibreNativeC.mln_runtime_offline_regions_merge_database( + runtime.raw, + cString(arena, path), + completion, + ) + } } - internal fun startUpdateOfflineRegionMetadata( + internal fun updateOfflineRegionMetadata( runtime: NativeRuntime, regionId: Long, metadata: ByteArray, - ): Long = + ): Deferred = Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeLongAddressLongOperationStartFunction( - "mln_runtime_offline_region_update_metadata_start" - ) - .invokeNative( - runtime, + CompletionBridge.submit( + { result -> offlineRegionInfo(completionValue(result, mln_offline_region_info.sizeof())) }, + { completion -> + MapLibreNativeC.mln_runtime_offline_region_update_metadata( + runtime.raw, regionId, nativeBytes(arena, metadata), metadata.size.toLong(), - outOperationId, - ) as Int + completion, + ) + }, ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) } - internal fun startOfflineRegionStatus(runtime: NativeRuntime, regionId: Long): Long = - startRuntimeLongOperation("mln_runtime_offline_region_get_status_start", runtime, regionId) + internal fun offlineRegionStatus( + runtime: NativeRuntime, + regionId: Long, + ): Deferred = + CompletionBridge.submit( + { result -> + offlineRegionStatus(completionValue(result, mln_offline_region_status.sizeof())) + }, + { completion -> + MapLibreNativeC.mln_runtime_offline_region_get_status(runtime.raw, regionId, completion) + }, + ) - internal fun startSetOfflineRegionObserved( + internal fun setOfflineRegionObserved( runtime: NativeRuntime, regionId: Long, observed: Boolean, - ): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeLongBooleanOperationStartFunction("mln_runtime_offline_region_set_observed_start") - .invokeNative(runtime, regionId, observed, outOperationId) as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) - } + ): Deferred = CompletionBridge.unit { completion -> + MapLibreNativeC.mln_runtime_offline_region_set_observed( + runtime.raw, + regionId, + observed, + completion, + ) + } - internal fun startSetOfflineRegionDownloadState( + internal fun setOfflineRegionDownloadState( runtime: NativeRuntime, regionId: Long, downloadState: Int, - ): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeLongIntOperationStartFunction("mln_runtime_offline_region_set_download_state_start") - .invokeNative(runtime, regionId, downloadState, outOperationId) as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) - } - - internal fun startInvalidateOfflineRegion(runtime: NativeRuntime, regionId: Long): Long = - startRuntimeLongOperation("mln_runtime_offline_region_invalidate_start", runtime, regionId) - - internal fun startDeleteOfflineRegion(runtime: NativeRuntime, regionId: Long): Long = - startRuntimeLongOperation("mln_runtime_offline_region_delete_start", runtime, regionId) - - internal fun discardOfflineOperation(runtime: NativeRuntime, operationId: Long): Int = - runtimeOfflineOperationDiscardFunction().invokeNative(runtime, operationId) as Int - - internal fun setResourceTransform(runtime: NativeRuntime, descriptor: MemorySegment): Int = - runtimeSetResourceTransformFunction().invokeNative(runtime, descriptor) as Int - - internal fun clearResourceTransform(runtime: NativeRuntime): Int = - runtimeClearResourceTransformFunction().invokeNative(runtime) as Int - - internal fun setHttpHeaderTransform(runtime: NativeRuntime, descriptor: MemorySegment): Int = - runtimeSetHttpHeaderTransformFunction().invokeNative(runtime, descriptor) as Int + ): Deferred = CompletionBridge.unit { completion -> + MapLibreNativeC.mln_runtime_offline_region_set_download_state( + runtime.raw, + regionId, + downloadState, + completion, + ) + } - internal fun clearHttpHeaderTransform(runtime: NativeRuntime): Int = - runtimeClearHttpHeaderTransformFunction().invokeNative(runtime) as Int + internal fun invalidateOfflineRegion(runtime: NativeRuntime, regionId: Long): Deferred = + CompletionBridge.unit { completion -> + MapLibreNativeC.mln_runtime_offline_region_invalidate(runtime.raw, regionId, completion) + } - internal fun completeResourceRequest( - handle: NativeResourceRequest, - response: ResourceResponse, - ): Int = - Arena.ofConfined().use { arena -> - resourceRequestCompleteFunction().invokeNative(handle, resourceResponse(response, arena)) - as Int + internal fun deleteOfflineRegion(runtime: NativeRuntime, regionId: Long): Deferred = + CompletionBridge.unit { completion -> + MapLibreNativeC.mln_runtime_offline_region_delete(runtime.raw, regionId, completion) } - internal fun isResourceRequestCancelled(handle: NativeResourceRequest): Boolean = - Arena.ofConfined().use { arena -> - val outCancelled = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check(resourceRequestCancelledFunction().invokeNative(handle, outCancelled) as Int) - outCancelled.get(ValueLayout.JAVA_BOOLEAN, 0) + internal fun setResourceTransform( + runtime: NativeRuntime, + descriptor: MemorySegment, + ): Deferred = command { completion -> + runtimeSetResourceTransformFunction().invokeNative(runtime.raw, descriptor, completion) as Int + } + + internal fun clearResourceTransform(runtime: NativeRuntime): Deferred = + command { completion -> + runtimeClearResourceTransformFunction().invokeNative(runtime.raw, completion) as Int } - internal fun releaseResourceRequest(handle: NativeResourceRequest) { - resourceRequestReleaseFunction().invokeNative(handle) + internal fun setHttpHeaderTransform( + runtime: NativeRuntime, + descriptor: MemorySegment, + ): Deferred = command { completion -> + runtimeSetHttpHeaderTransformFunction().invokeNative(runtime.raw, descriptor, completion) as Int } - internal fun createMap(runtime: NativeRuntime, options: MapOptions): NativeMap = + internal fun clearHttpHeaderTransform(runtime: NativeRuntime): Deferred = + command { completion -> + runtimeClearHttpHeaderTransformFunction().invokeNative(runtime.raw, completion) as Int + } + + internal fun createMap(runtime: NativeRuntime, options: MapOptions): Deferred = Arena.ofConfined().use { arena -> - val outMap = arena.allocate(ValueLayout.JAVA_LONG) - outMap.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapCreateFunction().invokeNative(runtime, mapOptions(options, arena), outMap) as Int + CompletionBridge.submit( + { result -> + val map = + NativeMap( + completionValue(result, ValueLayout.JAVA_LONG.byteSize()) + .get(ValueLayout.JAVA_LONG, 0) + ) + require(!map.isNull) { "mln_map_create completion returned the null handle" } + map + }, + { completion -> + MapLibreNativeC.mln_map_create(runtime.raw, mapOptions(options, arena), completion) + }, ) - NativeMap(outMap.get(ValueLayout.JAVA_LONG, 0)).also { map -> - require(!map.isNull) { "mln_map_create returned the null handle" } - } } - internal fun destroyMap(map: NativeMap): Int = - mapStatusFunction("mln_map_destroy").invokeNative(map) as Int + internal fun releaseMap(map: NativeMap): Deferred = + CompletionBridge.unitChecked { completion -> + MapLibreNativeC.mln_map_release(map.raw, completion) + } - internal fun setMapStyleUrl(map: NativeMap, url: String) { - Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_set_style_url").invokeNative(map, cString(arena, url)) - as Int - ) + internal fun setMapStyleUrl(map: NativeMap, url: String): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + mapAddressStatusFunction("mln_map_set_style_url") + .invokeNative(map, cString(arena, url), completion) as Int + ) + } } - } - internal fun setMapStyleJson(map: NativeMap, json: ByteArray) { - Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_set_style_json") - .invokeNative(map, byteArrayView(arena, json)) as Int - ) + internal fun setMapStyleJson(map: NativeMap, json: ByteArray): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + mapAddressStatusFunction("mln_map_set_style_json") + .invokeNative(map, byteArrayView(arena, json), completion) as Int + ) + } } - } internal fun setMapFeatureState( map: NativeMap, selector: FeatureStateSelector, value: ByteArray, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoAddressStatusFunction("mln_map_set_feature_state") - .invokeNative(map, featureStateSelector(arena, selector), byteArrayView(arena, value)) - as Int + .invokeNative( + map, + featureStateSelector(arena, selector), + byteArrayView(arena, value), + completion, + ) as Int ) } } - internal fun getMapFeatureState(map: NativeMap, selector: FeatureStateSelector): ByteArray = + internal fun getMapFeatureState( + map: NativeMap, + selector: FeatureStateSelector, + ): Deferred = Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapTwoAddressStatusFunction("mln_map_get_feature_state") - .invokeNative(map, featureStateSelector(arena, selector), outSnapshot) as Int + CompletionBridge.submit( + ::requiredBufferCompletion, + { completion -> + mapAddressStatusFunction("mln_map_get_feature_state") + .invokeNative(map, featureStateSelector(arena, selector), completion) as Int + }, ) - ownedBuffer(NativeOwnedBuffer(outSnapshot.get(ValueLayout.JAVA_LONG, 0)))!! } - internal fun removeMapFeatureState(map: NativeMap, selector: FeatureStateSelector) { + internal fun removeMapFeatureState( + map: NativeMap, + selector: FeatureStateSelector, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapAddressStatusFunction("mln_map_remove_feature_state") - .invokeNative(map, featureStateSelector(arena, selector)) as Int + .invokeNative(map, featureStateSelector(arena, selector), completion) as Int ) } } - internal fun addStyleSourceJson(map: NativeMap, sourceId: String, sourceJson: ByteArray) { + internal fun addStyleSourceJson( + map: NativeMap, + sourceId: String, + sourceJson: ByteArray, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_add_style_source_json") - .invokeNative(map, stringView(arena, sourceId), byteArrayView(arena, sourceJson)) as Int + .invokeNative( + map, + stringView(arena, sourceId), + byteArrayView(arena, sourceJson), + completion, + ) as Int ) } } - internal fun removeStyleSource(map: NativeMap, sourceId: String): Boolean = - Arena.ofConfined().use { arena -> - val outRemoved = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_remove_style_source") - .invokeNative(map, stringView(arena, sourceId), outRemoved) as Int - ) - outRemoved.get(ValueLayout.JAVA_BOOLEAN, 0) - } - - internal fun styleSourceExists(map: NativeMap, sourceId: String): Boolean = - Arena.ofConfined().use { arena -> - val outExists = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_style_source_exists") - .invokeNative(map, stringView(arena, sourceId), outExists) as Int - ) - outExists.get(ValueLayout.JAVA_BOOLEAN, 0) - } - - internal fun styleSourceType(map: NativeMap, sourceId: String): SourceType? = - Arena.ofConfined().use { arena -> - val outType = arena.allocate(ValueLayout.JAVA_INT) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewTwoAddressStatusFunction("mln_map_get_style_source_type") - .invokeNative(map, stringView(arena, sourceId), outType, outFound) as Int - ) - if (outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - SourceType.fromNative(outType.get(ValueLayout.JAVA_INT, 0)) - } else null + internal fun removeStyleSource(map: NativeMap, sourceId: String): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + downcall("mln_map_remove_style_source") + .invokeNative(map, stringView(arena, sourceId), completion) as Int + ) + } } - internal fun styleSourceInfo(map: NativeMap, sourceId: String): SourceInfo? = + internal fun styleSourceInfo(map: NativeMap, sourceId: String): Deferred = Arena.ofConfined().use { arena -> val sourceIdView = stringView(arena, sourceId) - val outInfo = arena.allocate(STYLE_SOURCE_INFO_SIZE) - outInfo.set( - ValueLayout.JAVA_INT, - STYLE_SOURCE_INFO_SIZE_OFFSET, - STYLE_SOURCE_INFO_SIZE.toInt(), - ) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewTwoAddressStatusFunction("mln_map_get_style_source_info") - .invokeNative(map, sourceIdView, outInfo, outFound) as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - return@use null - } - val attribution = - if (outInfo.get(ValueLayout.JAVA_BOOLEAN, STYLE_SOURCE_INFO_HAS_ATTRIBUTION_OFFSET)) { - copyStyleSourceAttribution( - map, - sourceIdView, - outInfo.get(ValueLayout.JAVA_LONG, STYLE_SOURCE_INFO_ATTRIBUTION_SIZE_OFFSET), - arena, - ) ?: return@use null - } else null - val fields = outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_FIELDS_OFFSET) - val url = - if (fields and STYLE_SOURCE_INFO_URL != 0) { - copyStyleSourceUrl( - map, - sourceIdView, - outInfo.get(ValueLayout.JAVA_LONG, STYLE_SOURCE_INFO_URL_SIZE_OFFSET), - arena, - ) - } else null - val tileJson = - if (fields and STYLE_SOURCE_INFO_TILEJSON != 0) { - TileJson( - styleSourceTileUrls(map, sourceIdView, arena), - outInfo.get(ValueLayout.JAVA_DOUBLE, STYLE_SOURCE_INFO_MIN_ZOOM_OFFSET), - outInfo.get(ValueLayout.JAVA_DOUBLE, STYLE_SOURCE_INFO_MAX_ZOOM_OFFSET), - TileScheme.fromNative( - outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_SCHEME_OFFSET) - ), - if (fields and STYLE_SOURCE_INFO_BOUNDS != 0) - latLngBounds(outInfo.asSlice(STYLE_SOURCE_INFO_BOUNDS_OFFSET, LAT_LNG_BOUNDS_SIZE)) - else null, - ) - } else null - SourceInfo( - SourceType.fromNative(outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_TYPE_OFFSET)), - outInfo.get(ValueLayout.JAVA_BOOLEAN, STYLE_SOURCE_INFO_IS_VOLATILE_OFFSET), - attribution, - url, - tileJson, - if (fields and STYLE_SOURCE_INFO_TILE_SIZE != 0) - Math.toIntExact( - Integer.toUnsignedLong( - outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_TILE_SIZE_OFFSET) - ) - ) - else null, - if (fields and STYLE_SOURCE_INFO_VECTOR_ENCODING != 0) - VectorTileEncoding.fromNative( - outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_VECTOR_ENCODING_OFFSET) - ) - else null, - if (fields and STYLE_SOURCE_INFO_RASTER_ENCODING != 0) - RasterDemEncoding.fromNative( - outInfo.get(ValueLayout.JAVA_INT, STYLE_SOURCE_INFO_RASTER_ENCODING_OFFSET) - ) - else null, + CompletionBridge.submit( + ::sourceInfoCompletion, + { completion -> + MapLibreNativeC.mln_map_get_style_source_info(map.raw, sourceIdView, completion) + }, ) } - internal fun styleSourceIds(map: NativeMap): List = - Arena.ofConfined().use { arena -> - val outList = arena.allocate(ValueLayout.JAVA_LONG) - outList.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(mapListStyleSourceIdsFunction().invokeNative(map, outList) as Int) - styleIdList(NativeStyleIdList(outList.get(ValueLayout.JAVA_LONG, 0))) - } + internal fun styleSourceIds(map: NativeMap): Deferred> = + CompletionBridge.submit( + ::stringViewsCompletion, + { completion -> MapLibreNativeC.mln_map_list_style_source_ids(map.raw, completion) }, + ) internal fun addGeoJsonSourceUrl( map: NativeMap, sourceId: String, url: String, options: GeoJsonSourceOptions?, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsAddressStatusFunction("mln_map_add_geojson_source_url") @@ -738,6 +722,7 @@ internal object NativeAccess { stringView(arena, sourceId), stringView(arena, url), geoJsonSourceOptions(arena, options), + completion, ) as Int ) } @@ -768,20 +753,24 @@ internal object NativeAccess { map: NativeMap, sourceId: String, data: NativeGeoJsonSourceData, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewHandleStatusFunction("mln_map_add_geojson_source_data") - .invokeNative(map, stringView(arena, sourceId), data) as Int + .invokeNative(map, stringView(arena, sourceId), data, completion) as Int ) } } - internal fun setGeoJsonSourceUrl(map: NativeMap, sourceId: String, url: String) { + internal fun setGeoJsonSourceUrl( + map: NativeMap, + sourceId: String, + url: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsStatusFunction("mln_map_set_geojson_source_url") - .invokeNative(map, stringView(arena, sourceId), stringView(arena, url)) as Int + .invokeNative(map, stringView(arena, sourceId), stringView(arena, url), completion) as Int ) } } @@ -790,11 +779,11 @@ internal object NativeAccess { map: NativeMap, sourceId: String, data: NativeGeoJsonSourceData, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewHandleStatusFunction("mln_map_set_geojson_source_data") - .invokeNative(map, stringView(arena, sourceId), data) as Int + .invokeNative(map, stringView(arena, sourceId), data, completion) as Int ) } } @@ -803,20 +792,24 @@ internal object NativeAccess { map: NativeMap, sourceId: String, enabled: Boolean, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewBooleanStatusFunction("mln_map_set_geojson_source_synchronous_tiling") - .invokeNative(map, stringView(arena, sourceId), enabled) as Int + .invokeNative(map, stringView(arena, sourceId), enabled, completion) as Int ) } } - internal fun addCustomGeometrySource(map: NativeMap, sourceId: String, options: MemorySegment) { + internal fun addCustomGeometrySource( + map: NativeMap, + sourceId: String, + options: MemorySegment, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_add_custom_geometry_source") - .invokeNative(map, stringView(arena, sourceId), options) as Int + .invokeNative(map, stringView(arena, sourceId), options, completion) as Int ) } } @@ -826,7 +819,7 @@ internal object NativeAccess { sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewCanonicalTileIdAddressStatusFunction( @@ -837,6 +830,7 @@ internal object NativeAccess { stringView(arena, sourceId), canonicalTileId(arena, tileId), byteArrayView(arena, data), + completion, ) as Int ) } @@ -846,20 +840,29 @@ internal object NativeAccess { map: NativeMap, sourceId: String, tileId: CanonicalTileId, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewCanonicalTileIdStatusFunction("mln_map_invalidate_custom_geometry_source_tile") - .invokeNative(map, stringView(arena, sourceId), canonicalTileId(arena, tileId)) as Int + .invokeNative( + map, + stringView(arena, sourceId), + canonicalTileId(arena, tileId), + completion, + ) as Int ) } } - internal fun addCustomMvtVectorSource(map: NativeMap, sourceId: String, options: MemorySegment) { + internal fun addCustomMvtVectorSource( + map: NativeMap, + sourceId: String, + options: MemorySegment, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_add_custom_mvt_vector_source") - .invokeNative(map, stringView(arena, sourceId), options) as Int + .invokeNative(map, stringView(arena, sourceId), options, completion) as Int ) } } @@ -869,7 +872,7 @@ internal object NativeAccess { sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewCanonicalTileIdAddressStatusFunction( @@ -880,6 +883,7 @@ internal object NativeAccess { stringView(arena, sourceId), canonicalTileId(arena, tileId), byteArrayView(arena, data), + completion, ) as Int ) } @@ -890,7 +894,7 @@ internal object NativeAccess { sourceId: String, tileId: CanonicalTileId, message: String, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewCanonicalTileIdAddressStatusFunction( @@ -901,6 +905,7 @@ internal object NativeAccess { stringView(arena, sourceId), canonicalTileId(arena, tileId), stringView(arena, message), + completion, ) as Int ) } @@ -910,13 +915,18 @@ internal object NativeAccess { map: NativeMap, sourceId: String, tileId: CanonicalTileId, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewCanonicalTileIdStatusFunction( "mln_map_invalidate_custom_mvt_vector_source_tile" ) - .invokeNative(map, stringView(arena, sourceId), canonicalTileId(arena, tileId)) as Int + .invokeNative( + map, + stringView(arena, sourceId), + canonicalTileId(arena, tileId), + completion, + ) as Int ) } } @@ -925,11 +935,12 @@ internal object NativeAccess { map: NativeMap, sourceId: String, bounds: LatLngBounds, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewLatLngBoundsStatusFunction("mln_map_invalidate_custom_geometry_source_region") - .invokeNative(map, stringView(arena, sourceId), latLngBounds(arena, bounds)) as Int + .invokeNative(map, stringView(arena, sourceId), latLngBounds(arena, bounds), completion) + as Int ) } } @@ -939,8 +950,8 @@ internal object NativeAccess { sourceId: String, url: String, options: TileSourceOptions?, - ) { - addTileSourceUrl("mln_map_add_vector_source_url", map, sourceId, url, options) + ): Deferred = command { completion -> + addTileSourceUrl("mln_map_add_vector_source_url", map, sourceId, url, options, completion) } internal fun addVectorSourceTiles( @@ -948,8 +959,8 @@ internal object NativeAccess { sourceId: String, tiles: List, options: TileSourceOptions?, - ) { - addTileSourceTiles("mln_map_add_vector_source_tiles", map, sourceId, tiles, options) + ): Deferred = command { completion -> + addTileSourceTiles("mln_map_add_vector_source_tiles", map, sourceId, tiles, options, completion) } internal fun addRasterSourceUrl( @@ -957,8 +968,8 @@ internal object NativeAccess { sourceId: String, url: String, options: TileSourceOptions?, - ) { - addTileSourceUrl("mln_map_add_raster_source_url", map, sourceId, url, options) + ): Deferred = command { completion -> + addTileSourceUrl("mln_map_add_raster_source_url", map, sourceId, url, options, completion) } internal fun addRasterSourceTiles( @@ -966,8 +977,8 @@ internal object NativeAccess { sourceId: String, tiles: List, options: TileSourceOptions?, - ) { - addTileSourceTiles("mln_map_add_raster_source_tiles", map, sourceId, tiles, options) + ): Deferred = command { completion -> + addTileSourceTiles("mln_map_add_raster_source_tiles", map, sourceId, tiles, options, completion) } internal fun addRasterDemSourceUrl( @@ -975,8 +986,8 @@ internal object NativeAccess { sourceId: String, url: String, options: TileSourceOptions?, - ) { - addTileSourceUrl("mln_map_add_raster_dem_source_url", map, sourceId, url, options) + ): Deferred = command { completion -> + addTileSourceUrl("mln_map_add_raster_dem_source_url", map, sourceId, url, options, completion) } internal fun addRasterDemSourceTiles( @@ -984,8 +995,15 @@ internal object NativeAccess { sourceId: String, tiles: List, options: TileSourceOptions?, - ) { - addTileSourceTiles("mln_map_add_raster_dem_source_tiles", map, sourceId, tiles, options) + ): Deferred = command { completion -> + addTileSourceTiles( + "mln_map_add_raster_dem_source_tiles", + map, + sourceId, + tiles, + options, + completion, + ) } internal fun setStyleImage( @@ -993,7 +1011,7 @@ internal object NativeAccess { imageId: String, image: PremultipliedRgba8Image, options: StyleImageOptions, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewTwoAddressStatusFunction("mln_map_set_style_image") @@ -1002,84 +1020,82 @@ internal object NativeAccess { stringView(arena, imageId), premultipliedRgba8Image(arena, image), styleImageOptions(arena, options), + completion, ) as Int ) } } - internal fun removeStyleImage(map: NativeMap, imageId: String): Boolean = - Arena.ofConfined().use { arena -> - val outRemoved = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_remove_style_image") - .invokeNative(map, stringView(arena, imageId), outRemoved) as Int - ) - outRemoved.get(ValueLayout.JAVA_BOOLEAN, 0) - } - - internal fun styleImageExists(map: NativeMap, imageId: String): Boolean = - Arena.ofConfined().use { arena -> - val outExists = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_style_image_exists") - .invokeNative(map, stringView(arena, imageId), outExists) as Int - ) - outExists.get(ValueLayout.JAVA_BOOLEAN, 0) + internal fun removeStyleImage(map: NativeMap, imageId: String): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + downcall("mln_map_remove_style_image") + .invokeNative(map, stringView(arena, imageId), completion) as Int + ) + } } - internal fun styleImageInfo(map: NativeMap, imageId: String): StyleImageInfo? = + internal fun styleImageInfo(map: NativeMap, imageId: String): Deferred = Arena.ofConfined().use { arena -> - val outInfo = styleImageInfoDefault(arena) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewTwoAddressStatusFunction("mln_map_get_style_image_info") - .invokeNative(map, stringView(arena, imageId), outInfo, outFound) as Int + CompletionBridge.submit( + { result -> + if (mln_completion_result.value_count(result) == 0L) null + else + styleImageInfo( + mln_style_image_result.info(completionValue(result, mln_style_image_result.sizeof())) + ) + }, + { completion -> + MapLibreNativeC.mln_map_get_style_image_info( + map.raw, + stringView(arena, imageId), + completion, + ) + }, ) - if (outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) styleImageInfo(outInfo) else null } - internal fun copyStyleImagePremultipliedRgba8(map: NativeMap, imageId: String): StyleImage? { - val info = styleImageInfo(map, imageId) ?: return null - return Arena.ofConfined().use { arena -> - val outPixels = arena.allocate(info.byteLength) - val outByteLength = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressLongTwoAddressStatusFunction( - "mln_map_copy_style_image_premultiplied_rgba8" - ) - .invokeNative( - map, + internal fun copyStyleImagePremultipliedRgba8( + map: NativeMap, + imageId: String, + ): Deferred = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + { result -> + if (mln_completion_result.value_count(result) == 0L) null + else { + val raw = completionValue(result, mln_style_image_result.sizeof()) + val info = styleImageInfo(mln_style_image_result.info(raw)) + val pixels = mln_style_image_result.pixels(raw) + StyleImage( + PremultipliedRgba8Image( + info.width, + info.height, + info.stride, + copyBytes(mln_buffer_view.data(pixels), mln_buffer_view.size(pixels)), + ), + info.pixelRatio, + info.sdf, + ) + } + }, + { completion -> + MapLibreNativeC.mln_map_get_style_image_info( + map.raw, stringView(arena, imageId), - outPixels, - info.byteLength, - outByteLength, - outFound, - ) as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - return@use null - } - val byteLength = outByteLength.get(ValueLayout.JAVA_LONG, 0) - StyleImage( - PremultipliedRgba8Image( - info.width, - info.height, - info.stride, - copyBytes(outPixels, byteLength), - ), - info.pixelRatio, - info.sdf, + completion, + ) + }, ) } - } internal fun addImageSourceUrl( map: NativeMap, sourceId: String, coordinates: List, url: String, - ) { + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() Arena.ofConfined().use { arena -> Status.check( @@ -1090,6 +1106,7 @@ internal object NativeAccess { latLngArray(arena, coordinateSnapshot), coordinateSnapshot.size.toLong(), stringView(arena, url), + completion, ) as Int ) } @@ -1100,7 +1117,7 @@ internal object NativeAccess { sourceId: String, coordinates: List, image: PremultipliedRgba8Image, - ) { + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() Arena.ofConfined().use { arena -> Status.check( @@ -1111,16 +1128,21 @@ internal object NativeAccess { latLngArray(arena, coordinateSnapshot), coordinateSnapshot.size.toLong(), premultipliedRgba8Image(arena, image), + completion, ) as Int ) } } - internal fun setImageSourceUrl(map: NativeMap, sourceId: String, url: String) { + internal fun setImageSourceUrl( + map: NativeMap, + sourceId: String, + url: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsStatusFunction("mln_map_set_image_source_url") - .invokeNative(map, stringView(arena, sourceId), stringView(arena, url)) as Int + .invokeNative(map, stringView(arena, sourceId), stringView(arena, url), completion) as Int ) } } @@ -1129,12 +1151,16 @@ internal object NativeAccess { map: NativeMap, sourceId: String, image: PremultipliedRgba8Image, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_set_image_source_image") - .invokeNative(map, stringView(arena, sourceId), premultipliedRgba8Image(arena, image)) - as Int + .invokeNative( + map, + stringView(arena, sourceId), + premultipliedRgba8Image(arena, image), + completion, + ) as Int ) } } @@ -1143,7 +1169,7 @@ internal object NativeAccess { map: NativeMap, sourceId: String, coordinates: List, - ) { + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() Arena.ofConfined().use { arena -> Status.check( @@ -1153,90 +1179,103 @@ internal object NativeAccess { stringView(arena, sourceId), latLngArray(arena, coordinateSnapshot), coordinateSnapshot.size.toLong(), + completion, ) as Int ) } } - internal fun imageSourceCoordinates(map: NativeMap, sourceId: String): List? = + internal fun imageSourceCoordinates(map: NativeMap, sourceId: String): Deferred?> = Arena.ofConfined().use { arena -> - val outCoordinates = arena.allocate(latLngLayout.byteSize() * IMAGE_SOURCE_COORDINATE_COUNT) - val outCoordinateCount = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressLongTwoAddressStatusFunction("mln_map_get_image_source_coordinates") - .invokeNative( - map, + CompletionBridge.submit( + { result -> + val count = checkedInt(mln_completion_result.value_count(result)) + if (count == 0) null + else latLngArray(completionValue(result, mln_lat_lng.sizeof() * count.toLong()), count) + }, + { completion -> + MapLibreNativeC.mln_map_get_image_source_coordinates( + map.raw, stringView(arena, sourceId), - outCoordinates, - IMAGE_SOURCE_COORDINATE_COUNT.toLong(), - outCoordinateCount, - outFound, - ) as Int + completion, + ) + }, ) - if (outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - latLngArray( - outCoordinates, - Math.toIntExact(outCoordinateCount.get(ValueLayout.JAVA_LONG, 0)), - ) - } else null } - internal fun addStyleLayerJson(map: NativeMap, layerJson: ByteArray, beforeLayerId: String) { + internal fun addStyleLayerJson( + map: NativeMap, + layerJson: ByteArray, + beforeLayerId: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapAddressStringViewStatusFunction("mln_map_add_style_layer_json") - .invokeNative(map, byteArrayView(arena, layerJson), stringView(arena, beforeLayerId)) - as Int + .invokeNative( + map, + byteArrayView(arena, layerJson), + stringView(arena, beforeLayerId), + completion, + ) as Int ) } } - internal fun removeStyleLayer(map: NativeMap, layerId: String): Boolean = - Arena.ofConfined().use { arena -> - val outRemoved = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_remove_style_layer") - .invokeNative(map, stringView(arena, layerId), outRemoved) as Int - ) - outRemoved.get(ValueLayout.JAVA_BOOLEAN, 0) + internal fun removeStyleLayer(map: NativeMap, layerId: String): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + downcall("mln_map_remove_style_layer") + .invokeNative(map, stringView(arena, layerId), completion) as Int + ) + } } - internal fun styleLayerExists(map: NativeMap, layerId: String): Boolean = - Arena.ofConfined().use { arena -> - val outExists = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewAddressStatusFunction("mln_map_style_layer_exists") - .invokeNative(map, stringView(arena, layerId), outExists) as Int + internal fun styleLayerInfo(map: NativeMap, layerId: String): Deferred = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + { result -> + if (mln_completion_result.value_count(result) == 0L) null + else { + val raw = completionValue(result, mln_style_layer_result.sizeof()) + val info = mln_style_layer_result.info(raw) + val fields = mln_style_layer_info.fields(info) + LayerInfo( + stringView(mln_style_layer_info.type(info)), + mln_style_layer_info.min_zoom(info), + mln_style_layer_info.max_zoom(info), + StyleLayerVisibility.fromNative(mln_style_layer_info.visibility(info)), + if (fields and STYLE_LAYER_INFO_SOURCE_ID != 0) + stringView(mln_style_layer_result.source_id(raw)) + else null, + if (fields and STYLE_LAYER_INFO_SOURCE_LAYER != 0) + stringView(mln_style_layer_result.source_layer(raw)) + else null, + ) + } + }, + { completion -> + MapLibreNativeC.mln_map_get_style_layer_info( + map.raw, + stringView(arena, layerId), + completion, + ) + }, ) - outExists.get(ValueLayout.JAVA_BOOLEAN, 0) } - internal fun styleLayerType(map: NativeMap, layerId: String): String? = - Arena.ofConfined().use { arena -> - val outType = arena.allocate(STRING_VIEW_SIZE) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapStringViewTwoAddressStatusFunction("mln_map_get_style_layer_type") - .invokeNative(map, stringView(arena, layerId), outType, outFound) as Int - ) - if (outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) stringView(outType) else null - } - - internal fun styleLayerIds(map: NativeMap): List = - Arena.ofConfined().use { arena -> - val outList = arena.allocate(ValueLayout.JAVA_LONG) - outList.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(mapListStyleLayerIdsFunction().invokeNative(map, outList) as Int) - styleIdList(NativeStyleIdList(outList.get(ValueLayout.JAVA_LONG, 0))) - } + internal fun styleLayerIds(map: NativeMap): Deferred> = + CompletionBridge.submit( + ::stringViewsCompletion, + { completion -> MapLibreNativeC.mln_map_list_style_layer_ids(map.raw, completion) }, + ) internal fun addHillshadeLayer( map: NativeMap, layerId: String, sourceId: String, beforeLayerId: String, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapThreeStringViewsStatusFunction("mln_map_add_hillshade_layer") @@ -1245,6 +1284,7 @@ internal object NativeAccess { stringView(arena, layerId), stringView(arena, sourceId), stringView(arena, beforeLayerId), + completion, ) as Int ) } @@ -1255,7 +1295,7 @@ internal object NativeAccess { layerId: String, sourceId: String, beforeLayerId: String, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapThreeStringViewsStatusFunction("mln_map_add_color_relief_layer") @@ -1264,16 +1304,26 @@ internal object NativeAccess { stringView(arena, layerId), stringView(arena, sourceId), stringView(arena, beforeLayerId), + completion, ) as Int ) } } - internal fun addLocationIndicatorLayer(map: NativeMap, layerId: String, beforeLayerId: String) { + internal fun addLocationIndicatorLayer( + map: NativeMap, + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsStatusFunction("mln_map_add_location_indicator_layer") - .invokeNative(map, stringView(arena, layerId), stringView(arena, beforeLayerId)) as Int + .invokeNative( + map, + stringView(arena, layerId), + stringView(arena, beforeLayerId), + completion, + ) as Int ) } } @@ -1283,29 +1333,43 @@ internal object NativeAccess { layerId: String, coordinate: LatLng, altitude: Double, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewLatLngDoubleStatusFunction("mln_map_set_location_indicator_location") - .invokeNative(map, stringView(arena, layerId), latLng(coordinate, arena), altitude) as Int + .invokeNative( + map, + stringView(arena, layerId), + latLng(coordinate, arena), + altitude, + completion, + ) as Int ) } } - internal fun setLocationIndicatorBearing(map: NativeMap, layerId: String, bearing: Double) { + internal fun setLocationIndicatorBearing( + map: NativeMap, + layerId: String, + bearing: Double, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewDoubleStatusFunction("mln_map_set_location_indicator_bearing") - .invokeNative(map, stringView(arena, layerId), bearing) as Int + .invokeNative(map, stringView(arena, layerId), bearing, completion) as Int ) } } - internal fun setLocationIndicatorAccuracyRadius(map: NativeMap, layerId: String, radius: Double) { + internal fun setLocationIndicatorAccuracyRadius( + map: NativeMap, + layerId: String, + radius: Double, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewDoubleStatusFunction("mln_map_set_location_indicator_accuracy_radius") - .invokeNative(map, stringView(arena, layerId), radius) as Int + .invokeNative(map, stringView(arena, layerId), radius, completion) as Int ) } } @@ -1315,7 +1379,7 @@ internal object NativeAccess { layerId: String, imageKind: LocationIndicatorImageKind, imageId: String, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewIntStringViewStatusFunction("mln_map_set_location_indicator_image_name") @@ -1324,88 +1388,114 @@ internal object NativeAccess { stringView(arena, layerId), imageKind.nativeValue, stringView(arena, imageId), + completion, ) as Int ) } } - internal fun moveStyleLayer(map: NativeMap, layerId: String, beforeLayerId: String) { + internal fun moveStyleLayer( + map: NativeMap, + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsStatusFunction("mln_map_move_style_layer") - .invokeNative(map, stringView(arena, layerId), stringView(arena, beforeLayerId)) as Int + .invokeNative( + map, + stringView(arena, layerId), + stringView(arena, beforeLayerId), + completion, + ) as Int ) } } - internal fun styleLayerJson(map: NativeMap, layerId: String): ByteArray? = + internal fun styleLayerJson(map: NativeMap, layerId: String): Deferred = Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapStringViewTwoAddressStatusFunction("mln_map_get_style_layer_json") - .invokeNative(map, stringView(arena, layerId), outSnapshot, outFound) as Int + CompletionBridge.submit( + ::optionalBufferCompletion, + { completion -> + MapLibreNativeC.mln_map_get_style_layer_json( + map.raw, + stringView(arena, layerId), + completion, + ) + }, ) - if (outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - ownedBuffer(NativeOwnedBuffer(outSnapshot.get(ValueLayout.JAVA_LONG, 0))) - } else null } - internal fun setStyleLightJson(map: NativeMap, lightJson: ByteArray) { + internal fun setStyleLightJson( + map: NativeMap, + lightJson: ByteArray, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapAddressStatusFunction("mln_map_set_style_light_json") - .invokeNative(map, byteArrayView(arena, lightJson)) as Int + .invokeNative(map, byteArrayView(arena, lightJson), completion) as Int ) } } - internal fun setStyleLightProperty(map: NativeMap, propertyName: String, value: ByteArray) { + internal fun setStyleLightProperty( + map: NativeMap, + propertyName: String, + value: ByteArray, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_set_style_light_property") - .invokeNative(map, stringView(arena, propertyName), byteArrayView(arena, value)) as Int + .invokeNative( + map, + stringView(arena, propertyName), + byteArrayView(arena, value), + completion, + ) as Int ) } } - internal fun styleLightProperty(map: NativeMap, propertyName: String): ByteArray? = + internal fun styleLightProperty(map: NativeMap, propertyName: String): Deferred = Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapStringViewAddressStatusFunction("mln_map_get_style_light_property") - .invokeNative(map, stringView(arena, propertyName), outSnapshot) as Int + CompletionBridge.submit( + ::optionalBufferCompletion, + { completion -> + MapLibreNativeC.mln_map_get_style_light_property( + map.raw, + stringView(arena, propertyName), + completion, + ) + }, ) - ownedBuffer(NativeOwnedBuffer(outSnapshot.get(ValueLayout.JAVA_LONG, 0))) } - internal fun setStyleTransitionOptions(map: NativeMap, options: StyleTransitionOptions) { + internal fun setStyleTransitionOptions( + map: NativeMap, + options: StyleTransitionOptions, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapAddressStatusFunction("mln_map_set_style_transition_options") - .invokeNative(map, styleTransitionOptions(arena, options)) as Int + .invokeNative(map, styleTransitionOptions(arena, options), completion) as Int ) } } - internal fun styleTransitionOptions(map: NativeMap): StyleTransitionOptions = - Arena.ofConfined().use { arena -> - val outOptions = styleTransitionOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_style_transition_options") - .invokeNative(map, outOptions) as Int - ) - styleTransitionOptions(outOptions) - } + internal fun styleTransitionOptions(map: NativeMap): Deferred = + CompletionBridge.submit( + { result -> + styleTransitionOptions(completionValue(result, mln_style_transition_options.sizeof())) + }, + { completion -> MapLibreNativeC.mln_map_get_style_transition_options(map.raw, completion) }, + ) internal fun setLayerProperty( map: NativeMap, layerId: String, propertyName: String, value: ByteArray, - ) { + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapTwoStringViewsAddressStatusFunction("mln_map_set_layer_property") @@ -1414,54 +1504,63 @@ internal object NativeAccess { stringView(arena, layerId), stringView(arena, propertyName), byteArrayView(arena, value), + completion, ) as Int ) } } - internal fun layerProperty(map: NativeMap, layerId: String, propertyName: String): ByteArray? = + internal fun layerProperty( + map: NativeMap, + layerId: String, + propertyName: String, + ): Deferred = Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapTwoStringViewsAddressStatusFunction("mln_map_get_layer_property") - .invokeNative( - map, + CompletionBridge.submit( + ::optionalBufferCompletion, + { completion -> + MapLibreNativeC.mln_map_get_layer_property( + map.raw, stringView(arena, layerId), stringView(arena, propertyName), - outSnapshot, - ) as Int + completion, + ) + }, ) - ownedBuffer(NativeOwnedBuffer(outSnapshot.get(ValueLayout.JAVA_LONG, 0))) } - internal fun setLayerFilter(map: NativeMap, layerId: String, filter: ByteArray) { + internal fun setLayerFilter( + map: NativeMap, + layerId: String, + filter: ByteArray, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewAddressStatusFunction("mln_map_set_layer_filter") - .invokeNative(map, stringView(arena, layerId), byteArrayView(arena, filter)) as Int + .invokeNative(map, stringView(arena, layerId), byteArrayView(arena, filter), completion) + as Int ) } } - internal fun clearLayerFilter(map: NativeMap, layerId: String) { - Arena.ofConfined().use { arena -> - Status.check( - mapStringViewAddressStatusFunction("mln_map_set_layer_filter") - .invokeNative(map, stringView(arena, layerId), MemorySegment.NULL) as Int - ) + internal fun clearLayerFilter(map: NativeMap, layerId: String): Deferred = + command { completion -> + Arena.ofConfined().use { arena -> + Status.check( + mapStringViewAddressStatusFunction("mln_map_set_layer_filter") + .invokeNative(map, stringView(arena, layerId), MemorySegment.NULL, completion) as Int + ) + } } - } - internal fun layerFilter(map: NativeMap, layerId: String): ByteArray? = + internal fun layerFilter(map: NativeMap, layerId: String): Deferred = Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapStringViewAddressStatusFunction("mln_map_get_layer_filter") - .invokeNative(map, stringView(arena, layerId), outSnapshot) as Int + CompletionBridge.submit( + ::optionalBufferCompletion, + { completion -> + MapLibreNativeC.mln_map_get_layer_filter(map.raw, stringView(arena, layerId), completion) + }, ) - ownedBuffer(NativeOwnedBuffer(outSnapshot.get(ValueLayout.JAVA_LONG, 0))) } private fun stretchArray(arena: Arena, stretches: List): MemorySegment { @@ -1475,56 +1574,34 @@ internal object NativeAccess { return array } - /** - * Probes the required interval counts, then copies. Null arrays with zero capacity are a size - * probe the C API answers with OK. - */ internal fun styleImageStretches( map: NativeMap, imageId: String, - ): Pair, List>? = - Arena.ofConfined().use { arena -> - val function = downcall("mln_map_copy_style_image_stretches") - val outXCount = arena.allocate(ValueLayout.JAVA_LONG) - val outYCount = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - function.invokeNative( - map, - stringView(arena, imageId), - MemorySegment.NULL, - 0L, - outXCount, - MemorySegment.NULL, - 0L, - outYCount, - outFound, - ) as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - return@use null - } - - val xCount = outXCount.get(ValueLayout.JAVA_LONG, 0) - val yCount = outYCount.get(ValueLayout.JAVA_LONG, 0) - val rawX = - if (xCount == 0L) MemorySegment.NULL else arena.allocate(IMAGE_STRETCH_SIZE * xCount) - val rawY = - if (yCount == 0L) MemorySegment.NULL else arena.allocate(IMAGE_STRETCH_SIZE * yCount) - Status.check( - function.invokeNative( - map, - stringView(arena, imageId), - rawX, - xCount, - outXCount, - rawY, - yCount, - outYCount, - outFound, - ) as Int + ): Deferred, List>?> = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + { result -> + if (mln_completion_result.value_count(result) == 0L) null + else { + val value = completionValue(result, mln_style_image_stretches_result.sizeof()) + readStretches( + mln_style_image_stretches_result.stretch_x(value), + mln_style_image_stretches_result.stretch_x_count(value), + ) to + readStretches( + mln_style_image_stretches_result.stretch_y(value), + mln_style_image_stretches_result.stretch_y_count(value), + ) + } + }, + { completion -> + MapLibreNativeC.mln_map_copy_style_image_stretches( + map.raw, + stringView(arena, imageId), + completion, + ) + }, ) - readStretches(rawX, xCount) to readStretches(rawY, yCount) } private fun readStretches(array: MemorySegment, count: Long): List = @@ -1536,813 +1613,744 @@ internal object NativeAccess { ) } - internal fun setLayerSourceLayer(map: NativeMap, layerId: String, sourceLayer: String) { + internal fun setLayerSourceLayer( + map: NativeMap, + layerId: String, + sourceLayer: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> - Status.check( - mapTwoStringViewsStatusFunction("mln_map_set_layer_source_layer") - .invokeNative(map, stringView(arena, layerId), stringView(arena, sourceLayer)) as Int - ) + mapTwoStringViewsStatusFunction("mln_map_set_layer_source_layer") + .invokeNative( + map.raw, + stringView(arena, layerId), + stringView(arena, sourceLayer), + completion, + ) as Int } } - internal fun layerSourceLayer(map: NativeMap, layerId: String): String = - copyLayerText(map, layerId, "mln_map_copy_layer_source_layer") + internal fun layerSourceLayer(map: NativeMap, layerId: String): Deferred = + layerText(map, layerId, MapLibreNativeC::mln_map_copy_layer_source_layer) - internal fun setLayerSourceId(map: NativeMap, layerId: String, sourceId: String) { + internal fun setLayerSourceId( + map: NativeMap, + layerId: String, + sourceId: String, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> - Status.check( - mapTwoStringViewsStatusFunction("mln_map_set_layer_source_id") - .invokeNative(map, stringView(arena, layerId), stringView(arena, sourceId)) as Int - ) + mapTwoStringViewsStatusFunction("mln_map_set_layer_source_id") + .invokeNative(map.raw, stringView(arena, layerId), stringView(arena, sourceId), completion) + as Int } } - internal fun layerSourceId(map: NativeMap, layerId: String): String = - copyLayerText(map, layerId, "mln_map_copy_layer_source_id") + internal fun layerSourceId(map: NativeMap, layerId: String): Deferred = + layerText(map, layerId, MapLibreNativeC::mln_map_copy_layer_source_id) - /** - * Probes the required length, then copies. A null buffer with zero capacity is a size probe the C - * API answers with OK. - */ - internal fun loadedStyleJson(map: NativeMap): ByteArray = - copyMapBytes(map, "mln_map_copy_loaded_style_json") + internal fun loadedStyleJson(map: NativeMap): Deferred = + styleBuffer(map, MapLibreNativeC::mln_map_loaded_style_json) - internal fun styleUrl(map: NativeMap): String = copyMapText(map, "mln_map_copy_style_url") + internal fun styleUrl(map: NativeMap): Deferred = + CompletionBridge.submit( + { result -> String(requiredBufferCompletion(result), StandardCharsets.UTF_8) }, + { completion -> MapLibreNativeC.mln_map_style_url(map.raw, completion) }, + ) - private fun copyMapBytes(map: NativeMap, name: String): ByteArray = - Arena.ofConfined().use { arena -> - val function = downcall(name) - val outSize = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(function.invokeNative(map, MemorySegment.NULL, 0L, outSize) as Int) - val required = outSize.get(ValueLayout.JAVA_LONG, 0) - if (required == 0L) return@use byteArrayOf() - val buffer = arena.allocate(required) - val outCopied = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(function.invokeNative(map, buffer, required, outCopied) as Int) - buffer.asSlice(0, outCopied.get(ValueLayout.JAVA_LONG, 0)).toArray(ValueLayout.JAVA_BYTE) - } - - private fun copyMapText(map: NativeMap, name: String): String = - Arena.ofConfined().use { arena -> - val function = downcall(name) - val outSize = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(function.invokeNative(map, MemorySegment.NULL, 0L, outSize) as Int) - val required = outSize.get(ValueLayout.JAVA_LONG, 0) - if (required == 0L) { - "" - } else { - val buffer = arena.allocate(required) - val outCopied = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(function.invokeNative(map, buffer, required, outCopied) as Int) - val copied = outCopied.get(ValueLayout.JAVA_LONG, 0) - buffer.asSlice(0, copied).toArray(ValueLayout.JAVA_BYTE).decodeToString() - } + private fun styleBuffer( + map: NativeMap, + submit: (Long, MemorySegment) -> Int, + ): Deferred = + CompletionBridge.submit(::requiredBufferCompletion) { completion -> + submit(map.raw, completion) } - private fun copyLayerText(map: NativeMap, layerId: String, name: String): String = + private fun layerText( + map: NativeMap, + layerId: String, + submit: (Long, MemorySegment, MemorySegment) -> Int, + ): Deferred = Arena.ofConfined().use { arena -> - val function = mapStringViewAddressLongAddressStatusFunction(name) - val outSize = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - function.invokeNative(map, stringView(arena, layerId), MemorySegment.NULL, 0L, outSize) - as Int + CompletionBridge.submit( + { result -> String(requiredBufferCompletion(result), StandardCharsets.UTF_8) }, + { completion -> submit(map.raw, stringView(arena, layerId), completion) }, ) - val required = outSize.get(ValueLayout.JAVA_LONG, 0) - if (required == 0L) { - "" - } else { - val buffer = arena.allocate(required) - val outCopied = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - function.invokeNative(map, stringView(arena, layerId), buffer, required, outCopied) as Int - ) - val copied = outCopied.get(ValueLayout.JAVA_LONG, 0) - buffer.asSlice(0, copied).toArray(ValueLayout.JAVA_BYTE).decodeToString() - } } - internal fun setLayerMinZoom(map: NativeMap, layerId: String, minZoom: Double) { + internal fun setLayerMinZoom( + map: NativeMap, + layerId: String, + minZoom: Double, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewDoubleStatusFunction("mln_map_set_layer_min_zoom") - .invokeNative(map, stringView(arena, layerId), minZoom) as Int + .invokeNative(map, stringView(arena, layerId), minZoom, completion) as Int ) } } - internal fun layerMinZoom(map: NativeMap, layerId: String): Double = - layerZoom(map, layerId, "mln_map_get_layer_min_zoom") - - internal fun setLayerMaxZoom(map: NativeMap, layerId: String, maxZoom: Double) { + internal fun setLayerMaxZoom( + map: NativeMap, + layerId: String, + maxZoom: Double, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> - Status.check( - mapStringViewDoubleStatusFunction("mln_map_set_layer_max_zoom") - .invokeNative(map, stringView(arena, layerId), maxZoom) as Int - ) + mapStringViewDoubleStatusFunction("mln_map_set_layer_max_zoom") + .invokeNative(map.raw, stringView(arena, layerId), maxZoom, completion) as Int } } - internal fun layerMaxZoom(map: NativeMap, layerId: String): Double = - layerZoom(map, layerId, "mln_map_get_layer_max_zoom") - - private fun layerZoom(map: NativeMap, layerId: String, name: String): Double = - Arena.ofConfined().use { arena -> - val outZoom = arena.allocate(ValueLayout.JAVA_DOUBLE) - Status.check( - mapStringViewAddressStatusFunction(name) - .invokeNative(map, stringView(arena, layerId), outZoom) as Int - ) - outZoom.get(ValueLayout.JAVA_DOUBLE, 0) - } - - internal fun setLayerVisibility(map: NativeMap, layerId: String, visibility: Int) { + internal fun setLayerVisibility( + map: NativeMap, + layerId: String, + visibility: Int, + ): Deferred = command { completion -> Arena.ofConfined().use { arena -> Status.check( mapStringViewIntStatusFunction("mln_map_set_layer_visibility") - .invokeNative(map, stringView(arena, layerId), visibility) as Int - ) - } - } - - internal fun layerVisibility(map: NativeMap, layerId: String): Int = - Arena.ofConfined().use { arena -> - val outVisibility = arena.allocate(ValueLayout.JAVA_INT) - Status.check( - mapStringViewAddressStatusFunction("mln_map_get_layer_visibility") - .invokeNative(map, stringView(arena, layerId), outVisibility) as Int + .invokeNative(map, stringView(arena, layerId), visibility, completion) as Int ) - outVisibility.get(ValueLayout.JAVA_INT, 0) } - - internal fun requestRepaint(map: NativeMap) { - Status.check(mapStatusFunction("mln_map_request_repaint").invokeNative(map) as Int) - } - - internal fun requestStillImage(map: NativeMap) { - Status.check(mapStatusFunction("mln_map_request_still_image").invokeNative(map) as Int) } - internal fun setDebugOptions(map: NativeMap, options: Set) { - val mask = options.fold(0) { acc, option -> acc or option.nativeMask } - Status.check(mapIntStatusFunction("mln_map_set_debug_options").invokeNative(map, mask) as Int) - } - - internal fun debugOptions(map: NativeMap): Set = - Arena.ofConfined().use { arena -> - val outOptions = arena.allocate(ValueLayout.JAVA_INT) - Status.check( - mapAddressStatusFunction("mln_map_get_debug_options").invokeNative(map, outOptions) as Int - ) - debugOptions(outOptions.get(ValueLayout.JAVA_INT, 0)) - } - - internal fun setRenderingStatsViewEnabled(map: NativeMap, enabled: Boolean) { - Status.check( - mapBooleanStatusFunction("mln_map_set_rendering_stats_view_enabled") - .invokeNative(map, enabled) as Int - ) + internal fun requestRepaint(map: NativeMap): Deferred = command { outCommand -> + MapLibreNativeC.mln_map_request_repaint(map.raw, outCommand) } - internal fun renderingStatsViewEnabled(map: NativeMap): Boolean = - Arena.ofConfined().use { arena -> - val outEnabled = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapAddressStatusFunction("mln_map_get_rendering_stats_view_enabled") - .invokeNative(map, outEnabled) as Int - ) - outEnabled.get(ValueLayout.JAVA_BOOLEAN, 0) + internal fun requestStillImage(map: NativeMap): Deferred = + command { completion -> + MapLibreNativeC.mln_map_request_still_image(map.raw, completion) } - internal fun isFullyLoaded(map: NativeMap): Boolean = + internal fun mapSnapshot(map: NativeMap): MapSnapshot = Arena.ofConfined().use { arena -> - val outLoaded = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapAddressStatusFunction("mln_map_is_fully_loaded").invokeNative(map, outLoaded) as Int + val snapshot = mln_map_snapshot.allocate(arena) + mln_map_snapshot.size(snapshot, mln_map_snapshot.sizeof().toInt()) + Status.check(MapLibreNativeC.mln_map_snapshot_get(map.raw, snapshot)) + val extent = mln_map_snapshot.logical_extent(snapshot) + MapSnapshot( + generation = mln_map_snapshot.generation(snapshot), + camera = cameraOptions(mln_map_snapshot.camera(snapshot)), + size = + MapSize( + mln_logical_extent.width(extent), + mln_logical_extent.height(extent), + mln_logical_extent.scale_factor(extent), + ), + projectionMode = projectionModeOptions(mln_map_snapshot.projection_mode(snapshot)), + viewportOptions = readViewportOptions(mln_map_snapshot.viewport(snapshot)), + debugOptions = debugOptions(mln_map_snapshot.debug_options(snapshot)), + isFullyLoaded = mln_map_snapshot.fully_loaded(snapshot), + renderingStatsViewEnabled = mln_map_snapshot.rendering_stats_view_enabled(snapshot), + repaintDemand = mln_map_snapshot.repaint_demand(snapshot), + latestRenderUpdateGeneration = mln_map_snapshot.latest_render_update_generation(snapshot), + tileOptions = readTileOptions(mln_map_snapshot.tile(snapshot)), + bounds = boundOptions(mln_map_snapshot.bounds(snapshot)), + freeCameraOptions = readFreeCameraOptions(mln_map_snapshot.free_camera(snapshot)), ) - outLoaded.get(ValueLayout.JAVA_BOOLEAN, 0) } - internal fun dumpDebugLogs(map: NativeMap) { - Status.check(mapStatusFunction("mln_map_dump_debug_logs").invokeNative(map) as Int) + internal fun setDebugOptions( + map: NativeMap, + options: Set, + ): Deferred = command { outCommand -> + MapLibreNativeC.mln_map_set_debug_options(map.raw, debugOptionMask(options), outCommand) } - internal fun mapSize(map: NativeMap): MapSize = - Arena.ofConfined().use { arena -> - val outWidth = arena.allocate(ValueLayout.JAVA_INT) - val outHeight = arena.allocate(ValueLayout.JAVA_INT) - val outScaleFactor = arena.allocate(ValueLayout.JAVA_DOUBLE) - Status.check( - mapAddressAddressAddressStatusFunction("mln_map_get_size") - .invokeNative(map, outWidth, outHeight, outScaleFactor) as Int - ) - MapSize( - outWidth.get(ValueLayout.JAVA_INT, 0), - outHeight.get(ValueLayout.JAVA_INT, 0), - outScaleFactor.get(ValueLayout.JAVA_DOUBLE, 0), - ) - } + internal fun setRenderingStatsViewEnabled( + map: NativeMap, + enabled: Boolean, + ): Deferred = command { outCommand -> + MapLibreNativeC.mln_map_set_rendering_stats_view_enabled(map.raw, enabled, outCommand) + } - internal fun viewportOptions(map: NativeMap): ViewportOptions = + internal fun setViewportOptions( + map: NativeMap, + options: ViewportOptions, + ): Deferred = Arena.ofConfined().use { arena -> - val outOptions = viewportOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_viewport_options").invokeNative(map, outOptions) - as Int - ) - readViewportOptions(outOptions) + val native = viewportOptions(arena, options) + command { outCommand -> + MapLibreNativeC.mln_map_set_viewport_options(map.raw, native, outCommand) + } } - internal fun setViewportOptions(map: NativeMap, options: ViewportOptions) { + internal fun setTileOptions(map: NativeMap, options: TileOptions): Deferred = Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_set_viewport_options") - .invokeNative(map, viewportOptions(arena, options)) as Int - ) + val native = tileOptions(arena, options) + command { outCommand -> + MapLibreNativeC.mln_map_set_tile_options(map.raw, native, outCommand) + } } - } - internal fun tileOptions(map: NativeMap): TileOptions = + internal fun setBounds(map: NativeMap, options: BoundOptions): Deferred = Arena.ofConfined().use { arena -> - val outOptions = tileOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_tile_options").invokeNative(map, outOptions) as Int - ) - readTileOptions(outOptions) + val native = boundOptions(arena, options) + command { outCommand -> MapLibreNativeC.mln_map_set_bounds(map.raw, native, outCommand) } } - internal fun setTileOptions(map: NativeMap, options: TileOptions) { + internal fun setFreeCameraOptions( + map: NativeMap, + options: FreeCameraOptions, + ): Deferred = Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_set_tile_options") - .invokeNative(map, tileOptions(arena, options)) as Int - ) + val native = freeCameraOptions(arena, options) + command { outCommand -> + MapLibreNativeC.mln_map_set_free_camera_options(map.raw, native, outCommand) + } } - } - internal fun projectionMode(map: NativeMap): ProjectionModeOptions = + internal fun resizeMap(map: NativeMap, size: MapSize): Deferred = Arena.ofConfined().use { arena -> - val outMode = projectionModeDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_projection_mode").invokeNative(map, outMode) as Int - ) - projectionModeOptions(outMode) + val extent = mln_logical_extent.allocate(arena) + mln_logical_extent.width(extent, size.width) + mln_logical_extent.height(extent, size.height) + mln_logical_extent.scale_factor(extent, size.scaleFactor) + command { outCommand -> + downcall("mln_map_resize").invokeNative(map.raw, extent, outCommand) as Int + } } - internal fun setProjectionMode(map: NativeMap, mode: ProjectionModeOptions) { + internal fun cameraSnapshot(map: NativeMap): CameraSnapshot = Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_set_projection_mode") - .invokeNative(map, projectionModeOptions(arena, mode)) as Int - ) + val outCamera = cameraOptionsDefault(arena) + val outGeneration = arena.allocate(ValueLayout.JAVA_LONG) + Status.check(MapLibreNativeC.mln_map_camera_snapshot_get(map.raw, outCamera, outGeneration)) + CameraSnapshot(outGeneration.get(ValueLayout.JAVA_LONG, 0), cameraOptions(outCamera)) } - } - internal fun camera(map: NativeMap): CameraOptions = + internal fun updateCamera(map: NativeMap, update: CameraUpdate): Deferred = Arena.ofConfined().use { arena -> - val outCamera = cameraOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_camera").invokeNative(map, outCamera) as Int - ) - cameraOptions(outCamera) + val nativeUpdate = MapLibreNativeC.mln_camera_update_default(arena) + mln_camera_update.mode(nativeUpdate, update.mode.nativeValue) + mln_camera_update.camera(nativeUpdate, cameraOptions(arena, update.camera)) + mln_camera_update.animation(nativeUpdate, animationOptions(arena, update.animation)) + mln_camera_update.gesture_phase(nativeUpdate, update.gesturePhase.nativeValue) + command { outCommand -> + MapLibreNativeC.mln_map_update_camera(map.raw, nativeUpdate, outCommand) + } } - internal fun jumpTo(map: NativeMap, camera: CameraOptions) { + internal fun applyCameraDelta(map: NativeMap, delta: CameraDelta): Deferred = Arena.ofConfined().use { arena -> - Status.check( - mapAddressStatusFunction("mln_map_jump_to").invokeNative(map, cameraOptions(arena, camera)) - as Int - ) + val nativeDelta = MapLibreNativeC.mln_camera_delta_default(arena) + mln_camera_delta.kind(nativeDelta, delta.kind.nativeValue) + mln_camera_delta.offset(nativeDelta, screenPoint(delta.offset, arena)) + mln_camera_delta.amount(nativeDelta, delta.amount) + mln_camera_delta.has_anchor(nativeDelta, delta.anchor != null) + delta.anchor?.let { mln_camera_delta.anchor(nativeDelta, screenPoint(it, arena)) } + mln_camera_delta.animation(nativeDelta, animationOptions(arena, delta.animation)) + command { outCommand -> + MapLibreNativeC.mln_map_apply_camera_delta(map.raw, nativeDelta, outCommand) + } } - } - - internal fun easeTo(map: NativeMap, camera: CameraOptions, animation: AnimationOptions?) { - mapCameraAnimationCommand("mln_map_ease_to", map, camera, animation) - } - internal fun flyTo(map: NativeMap, camera: CameraOptions, animation: AnimationOptions?) { - mapCameraAnimationCommand("mln_map_fly_to", map, camera, animation) - } - - internal fun moveBy(map: NativeMap, deltaX: Double, deltaY: Double) { - Status.check( - mapDoubleDoubleStatusFunction("mln_map_move_by").invokeNative(map, deltaX, deltaY) as Int + internal fun queryCamera(map: NativeMap): Deferred = + CompletionBridge.submit( + { completion -> + val result = completionValue(completion, mln_camera_query_result.sizeof()) + CameraSnapshot( + mln_camera_query_result.generation(result), + cameraOptions(mln_camera_query_result.camera(result)), + ) + }, + { completion -> MapLibreNativeC.mln_map_camera_query(map.raw, completion) }, ) - } - internal fun moveByAnimated( + internal fun attachMetalOwnedTexture( map: NativeMap, - deltaX: Double, - deltaY: Double, - animation: AnimationOptions?, - ) { - Arena.ofConfined().use { arena -> - Status.check( - mapDoubleDoubleAddressStatusFunction("mln_map_move_by_animated") - .invokeNative(map, deltaX, deltaY, animationOptions(arena, animation)) as Int - ) - } - } - - internal fun scaleBy(map: NativeMap, scale: Double, anchor: ScreenPoint?) { - Arena.ofConfined().use { arena -> - Status.check( - mapDoubleAddressStatusFunction("mln_map_scale_by") - .invokeNative(map, scale, anchor?.let { screenPoint(it, arena) } ?: MemorySegment.NULL) - as Int - ) - } - } + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_metal_owned_texture_attach", + metalOwnedTextureDescriptor(descriptor), + options, + ) - internal fun scaleByAnimated( + internal fun attachMetalBorrowedTexture( map: NativeMap, - scale: Double, - anchor: ScreenPoint?, - animation: AnimationOptions?, - ) { - Arena.ofConfined().use { arena -> - Status.check( - mapDoubleAddressAddressStatusFunction("mln_map_scale_by_animated") - .invokeNative( - map, - scale, - anchor?.let { screenPoint(it, arena) } ?: MemorySegment.NULL, - animationOptions(arena, animation), - ) as Int - ) - } - } - - internal fun rotateBy(map: NativeMap, first: ScreenPoint, second: ScreenPoint) { - Arena.ofConfined().use { arena -> - Status.check( - mapTwoScreenPointsStatusFunction("mln_map_rotate_by") - .invokeNative(map, screenPoint(first, arena), screenPoint(second, arena)) as Int - ) - } - } + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_metal_borrowed_texture_attach", + metalBorrowedTextureDescriptor(descriptor), + options, + ) - internal fun rotateByAnimated( + internal fun attachVulkanOwnedTexture( map: NativeMap, - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions?, - ) { - Arena.ofConfined().use { arena -> - Status.check( - mapTwoScreenPointsAddressStatusFunction("mln_map_rotate_by_animated") - .invokeNative( - map, - screenPoint(first, arena), - screenPoint(second, arena), - animationOptions(arena, animation), - ) as Int - ) - } - } - - internal fun pitchBy(map: NativeMap, pitch: Double) { - Status.check(mapDoubleStatusFunction("mln_map_pitch_by").invokeNative(map, pitch) as Int) - } - - internal fun pitchByAnimated(map: NativeMap, pitch: Double, animation: AnimationOptions?) { - Arena.ofConfined().use { arena -> - Status.check( - mapDoubleAddressStatusFunction("mln_map_pitch_by_animated") - .invokeNative(map, pitch, animationOptions(arena, animation)) as Int - ) - } - } - - internal fun cancelTransitions(map: NativeMap) { - Status.check(mapStatusFunction("mln_map_cancel_transitions").invokeNative(map) as Int) - } - - internal fun setGestureInProgress(map: NativeMap, inProgress: Boolean) { - Status.check( - mapBooleanStatusFunction("mln_map_set_gesture_in_progress").invokeNative(map, inProgress) - as Int + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_vulkan_owned_texture_attach", + vulkanOwnedTextureDescriptor(descriptor), + options, ) - } - internal fun isGestureInProgress(map: NativeMap): Boolean = - Arena.ofConfined().use { arena -> - val outInProgress = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapAddressStatusFunction("mln_map_is_gesture_in_progress").invokeNative(map, outInProgress) - as Int - ) - outInProgress.get(ValueLayout.JAVA_BOOLEAN, 0) - } + internal fun attachVulkanBorrowedTexture( + map: NativeMap, + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_vulkan_borrowed_texture_attach", + vulkanBorrowedTextureDescriptor(descriptor), + options, + ) - internal fun cameraForLatLngBounds( + internal fun attachOpenGLOwnedTexture( map: NativeMap, - bounds: LatLngBounds, - fitOptions: CameraFitOptions?, - ): CameraOptions = - Arena.ofConfined().use { arena -> - val outCamera = cameraOptionsDefault(arena) - Status.check( - mapLatLngBoundsAddressAddressStatusFunction("mln_map_camera_for_lat_lng_bounds") - .invokeNative( - map, - latLngBounds(arena, bounds), - cameraFitOptions(arena, fitOptions), - outCamera, - ) as Int - ) - cameraOptions(outCamera) - } + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_opengl_owned_texture_attach", + openglOwnedTextureDescriptor(descriptor), + options, + ) - internal fun cameraForLatLngs( + internal fun attachOpenGLBorrowedTexture( map: NativeMap, - coordinates: List, - fitOptions: CameraFitOptions?, - ): CameraOptions { - val coordinateSnapshot = coordinates.toList() - return Arena.ofConfined().use { arena -> - val outCamera = cameraOptionsDefault(arena) - Status.check( - mapAddressLongAddressAddressStatusFunction("mln_map_camera_for_lat_lngs") - .invokeNative( - map, - latLngArray(arena, coordinateSnapshot), - coordinateSnapshot.size.toLong(), - cameraFitOptions(arena, fitOptions), - outCamera, - ) as Int - ) - cameraOptions(outCamera) - } - } + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_opengl_borrowed_texture_attach", + openglBorrowedTextureDescriptor(descriptor), + options, + ) - internal fun cameraForGeometry( + internal fun attachMetalSurface( map: NativeMap, - geometry: ByteArray, - fitOptions: CameraFitOptions?, - ): CameraOptions = - Arena.ofConfined().use { arena -> - val outCamera = cameraOptionsDefault(arena) - Status.check( - mapAddressAddressAddressStatusFunction("mln_map_camera_for_geometry") - .invokeNative( - map, - byteArrayView(arena, geometry), - cameraFitOptions(arena, fitOptions), - outCamera, - ) as Int - ) - cameraOptions(outCamera) - } + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_metal_surface_attach", + metalSurfaceDescriptor(descriptor), + options, + ) - internal fun latLngBoundsForCamera(map: NativeMap, camera: CameraOptions): LatLngBounds = - mapLatLngBoundsForCamera("mln_map_lat_lng_bounds_for_camera", map, camera) + internal fun attachVulkanSurface( + map: NativeMap, + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_vulkan_surface_attach", + vulkanSurfaceDescriptor(descriptor), + options, + ) - internal fun latLngBoundsForCameraUnwrapped(map: NativeMap, camera: CameraOptions): LatLngBounds = - mapLatLngBoundsForCamera("mln_map_lat_lng_bounds_for_camera_unwrapped", map, camera) + internal fun attachOpenGLSurface( + map: NativeMap, + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): Pair> = + attachRenderSession( + map, + "mln_opengl_surface_attach", + openglSurfaceDescriptor(descriptor), + options, + ) - internal fun bounds(map: NativeMap): BoundOptions = - Arena.ofConfined().use { arena -> - val outBounds = boundOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_bounds").invokeNative(map, outBounds) as Int - ) - boundOptions(outBounds) - } + internal fun destroyRenderSession(session: NativeRenderSession): Int = + dynamicStatusDowncall("mln_render_session_destroy", ValueLayout.JAVA_LONG).invokeNative(session) + as Int - internal fun setBounds(map: NativeMap, options: BoundOptions) { + internal fun renderSessionCapabilities(session: NativeRenderSession): RenderSessionCapabilities = Arena.ofConfined().use { arena -> + val out = arena.allocate(RENDER_SESSION_CAPABILITIES_SIZE) + out.set(ValueLayout.JAVA_INT, 0, RENDER_SESSION_CAPABILITIES_SIZE.toInt()) Status.check( - mapAddressStatusFunction("mln_map_set_bounds") - .invokeNative(map, boundOptions(arena, options)) as Int + dynamicStatusDowncall( + "mln_render_session_get_capabilities", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, out) as Int ) - } - } - - internal fun freeCameraOptions(map: NativeMap): FreeCameraOptions = - Arena.ofConfined().use { arena -> - val outOptions = freeCameraOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_get_free_camera_options").invokeNative(map, outOptions) - as Int + val flags = out.get(ValueLayout.JAVA_INT, 12) + RenderSessionCapabilities( + driver = RenderDriver.fromNative(out.get(ValueLayout.JAVA_INT, 4)), + textureRingDepth = out.get(ValueLayout.JAVA_INT, 8), + frameAcquisition = flags and RENDER_CAPABILITY_FRAME_ACQUISITION != 0, + readback = flags and RENDER_CAPABILITY_READBACK != 0, + consumerSync = flags and RENDER_CAPABILITY_CONSUMER_SYNC != 0, + presentation = flags and RENDER_CAPABILITY_PRESENTATION != 0, ) - readFreeCameraOptions(outOptions) } - internal fun setFreeCameraOptions(map: NativeMap, options: FreeCameraOptions) { + internal fun renderSessionSnapshot(session: NativeRenderSession): RenderSessionSnapshot = Arena.ofConfined().use { arena -> + val out = arena.allocate(RENDER_SESSION_SNAPSHOT_SIZE) + out.set(ValueLayout.JAVA_INT, 0, RENDER_SESSION_SNAPSHOT_SIZE.toInt()) Status.check( - mapAddressStatusFunction("mln_map_set_free_camera_options") - .invokeNative(map, freeCameraOptions(arena, options)) as Int + dynamicStatusDowncall( + "mln_render_session_get_snapshot", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, out) as Int + ) + val extent = out.asSlice(16, mln_render_target_extent.sizeof()) + RenderSessionSnapshot( + state = RenderSessionState.fromNative(out.get(ValueLayout.JAVA_INT, 4)), + driver = RenderDriver.fromNative(out.get(ValueLayout.JAVA_INT, 8)), + latestResult = RenderResult.fromNative(out.get(ValueLayout.JAVA_INT, 12)), + extent = + RenderTargetExtent( + mln_render_target_extent.width(extent), + mln_render_target_extent.height(extent), + mln_render_target_extent.scale_factor(extent), + ), + generation = out.get(ValueLayout.JAVA_LONG, 32).toULong(), + mapUpdateGeneration = out.get(ValueLayout.JAVA_LONG, 40).toULong(), + renderedUpdateGeneration = out.get(ValueLayout.JAVA_LONG, 48).toULong(), + extentGeneration = out.get(ValueLayout.JAVA_LONG, 56).toULong(), + frameGeneration = out.get(ValueLayout.JAVA_LONG, 64).toULong(), + latestDemandToken = out.get(ValueLayout.JAVA_LONG, 72).toULong(), + pendingDemandCount = out.get(ValueLayout.JAVA_INT, 80), + acquiredFrameCount = out.get(ValueLayout.JAVA_INT, 84), + targetReady = out.get(ValueLayout.JAVA_BOOLEAN, 88), + pendingChanges = out.get(ValueLayout.JAVA_BOOLEAN, 89), + ) + } + + internal fun requestRenderFrame(session: NativeRenderSession, demand: FrameDemand) { + Arena.ofConfined().use { arena -> + val nativeDemand = arena.allocate(FRAME_DEMAND_SIZE) + nativeDemand.set(ValueLayout.JAVA_INT, 0, FRAME_DEMAND_SIZE.toInt()) + var flags = 0 + if (demand.ifNeeded) flags = flags or FRAME_DEMAND_IF_NEEDED + if (demand.present) flags = flags or FRAME_DEMAND_PRESENT + nativeDemand.set(ValueLayout.JAVA_INT, 4, flags) + nativeDemand.set(ValueLayout.JAVA_LONG, 8, demand.token.toLong()) + nativeDemand.set(ValueLayout.JAVA_LONG, 16, demand.coalescingBoundary.toLong()) + nativeDemand.set(ValueLayout.JAVA_LONG, 24, demand.timeoutNanoseconds.toLong()) + Status.check( + dynamicStatusDowncall( + "mln_render_session_request_frame", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, nativeDemand) as Int ) } } - internal fun pixelForLatLng(map: NativeMap, coordinate: LatLng): ScreenPoint = - Arena.ofConfined().use { arena -> - val outPoint = arena.allocate(screenPointLayout) - Status.check( - mapLatLngAddressStatusFunction("mln_map_pixel_for_lat_lng") - .invokeNative(map, latLng(coordinate, arena), outPoint) as Int - ) - screenPoint(outPoint) - } - - internal fun latLngForPixel(map: NativeMap, point: ScreenPoint): LatLng = + internal fun drainRenderFrameResults(session: NativeRenderSession): List = Arena.ofConfined().use { arena -> - val outCoordinate = arena.allocate(latLngLayout) + val outBatch = zeroHandle(arena) Status.check( - mapScreenPointAddressStatusFunction("mln_map_lat_lng_for_pixel") - .invokeNative(map, screenPoint(point, arena), outCoordinate) as Int - ) - latLng(outCoordinate) - } - - internal fun pixelsForLatLngs(map: NativeMap, coordinates: List): List { - val coordinateSnapshot = coordinates.toList() - if (coordinateSnapshot.isEmpty()) { - Arena.ofConfined().use { arena -> - Status.check( - mapAddressLongAddressStatusFunction("mln_map_pixels_for_lat_lngs") - .invokeNative(map, MemorySegment.NULL, 0L, MemorySegment.NULL) as Int - ) - } - return emptyList() - } - return Arena.ofConfined().use { arena -> - val outPoints = arena.allocate(screenPointLayout.byteSize() * coordinateSnapshot.size) - Status.check( - mapAddressLongAddressStatusFunction("mln_map_pixels_for_lat_lngs") - .invokeNative( - map, - latLngArray(arena, coordinateSnapshot), - coordinateSnapshot.size.toLong(), - outPoints, - ) as Int + dynamicStatusDowncall( + "mln_render_session_drain_frame_results", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, outBatch) as Int ) - screenPointArray(outPoints, coordinateSnapshot.size) - } - } - - internal fun latLngsForPixels(map: NativeMap, points: List): List { - val pointSnapshot = points.toList() - if (pointSnapshot.isEmpty()) { - Arena.ofConfined().use { arena -> + val batch = outBatch.get(ValueLayout.JAVA_LONG, 0) + if (batch == 0L) return@use emptyList() + try { + val outCount = arena.allocate(ValueLayout.JAVA_LONG) Status.check( - mapAddressLongAddressStatusFunction("mln_map_lat_lngs_for_pixels") - .invokeNative(map, MemorySegment.NULL, 0L, MemorySegment.NULL) as Int + dynamicStatusDowncall( + "mln_render_frame_batch_count", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(batch, outCount) as Int ) - } - return emptyList() - } - return Arena.ofConfined().use { arena -> - val outCoordinates = arena.allocate(latLngLayout.byteSize() * pointSnapshot.size) - Status.check( - mapAddressLongAddressStatusFunction("mln_map_lat_lngs_for_pixels") - .invokeNative( - map, - screenPointArray(arena, pointSnapshot), - pointSnapshot.size.toLong(), - outCoordinates, - ) as Int - ) - latLngArray(outCoordinates, pointSnapshot.size) - } - } - - internal fun attachMetalOwnedTexture( - map: NativeMap, - descriptor: MetalOwnedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_metal_owned_texture_attach", - metalOwnedTextureDescriptor(descriptor), - ) - - internal fun attachMetalBorrowedTexture( - map: NativeMap, - descriptor: MetalBorrowedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_metal_borrowed_texture_attach", - metalBorrowedTextureDescriptor(descriptor), - ) - - internal fun attachVulkanOwnedTexture( - map: NativeMap, - descriptor: VulkanOwnedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_vulkan_owned_texture_attach", - vulkanOwnedTextureDescriptor(descriptor), - ) + val count = outCount.get(ValueLayout.JAVA_LONG, 0) + List(Math.toIntExact(count)) { index -> + val outResult = arena.allocate(RENDER_FRAME_RESULT_SIZE) + outResult.set(ValueLayout.JAVA_INT, 0, RENDER_FRAME_RESULT_SIZE.toInt()) + Status.check( + dynamicStatusDowncall( + "mln_render_frame_batch_get", + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(batch, index.toLong(), outResult) as Int + ) + renderFrameResult(outResult) + } + } finally { + dynamicDowncall( + "mln_render_frame_batch_release", + FunctionDescriptor.ofVoid(ValueLayout.JAVA_LONG), + ) + .invokeNative(batch) + } + } - internal fun attachVulkanBorrowedTexture( - map: NativeMap, - descriptor: VulkanBorrowedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_vulkan_borrowed_texture_attach", - vulkanBorrowedTextureDescriptor(descriptor), - ) + internal fun serviceRenderDriverWork(session: NativeRenderSession, maxWork: Int): Int = + Arena.ofConfined().use { arena -> + val outServiced = arena.allocate(ValueLayout.JAVA_LONG) + Status.check( + dynamicStatusDowncall( + "mln_render_session_service_driver_work", + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, maxWork.toLong(), outServiced) as Int + ) + Math.toIntExact(outServiced.get(ValueLayout.JAVA_LONG, 0)) + } - internal fun attachOpenGLOwnedTexture( - map: NativeMap, - descriptor: OpenGLOwnedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_opengl_owned_texture_attach", - openglOwnedTextureDescriptor(descriptor), - ) + internal fun acquireRenderFrame(session: NativeRenderSession): Long? = + Arena.ofConfined().use { arena -> + val outFrame = zeroHandle(arena) + val status = + dynamicStatusDowncall( + "mln_render_session_acquire_frame", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, outFrame) as Int + if (status == -9) return@use null + Status.check(status) + outFrame.get(ValueLayout.JAVA_LONG, 0).also { + require(it != 0L) { "frame acquisition returned the null handle" } + } + } - internal fun attachOpenGLBorrowedTexture( - map: NativeMap, - descriptor: OpenGLBorrowedTextureDescriptor, - ): NativeRenderSession = - attachRenderSession( - map, - "mln_opengl_borrowed_texture_attach", - openglBorrowedTextureDescriptor(descriptor), - ) + internal fun acquiredFrameResult(frame: Long): RenderFrameResult = + Arena.ofConfined().use { arena -> + val out = arena.allocate(RENDER_FRAME_RESULT_SIZE) + out.set(ValueLayout.JAVA_INT, 0, RENDER_FRAME_RESULT_SIZE.toInt()) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_get_result", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(frame, out) as Int + ) + renderFrameResult(out) + } - internal fun attachMetalSurface( - map: NativeMap, - descriptor: MetalSurfaceDescriptor, - ): NativeRenderSession = - attachRenderSession(map, "mln_metal_surface_attach", metalSurfaceDescriptor(descriptor)) + internal fun acquiredFrameProducerSync(frame: Long): GpuSync = + Arena.ofConfined().use { arena -> + val out = gpuSync(arena, GpuSync()) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_get_producer_sync", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(frame, out) as Int + ) + readGpuSync(out) + } - internal fun attachVulkanSurface( - map: NativeMap, - descriptor: VulkanSurfaceDescriptor, - ): NativeRenderSession = - attachRenderSession(map, "mln_vulkan_surface_attach", vulkanSurfaceDescriptor(descriptor)) + internal fun acquiredMetalTexture(frame: Long, scope: FrameScope): MetalOwnedTextureFrame = + Arena.ofConfined().use { arena -> + val out = mln_metal_owned_texture_frame.allocate(arena) + mln_metal_owned_texture_frame.size(out, mln_metal_owned_texture_frame.sizeof().toInt()) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_get_metal_texture", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(frame, out) as Int + ) + metalOwnedTextureFrame(out, scope) + } - internal fun attachOpenGLSurface( - map: NativeMap, - descriptor: OpenGLSurfaceDescriptor, - ): NativeRenderSession = - attachRenderSession(map, "mln_opengl_surface_attach", openglSurfaceDescriptor(descriptor)) + internal fun acquiredVulkanTexture(frame: Long, scope: FrameScope): VulkanOwnedTextureFrame = + Arena.ofConfined().use { arena -> + val out = mln_vulkan_owned_texture_frame.allocate(arena) + mln_vulkan_owned_texture_frame.size(out, mln_vulkan_owned_texture_frame.sizeof().toInt()) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_get_vulkan_texture", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(frame, out) as Int + ) + vulkanOwnedTextureFrame(out, scope) + } - internal fun destroyRenderSession(session: NativeRenderSession): Int = - renderSessionStatusFunction("mln_render_session_destroy").invokeNative(session) as Int + internal fun acquiredOpenGLTexture(frame: Long, scope: FrameScope): OpenGLOwnedTextureFrame = + Arena.ofConfined().use { arena -> + val out = mln_opengl_owned_texture_frame.allocate(arena) + mln_opengl_owned_texture_frame.size(out, mln_opengl_owned_texture_frame.sizeof().toInt()) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_get_opengl_texture", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(frame, out) as Int + ) + openglOwnedTextureFrame(out, scope) + } + + internal fun releaseAcquiredFrame(frame: Long, sync: GpuSync): Long = + Arena.ofConfined().use { arena -> + val inOutFrame = arena.allocate(ValueLayout.JAVA_LONG) + inOutFrame.set(ValueLayout.JAVA_LONG, 0, frame) + Status.check( + dynamicStatusDowncall( + "mln_acquired_frame_release", + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative(inOutFrame, gpuSync(arena, sync)) as Int + ) + inOutFrame.get(ValueLayout.JAVA_LONG, 0) + } internal fun resizeRenderSession( session: NativeRenderSession, - width: Int, - height: Int, - scaleFactor: Double, - ) { - Status.check( - renderSessionResizeFunction().invokeNative(session, width, height, scaleFactor) as Int - ) - } + extent: RenderTargetExtent, + ): Deferred = + Arena.ofConfined().use { arena -> + val nativeExtent = mln_render_target_extent.allocate(arena) + fillRenderTargetExtent(nativeExtent, extent) + renderOperation("mln_render_session_resize", session, nativeExtent) + } internal fun setMetalSurfaceTarget( session: NativeRenderSession, descriptor: MetalSurfaceDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_metal_surface_set_target", metalSurfaceDescriptor(descriptor), ) - } internal fun setVulkanSurfaceTarget( session: NativeRenderSession, descriptor: VulkanSurfaceDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_vulkan_surface_set_target", vulkanSurfaceDescriptor(descriptor), ) - } internal fun setOpenGLSurfaceTarget( session: NativeRenderSession, descriptor: OpenGLSurfaceDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_opengl_surface_set_target", openglSurfaceDescriptor(descriptor), ) - } internal fun setMetalBorrowedTextureTarget( session: NativeRenderSession, descriptor: MetalBorrowedTextureDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_metal_borrowed_texture_set_target", metalBorrowedTextureDescriptor(descriptor), ) - } internal fun setVulkanBorrowedTextureTarget( session: NativeRenderSession, descriptor: VulkanBorrowedTextureDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_vulkan_borrowed_texture_set_target", vulkanBorrowedTextureDescriptor(descriptor), ) - } internal fun setOpenGLBorrowedTextureTarget( session: NativeRenderSession, descriptor: OpenGLBorrowedTextureDescriptor, - ) { - setRenderSessionTarget( + ): Deferred = + renderTargetOperation( session, "mln_opengl_borrowed_texture_set_target", openglBorrowedTextureDescriptor(descriptor), ) - } - internal fun renderUpdate(session: NativeRenderSession): RenderUpdate = - Arena.ofConfined().use { arena -> - val outResult = arena.allocate(ValueLayout.JAVA_INT) - val outNeedsRepaint = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - renderSessionRenderUpdateFunction().invokeNative(session, outResult, outNeedsRepaint) as Int - ) - RenderUpdate( - RenderResult.fromNative(outResult.get(ValueLayout.JAVA_INT, 0)), - outNeedsRepaint.get(ValueLayout.JAVA_BOOLEAN, 0), - ) + internal fun renderBarrier(session: NativeRenderSession): Deferred = + CompletionBridge.unit { completion -> + (dynamicStatusDowncall( + "mln_render_session_barrier", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, completion) as Int) } - internal fun detachRenderSession(session: NativeRenderSession) { - Status.check( - renderSessionStatusFunction("mln_render_session_detach").invokeNative(session) as Int - ) - } - - internal fun reduceRenderSessionMemoryUse(session: NativeRenderSession) { - Status.check( - renderSessionStatusFunction("mln_render_session_reduce_memory_use").invokeNative(session) - as Int - ) - } - - internal fun clearRenderSessionData(session: NativeRenderSession) { - Status.check( - renderSessionStatusFunction("mln_render_session_clear_data").invokeNative(session) as Int - ) - } - - internal fun dumpRenderSessionDebugLogs(session: NativeRenderSession) { - Status.check( - renderSessionStatusFunction("mln_render_session_dump_debug_logs").invokeNative(session) as Int - ) - } + internal fun renderControl(session: NativeRenderSession, functionName: String): Deferred = + renderOperation(functionName, session) internal fun queryRenderedFeatures( session: NativeRenderSession, geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions?, - ): List = - Arena.ofConfined().use { arena -> - val outResult = arena.allocate(ValueLayout.JAVA_LONG) - outResult.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - renderSessionQueryRenderedFeaturesFunction() - .invokeNative( - session, - renderedQueryGeometry(arena, geometry), - renderedFeatureQueryOptions(arena, options), - outResult, - ) as Int + ): Deferred> = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + ::queriedFeaturesCompletion, + { completion -> + dynamicStatusDowncall( + "mln_render_session_query_rendered_features", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative( + session, + renderedQueryGeometry(arena, geometry), + renderedFeatureQueryOptions(arena, options), + completion, + ) as Int + }, ) - queriedFeatureList(NativeQueriedFeatureList(outResult.get(ValueLayout.JAVA_LONG, 0))) } internal fun querySourceFeatures( session: NativeRenderSession, sourceId: String, options: SourceFeatureQueryOptions?, - ): List = - Arena.ofConfined().use { arena -> - val outResult = arena.allocate(ValueLayout.JAVA_LONG) - outResult.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - renderSessionQuerySourceFeaturesFunction() - .invokeNative( - session, - stringView(arena, sourceId), - sourceFeatureQueryOptions(arena, options), - outResult, - ) as Int + ): Deferred> = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + ::queriedFeaturesCompletion, + { completion -> + dynamicStatusDowncall( + "mln_render_session_query_source_features", + ValueLayout.JAVA_LONG, + stringViewLayout, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative( + session, + stringView(arena, sourceId), + sourceFeatureQueryOptions(arena, options), + completion, + ) as Int + }, ) - queriedFeatureList(NativeQueriedFeatureList(outResult.get(ValueLayout.JAVA_LONG, 0))) } internal fun queryFeatureExtension( @@ -2352,111 +2360,59 @@ internal object NativeAccess { extension: String, extensionField: String, arguments: ByteArray?, - ): ByteArray = + ): Deferred = + Arena.ofConfined().use { arena -> + CompletionBridge.submit( + ::requiredBufferCompletion, + { completion -> + dynamicStatusDowncall( + "mln_render_session_query_feature_extensions", + ValueLayout.JAVA_LONG, + stringViewLayout, + stringViewLayout, + stringViewLayout, + stringViewLayout, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative( + session, + stringView(arena, sourceId), + byteArrayView(arena, feature), + stringView(arena, extension), + stringView(arena, extensionField), + arguments?.let { byteArrayView(arena, it) } ?: MemorySegment.NULL, + completion, + ) as Int + }, + ) + } + + internal fun textureReadback(session: NativeRenderSession): Deferred = + CompletionBridge.submit( + ::textureReadbackCompletion, + { completion -> + MapLibreNativeC.mln_texture_read_premultiplied_rgba8(session.raw, completion) + }, + ) + + internal fun abandonRenderSession(session: NativeRenderSession): RenderAbandonResult = Arena.ofConfined().use { arena -> - val outResult = arena.allocate(ValueLayout.JAVA_LONG) - outResult.set(ValueLayout.JAVA_LONG, 0, 0L) + val out = arena.allocate(RENDER_ABANDON_RESULT_SIZE) + out.set(ValueLayout.JAVA_INT, 0, RENDER_ABANDON_RESULT_SIZE.toInt()) Status.check( - renderSessionQueryFeatureExtensionsFunction() - .invokeNative( - session, - stringView(arena, sourceId), - byteArrayView(arena, feature), - stringView(arena, extension), - stringView(arena, extensionField), - arguments?.let { byteArrayView(arena, it) } ?: MemorySegment.NULL, - outResult, - ) as Int + dynamicStatusDowncall( + "mln_render_session_abandon", + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ) + .invokeNative(session, out) as Int + ) + RenderAbandonResult( + RenderAbandonDisposition.fromNative(out.get(ValueLayout.JAVA_INT, 4)), + out.get(ValueLayout.JAVA_INT, 8), ) - ownedBuffer(NativeOwnedBuffer(outResult.get(ValueLayout.JAVA_LONG, 0)))!! - } - - internal fun textureImageInfo(session: NativeRenderSession): TextureImageInfo = - Arena.ofConfined().use { arena -> - val outInfo = textureImageInfo(arena) - val status = - textureReadPremultipliedRgba8Function() - .invokeNative(session, MemorySegment.NULL, 0L, outInfo) as Int - val info = readTextureImageInfo(outInfo) - if (status == 0 || (status == -1 && info.byteLength > 0L)) { - info - } else { - Status.check(status) - error("unreachable") - } - } - - internal fun readPremultipliedRgba8( - session: NativeRenderSession, - buffer: NativeBuffer, - ): TextureImageInfo = - Arena.ofConfined().use { arena -> - val outInfo = textureImageInfo(arena) - buffer.borrow { segment, length -> - Status.check( - textureReadPremultipliedRgba8Function().invokeNative(session, segment, length, outInfo) - as Int - ) - } - readTextureImageInfo(outInfo) - } - - internal fun acquireMetalOwnedTextureFrame( - session: NativeRenderSession - ): OwnedTextureFrameSegment { - val arena = Arena.ofShared() - val frame = mln_metal_owned_texture_frame.allocate(arena) - mln_metal_owned_texture_frame.size(frame, mln_metal_owned_texture_frame.sizeof().toInt()) - try { - Status.check(metalOwnedTextureAcquireFrameFunction().invokeNative(session, frame) as Int) - return OwnedTextureFrameSegment(frame, arena) - } catch (error: Throwable) { - arena.close() - throw error - } - } - - internal fun acquireVulkanOwnedTextureFrame( - session: NativeRenderSession - ): OwnedTextureFrameSegment { - val arena = Arena.ofShared() - val frame = mln_vulkan_owned_texture_frame.allocate(arena) - mln_vulkan_owned_texture_frame.size(frame, mln_vulkan_owned_texture_frame.sizeof().toInt()) - try { - Status.check(vulkanOwnedTextureAcquireFrameFunction().invokeNative(session, frame) as Int) - return OwnedTextureFrameSegment(frame, arena) - } catch (error: Throwable) { - arena.close() - throw error - } - } - - internal fun acquireOpenGLOwnedTextureFrame( - session: NativeRenderSession - ): OwnedTextureFrameSegment { - val arena = Arena.ofShared() - val frame = mln_opengl_owned_texture_frame.allocate(arena) - mln_opengl_owned_texture_frame.size(frame, mln_opengl_owned_texture_frame.sizeof().toInt()) - try { - Status.check(openglOwnedTextureAcquireFrameFunction().invokeNative(session, frame) as Int) - return OwnedTextureFrameSegment(frame, arena) - } catch (error: Throwable) { - arena.close() - throw error } - } - - internal fun releaseMetalOwnedTextureFrame(session: NativeRenderSession, frame: MemorySegment) { - Status.check(metalOwnedTextureReleaseFrameFunction().invokeNative(session, frame) as Int) - } - - internal fun releaseVulkanOwnedTextureFrame(session: NativeRenderSession, frame: MemorySegment) { - Status.check(vulkanOwnedTextureReleaseFrameFunction().invokeNative(session, frame) as Int) - } - - internal fun releaseOpenGLOwnedTextureFrame(session: NativeRenderSession, frame: MemorySegment) { - Status.check(openglOwnedTextureReleaseFrameFunction().invokeNative(session, frame) as Int) - } internal fun metalOwnedTextureFrame( segment: MemorySegment, @@ -2510,37 +2466,34 @@ internal object NativeAccess { mln_opengl_owned_texture_frame.type(segment), ) - internal fun createMapProjection(map: NativeMap): NativeMapProjection = - Arena.ofConfined().use { arena -> - val outProjection = arena.allocate(ValueLayout.JAVA_LONG) - outProjection.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapAddressStatusFunction("mln_map_projection_create").invokeNative(map, outProjection) - as Int - ) - NativeMapProjection(outProjection.get(ValueLayout.JAVA_LONG, 0)).also { projection -> - require(!projection.isNull) { "mln_map_projection_create returned the null handle" } - } - } + internal fun createMapProjection(map: NativeMap): Deferred = + CompletionBridge.submit( + { result -> + NativeMapProjection( + completionValue(result, ValueLayout.JAVA_LONG.byteSize()).get(ValueLayout.JAVA_LONG, 0) + ) + .also { projection -> + require(!projection.isNull) { "mln_map_projection_create returned the null handle" } + } + }, + { completion -> MapLibreNativeC.mln_map_projection_create(map.raw, completion) }, + ) - internal fun destroyMapProjection(projection: NativeMapProjection): Int = - mapStatusFunction("mln_map_projection_destroy").invokeNative(projection) as Int + internal fun closeProjection(projection: NativeMapProjection) { + Status.check(MapLibreNativeC.mln_map_projection_close(projection.raw)) + } internal fun projectionCamera(projection: NativeMapProjection): CameraOptions = Arena.ofConfined().use { arena -> val outCamera = cameraOptionsDefault(arena) - Status.check( - mapAddressStatusFunction("mln_map_projection_get_camera") - .invokeNative(projection, outCamera) as Int - ) + Status.check(MapLibreNativeC.mln_map_projection_get_camera(projection.raw, outCamera)) cameraOptions(outCamera) } internal fun setProjectionCamera(projection: NativeMapProjection, camera: CameraOptions) { Arena.ofConfined().use { arena -> Status.check( - mapAddressStatusFunction("mln_map_projection_set_camera") - .invokeNative(projection, cameraOptions(arena, camera)) as Int + MapLibreNativeC.mln_map_projection_set_camera(projection.raw, cameraOptions(arena, camera)) ) } } @@ -2553,13 +2506,12 @@ internal object NativeAccess { val coordinateSnapshot = coordinates.toList() Arena.ofConfined().use { arena -> Status.check( - projectionAddressLongEdgeInsetsStatusFunction("mln_map_projection_set_visible_coordinates") - .invokeNative( - projection, - latLngArray(arena, coordinateSnapshot), - coordinateSnapshot.size.toLong(), - edgeInsets(arena, padding), - ) as Int + MapLibreNativeC.mln_map_projection_set_visible_coordinates( + projection.raw, + latLngArray(arena, coordinateSnapshot), + coordinateSnapshot.size.toLong(), + edgeInsets(arena, padding), + ) ) } } @@ -2571,9 +2523,11 @@ internal object NativeAccess { ) { Arena.ofConfined().use { arena -> Status.check( - projectionAddressEdgeInsetsStatusFunction("mln_map_projection_set_visible_geometry") - .invokeNative(projection, byteArrayView(arena, geometry), edgeInsets(arena, padding)) - as Int + MapLibreNativeC.mln_map_projection_set_visible_geometry( + projection.raw, + byteArrayView(arena, geometry), + edgeInsets(arena, padding), + ) ) } } @@ -2585,8 +2539,11 @@ internal object NativeAccess { Arena.ofConfined().use { arena -> val outPoint = arena.allocate(screenPointLayout) Status.check( - projectionLatLngAddressStatusFunction("mln_map_projection_pixel_for_lat_lng") - .invokeNative(projection, latLng(coordinate, arena), outPoint) as Int + MapLibreNativeC.mln_map_projection_pixel_for_lat_lng( + projection.raw, + latLng(coordinate, arena), + outPoint, + ) ) screenPoint(outPoint) } @@ -2598,8 +2555,11 @@ internal object NativeAccess { Arena.ofConfined().use { arena -> val outCoordinate = arena.allocate(latLngLayout) Status.check( - projectionScreenPointAddressStatusFunction("mln_map_projection_lat_lng_for_pixel") - .invokeNative(projection, screenPoint(point, arena), outCoordinate) as Int + MapLibreNativeC.mln_map_projection_lat_lng_for_pixel( + projection.raw, + screenPoint(point, arena), + outCoordinate, + ) ) latLng(outCoordinate) } @@ -2629,6 +2589,25 @@ internal object NativeAccess { ) as Int } + internal fun completeResourceRequest( + request: NativeResourceRequest, + response: ResourceResponse, + ): Int = + Arena.ofConfined().use { arena -> + MapLibreNativeC.mln_resource_request_complete(request.raw, resourceResponse(response, arena)) + } + + internal fun isResourceRequestCancelled(request: NativeResourceRequest): Boolean = + Arena.ofConfined().use { arena -> + val cancelled = arena.allocate(ValueLayout.JAVA_BOOLEAN) + Status.check(MapLibreNativeC.mln_resource_request_cancelled(request.raw, cancelled)) + cancelled.get(ValueLayout.JAVA_BOOLEAN, 0) + } + + internal fun releaseResourceRequest(request: NativeResourceRequest) { + MapLibreNativeC.mln_resource_request_release(request.raw) + } + internal fun customGeometrySourceOptions( arena: Arena, value: CustomGeometrySourceOptions, @@ -2742,34 +2721,65 @@ internal object NativeAccess { map: NativeMap, functionName: String, descriptor: (Arena) -> MemorySegment, - ): NativeRenderSession = - Arena.ofConfined().use { arena -> - val outSession = arena.allocate(ValueLayout.JAVA_LONG) - outSession.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check( - mapAddressAddressStatusFunction(functionName) - .invokeNative(map, descriptor(arena), outSession) as Int - ) - NativeRenderSession(outSession.get(ValueLayout.JAVA_LONG, 0)).also { session -> - require(!session.isNull) { "render session attach returned the null handle" } + options: RenderSessionAttachOptions, + ): Pair> = + Arena.ofConfined().use { arena -> + val nativeOptions = arena.allocate(RENDER_SESSION_ATTACH_OPTIONS_SIZE) + nativeOptions.set(ValueLayout.JAVA_INT, 0, RENDER_SESSION_ATTACH_OPTIONS_SIZE.toInt()) + nativeOptions.set(ValueLayout.JAVA_INT, 4, options.driver.nativeValue) + nativeOptions.set(ValueLayout.JAVA_INT, 8, options.requestedTextureRingDepth) + // Both wakes stay disabled, and a disabled wake still carries its own size. + nativeOptions.set(ValueLayout.JAVA_INT, ATTACH_OPTIONS_FRAME_WAKE_OFFSET, WAKE_SIZE) + nativeOptions.set(ValueLayout.JAVA_INT, ATTACH_OPTIONS_DRIVER_WORK_WAKE_OFFSET, WAKE_SIZE) + val outSession = zeroHandle(arena) + val completed = CompletionBridge.unitChecked { completion -> + dynamicStatusDowncall( + functionName, + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative(map, descriptor(arena), nativeOptions, outSession, completion) as Int } + val session = NativeRenderSession(outSession.get(ValueLayout.JAVA_LONG, 0)) + require(!session.isNull) { "render session attach returned the null handle" } + session to completed } - // The C API borrows the replacement descriptor for the call alone, so the - // confined arena that materialized it closes once the call returns. - private fun setRenderSessionTarget( + private fun renderTargetOperation( session: NativeRenderSession, functionName: String, descriptor: (Arena) -> MemorySegment, - ) { - Arena.ofConfined().use { arena -> - Status.check( - renderSessionAddressStatusFunction(functionName).invokeNative(session, descriptor(arena)) - as Int - ) + ): Deferred = + Arena.ofConfined().use { arena -> renderOperation(functionName, session, descriptor(arena)) } + + private fun renderOperation( + functionName: String, + session: NativeRenderSession, + argument: MemorySegment? = null, + ): Deferred = CompletionBridge.unit { completion -> + if (argument == null) { + dynamicStatusDowncall(functionName, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) + .invokeNative(session, completion) as Int + } else { + dynamicStatusDowncall( + functionName, + ValueLayout.JAVA_LONG, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ) + .invokeNative(session, argument, completion) as Int } } + private fun optionalStringView(arena: Arena, value: String?): MemorySegment = + stringView(arena, value.orEmpty()) + + private fun zeroHandle(arena: Arena): MemorySegment = + arena.allocate(ValueLayout.JAVA_LONG).also { it.set(ValueLayout.JAVA_LONG, 0, 0L) } + private fun metalOwnedTextureDescriptor( descriptor: MetalOwnedTextureDescriptor ): (Arena) -> MemorySegment = { arena -> @@ -3104,180 +3114,85 @@ internal object NativeAccess { TextureImageInfo( mln_texture_image_info.width(segment), mln_texture_image_info.height(segment), - mln_texture_image_info.stride(segment), - checkedLong(mln_texture_image_info.byte_length(segment), "texture image byte length"), - ) - - private fun nativePointer(pointer: NativePointer): MemorySegment = - if (pointer.isNull) MemorySegment.NULL else MemorySegment.ofAddress(pointer.address) - - private fun scopedPointer(pointer: MemorySegment, scope: FrameScope): NativePointer = - if (pointer == MemorySegment.NULL) NativePointer.NULL_POINTER - else NativePointer.scoped(pointer.address(), scope) - - internal fun takeOfflineRegionStatusResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): OfflineRegionStatus = - Arena.ofConfined().use { arena -> - val status = arena.allocate(OFFLINE_REGION_STATUS_SIZE) - status.set( - ValueLayout.JAVA_INT, - OFFLINE_REGION_STATUS_SIZE_OFFSET, - OFFLINE_REGION_STATUS_SIZE.toInt(), - ) - Status.check( - runtimeOfflineRegionStatusTakeResultFunction().invokeNative(runtime, operationId, status) - as Int - ) - try { - offlineRegionStatus(status) - } finally { - markTaken() - } - } - - internal fun takeCreateOfflineRegionResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): OfflineRegionInfo = - takeOfflineRegionSnapshot( - runtime, - operationId, - runtimeOfflineRegionCreateTakeResultFunction(), - markTaken, - ) - - internal fun takeOfflineRegionResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): OfflineRegionInfo? = - Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - outFound.set(ValueLayout.JAVA_BOOLEAN, 0, false) - Status.check( - runtimeOfflineRegionGetTakeResultFunction() - .invokeNative(runtime, operationId, outSnapshot, outFound) as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - markTaken() - return@use null - } - try { - offlineRegionSnapshot( - NativeOfflineRegionSnapshot(outSnapshot.get(ValueLayout.JAVA_LONG, 0)) - ) - } finally { - markTaken() - } - } - - internal fun takeOfflineRegionsResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): List = - takeOfflineRegionList( - runtime, - operationId, - runtimeOfflineRegionsListTakeResultFunction(), - markTaken, - ) - - internal fun takeMergeOfflineRegionsDatabaseResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): List = - takeOfflineRegionList( - runtime, - operationId, - runtimeOfflineRegionsMergeDatabaseTakeResultFunction(), - markTaken, + mln_texture_image_info.stride(segment), + checkedLong(mln_texture_image_info.byte_length(segment), "texture image byte length"), ) - internal fun takeUpdateOfflineRegionMetadataResult( - runtime: NativeRuntime, - operationId: Long, - markTaken: () -> Unit, - ): OfflineRegionInfo = - takeOfflineRegionSnapshot( - runtime, - operationId, - runtimeOfflineRegionUpdateMetadataTakeResultFunction(), - markTaken, - ) + private fun nativePointer(pointer: NativePointer): MemorySegment = + if (pointer.isNull) MemorySegment.NULL else MemorySegment.ofAddress(pointer.address) - /** Allocates the reused batch struct that [drainRuntimeEvents] fills. */ - internal fun allocateRuntimeEventBatch(arena: Arena): MemorySegment = - arena.allocate(RUNTIME_EVENT_BATCH_SIZE) + private fun scopedPointer(pointer: MemorySegment, scope: FrameScope): NativePointer = + if (pointer == MemorySegment.NULL) NativePointer.NULL_POINTER + else NativePointer.scoped(pointer.address(), scope) - /** - * Drains at most [maxEvents] events into [batch], zero draining every queued event, and copies - * every field of every event out of the runtime-owned arena before returning. - */ - internal fun drainRuntimeEvents( - runtime: NativeRuntime, - maxEvents: Long, - batch: MemorySegment, - ): NativeRuntimeEventBatch { - batch.set( - ValueLayout.JAVA_INT, - RUNTIME_EVENT_BATCH_SIZE_OFFSET, - RUNTIME_EVENT_BATCH_SIZE.toInt(), - ) - Status.check(MapLibreNativeC.mln_runtime_drain_events(runtime.raw, maxEvents, batch)) - val eventCount = batch.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_BATCH_EVENT_COUNT_OFFSET) - val remainingCount = batch.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_BATCH_REMAINING_OFFSET) - if (eventCount == 0L) { - return NativeRuntimeEventBatch(emptyList(), remainingCount) - } - // The stride the batch reports can exceed this binding's compiled event - // record, so index by it rather than by mln_runtime_event.sizeof(). - val eventSize = - Integer.toUnsignedLong(batch.get(ValueLayout.JAVA_INT, RUNTIME_EVENT_BATCH_EVENT_SIZE_OFFSET)) - val payloadExtent = eventSize - RUNTIME_EVENT_PAYLOAD_OFFSET - val events = - batch - .get(ValueLayout.ADDRESS, RUNTIME_EVENT_BATCH_EVENTS_OFFSET) - .reinterpret(eventCount * eventSize) - val messagesSize = batch.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_BATCH_MESSAGES_SIZE_OFFSET) - val messages = - batch.get(ValueLayout.ADDRESS, RUNTIME_EVENT_BATCH_MESSAGES_OFFSET).reinterpret(messagesSize) - return NativeRuntimeEventBatch( - List(Math.toIntExact(eventCount)) { index -> - val base = index * eventSize - val payloadType = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_PAYLOAD_TYPE_OFFSET) - NativeRuntimeEvent( - type = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_TYPE_OFFSET), - sourceType = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_SOURCE_TYPE_OFFSET), - sourceId = events.get(ValueLayout.JAVA_LONG, base + RUNTIME_EVENT_SOURCE_OFFSET), - code = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_CODE_OFFSET), - payload = - runtimeEventPayload( - payloadType, - events.asSlice(base + RUNTIME_EVENT_PAYLOAD_OFFSET, payloadExtent), - ), - message = - copyMessage( - messages, - Integer.toUnsignedLong( - events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_MESSAGE_OFFSET_OFFSET) - ), - Integer.toUnsignedLong( - events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_MESSAGE_SIZE_OFFSET) - ), - ), + /** Copies every queued event out of the owned batch before releasing it. */ + internal fun drainRuntimeEvents(runtime: NativeRuntime): NativeRuntimeEventBatch = + Arena.ofConfined().use { arena -> + val batch = arena.allocate(RUNTIME_EVENT_BATCH_SIZE) + val outBatch = arena.allocate(ValueLayout.JAVA_LONG) + outBatch.set(ValueLayout.JAVA_LONG, 0, 0L) + Status.check(MapLibreNativeC.mln_runtime_drain_events(runtime.raw, outBatch)) + val batchHandle = outBatch.get(ValueLayout.JAVA_LONG, 0) + require(batchHandle != 0L) { "mln_runtime_drain_events returned the null handle" } + try { + batch.set( + ValueLayout.JAVA_INT, + RUNTIME_EVENT_BATCH_SIZE_OFFSET, + RUNTIME_EVENT_BATCH_SIZE.toInt(), ) - }, - remainingCount, - ) - } + Status.check(MapLibreNativeC.mln_event_batch_get(batchHandle, batch)) + val eventCount = batch.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_BATCH_EVENT_COUNT_OFFSET) + if (eventCount == 0L) { + return@use NativeRuntimeEventBatch(emptyList()) + } + // The stride the batch reports can exceed this binding's compiled event + // record, so index by it rather than by mln_runtime_event.sizeof(). + val eventSize = + Integer.toUnsignedLong( + batch.get(ValueLayout.JAVA_INT, RUNTIME_EVENT_BATCH_EVENT_SIZE_OFFSET) + ) + val payloadExtent = eventSize - RUNTIME_EVENT_PAYLOAD_OFFSET + val events = + batch + .get(ValueLayout.ADDRESS, RUNTIME_EVENT_BATCH_EVENTS_OFFSET) + .reinterpret(eventCount * eventSize) + val messagesSize = + batch.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_BATCH_MESSAGES_SIZE_OFFSET) + val messages = + batch + .get(ValueLayout.ADDRESS, RUNTIME_EVENT_BATCH_MESSAGES_OFFSET) + .reinterpret(messagesSize) + NativeRuntimeEventBatch( + List(Math.toIntExact(eventCount)) { index -> + val base = index * eventSize + val payloadType = + events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_PAYLOAD_TYPE_OFFSET) + NativeRuntimeEvent( + type = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_TYPE_OFFSET), + sourceType = + events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_SOURCE_TYPE_OFFSET), + sourceId = events.get(ValueLayout.JAVA_LONG, base + RUNTIME_EVENT_SOURCE_OFFSET), + code = events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_CODE_OFFSET), + payload = + runtimeEventPayload( + payloadType, + events.asSlice(base + RUNTIME_EVENT_PAYLOAD_OFFSET, payloadExtent), + ), + message = + copyMessage( + messages, + events.get(ValueLayout.JAVA_LONG, base + RUNTIME_EVENT_MESSAGE_OFFSET_OFFSET), + Integer.toUnsignedLong( + events.get(ValueLayout.JAVA_INT, base + RUNTIME_EVENT_MESSAGE_SIZE_OFFSET) + ), + ), + ) + } + ) + } finally { + MapLibreNativeC.mln_event_batch_release(batchHandle) + } + } internal fun setRuntimeEventMask(runtime: NativeRuntime, mask: Long) { Status.check(MapLibreNativeC.mln_runtime_set_event_mask(runtime.raw, mask)) @@ -3290,15 +3205,9 @@ internal object NativeAccess { outMask.get(ValueLayout.JAVA_LONG, 0) } - internal fun setMapEventMask(map: NativeMap, mask: Long) { - Status.check(MapLibreNativeC.mln_map_set_event_mask(map.raw, mask)) - } - - internal fun mapEventMask(map: NativeMap): Long = - Arena.ofConfined().use { arena -> - val outMask = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(MapLibreNativeC.mln_map_get_event_mask(map.raw, outMask)) - outMask.get(ValueLayout.JAVA_LONG, 0) + internal fun setMapEventMask(map: NativeMap, mask: Long): Deferred = + command { completion -> + MapLibreNativeC.mln_map_set_event_mask(map.raw, mask, completion) } private fun copyMessage(messages: MemorySegment, offset: Long, size: Long): String { @@ -3324,30 +3233,14 @@ internal object NativeAccess { private fun latLngForProjectedMetersFunction(): MethodHandle = downcall("mln_lat_lng_for_projected_meters") - private fun runtimeCreateFunction(): MethodHandle = downcall("mln_runtime_create") - - private fun runtimeStatusFunction(name: String): MethodHandle = downcall(name) - - private fun runtimePumpFunction(): MethodHandle = downcall("mln_runtime_pump") - - private fun runtimeWakeSourceAcquireFunction(): MethodHandle = - downcall("mln_runtime_wake_source_acquire") - - private fun wakeSourceSignalFunction(): MethodHandle = downcall("mln_wake_source_signal") - - private fun wakeSourceDestroyFunction(): MethodHandle = downcall("mln_wake_source_destroy") - private fun runtimeAmbientCacheOperationStartFunction(): MethodHandle = - downcall("mln_runtime_run_ambient_cache_operation_start") + downcall("mln_runtime_run_ambient_cache_operation") private fun runtimeSetMaximumAmbientCacheSizeStartFunction(): MethodHandle = - downcall("mln_runtime_set_maximum_ambient_cache_size_start") + downcall("mln_runtime_set_maximum_ambient_cache_size") private fun runtimeOfflineRegionCreateStartFunction(): MethodHandle = - downcall("mln_runtime_offline_region_create_start") - - private fun runtimeOfflineOperationDiscardFunction(): MethodHandle = - downcall("mln_runtime_offline_operation_discard") + downcall("mln_runtime_offline_region_create") private fun runtimeSetResourceProviderFunction(): MethodHandle = downcall("mln_runtime_set_resource_provider") @@ -3382,19 +3275,9 @@ internal object NativeAccess { private fun resourceRequestReleaseFunction(): MethodHandle = downcall("mln_resource_request_release") - private fun mapCreateFunction(): MethodHandle = downcall("mln_map_create") - - private fun mapStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapIntStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapBooleanStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapAddressStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapAddressAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapAddressAddressAddressStatusFunction(name: String): MethodHandle = downcall(name) + private fun mapTwoAddressStatusFunction(name: String): MethodHandle = downcall(name) private fun mapStringViewAddressStatusFunction(name: String): MethodHandle = downcall(name) @@ -3426,95 +3309,16 @@ internal object NativeAccess { private fun mapStringViewLatLngDoubleStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapDoubleStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapDoubleDoubleStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapDoubleAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapDoubleDoubleAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapDoubleAddressAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapTwoAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapTwoScreenPointsStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapTwoScreenPointsAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun mapLatLngBoundsAddressAddressStatusFunction(name: String): MethodHandle = - downcall(name) - - private fun mapAddressLongAddressAddressStatusFunction(name: String): MethodHandle = - downcall(name) - - private fun projectionAddressLongEdgeInsetsStatusFunction(name: String): MethodHandle = - downcall(name) - - private fun projectionAddressEdgeInsetsStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapLatLngAddressStatusFunction(name: String): MethodHandle = downcall(name) private fun mapScreenPointAddressStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapAddressLongAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun projectionLatLngAddressStatusFunction(name: String): MethodHandle = - mapLatLngAddressStatusFunction(name) - - private fun projectionScreenPointAddressStatusFunction(name: String): MethodHandle = - mapScreenPointAddressStatusFunction(name) - - private fun renderSessionStatusFunction(name: String): MethodHandle = downcall(name) - - private fun renderSessionRenderUpdateFunction(): MethodHandle = - downcall("mln_render_session_render_update") - - private fun renderSessionResizeFunction(): MethodHandle = downcall("mln_render_session_resize") - - private fun renderSessionAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun renderSessionTwoAddressStatusFunction(name: String): MethodHandle = downcall(name) - - private fun renderSessionQueryRenderedFeaturesFunction(): MethodHandle = - downcall("mln_render_session_query_rendered_features") - - private fun renderSessionQuerySourceFeaturesFunction(): MethodHandle = - downcall("mln_render_session_query_source_features") - - private fun renderSessionQueryFeatureExtensionsFunction(): MethodHandle = - downcall("mln_render_session_query_feature_extensions") - - private fun textureReadPremultipliedRgba8Function(): MethodHandle = - downcall("mln_texture_read_premultiplied_rgba8") - - private fun metalOwnedTextureAcquireFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_metal_owned_texture_acquire_frame") - - private fun vulkanOwnedTextureAcquireFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_vulkan_owned_texture_acquire_frame") - - private fun openglOwnedTextureAcquireFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_opengl_owned_texture_acquire_frame") - - private fun metalOwnedTextureReleaseFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_metal_owned_texture_release_frame") - - private fun vulkanOwnedTextureReleaseFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_vulkan_owned_texture_release_frame") - - private fun openglOwnedTextureReleaseFrameFunction(): MethodHandle = - renderSessionAddressStatusFunction("mln_opengl_owned_texture_release_frame") - private fun mapStringViewDoubleStatusFunction(name: String): MethodHandle = downcall(name) private fun mapStringViewIntStringViewStatusFunction(name: String): MethodHandle = downcall(name) private fun mapStringViewTwoAddressStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapStringViewAddressLongTwoAddressStatusFunction(name: String): MethodHandle = - downcall(name) - private fun mapStringViewAddressLongStatusFunction(name: String): MethodHandle = downcall(name) private fun mapStringViewIntStatusFunction(name: String): MethodHandle = downcall(name) @@ -3525,12 +3329,6 @@ internal object NativeAccess { private fun mapStringViewAddressLongAddressStatusFunction(name: String): MethodHandle = downcall(name) - private fun mapListStyleSourceIdsFunction(): MethodHandle = - downcall("mln_map_list_style_source_ids") - - private fun mapListStyleLayerIdsFunction(): MethodHandle = - downcall("mln_map_list_style_layer_ids") - private fun styleIdListCountFunction(): MethodHandle = downcall("mln_style_id_list_count") private fun styleIdListGetFunction(): MethodHandle = downcall("mln_style_id_list_get") @@ -3546,9 +3344,6 @@ internal object NativeAccess { private fun queriedFeatureListDestroyFunction(): MethodHandle = downcall("mln_queried_feature_list_destroy") - private fun mapGetStyleSourceTileUrlsFunction(): MethodHandle = - downcall("mln_map_get_style_source_tile_urls") - private fun styleStringListCountFunction(): MethodHandle = downcall("mln_style_string_list_count") private fun styleStringListGetFunction(): MethodHandle = downcall("mln_style_string_list_get") @@ -3556,41 +3351,6 @@ internal object NativeAccess { private fun styleStringListDestroyFunction(): MethodHandle = downcall("mln_style_string_list_destroy") - private fun copyStyleSourceAttributionFunction(): MethodHandle = - downcall("mln_map_copy_style_source_attribution") - - private fun copyStyleSourceUrlFunction(): MethodHandle = downcall("mln_map_copy_style_source_url") - - private fun runtimeOfflineRegionStatusTakeResultFunction(): MethodHandle = - downcall("mln_runtime_offline_region_get_status_take_result") - - private fun runtimeOfflineRegionCreateTakeResultFunction(): MethodHandle = - runtimeOfflineOperationSnapshotTakeResultFunction( - "mln_runtime_offline_region_create_take_result" - ) - - private fun runtimeOfflineRegionGetTakeResultFunction(): MethodHandle = - downcall("mln_runtime_offline_region_get_take_result") - - private fun runtimeOfflineRegionsListTakeResultFunction(): MethodHandle = - runtimeOfflineOperationListTakeResultFunction("mln_runtime_offline_regions_list_take_result") - - private fun runtimeOfflineRegionsMergeDatabaseTakeResultFunction(): MethodHandle = - runtimeOfflineOperationListTakeResultFunction( - "mln_runtime_offline_regions_merge_database_take_result" - ) - - private fun runtimeOfflineRegionUpdateMetadataTakeResultFunction(): MethodHandle = - runtimeOfflineOperationSnapshotTakeResultFunction( - "mln_runtime_offline_region_update_metadata_take_result" - ) - - private fun runtimeOfflineOperationSnapshotTakeResultFunction(name: String): MethodHandle = - downcall(name) - - private fun runtimeOfflineOperationListTakeResultFunction(name: String): MethodHandle = - runtimeOfflineOperationSnapshotTakeResultFunction(name) - private fun offlineRegionSnapshotGetFunction(): MethodHandle = downcall("mln_offline_region_snapshot_get") @@ -3611,6 +3371,7 @@ internal object NativeAccess { sourceId: String, url: String, options: TileSourceOptions?, + completion: MemorySegment, ) { Arena.ofConfined().use { arena -> Status.check( @@ -3620,6 +3381,7 @@ internal object NativeAccess { stringView(arena, sourceId), stringView(arena, url), tileSourceOptions(arena, options), + completion, ) as Int ) } @@ -3631,6 +3393,7 @@ internal object NativeAccess { sourceId: String, tiles: List, options: TileSourceOptions?, + completion: MemorySegment, ) { val tileSnapshot = tiles.toList() Arena.ofConfined().use { arena -> @@ -3642,40 +3405,12 @@ internal object NativeAccess { stringViewArray(arena, tileSnapshot), tileSnapshot.size.toLong(), tileSourceOptions(arena, options), + completion, ) as Int ) } } - private fun startRuntimeOperation(name: String, runtime: NativeRuntime): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check(runtimeOperationStartFunction(name).invokeNative(runtime, outOperationId) as Int) - outOperationId.get(ValueLayout.JAVA_LONG, 0) - } - - private fun startRuntimeLongOperation(name: String, runtime: NativeRuntime, value: Long): Long = - Arena.ofConfined().use { arena -> - val outOperationId = arena.allocate(ValueLayout.JAVA_LONG) - Status.check( - runtimeLongOperationStartFunction(name).invokeNative(runtime, value, outOperationId) as Int - ) - outOperationId.get(ValueLayout.JAVA_LONG, 0) - } - - private fun runtimeOperationStartFunction(name: String): MethodHandle = downcall(name) - - private fun runtimeLongOperationStartFunction(name: String): MethodHandle = downcall(name) - - private fun runtimeAddressOperationStartFunction(name: String): MethodHandle = downcall(name) - - private fun runtimeLongAddressLongOperationStartFunction(name: String): MethodHandle = - downcall(name) - - private fun runtimeLongBooleanOperationStartFunction(name: String): MethodHandle = downcall(name) - - private fun runtimeLongIntOperationStartFunction(name: String): MethodHandle = downcall(name) - internal fun defaultRuntimeOptionsEventMask(): Long { ensureLoaded() return Arena.ofConfined().use { arena -> @@ -4153,35 +3888,6 @@ internal object NativeAccess { return segment } - private fun mapCameraAnimationCommand( - functionName: String, - map: NativeMap, - camera: CameraOptions, - animation: AnimationOptions?, - ) { - Arena.ofConfined().use { arena -> - Status.check( - mapTwoAddressStatusFunction(functionName) - .invokeNative(map, cameraOptions(arena, camera), animationOptions(arena, animation)) - as Int - ) - } - } - - private fun mapLatLngBoundsForCamera( - functionName: String, - map: NativeMap, - camera: CameraOptions, - ): LatLngBounds = - Arena.ofConfined().use { arena -> - val outBounds = arena.allocate(latLngBoundsLayout) - Status.check( - mapTwoAddressStatusFunction(functionName) - .invokeNative(map, cameraOptions(arena, camera), outBounds) as Int - ) - latLngBounds(outBounds) - } - private fun boundOptionsDefault(arena: Arena): MemorySegment = MapLibreNativeC.mln_bound_options_default(arena) @@ -4300,7 +4006,7 @@ internal object NativeAccess { fields = fields or STYLE_IMAGE_OPTION_SDF segment.set(ValueLayout.JAVA_BOOLEAN, STYLE_IMAGE_OPTIONS_SDF_OFFSET, it) } - value?.stretchX?.let { + value.stretchX?.let { fields = fields or STYLE_IMAGE_OPTION_STRETCH_X segment.set( ValueLayout.ADDRESS, @@ -4313,7 +4019,7 @@ internal object NativeAccess { it.size.toLong(), ) } - value?.stretchY?.let { + value.stretchY?.let { fields = fields or STYLE_IMAGE_OPTION_STRETCH_Y segment.set( ValueLayout.ADDRESS, @@ -4326,7 +4032,7 @@ internal object NativeAccess { it.size.toLong(), ) } - value?.content?.let { + value.content?.let { fields = fields or STYLE_IMAGE_OPTION_CONTENT val content = segment.asSlice(STYLE_IMAGE_OPTIONS_CONTENT_OFFSET, mln_image_content.sizeof()) content.set(ValueLayout.JAVA_FLOAT, IMAGE_CONTENT_LEFT_OFFSET, it.left) @@ -4334,11 +4040,11 @@ internal object NativeAccess { content.set(ValueLayout.JAVA_FLOAT, IMAGE_CONTENT_RIGHT_OFFSET, it.right) content.set(ValueLayout.JAVA_FLOAT, IMAGE_CONTENT_BOTTOM_OFFSET, it.bottom) } - value?.textFitWidth?.let { + value.textFitWidth?.let { fields = fields or STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH segment.set(ValueLayout.JAVA_INT, STYLE_IMAGE_OPTIONS_TEXT_FIT_WIDTH_OFFSET, it.nativeValue) } - value?.textFitHeight?.let { + value.textFitHeight?.let { fields = fields or STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT segment.set(ValueLayout.JAVA_INT, STYLE_IMAGE_OPTIONS_TEXT_FIT_HEIGHT_OFFSET, it.nativeValue) } @@ -4461,6 +4167,9 @@ internal object NativeAccess { private fun debugOptions(mask: Int): Set = DebugOption.entries.filterTo(mutableSetOf()) { option -> (mask and option.nativeMask) != 0 } + private fun debugOptionMask(options: Set): Int = + options.fold(0) { mask, option -> mask or option.nativeMask } + private fun latLng(value: LatLng, arena: Arena): MemorySegment { val segment = arena.allocate(latLngLayout) segment.set(ValueLayout.JAVA_DOUBLE, 0, value.latitude) @@ -4768,15 +4477,16 @@ internal object NativeAccess { private fun mapOptions(options: MapOptions, arena: Arena): MemorySegment { val segment = MapLibreNativeC.mln_map_options_default(arena) + val extent = mln_map_options.initial_extent(segment) options.width?.let { Status.requireArgument(it >= 0) { "width must be non-negative" } - mln_map_options.width(segment, it) + mln_logical_extent.width(extent, it) } options.height?.let { Status.requireArgument(it >= 0) { "height must be non-negative" } - mln_map_options.height(segment, it) + mln_logical_extent.height(extent, it) } - options.scaleFactor?.let { mln_map_options.scale_factor(segment, it) } + options.scaleFactor?.let { mln_logical_extent.scale_factor(extent, it) } options.mapMode?.let { Status.requireArgument(it.isKnown) { "Unknown map mode cannot be used as input: ${it.nativeValue}" @@ -4806,6 +4516,120 @@ internal object NativeAccess { status.get(ValueLayout.JAVA_BOOLEAN, OFFLINE_REGION_STATUS_COMPLETE_OFFSET), ) + private fun completionValue(result: MemorySegment, byteSize: Long): MemorySegment { + val value = mln_completion_result.value(result) + check(value != MemorySegment.NULL) { "native completion omitted its result value" } + return value.reinterpret(byteSize) + } + + private fun stringViewsCompletion(result: MemorySegment): List { + val count = checkedInt(mln_completion_result.value_count(result)) + if (count == 0) return emptyList() + val values = completionValue(result, mln_buffer_view.sizeof() * count.toLong()) + return List(count) { index -> + stringView(values.asSlice(index * mln_buffer_view.sizeof(), mln_buffer_view.sizeof())) + } + } + + private fun optionalBufferCompletion(result: MemorySegment): ByteArray? { + if (mln_completion_result.value_count(result) == 0L) return null + val view = completionValue(result, mln_buffer_view.sizeof()) + return copyBytes(mln_buffer_view.data(view), mln_buffer_view.size(view)) + } + + private fun requiredBufferCompletion(result: MemorySegment): ByteArray = + checkNotNull(optionalBufferCompletion(result)) { "native completion omitted its byte result" } + + private fun queriedFeaturesCompletion(result: MemorySegment): List { + val count = checkedInt(mln_completion_result.value_count(result)) + if (count == 0) return emptyList() + val values = completionValue(result, mln_queried_feature.sizeof() * count.toLong()) + return List(count) { index -> + queriedFeature( + values.asSlice(index * mln_queried_feature.sizeof(), mln_queried_feature.sizeof()) + ) + } + } + + private fun textureReadbackCompletion(result: MemorySegment): TextureReadback { + val value = completionValue(result, mln_texture_readback_result.sizeof()) + return TextureReadback( + copyBufferView(mln_texture_readback_result.data(value)), + readTextureImageInfo(mln_texture_readback_result.info(value)), + ) + } + + private fun sourceInfoCompletion(result: MemorySegment): SourceInfo? { + if (mln_completion_result.value_count(result) == 0L) return null + val raw = completionValue(result, mln_style_source_result.sizeof()) + val info = mln_style_source_result.info(raw) + val fields = mln_style_source_info.fields(info) + val tileUrls = + if (mln_style_source_result.tile_url_count(raw) == 0L) emptyList() + else { + val count = checkedInt(mln_style_source_result.tile_url_count(raw)) + val views = + mln_style_source_result + .tile_urls(raw) + .reinterpret(mln_buffer_view.sizeof() * count.toLong()) + List(count) { index -> + stringView(views.asSlice(index * mln_buffer_view.sizeof(), mln_buffer_view.sizeof())) + } + } + return SourceInfo( + SourceType.fromNative(mln_style_source_info.type(info)), + mln_style_source_info.is_volatile(info), + if (mln_style_source_info.has_attribution(info)) + stringView(mln_style_source_result.attribution(raw)) + else null, + if (fields and STYLE_SOURCE_INFO_URL != 0) stringView(mln_style_source_result.url(raw)) + else null, + if (fields and STYLE_SOURCE_INFO_TILEJSON != 0) + TileJson( + tileUrls, + mln_style_source_info.min_zoom(info), + mln_style_source_info.max_zoom(info), + TileScheme.fromNative(mln_style_source_info.scheme(info)), + if (fields and STYLE_SOURCE_INFO_BOUNDS != 0) + latLngBounds(mln_style_source_info.bounds(info)) + else null, + ) + else null, + if (fields and STYLE_SOURCE_INFO_TILE_SIZE != 0) + Math.toIntExact(Integer.toUnsignedLong(mln_style_source_info.tile_size(info))) + else null, + if (fields and STYLE_SOURCE_INFO_VECTOR_ENCODING != 0) + VectorTileEncoding.fromNative(mln_style_source_info.vector_encoding(info)) + else null, + if (fields and STYLE_SOURCE_INFO_RASTER_ENCODING != 0) + RasterDemEncoding.fromNative(mln_style_source_info.raster_encoding(info)) + else null, + ) + } + + private fun offlineRegionList( + runtime: NativeRuntime, + submit: (MemorySegment) -> Int, + ): Deferred> = + CompletionBridge.submit( + { result -> + val count = Math.toIntExact(mln_completion_result.value_count(result)) + if (count == 0) emptyList() + else { + val values = completionValue(result, mln_offline_region_info.sizeof() * count.toLong()) + List(count) { index -> + offlineRegionInfo( + values.asSlice( + index * mln_offline_region_info.sizeof(), + mln_offline_region_info.sizeof(), + ) + ) + } + } + }, + submit, + ) + private fun offlineRegionDefinition(value: OfflineRegionDefinition, arena: Arena): MemorySegment { val segment = arena.allocate(OFFLINE_REGION_DEFINITION_SIZE) segment.set( @@ -5063,37 +4887,6 @@ internal object NativeAccess { private fun copyBufferView(view: MemorySegment): ByteArray = copyBytes(mln_buffer_view.data(view), mln_buffer_view.size(view)) - private fun styleSourceTileUrls( - map: NativeMap, - sourceId: MemorySegment, - arena: Arena, - ): List { - val outList = arena.allocate(ValueLayout.JAVA_LONG) - outList.set(ValueLayout.JAVA_LONG, 0, 0L) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - mapGetStyleSourceTileUrlsFunction().invokeNative(map, sourceId, outList, outFound) as Int - ) - check(outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - "style source disappeared while its metadata was copied" - } - val list = NativeStyleStringList(outList.get(ValueLayout.JAVA_LONG, 0)) - check(!list.isNull) { "mln_map_get_style_source_tile_urls returned the null handle" } - return styleStringList(list) - } - - private fun styleStringList(list: NativeStyleStringList): List = - styleStringList( - list, - counter = { handle, outCount -> - styleStringListCountFunction().invokeNative(handle, outCount) as Int - }, - getter = { handle, index, outValue -> - styleStringListGetFunction().invokeNative(handle, index, outValue) as Int - }, - destroyer = { handle -> styleStringListDestroyFunction().invokeNative(handle) }, - ) - private fun styleStringList( list: NativeStyleStringList, counter: (NativeStyleStringList, MemorySegment) -> Int, @@ -5115,82 +4908,6 @@ internal object NativeAccess { destroyer(list) } - private fun copyStyleSourceAttribution( - map: NativeMap, - sourceId: MemorySegment, - attributionSize: Long, - arena: Arena, - ): String? { - if (attributionSize == 0L) { - return "" - } - val outAttribution = arena.allocate(attributionSize) - val outAttributionSize = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - copyStyleSourceAttributionFunction() - .invokeNative(map, sourceId, outAttribution, attributionSize, outAttributionSize, outFound) - as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) { - return null - } - return copyString(outAttribution, outAttributionSize.get(ValueLayout.JAVA_LONG, 0)) - } - - private fun copyStyleSourceUrl( - map: NativeMap, - sourceId: MemorySegment, - urlSize: Long, - arena: Arena, - ): String? { - val outUrl = if (urlSize == 0L) MemorySegment.NULL else arena.allocate(urlSize) - val outUrlSize = arena.allocate(ValueLayout.JAVA_LONG) - val outFound = arena.allocate(ValueLayout.JAVA_BOOLEAN) - Status.check( - copyStyleSourceUrlFunction() - .invokeNative(map, sourceId, outUrl, urlSize, outUrlSize, outFound) as Int - ) - if (!outFound.get(ValueLayout.JAVA_BOOLEAN, 0)) return null - return if (urlSize == 0L) "" else copyString(outUrl, outUrlSize.get(ValueLayout.JAVA_LONG, 0)) - } - - private fun takeOfflineRegionSnapshot( - runtime: NativeRuntime, - operationId: Long, - function: MethodHandle, - markTaken: () -> Unit, - ): OfflineRegionInfo = - Arena.ofConfined().use { arena -> - val outSnapshot = arena.allocate(ValueLayout.JAVA_LONG) - outSnapshot.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(function.invokeNative(runtime, operationId, outSnapshot) as Int) - try { - offlineRegionSnapshot( - NativeOfflineRegionSnapshot(outSnapshot.get(ValueLayout.JAVA_LONG, 0)) - ) - } finally { - markTaken() - } - } - - private fun takeOfflineRegionList( - runtime: NativeRuntime, - operationId: Long, - function: MethodHandle, - markTaken: () -> Unit, - ): List = - Arena.ofConfined().use { arena -> - val outList = arena.allocate(ValueLayout.JAVA_LONG) - outList.set(ValueLayout.JAVA_LONG, 0, 0L) - Status.check(function.invokeNative(runtime, operationId, outList) as Int) - try { - offlineRegionList(NativeOfflineRegionList(outList.get(ValueLayout.JAVA_LONG, 0))) - } finally { - markTaken() - } - } - private fun offlineRegionInfo(info: MemorySegment): OfflineRegionInfo = OfflineRegionInfo( info.get(ValueLayout.JAVA_LONG, OFFLINE_REGION_INFO_ID_OFFSET), @@ -5284,7 +5001,6 @@ internal object NativeAccess { PAYLOAD_OFFLINE_REGION_STATUS -> offlineRegionStatusPayload(payload) PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR -> offlineRegionResponseErrorPayload(payload) PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT -> offlineRegionTileCountLimitPayload(payload) - PAYLOAD_OFFLINE_OPERATION_COMPLETED -> offlineOperationCompletedPayload(payload) PAYLOAD_CAMERA_TRANSITION_FINISHED -> cameraTransitionFinishedPayload(payload) else -> unknownPayload(payloadType, payload) } @@ -5370,27 +5086,6 @@ internal object NativeAccess { payload.get(ValueLayout.JAVA_LONG, RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_LIMIT_OFFSET), ) - private fun offlineOperationCompletedPayload( - payload: MemorySegment - ): RuntimeEventPayload.OfflineOperationCompleted = - RuntimeEventPayload.OfflineOperationCompleted( - payload.get( - ValueLayout.JAVA_LONG, - RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_OPERATION_ID_OFFSET, - ), - OfflineOperationKind.fromNative( - payload.get(ValueLayout.JAVA_INT, RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_KIND_OFFSET) - ), - OfflineOperationResultKind.fromNative( - payload.get( - ValueLayout.JAVA_INT, - RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_RESULT_KIND_OFFSET, - ) - ), - payload.get(ValueLayout.JAVA_INT, RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_STATUS_OFFSET), - payload.get(ValueLayout.JAVA_BOOLEAN, RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_FOUND_OFFSET), - ) - private fun cameraTransitionFinishedPayload( payload: MemorySegment ): RuntimeEventPayload.CameraTransitionFinished = @@ -5401,6 +5096,36 @@ internal object NativeAccess { ) ) + private fun renderFrameResult(segment: MemorySegment): RenderFrameResult = + RenderFrameResult( + disposition = RenderResult.fromNative(segment.get(ValueLayout.JAVA_INT, 4)), + token = segment.get(ValueLayout.JAVA_LONG, 8).toULong(), + mapUpdateGeneration = segment.get(ValueLayout.JAVA_LONG, 16).toULong(), + extentGeneration = segment.get(ValueLayout.JAVA_LONG, 24).toULong(), + frameGeneration = segment.get(ValueLayout.JAVA_LONG, 32).toULong(), + needsRepaint = segment.get(ValueLayout.JAVA_BOOLEAN, 40), + ) + + private fun gpuSync(arena: Arena, sync: GpuSync): MemorySegment = + arena.allocate(GPU_SYNC_SIZE).also { segment -> + segment.set(ValueLayout.JAVA_INT, 0, GPU_SYNC_SIZE.toInt()) + segment.set(ValueLayout.JAVA_INT, 4, sync.kind.nativeValue) + segment.set( + ValueLayout.ADDRESS, + 8, + if (sync.objectHandle == 0uL) MemorySegment.NULL + else MemorySegment.ofAddress(sync.objectHandle.toLong()), + ) + segment.set(ValueLayout.JAVA_LONG, 16, sync.value.toLong()) + } + + private fun readGpuSync(segment: MemorySegment): GpuSync = + GpuSync( + kind = GpuSyncKind.fromNative(segment.get(ValueLayout.JAVA_INT, 4)), + objectHandle = segment.get(ValueLayout.ADDRESS, 8).address().toULong(), + value = segment.get(ValueLayout.JAVA_LONG, 16).toULong(), + ) + /** * Invokes a downcall, passing each handle as the integer the C API declares. * @@ -5413,6 +5138,26 @@ internal object NativeAccess { private fun downcall(name: String): MethodHandle = MapLibreNativeC::class.java.getMethod("${name}\$handle").invoke(null) as MethodHandle + private fun dynamicStatusDowncall( + name: String, + vararg argumentLayouts: MemoryLayout, + ): MethodHandle = + dynamicDowncallCache[name] + ?: dynamicDowncall(name, FunctionDescriptor.of(ValueLayout.JAVA_INT, *argumentLayouts)) + + private fun dynamicDowncall(name: String, descriptor: FunctionDescriptor): MethodHandle { + dynamicDowncallCache[name]?.let { + return it + } + ensureLoaded() + val address = + SymbolLookup.loaderLookup().find(name).orElseThrow { + UnsatisfiedLinkError("Loaded native library does not expose $name.") + } + val handle = Linker.nativeLinker().downcallHandle(address, descriptor) + return dynamicDowncallCache.putIfAbsent(name, handle) ?: handle + } + private fun nativeAccessFailure(cause: Throwable): IllegalStateException = IllegalStateException( "Java FFM native access is not enabled. Run the JVM with " + @@ -5442,6 +5187,27 @@ internal object NativeAccess { private val latLngBoundsLayout = mln_lat_lng_bounds.layout() private val canonicalTileIdLayout = mln_canonical_tile_id.layout() private val unitBezierLayout = mln_unit_bezier.layout() + + private val RENDER_SESSION_ATTACH_OPTIONS_SIZE: Long = mln_render_session_attach_options.sizeof() + private val ATTACH_OPTIONS_FRAME_WAKE_OFFSET: Long = + mln_render_session_attach_options.`frame_wake$offset`() + private val ATTACH_OPTIONS_DRIVER_WORK_WAKE_OFFSET: Long = + mln_render_session_attach_options.`driver_work_wake$offset`() + private val WAKE_SIZE: Int = mln_wake.sizeof().toInt() + private val dynamicDowncallCache = ConcurrentHashMap() + + private val RENDER_SESSION_CAPABILITIES_SIZE: Long = mln_render_session_capabilities.sizeof() + private val FRAME_DEMAND_SIZE: Long = mln_frame_demand.sizeof() + private val RENDER_FRAME_RESULT_SIZE: Long = mln_render_frame_result.sizeof() + private val RENDER_SESSION_SNAPSHOT_SIZE: Long = mln_render_session_snapshot.sizeof() + private val GPU_SYNC_SIZE: Long = mln_gpu_sync.sizeof() + private val RENDER_ABANDON_RESULT_SIZE: Long = mln_render_abandon_result.sizeof() + private const val RENDER_CAPABILITY_FRAME_ACQUISITION: Int = 1 shl 0 + private const val RENDER_CAPABILITY_READBACK: Int = 1 shl 1 + private const val RENDER_CAPABILITY_CONSUMER_SYNC: Int = 1 shl 2 + private const val RENDER_CAPABILITY_PRESENTATION: Int = 1 shl 3 + private const val FRAME_DEMAND_IF_NEEDED: Int = 1 shl 0 + private const val FRAME_DEMAND_PRESENT: Int = 1 shl 1 private val vec3Layout = mln_vec3.layout() private val quaternionLayout = mln_quaternion.layout() private val stringViewLayout = mln_buffer_view.layout() @@ -5461,18 +5227,15 @@ internal object NativeAccess { private val SCREEN_LINE_STRING_POINT_COUNT_OFFSET: Long = mln_screen_line_string.`point_count$offset`() - private val UNIT_BEZIER_SIZE: Long = mln_unit_bezier.sizeof() private val UNIT_BEZIER_X1_OFFSET: Long = mln_unit_bezier.`x1$offset`() private val UNIT_BEZIER_Y1_OFFSET: Long = mln_unit_bezier.`y1$offset`() private val UNIT_BEZIER_X2_OFFSET: Long = mln_unit_bezier.`x2$offset`() private val UNIT_BEZIER_Y2_OFFSET: Long = mln_unit_bezier.`y2$offset`() - private val VEC3_SIZE: Long = mln_vec3.sizeof() private val VEC3_X_OFFSET: Long = mln_vec3.`x$offset`() private val VEC3_Y_OFFSET: Long = mln_vec3.`y$offset`() private val VEC3_Z_OFFSET: Long = mln_vec3.`z$offset`() - private val QUATERNION_SIZE: Long = mln_quaternion.sizeof() private val QUATERNION_X_OFFSET: Long = mln_quaternion.`x$offset`() private val QUATERNION_Y_OFFSET: Long = mln_quaternion.`y$offset`() private val QUATERNION_Z_OFFSET: Long = mln_quaternion.`z$offset`() @@ -5480,7 +5243,6 @@ internal object NativeAccess { private val LAT_LNG_BOUNDS_SIZE: Long = mln_lat_lng_bounds.sizeof() - private val CANONICAL_TILE_ID_SIZE: Long = mln_canonical_tile_id.sizeof() private val CANONICAL_TILE_ID_Z_OFFSET: Long = mln_canonical_tile_id.`z$offset`() private val CANONICAL_TILE_ID_X_OFFSET: Long = mln_canonical_tile_id.`x$offset`() private val CANONICAL_TILE_ID_Y_OFFSET: Long = mln_canonical_tile_id.`y$offset`() @@ -5537,9 +5299,6 @@ internal object NativeAccess { mln_style_source_info.`is_volatile$offset`() private val STYLE_SOURCE_INFO_HAS_ATTRIBUTION_OFFSET: Long = mln_style_source_info.`has_attribution$offset`() - private val STYLE_SOURCE_INFO_ATTRIBUTION_SIZE_OFFSET: Long = - mln_style_source_info.`attribution_size$offset`() - private val STYLE_SOURCE_INFO_URL_SIZE_OFFSET: Long = mln_style_source_info.`url_size$offset`() private val STYLE_SOURCE_INFO_MIN_ZOOM_OFFSET: Long = mln_style_source_info.`min_zoom$offset`() private val STYLE_SOURCE_INFO_MAX_ZOOM_OFFSET: Long = mln_style_source_info.`max_zoom$offset`() private val STYLE_SOURCE_INFO_SCHEME_OFFSET: Long = mln_style_source_info.`scheme$offset`() @@ -5557,9 +5316,11 @@ internal object NativeAccess { private const val STYLE_SOURCE_INFO_VECTOR_ENCODING: Int = 1 shl 4 private const val STYLE_SOURCE_INFO_RASTER_ENCODING: Int = 1 shl 5 + private const val STYLE_LAYER_INFO_SOURCE_ID: Int = 1 shl 0 + private const val STYLE_LAYER_INFO_SOURCE_LAYER: Int = 1 shl 1 + private const val IMAGE_SOURCE_COORDINATE_COUNT: Int = 4 - private val EDGE_INSETS_SIZE: Long = mln_edge_insets.sizeof() private val EDGE_INSETS_TOP_OFFSET: Long = mln_edge_insets.`top$offset`() private val EDGE_INSETS_LEFT_OFFSET: Long = mln_edge_insets.`left$offset`() private val EDGE_INSETS_BOTTOM_OFFSET: Long = mln_edge_insets.`bottom$offset`() @@ -5803,19 +5564,18 @@ internal object NativeAccess { mln_runtime_event.`message_offset$offset`() private val RUNTIME_EVENT_MESSAGE_SIZE_OFFSET: Long = mln_runtime_event.`message_size$offset`() - private val RUNTIME_EVENT_BATCH_SIZE: Long = mln_runtime_event_batch.sizeof() - private val RUNTIME_EVENT_BATCH_SIZE_OFFSET: Long = mln_runtime_event_batch.`size$offset`() + private val RUNTIME_EVENT_BATCH_SIZE: Long = mln_runtime_event_batch_view.sizeof() + private val RUNTIME_EVENT_BATCH_SIZE_OFFSET: Long = mln_runtime_event_batch_view.`size$offset`() private val RUNTIME_EVENT_BATCH_EVENT_SIZE_OFFSET: Long = - mln_runtime_event_batch.`event_size$offset`() - private val RUNTIME_EVENT_BATCH_EVENTS_OFFSET: Long = mln_runtime_event_batch.`events$offset`() + mln_runtime_event_batch_view.`event_size$offset`() + private val RUNTIME_EVENT_BATCH_EVENTS_OFFSET: Long = + mln_runtime_event_batch_view.`events$offset`() private val RUNTIME_EVENT_BATCH_EVENT_COUNT_OFFSET: Long = - mln_runtime_event_batch.`event_count$offset`() + mln_runtime_event_batch_view.`event_count$offset`() private val RUNTIME_EVENT_BATCH_MESSAGES_OFFSET: Long = - mln_runtime_event_batch.`messages$offset`() + mln_runtime_event_batch_view.`messages$offset`() private val RUNTIME_EVENT_BATCH_MESSAGES_SIZE_OFFSET: Long = - mln_runtime_event_batch.`messages_size$offset`() - private val RUNTIME_EVENT_BATCH_REMAINING_OFFSET: Long = - mln_runtime_event_batch.`remaining_count$offset`() + mln_runtime_event_batch_view.`messages_size$offset`() private val RESOURCE_REQUEST_REQUESTED_URL_OFFSET: Long = mln_resource_request.`requested_url$offset`() @@ -5881,11 +5641,8 @@ internal object NativeAccess { MapLibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR() private val PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT: Int = MapLibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT() - private val PAYLOAD_OFFLINE_OPERATION_COMPLETED: Int = - MapLibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED() private val PAYLOAD_CAMERA_TRANSITION_FINISHED: Int = MapLibreNativeC.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED() - private val OFFLINE_REGION_STATUS_SIZE: Long = mln_offline_region_status.sizeof() private val OFFLINE_REGION_STATUS_SIZE_OFFSET: Long = mln_offline_region_status.`size$offset`() private val OFFLINE_REGION_STATUS_DOWNLOAD_STATE_OFFSET: Long = @@ -5956,17 +5713,6 @@ internal object NativeAccess { private val RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_LIMIT_OFFSET: Long = mln_runtime_event_offline_region_tile_count_limit.`limit$offset`() - private val RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_OPERATION_ID_OFFSET: Long = - mln_runtime_event_offline_operation_completed.`operation_id$offset`() - private val RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_KIND_OFFSET: Long = - mln_runtime_event_offline_operation_completed.`operation_kind$offset`() - private val RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_RESULT_KIND_OFFSET: Long = - mln_runtime_event_offline_operation_completed.`result_kind$offset`() - private val RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_STATUS_OFFSET: Long = - mln_runtime_event_offline_operation_completed.`result_status$offset`() - private val RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED_FOUND_OFFSET: Long = - mln_runtime_event_offline_operation_completed.`found$offset`() - private val RUNTIME_EVENT_CAMERA_TRANSITION_FINISHED_TRANSITION_ID_OFFSET: Long = mln_runtime_event_camera_transition_finished.`transition_id$offset`() @@ -6071,35 +5817,6 @@ internal object NativeAccess { native.get(ValueLayout.JAVA_INT, RENDERED_QUERY_GEOMETRY_TYPE_OFFSET) } - fun featureStateSelectorSnapshot(value: FeatureStateSelector): FeatureStateSelectorSnapshot = - Arena.ofConfined().use { arena -> - val native = featureStateSelector(arena, value) - val fields = native.get(ValueLayout.JAVA_INT, FEATURE_STATE_SELECTOR_FIELDS_OFFSET) - FeatureStateSelectorSnapshot( - fields, - stringView(native.asSlice(FEATURE_STATE_SELECTOR_SOURCE_ID_OFFSET, STRING_VIEW_SIZE)), - if ((fields and FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID) != 0) - stringView( - native.asSlice(FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID_OFFSET, STRING_VIEW_SIZE) - ) - else null, - if ((fields and FEATURE_STATE_SELECTOR_FEATURE_ID) != 0) - stringView(native.asSlice(FEATURE_STATE_SELECTOR_FEATURE_ID_OFFSET, STRING_VIEW_SIZE)) - else null, - if ((fields and FEATURE_STATE_SELECTOR_STATE_KEY) != 0) - stringView(native.asSlice(FEATURE_STATE_SELECTOR_STATE_KEY_OFFSET, STRING_VIEW_SIZE)) - else null, - ) - } - - data class FeatureStateSelectorSnapshot( - val fields: Int, - val sourceId: String, - val sourceLayerId: String?, - val featureId: String?, - val stateKey: String?, - ) - fun offlineRegionDefinitionRoundTrip(value: OfflineRegionDefinition): OfflineRegionDefinition = Arena.ofConfined().use { arena -> offlineRegionDefinition(offlineRegionDefinition(value, arena)) @@ -6260,10 +5977,11 @@ internal object NativeAccess { fun mapOptionsSnapshot(value: MapOptions): MapOptionsSnapshot = Arena.ofConfined().use { arena -> val native = mapOptions(value, arena) + val extent = mln_map_options.initial_extent(native) MapOptionsSnapshot( - mln_map_options.width(native), - mln_map_options.height(native), - mln_map_options.scale_factor(native), + mln_logical_extent.width(extent), + mln_logical_extent.height(extent), + mln_logical_extent.scale_factor(extent), mln_map_options.map_mode(native), mln_map_options.fast_pfor_enabled(native), ) @@ -6375,16 +6093,6 @@ internal object NativeAccess { val message: String, ) - /** Events copied out of one drained batch, plus the count the drain left queued. */ - internal data class NativeRuntimeEventBatch( - val events: List, - val remainingCount: Long, - ) - - internal class OwnedTextureFrameSegment(val segment: MemorySegment, private val arena: Arena) : - AutoCloseable { - override fun close() { - arena.close() - } - } + /** Events copied out of one drained batch. */ + internal data class NativeRuntimeEventBatch(val events: List) } diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt index 24d9ee7cf..cccf0816d 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt @@ -15,8 +15,8 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions /** * Owns map/style-scoped custom geometry source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ internal class CustomGeometrySourceState( private val options: CustomGeometrySourceOptions, diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt index 785253f0e..337728580 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt @@ -15,8 +15,8 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions /** * Owns map/style-scoped custom MVT vector source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ internal class CustomMvtVectorSourceState( private val options: CustomMvtVectorSourceOptions, diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt index a787ca512..c180e5103 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt @@ -1,14 +1,15 @@ package org.maplibre.nativeffi.map -import org.maplibre.nativeffi.camera.AnimationOptions +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.camera.BoundOptions -import org.maplibre.nativeffi.camera.CameraFitOptions -import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraSnapshot +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds -import org.maplibre.nativeffi.geo.ScreenPoint +import org.maplibre.nativeffi.internal.async.mapDeferred import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.lifecycle.NativeMap @@ -21,10 +22,13 @@ import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLOwnedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor import org.maplibre.nativeffi.render.PremultipliedRgba8Image +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor import org.maplibre.nativeffi.render.VulkanOwnedTextureDescriptor import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor +import org.maplibre.nativeffi.runtime.CommandCompletion import org.maplibre.nativeffi.runtime.RuntimeEventMask import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @@ -32,9 +36,9 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions import org.maplibre.nativeffi.style.GeoJsonSourceDataHandle import org.maplibre.nativeffi.style.GeoJsonSourceOptions import org.maplibre.nativeffi.style.ImageStretch +import org.maplibre.nativeffi.style.LayerInfo import org.maplibre.nativeffi.style.LocationIndicatorImageKind import org.maplibre.nativeffi.style.SourceInfo -import org.maplibre.nativeffi.style.SourceType import org.maplibre.nativeffi.style.StyleImage import org.maplibre.nativeffi.style.StyleImageInfo import org.maplibre.nativeffi.style.StyleImageOptions @@ -44,10 +48,14 @@ import org.maplibre.nativeffi.style.TileSourceOptions /** Owned JVM FFM map handle. */ public actual class MapHandle -private constructor(private val runtime: RuntimeHandle, private val handle: NativeMap) : - AutoCloseable { - private val runtimeRetention = runtime.retainChild("MapHandle") +private constructor( + private val runtime: RuntimeHandle, + private val handle: NativeMap, + cachedEventMask: RuntimeEventMask, +) { private val core = HandleStateCore("MapHandle", handle.raw) + private var cachedEventMask = + RuntimeEventMask(cachedEventMask.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) init { HandleLeakCleaner.register(this, core.leakReport) @@ -63,77 +71,77 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati public actual fun runtime(): RuntimeHandle = runtime - public actual var eventMask: RuntimeEventMask - get() { - NativeAccess.ensureLoaded() - return RuntimeEventMask(NativeAccess.mapEventMask(requireLiveHandle())) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setMapEventMask(requireLiveHandle(), value.nativeValue) + public actual val eventMask: RuntimeEventMask + get() = cachedEventMask + + public actual fun setEventMask(value: RuntimeEventMask): Deferred { + NativeAccess.ensureLoaded() + return NativeAccess.setMapEventMask(requireLiveHandle(), value.nativeValue).mapDeferred { + cachedEventMask = + RuntimeEventMask(value.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) + it } + } - public actual fun setStyleUrl(url: String) { + public actual fun setStyleUrl(url: String): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setMapStyleUrl(requireLiveHandle(), url) } - public actual fun setStyleJson(json: ByteArray) { + public actual fun setStyleJson(json: ByteArray): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setMapStyleJson(requireLiveHandle(), json) } - public actual fun setFeatureState(selector: FeatureStateSelector, value: ByteArray) { + public actual fun setFeatureState( + selector: FeatureStateSelector, + value: ByteArray, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setMapFeatureState(requireLiveHandle(), selector, value) } - public actual fun getFeatureState(selector: FeatureStateSelector): ByteArray { + public actual fun getFeatureState(selector: FeatureStateSelector): Deferred { NativeAccess.ensureLoaded() return NativeAccess.getMapFeatureState(requireLiveHandle(), selector) } - public actual fun removeFeatureState(selector: FeatureStateSelector) { + public actual fun removeFeatureState( + selector: FeatureStateSelector + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.removeMapFeatureState(requireLiveHandle(), selector) } - public actual fun loadedStyleJson(): ByteArray { + public actual fun loadedStyleJson(): Deferred { NativeAccess.ensureLoaded() return NativeAccess.loadedStyleJson(requireLiveHandle()) } - public actual fun styleUrl(): String { + public actual fun styleUrl(): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleUrl(requireLiveHandle()) } - public actual fun addStyleSourceJson(sourceId: String, sourceJson: ByteArray) { + public actual fun addStyleSourceJson( + sourceId: String, + sourceJson: ByteArray, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addStyleSourceJson(requireLiveHandle(), sourceId, sourceJson) } - public actual fun removeStyleSource(sourceId: String): Boolean { - NativeAccess.ensureLoaded() - return NativeAccess.removeStyleSource(requireLiveHandle(), sourceId) - } - - public actual fun styleSourceExists(sourceId: String): Boolean { + public actual fun removeStyleSource(sourceId: String): Deferred = command { NativeAccess.ensureLoaded() - return NativeAccess.styleSourceExists(requireLiveHandle(), sourceId) + NativeAccess.removeStyleSource(requireLiveHandle(), sourceId) } - public actual fun styleSourceType(sourceId: String): SourceType? { - NativeAccess.ensureLoaded() - return NativeAccess.styleSourceType(requireLiveHandle(), sourceId) - } - - public actual fun styleSourceInfo(sourceId: String): SourceInfo? { + public actual fun styleSourceInfo(sourceId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleSourceInfo(requireLiveHandle(), sourceId) } - public actual fun styleSourceIds(): List { + public actual fun styleSourceIds(): Deferred> { NativeAccess.ensureLoaded() return NativeAccess.styleSourceIds(requireLiveHandle()) } @@ -142,31 +150,43 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, url: String, options: GeoJsonSourceOptions?, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addGeoJsonSourceUrl(requireLiveHandle(), sourceId, url, options) } - public actual fun addGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun addGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { NativeAccess.ensureLoaded() data.withNativeHandle { nativeData -> NativeAccess.addGeoJsonSourceData(requireLiveHandle(), sourceId, nativeData) } } - public actual fun setGeoJsonSourceUrl(sourceId: String, url: String) { + public actual fun setGeoJsonSourceUrl( + sourceId: String, + url: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setGeoJsonSourceUrl(requireLiveHandle(), sourceId, url) } - public actual fun setGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun setGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { NativeAccess.ensureLoaded() data.withNativeHandle { nativeData -> NativeAccess.setGeoJsonSourceData(requireLiveHandle(), sourceId, nativeData) } } - public actual fun setGeoJsonSourceSynchronousTiling(sourceId: String, enabled: Boolean) { + public actual fun setGeoJsonSourceSynchronousTiling( + sourceId: String, + enabled: Boolean, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setGeoJsonSourceSynchronousTiling(requireLiveHandle(), sourceId, enabled) } @@ -174,33 +194,44 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati public actual fun addCustomGeometrySource( sourceId: String, options: CustomGeometrySourceOptions, - ) { + ): Deferred { NativeAccess.ensureLoaded() - // The release callback captures the registry rather than this map, so a map a - // host leaks with a live source still reports as leaked. val registry = customGeometrySources val sourceState = CustomGeometrySourceState(options) { registry.remove(sourceId) } + lateinit var completed: Deferred registry.install(sourceId, sourceState) { - NativeAccess.addCustomGeometrySource(requireLiveHandle(), sourceId, sourceState.descriptor()) + completed = + NativeAccess.addCustomGeometrySource( + requireLiveHandle(), + sourceId, + sourceState.descriptor(), + ) HandleLeakCleaner.retainNativeCallbackRoot(sourceState) } + return completed } public actual fun setCustomGeometrySourceTileData( sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setCustomGeometrySourceTileData(requireLiveHandle(), sourceId, tileId, data) } - public actual fun invalidateCustomGeometrySourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomGeometrySourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.invalidateCustomGeometrySourceTile(requireLiveHandle(), sourceId, tileId) } - public actual fun invalidateCustomGeometrySourceRegion(sourceId: String, bounds: LatLngBounds) { + public actual fun invalidateCustomGeometrySourceRegion( + sourceId: String, + bounds: LatLngBounds, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.invalidateCustomGeometrySourceRegion(requireLiveHandle(), sourceId, bounds) } @@ -208,21 +239,28 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati public actual fun addCustomMvtVectorSource( sourceId: String, options: CustomMvtVectorSourceOptions, - ) { + ): Deferred { NativeAccess.ensureLoaded() val registry = customMvtVectorSources val sourceState = CustomMvtVectorSourceState(options) { registry.remove(sourceId) } + lateinit var completed: Deferred registry.install(sourceId, sourceState) { - NativeAccess.addCustomMvtVectorSource(requireLiveHandle(), sourceId, sourceState.descriptor()) + completed = + NativeAccess.addCustomMvtVectorSource( + requireLiveHandle(), + sourceId, + sourceState.descriptor(), + ) HandleLeakCleaner.retainNativeCallbackRoot(sourceState) } + return completed } public actual fun setCustomMvtVectorSourceTileData( sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setCustomMvtVectorSourceTileData(requireLiveHandle(), sourceId, tileId, data) } @@ -231,17 +269,24 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, tileId: CanonicalTileId, message: String, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setCustomMvtVectorSourceTileError(requireLiveHandle(), sourceId, tileId, message) } - public actual fun invalidateCustomMvtVectorSourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomMvtVectorSourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.invalidateCustomMvtVectorSourceTile(requireLiveHandle(), sourceId, tileId) } - public actual fun addVectorSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addVectorSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addVectorSourceUrl(requireLiveHandle(), sourceId, url, options) } @@ -250,12 +295,16 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addVectorSourceTiles(requireLiveHandle(), sourceId, tiles, options) } - public actual fun addRasterSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addRasterSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addRasterSourceUrl(requireLiveHandle(), sourceId, url, options) } @@ -264,7 +313,7 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addRasterSourceTiles(requireLiveHandle(), sourceId, tiles, options) } @@ -273,7 +322,7 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, url: String, options: TileSourceOptions?, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addRasterDemSourceUrl(requireLiveHandle(), sourceId, url, options) } @@ -282,7 +331,7 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addRasterDemSourceTiles(requireLiveHandle(), sourceId, tiles, options) } @@ -291,32 +340,31 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati imageId: String, image: PremultipliedRgba8Image, options: StyleImageOptions, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setStyleImage(requireLiveHandle(), imageId, image, options) } - public actual fun removeStyleImage(imageId: String): Boolean { - NativeAccess.ensureLoaded() - return NativeAccess.removeStyleImage(requireLiveHandle(), imageId) - } - - public actual fun styleImageExists(imageId: String): Boolean { + public actual fun removeStyleImage(imageId: String): Deferred = command { NativeAccess.ensureLoaded() - return NativeAccess.styleImageExists(requireLiveHandle(), imageId) + NativeAccess.removeStyleImage(requireLiveHandle(), imageId) } - public actual fun styleImageInfo(imageId: String): StyleImageInfo? { + public actual fun styleImageInfo(imageId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleImageInfo(requireLiveHandle(), imageId) } - public actual fun copyStyleImagePremultipliedRgba8(imageId: String): StyleImage? { + public actual fun copyStyleImagePremultipliedRgba8(imageId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.copyStyleImagePremultipliedRgba8(requireLiveHandle(), imageId) } - public actual fun addImageSourceUrl(sourceId: String, coordinates: List, url: String) { + public actual fun addImageSourceUrl( + sourceId: String, + coordinates: List, + url: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addImageSourceUrl(requireLiveHandle(), sourceId, coordinates, url) } @@ -325,47 +373,68 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati sourceId: String, coordinates: List, image: PremultipliedRgba8Image, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addImageSourceImage(requireLiveHandle(), sourceId, coordinates, image) } - public actual fun setImageSourceUrl(sourceId: String, url: String) { - NativeAccess.ensureLoaded() - NativeAccess.setImageSourceUrl(requireLiveHandle(), sourceId, url) - } + public actual fun setImageSourceUrl(sourceId: String, url: String): Deferred = + command { + NativeAccess.ensureLoaded() + NativeAccess.setImageSourceUrl(requireLiveHandle(), sourceId, url) + } - public actual fun setImageSourceImage(sourceId: String, image: PremultipliedRgba8Image) { + public actual fun setImageSourceImage( + sourceId: String, + image: PremultipliedRgba8Image, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setImageSourceImage(requireLiveHandle(), sourceId, image) } - public actual fun setImageSourceCoordinates(sourceId: String, coordinates: List) { + public actual fun setImageSourceCoordinates( + sourceId: String, + coordinates: List, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setImageSourceCoordinates(requireLiveHandle(), sourceId, coordinates) } - public actual fun imageSourceCoordinates(sourceId: String): List? { + public actual fun imageSourceCoordinates(sourceId: String): Deferred?> { NativeAccess.ensureLoaded() return NativeAccess.imageSourceCoordinates(requireLiveHandle(), sourceId) } - public actual fun addStyleLayerJson(layerJson: ByteArray, beforeLayerId: String) { + public actual fun addStyleLayerJson( + layerJson: ByteArray, + beforeLayerId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addStyleLayerJson(requireLiveHandle(), layerJson, beforeLayerId) } - public actual fun addHillshadeLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addHillshadeLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addHillshadeLayer(requireLiveHandle(), layerId, sourceId, beforeLayerId) } - public actual fun addColorReliefLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addColorReliefLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addColorReliefLayer(requireLiveHandle(), layerId, sourceId, beforeLayerId) } - public actual fun addLocationIndicatorLayer(layerId: String, beforeLayerId: String) { + public actual fun addLocationIndicatorLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.addLocationIndicatorLayer(requireLiveHandle(), layerId, beforeLayerId) } @@ -374,17 +443,23 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati layerId: String, coordinate: LatLng, altitude: Double, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLocationIndicatorLocation(requireLiveHandle(), layerId, coordinate, altitude) } - public actual fun setLocationIndicatorBearing(layerId: String, bearing: Double) { + public actual fun setLocationIndicatorBearing( + layerId: String, + bearing: Double, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLocationIndicatorBearing(requireLiveHandle(), layerId, bearing) } - public actual fun setLocationIndicatorAccuracyRadius(layerId: String, radius: Double) { + public actual fun setLocationIndicatorAccuracyRadius( + layerId: String, + radius: Double, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLocationIndicatorAccuracyRadius(requireLiveHandle(), layerId, radius) } @@ -393,497 +468,303 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati layerId: String, imageKind: LocationIndicatorImageKind, imageId: String, - ) { + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLocationIndicatorImageName(requireLiveHandle(), layerId, imageKind, imageId) } - public actual fun removeStyleLayer(layerId: String): Boolean { - NativeAccess.ensureLoaded() - return NativeAccess.removeStyleLayer(requireLiveHandle(), layerId) - } - - public actual fun styleLayerExists(layerId: String): Boolean { + public actual fun removeStyleLayer(layerId: String): Deferred = command { NativeAccess.ensureLoaded() - return NativeAccess.styleLayerExists(requireLiveHandle(), layerId) + NativeAccess.removeStyleLayer(requireLiveHandle(), layerId) } - public actual fun styleLayerType(layerId: String): String? { + public actual fun styleLayerInfo(layerId: String): Deferred { NativeAccess.ensureLoaded() - return NativeAccess.styleLayerType(requireLiveHandle(), layerId) + return NativeAccess.styleLayerInfo(requireLiveHandle(), layerId) } - public actual fun styleLayerIds(): List { + public actual fun styleLayerIds(): Deferred> { NativeAccess.ensureLoaded() return NativeAccess.styleLayerIds(requireLiveHandle()) } - public actual fun moveStyleLayer(layerId: String, beforeLayerId: String) { + public actual fun moveStyleLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.moveStyleLayer(requireLiveHandle(), layerId, beforeLayerId) } - public actual fun styleLayerJson(layerId: String): ByteArray? { + public actual fun styleLayerJson(layerId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleLayerJson(requireLiveHandle(), layerId) } - public actual fun setStyleLightJson(lightJson: ByteArray) { + public actual fun setStyleLightJson(lightJson: ByteArray): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setStyleLightJson(requireLiveHandle(), lightJson) } - public actual fun setStyleLightProperty(propertyName: String, value: ByteArray) { + public actual fun setStyleLightProperty( + propertyName: String, + value: ByteArray, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setStyleLightProperty(requireLiveHandle(), propertyName, value) } - public actual fun styleLightProperty(propertyName: String): ByteArray? { + public actual fun styleLightProperty(propertyName: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleLightProperty(requireLiveHandle(), propertyName) } - public actual fun setStyleTransitionOptions(options: StyleTransitionOptions) { + public actual fun setStyleTransitionOptions( + options: StyleTransitionOptions + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setStyleTransitionOptions(requireLiveHandle(), options) } - public actual fun styleTransitionOptions(): StyleTransitionOptions { + public actual fun styleTransitionOptions(): Deferred { NativeAccess.ensureLoaded() return NativeAccess.styleTransitionOptions(requireLiveHandle()) } - public actual fun setLayerProperty(layerId: String, propertyName: String, value: ByteArray) { + public actual fun setLayerProperty( + layerId: String, + propertyName: String, + value: ByteArray, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLayerProperty(requireLiveHandle(), layerId, propertyName, value) } - public actual fun layerProperty(layerId: String, propertyName: String): ByteArray? { + public actual fun layerProperty(layerId: String, propertyName: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.layerProperty(requireLiveHandle(), layerId, propertyName) } - public actual fun setLayerFilter(layerId: String, filter: ByteArray) { + public actual fun setLayerFilter( + layerId: String, + filter: ByteArray, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLayerFilter(requireLiveHandle(), layerId, filter) } - public actual fun clearLayerFilter(layerId: String) { + public actual fun clearLayerFilter(layerId: String): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.clearLayerFilter(requireLiveHandle(), layerId) } - public actual fun layerFilter(layerId: String): ByteArray? { + public actual fun layerFilter(layerId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.layerFilter(requireLiveHandle(), layerId) } public actual fun styleImageStretches( imageId: String - ): Pair, List>? { + ): Deferred, List>?> { NativeAccess.ensureLoaded() return NativeAccess.styleImageStretches(requireLiveHandle(), imageId) } - public actual fun setLayerSourceLayer(layerId: String, sourceLayer: String) { + public actual fun setLayerSourceLayer( + layerId: String, + sourceLayer: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLayerSourceLayer(requireLiveHandle(), layerId, sourceLayer) } - public actual fun layerSourceLayer(layerId: String): String { + public actual fun layerSourceLayer(layerId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.layerSourceLayer(requireLiveHandle(), layerId) } - public actual fun setLayerSourceId(layerId: String, sourceId: String) { + public actual fun setLayerSourceId( + layerId: String, + sourceId: String, + ): Deferred = command { NativeAccess.ensureLoaded() NativeAccess.setLayerSourceId(requireLiveHandle(), layerId, sourceId) } - public actual fun layerSourceId(layerId: String): String { + public actual fun layerSourceId(layerId: String): Deferred { NativeAccess.ensureLoaded() return NativeAccess.layerSourceId(requireLiveHandle(), layerId) } - public actual fun setLayerMinZoom(layerId: String, minZoom: Double) { - NativeAccess.ensureLoaded() - NativeAccess.setLayerMinZoom(requireLiveHandle(), layerId, minZoom) - } - - public actual fun layerMinZoom(layerId: String): Double { - NativeAccess.ensureLoaded() - return NativeAccess.layerMinZoom(requireLiveHandle(), layerId) - } - - public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double) { - NativeAccess.ensureLoaded() - NativeAccess.setLayerMaxZoom(requireLiveHandle(), layerId, maxZoom) - } - - public actual fun layerMaxZoom(layerId: String): Double { - NativeAccess.ensureLoaded() - return NativeAccess.layerMaxZoom(requireLiveHandle(), layerId) - } - - public actual fun setLayerVisibility(layerId: String, visibility: StyleLayerVisibility) { - NativeAccess.ensureLoaded() - NativeAccess.setLayerVisibility(requireLiveHandle(), layerId, visibility.nativeValue) - } - - public actual fun layerVisibility(layerId: String): StyleLayerVisibility { - NativeAccess.ensureLoaded() - return StyleLayerVisibility.fromNative( - NativeAccess.layerVisibility(requireLiveHandle(), layerId) - ) - } - - public actual fun requestRepaint() { - NativeAccess.ensureLoaded() - NativeAccess.requestRepaint(requireLiveHandle()) - } - - public actual fun requestStillImage() { - NativeAccess.ensureLoaded() - NativeAccess.requestStillImage(requireLiveHandle()) - } - - public actual var debugOptions: Set - get() { - NativeAccess.ensureLoaded() - return NativeAccess.debugOptions(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setDebugOptions(requireLiveHandle(), value) - } - - public actual var isRenderingStatsViewEnabled: Boolean - get() { - NativeAccess.ensureLoaded() - return NativeAccess.renderingStatsViewEnabled(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setRenderingStatsViewEnabled(requireLiveHandle(), value) - } - - public actual val isFullyLoaded: Boolean - get() { - NativeAccess.ensureLoaded() - return NativeAccess.isFullyLoaded(requireLiveHandle()) - } - - public actual fun dumpDebugLogs() { - NativeAccess.ensureLoaded() - NativeAccess.dumpDebugLogs(requireLiveHandle()) - } - - public actual val size: MapSize - get() { + public actual fun setLayerMinZoom(layerId: String, minZoom: Double): Deferred = + command { NativeAccess.ensureLoaded() - return NativeAccess.mapSize(requireLiveHandle()) + NativeAccess.setLayerMinZoom(requireLiveHandle(), layerId, minZoom) } - public actual var viewportOptions: ViewportOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.viewportOptions(requireLiveHandle()) - } - set(value) { + public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double): Deferred = + command { NativeAccess.ensureLoaded() - NativeAccess.setViewportOptions(requireLiveHandle(), value) + NativeAccess.setLayerMaxZoom(requireLiveHandle(), layerId, maxZoom) } - public actual var tileOptions: TileOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.tileOptions(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setTileOptions(requireLiveHandle(), value) - } - - public actual val camera: CameraOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.camera(requireLiveHandle()) - } - - public actual fun jumpTo(camera: CameraOptions) { - NativeAccess.ensureLoaded() - NativeAccess.jumpTo(requireLiveHandle(), camera) - } - - public actual fun easeTo(camera: CameraOptions, animation: AnimationOptions?) { + public actual fun setLayerVisibility( + layerId: String, + visibility: StyleLayerVisibility, + ): Deferred = command { NativeAccess.ensureLoaded() - NativeAccess.easeTo(requireLiveHandle(), camera, animation) + NativeAccess.setLayerVisibility(requireLiveHandle(), layerId, visibility.nativeValue) } - public actual fun flyTo(camera: CameraOptions, animation: AnimationOptions?) { + public actual fun requestRepaint(): Deferred { NativeAccess.ensureLoaded() - NativeAccess.flyTo(requireLiveHandle(), camera, animation) + return NativeAccess.requestRepaint(requireLiveHandle()) } - public actual fun moveBy(deltaX: Double, deltaY: Double) { + public actual fun requestStillImage(): Deferred { NativeAccess.ensureLoaded() - NativeAccess.moveBy(requireLiveHandle(), deltaX, deltaY) + return NativeAccess.requestStillImage(requireLiveHandle()) } - public actual fun moveByAnimated(deltaX: Double, deltaY: Double, animation: AnimationOptions?) { + public actual fun snapshot(): MapSnapshot { NativeAccess.ensureLoaded() - NativeAccess.moveByAnimated(requireLiveHandle(), deltaX, deltaY, animation) + return NativeAccess.mapSnapshot(requireLiveHandle()) } - public actual fun scaleBy(scale: Double, anchor: ScreenPoint?) { + public actual fun setDebugOptions(options: Set): Deferred { NativeAccess.ensureLoaded() - NativeAccess.scaleBy(requireLiveHandle(), scale, anchor) + return NativeAccess.setDebugOptions(requireLiveHandle(), options) } - public actual fun scaleByAnimated( - scale: Double, - anchor: ScreenPoint?, - animation: AnimationOptions?, - ) { + public actual fun setRenderingStatsViewEnabled(enabled: Boolean): Deferred { NativeAccess.ensureLoaded() - NativeAccess.scaleByAnimated(requireLiveHandle(), scale, anchor, animation) + return NativeAccess.setRenderingStatsViewEnabled(requireLiveHandle(), enabled) } - public actual fun rotateBy(first: ScreenPoint, second: ScreenPoint) { + public actual fun setViewportOptions(options: ViewportOptions): Deferred { NativeAccess.ensureLoaded() - NativeAccess.rotateBy(requireLiveHandle(), first, second) + return NativeAccess.setViewportOptions(requireLiveHandle(), options) } - public actual fun rotateByAnimated( - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions?, - ) { + public actual fun setTileOptions(options: TileOptions): Deferred { NativeAccess.ensureLoaded() - NativeAccess.rotateByAnimated(requireLiveHandle(), first, second, animation) + return NativeAccess.setTileOptions(requireLiveHandle(), options) } - public actual fun pitchBy(pitch: Double) { + public actual fun setBounds(options: BoundOptions): Deferred { NativeAccess.ensureLoaded() - NativeAccess.pitchBy(requireLiveHandle(), pitch) + return NativeAccess.setBounds(requireLiveHandle(), options) } - public actual fun pitchByAnimated(pitch: Double, animation: AnimationOptions?) { + public actual fun setFreeCameraOptions(options: FreeCameraOptions): Deferred { NativeAccess.ensureLoaded() - NativeAccess.pitchByAnimated(requireLiveHandle(), pitch, animation) + return NativeAccess.setFreeCameraOptions(requireLiveHandle(), options) } - public actual fun cancelTransitions() { + public actual fun resize(size: MapSize): Deferred { NativeAccess.ensureLoaded() - NativeAccess.cancelTransitions(requireLiveHandle()) + return NativeAccess.resizeMap(requireLiveHandle(), size) } - public actual var isGestureInProgress: Boolean - get() { - NativeAccess.ensureLoaded() - return NativeAccess.isGestureInProgress(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setGestureInProgress(requireLiveHandle(), value) - } - - public actual fun cameraForLatLngBounds( - bounds: LatLngBounds, - fitOptions: CameraFitOptions?, - ): CameraOptions { + public actual fun cameraSnapshot(): CameraSnapshot { NativeAccess.ensureLoaded() - return NativeAccess.cameraForLatLngBounds(requireLiveHandle(), bounds, fitOptions) + return NativeAccess.cameraSnapshot(requireLiveHandle()) } - public actual fun cameraForLatLngs( - coordinates: List, - fitOptions: CameraFitOptions?, - ): CameraOptions { + public actual fun updateCamera(update: CameraUpdate): Deferred { NativeAccess.ensureLoaded() - return NativeAccess.cameraForLatLngs(requireLiveHandle(), coordinates, fitOptions) + return NativeAccess.updateCamera(requireLiveHandle(), update) } - public actual fun cameraForGeometry( - geometry: ByteArray, - fitOptions: CameraFitOptions?, - ): CameraOptions { - NativeAccess.ensureLoaded() - return NativeAccess.cameraForGeometry(requireLiveHandle(), geometry, fitOptions) - } + public actual fun applyCameraDelta(delta: CameraDelta): Deferred = + NativeAccess.applyCameraDelta(requireLiveHandle(), delta) - public actual fun latLngBoundsForCamera(camera: CameraOptions): LatLngBounds { + public actual fun queryCamera(): Deferred { NativeAccess.ensureLoaded() - return NativeAccess.latLngBoundsForCamera(requireLiveHandle(), camera) - } - - public actual fun latLngBoundsForCameraUnwrapped(camera: CameraOptions): LatLngBounds { - NativeAccess.ensureLoaded() - return NativeAccess.latLngBoundsForCameraUnwrapped(requireLiveHandle(), camera) - } - - public actual var bounds: BoundOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.bounds(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setBounds(requireLiveHandle(), value) - } - - public actual var freeCameraOptions: FreeCameraOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.freeCameraOptions(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setFreeCameraOptions(requireLiveHandle(), value) - } - - public actual var projectionMode: ProjectionModeOptions - get() { - NativeAccess.ensureLoaded() - return NativeAccess.projectionMode(requireLiveHandle()) - } - set(value) { - NativeAccess.ensureLoaded() - NativeAccess.setProjectionMode(requireLiveHandle(), value) - } - - public actual fun pixelForLatLng(coordinate: LatLng): ScreenPoint { - NativeAccess.ensureLoaded() - return NativeAccess.pixelForLatLng(requireLiveHandle(), coordinate) - } - - public actual fun latLngForPixel(point: ScreenPoint): LatLng { - NativeAccess.ensureLoaded() - return NativeAccess.latLngForPixel(requireLiveHandle(), point) - } - - public actual fun pixelsForLatLngs(coordinates: List): List { - NativeAccess.ensureLoaded() - return NativeAccess.pixelsForLatLngs(requireLiveHandle(), coordinates) - } - - public actual fun latLngsForPixels(points: List): List { - NativeAccess.ensureLoaded() - return NativeAccess.latLngsForPixels(requireLiveHandle(), points) + return NativeAccess.queryCamera(requireLiveHandle()) } public actual fun attachMetalOwnedTexture( - descriptor: MetalOwnedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachMetalOwnedTexture(requireLiveHandle(), descriptor), - ) - } + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalOwnedTexture(this, descriptor, options) public actual fun attachMetalBorrowedTexture( - descriptor: MetalBorrowedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachMetalBorrowedTexture(requireLiveHandle(), descriptor), - ) - } + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalBorrowedTexture(this, descriptor, options) public actual fun attachVulkanOwnedTexture( - descriptor: VulkanOwnedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachVulkanOwnedTexture(requireLiveHandle(), descriptor), - ) - } + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanOwnedTexture(this, descriptor, options) public actual fun attachVulkanBorrowedTexture( - descriptor: VulkanBorrowedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachVulkanBorrowedTexture(requireLiveHandle(), descriptor), - ) - } + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanBorrowedTexture(this, descriptor, options) public actual fun attachOpenGLOwnedTexture( - descriptor: OpenGLOwnedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachOpenGLOwnedTexture(requireLiveHandle(), descriptor), - ) - } + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLOwnedTexture(this, descriptor, options) public actual fun attachOpenGLBorrowedTexture( - descriptor: OpenGLBorrowedTextureDescriptor - ): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachOpenGLBorrowedTexture(requireLiveHandle(), descriptor), - ) - } - - public actual fun attachMetalSurface(descriptor: MetalSurfaceDescriptor): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachMetalSurface(requireLiveHandle(), descriptor), - ) - } - - public actual fun attachVulkanSurface(descriptor: VulkanSurfaceDescriptor): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachVulkanSurface(requireLiveHandle(), descriptor), - ) - } - - public actual fun attachOpenGLSurface(descriptor: OpenGLSurfaceDescriptor): RenderSessionHandle { - NativeAccess.ensureLoaded() - return RenderSessionHandle( - this, - NativeAccess.attachOpenGLSurface(requireLiveHandle(), descriptor), - ) - } - - public actual fun createProjection(): MapProjectionHandle { - NativeAccess.ensureLoaded() - return MapProjectionHandle(NativeAccess.createMapProjection(requireLiveHandle())) - } - - public actual override fun close() { - core.closeOnce( - destroy = { NativeAccess.destroyMap(handle) }, - afterSuccess = { - runtime.unregisterMap(this) - runtimeRetention.close() - }, - ) + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLBorrowedTexture(this, descriptor, options) + + public actual fun attachMetalSurface( + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachMetalSurface(this, descriptor, options) + + public actual fun attachVulkanSurface( + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachVulkanSurface(this, descriptor, options) + + public actual fun attachOpenGLSurface( + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachOpenGLSurface(this, descriptor, options) + + public actual fun createProjection(): Deferred { + NativeAccess.ensureLoaded() + return NativeAccess.createMapProjection(requireLiveHandle()).mapDeferred(::MapProjectionHandle) + } + + public actual fun close(): Deferred { + if (!core.beginClose()) return kotlinx.coroutines.CompletableDeferred(Unit) + val completed = + try { + NativeAccess.releaseMap(handle) + } catch (error: Throwable) { + core.abortClose() + throw error + } + core.completeClose { runtime.unregisterMap(this) } + return completed } public actual companion object { - public actual fun create(runtime: RuntimeHandle, options: MapOptions): MapHandle { + public actual fun create(runtime: RuntimeHandle, options: MapOptions): Deferred { NativeAccess.ensureLoaded() - return MapHandle(runtime, NativeAccess.createMap(runtime.nativeHandle(), options)).also { - runtime.registerMap(it) + return NativeAccess.createMap(runtime.nativeHandle(), options).mapDeferred { handle -> + MapHandle(runtime, handle, options.eventMask).also { runtime.registerMap(it) } } } } internal fun nativeHandleId(): Long = handle.raw - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - core.retainChild(childTypeName) + internal fun nativeHandle(): NativeMap = requireLiveHandle() internal fun customGeometrySourceCountForTesting(): Int = customGeometrySources.size @@ -893,6 +774,9 @@ private constructor(private val runtime: RuntimeHandle, private val handle: Nati } } +private inline fun command(call: () -> Deferred): Deferred = + call() + private fun releaseCallbackRoot(root: AutoCloseable?) { HandleLeakCleaner.releaseNativeCallbackRoot(root) closeQuietly(root) diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt index 8a77a6a70..573b898ed 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt @@ -9,20 +9,25 @@ import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.lifecycle.NativeMapProjection import org.maplibre.nativeffi.internal.loader.NativeAccess -/** Any-thread JVM FFM standalone projection snapshot. */ +/** + * Owned JVM FFM standalone projection snapshot. + * + * Every call is synchronous, runs on the calling thread, is internally serialized, and may be made + * from any thread. A projection copies the map's transform state at creation and never observes map + * changes made after that and remains usable after its source map and runtime close. + */ public actual class MapProjectionHandle -internal constructor(private val handle: NativeMapProjection) : AutoCloseable { +internal constructor(private val handle: NativeMapProjection) { private val core = HandleStateCore("MapProjectionHandle", handle.raw) init { HandleLeakCleaner.register(this, core.leakReport) } - public actual val camera: CameraOptions - get() { - NativeAccess.ensureLoaded() - return withLiveHandle(NativeAccess::projectionCamera) - } + public actual fun camera(): CameraOptions { + NativeAccess.ensureLoaded() + return withLiveHandle { handle -> NativeAccess.projectionCamera(handle) } + } public actual fun setCamera(camera: CameraOptions) { NativeAccess.ensureLoaded() @@ -56,8 +61,15 @@ internal constructor(private val handle: NativeMapProjection) : AutoCloseable { public actual val isClosed: Boolean get() = core.isReleased() - public actual override fun close() { - core.closeOnce(destroy = { NativeAccess.destroyMapProjection(handle) }) + public actual fun close() { + if (!core.beginClose()) return + try { + NativeAccess.closeProjection(handle) + } catch (error: Throwable) { + core.abortClose() + throw error + } + core.completeClose() } private fun withLiveHandle(block: (NativeMapProjection) -> T): T = core.withLive { diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt deleted file mode 100644 index 43bf2bc6a..000000000 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner -import org.maplibre.nativeffi.internal.loader.NativeAccess - -/** Explicit handle for a Metal session-owned texture frame. */ -public actual class MetalOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val frameSegment: NativeAccess.OwnedTextureFrameSegment, - private val scope: FrameScope, - private val frameValue: MetalOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("MetalOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): MetalOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseMetalFrame(frameSegment.segment) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - frameSegment.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt deleted file mode 100644 index 06b4df130..000000000 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner -import org.maplibre.nativeffi.internal.loader.NativeAccess - -/** Explicit handle for an OpenGL session-owned texture frame. */ -public actual class OpenGLOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val frameSegment: NativeAccess.OwnedTextureFrameSegment, - private val scope: FrameScope, - private val frameValue: OpenGLOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("OpenGLOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): OpenGLOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseOpenGLFrame(frameSegment.segment) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - frameSegment.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt index 265393b33..ef916d61b 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt @@ -1,6 +1,7 @@ package org.maplibre.nativeffi.render -import java.lang.foreign.MemorySegment +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.lifecycle.NativeRenderSession @@ -12,116 +13,106 @@ import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.query.SourceFeatureQueryOptions -/** Owned JVM FFM render session handle. Close it on the thread that attached it. */ +/** Owned JVM FFM render-session control handle. */ public actual class RenderSessionHandle -internal constructor(private val map: MapHandle, private val handle: NativeRenderSession) : +internal constructor(private val ownerMap: MapHandle, private val handle: NativeRenderSession) : AutoCloseable { - private val mapRetention = map.retainChild("RenderSessionHandle") - private val core = HandleStateCore("RenderSessionHandle", handle.raw, map) + private val core = HandleStateCore("RenderSessionHandle", handle.raw, ownerMap) + private val runtime = ownerMap.runtime() + private val acquiredFrameScopes = ConcurrentHashMap.newKeySet() init { HandleLeakCleaner.register(this, core.leakReport) } - private val activeFrame = ActiveFrameState() - public actual val isClosed: Boolean get() = core.isReleased() - public actual fun map(): MapHandle = map + public actual fun map(): MapHandle = ownerMap - public actual fun resize(width: Int, height: Int, scaleFactor: Double) { + public actual fun capabilities(): RenderSessionCapabilities { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("resize") - Status.requireArgument(width >= 0) { "width must be non-negative" } - Status.requireArgument(height >= 0) { "height must be non-negative" } - NativeAccess.resizeRenderSession(requireLiveHandle(), width, height, scaleFactor) + return NativeAccess.renderSessionCapabilities(requireLiveHandle()) } - public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor) { + public actual fun snapshot(): RenderSessionSnapshot { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setMetalSurfaceTarget(requireLiveHandle(), descriptor) + return NativeAccess.renderSessionSnapshot(requireLiveHandle()) } - public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor) { + public actual fun requestFrame(demand: FrameDemand) { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setVulkanSurfaceTarget(requireLiveHandle(), descriptor) + NativeAccess.requestRenderFrame(requireLiveHandle(), demand) } - public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor) { + public actual fun drainFrameResults(): List { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setOpenGLSurfaceTarget(requireLiveHandle(), descriptor) + return NativeAccess.drainRenderFrameResults(requireLiveHandle()) } - public actual fun setMetalBorrowedTextureTarget(descriptor: MetalBorrowedTextureDescriptor) { + public actual fun serviceDriverWork(maxWork: Int): Int { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setMetalBorrowedTextureTarget(requireLiveHandle(), descriptor) + Status.requireArgument(maxWork >= 0) { "maxWork must be non-negative" } + return NativeAccess.serviceRenderDriverWork(requireLiveHandle(), maxWork) } - public actual fun setVulkanBorrowedTextureTarget(descriptor: VulkanBorrowedTextureDescriptor) { + public actual fun acquireFrame(): AcquiredFrameHandle? { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setVulkanBorrowedTextureTarget(requireLiveHandle(), descriptor) + val nativeFrame = NativeAccess.acquireRenderFrame(requireLiveHandle()) ?: return null + val scope = FrameScope() + acquiredFrameScopes.add(scope) + return AcquiredFrameHandle(this, nativeFrame, scope) } - public actual fun setOpenGLBorrowedTextureTarget(descriptor: OpenGLBorrowedTextureDescriptor) { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("set target") - NativeAccess.setOpenGLBorrowedTextureTarget(requireLiveHandle(), descriptor) - } + public actual fun resize(extent: RenderTargetExtent): Deferred = + NativeAccess.resizeRenderSession(requireLiveHandle(), extent) - public actual fun renderUpdate(): RenderUpdate { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("render") - return NativeAccess.renderUpdate(requireLiveHandle()) - } + public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor): Deferred = + NativeAccess.setMetalSurfaceTarget(requireLiveHandle(), descriptor) - public actual fun detach() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("detach") - NativeAccess.detachRenderSession(requireLiveHandle()) - mapRetention.close() - } + public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor): Deferred = + NativeAccess.setVulkanSurfaceTarget(requireLiveHandle(), descriptor) - public actual fun reduceMemoryUse() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("reduce memory use") - NativeAccess.reduceRenderSessionMemoryUse(requireLiveHandle()) - } + public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor): Deferred = + NativeAccess.setOpenGLSurfaceTarget(requireLiveHandle(), descriptor) - public actual fun clearData() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("clear data") - NativeAccess.clearRenderSessionData(requireLiveHandle()) - } + public actual fun setMetalBorrowedTextureTarget( + descriptor: MetalBorrowedTextureDescriptor + ): Deferred = NativeAccess.setMetalBorrowedTextureTarget(requireLiveHandle(), descriptor) - public actual fun dumpDebugLogs() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("dump debug logs") - NativeAccess.dumpRenderSessionDebugLogs(requireLiveHandle()) - } + public actual fun setVulkanBorrowedTextureTarget( + descriptor: VulkanBorrowedTextureDescriptor + ): Deferred = NativeAccess.setVulkanBorrowedTextureTarget(requireLiveHandle(), descriptor) + + public actual fun setOpenGLBorrowedTextureTarget( + descriptor: OpenGLBorrowedTextureDescriptor + ): Deferred = NativeAccess.setOpenGLBorrowedTextureTarget(requireLiveHandle(), descriptor) + + public actual fun reduceMemoryUse(): Deferred = + NativeAccess.renderControl(requireLiveHandle(), "mln_render_session_reduce_memory_use") + + public actual fun clearData(): Deferred = + NativeAccess.renderControl(requireLiveHandle(), "mln_render_session_clear_data") + + public actual fun dumpDebugLogs(): Deferred = + NativeAccess.renderControl(requireLiveHandle(), "mln_render_session_dump_debug_logs") + + public actual fun barrier(): Deferred = NativeAccess.renderBarrier(requireLiveHandle()) + + public actual fun detach(): Deferred = + NativeAccess.renderControl(requireLiveHandle(), "mln_render_session_detach") public actual fun queryRenderedFeatures( geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions?, - ): List { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query rendered features") - return NativeAccess.queryRenderedFeatures(requireLiveHandle(), geometry, options) - } + ): Deferred> = + NativeAccess.queryRenderedFeatures(requireLiveHandle(), geometry, options) public actual fun querySourceFeatures( sourceId: String, options: SourceFeatureQueryOptions?, - ): List { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query source features") - return NativeAccess.querySourceFeatures(requireLiveHandle(), sourceId, options) - } + ): Deferred> = + NativeAccess.querySourceFeatures(requireLiveHandle(), sourceId, options) public actual fun queryFeatureExtension( sourceId: String, @@ -129,10 +120,8 @@ internal constructor(private val map: MapHandle, private val handle: NativeRende extension: String, extensionField: String, arguments: ByteArray?, - ): ByteArray { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("query feature extension") - return NativeAccess.queryFeatureExtension( + ): Deferred = + NativeAccess.queryFeatureExtension( requireLiveHandle(), sourceId, feature, @@ -140,113 +129,172 @@ internal constructor(private val map: MapHandle, private val handle: NativeRende extensionField, arguments, ) - } - public actual fun textureImageInfo(): TextureImageInfo { + public actual fun readPremultipliedRgba8(): Deferred = + NativeAccess.textureReadback(requireLiveHandle()) + + public actual fun abandon(): RenderAbandonResult { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("read texture data") - return NativeAccess.textureImageInfo(requireLiveHandle()) + val result = NativeAccess.abandonRenderSession(requireLiveHandle()) + acquiredFrameScopes.forEach(FrameScope::close) + return result } - public actual fun readPremultipliedRgba8(buffer: NativeBuffer): TextureImageInfo { + public actual override fun close() { NativeAccess.ensureLoaded() - activeFrame.ensureInactive("read texture data") - val info = NativeAccess.readPremultipliedRgba8(requireLiveHandle(), buffer) - // Native reads an empty destination as a size probe rather than a copy, so the - // capacity is rechecked here. - buffer.ensureCapacity(info.byteLength) - return info + core.closeOnce(destroy = { NativeAccess.destroyRenderSession(handle) }) } - public actual fun acquireMetalOwnedTextureFrame(): MetalOwnedTextureFrameHandle { - NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - var frame: NativeAccess.OwnedTextureFrameSegment? = null - try { - frame = NativeAccess.acquireMetalOwnedTextureFrame(requireLiveHandle()) - val scope = FrameScope() - return MetalOwnedTextureFrameHandle( - this, - frame, - scope, - NativeAccess.metalOwnedTextureFrame(frame.segment, scope), - ) - } catch (error: Throwable) { - activeFrame.endBorrow() - frame?.close() - throw error - } + internal fun frameReleased(scope: FrameScope) { + acquiredFrameScopes.remove(scope) } - public actual fun acquireVulkanOwnedTextureFrame(): VulkanOwnedTextureFrameHandle { - NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - var frame: NativeAccess.OwnedTextureFrameSegment? = null - try { - frame = NativeAccess.acquireVulkanOwnedTextureFrame(requireLiveHandle()) - val scope = FrameScope() - return VulkanOwnedTextureFrameHandle( - this, - frame, - scope, - NativeAccess.vulkanOwnedTextureFrame(frame.segment, scope), - ) - } catch (error: Throwable) { - activeFrame.endBorrow() - frame?.close() - throw error - } + private fun requireLiveHandle(): NativeRenderSession { + core.requireLive() + return handle } - public actual fun acquireOpenGLOwnedTextureFrame(): OpenGLOwnedTextureFrameHandle { - NativeAccess.ensureLoaded() - activeFrame.beginAcquire() - var frame: NativeAccess.OwnedTextureFrameSegment? = null - try { - frame = NativeAccess.acquireOpenGLOwnedTextureFrame(requireLiveHandle()) - val scope = FrameScope() - return OpenGLOwnedTextureFrameHandle( - this, - frame, - scope, - NativeAccess.openglOwnedTextureFrame(frame.segment, scope), + internal companion object { + fun attachMetalOwnedTexture( + map: MapHandle, + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment(map, NativeAccess.attachMetalOwnedTexture(map.nativeHandle(), descriptor, options)) + + fun attachMetalBorrowedTexture( + map: MapHandle, + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment( + map, + NativeAccess.attachMetalBorrowedTexture(map.nativeHandle(), descriptor, options), ) - } catch (error: Throwable) { - activeFrame.endBorrow() - frame?.close() - throw error - } - } - public actual override fun close() { - NativeAccess.ensureLoaded() - activeFrame.ensureInactive("destroy") - core.closeOnce( - destroy = { NativeAccess.destroyRenderSession(handle) }, - afterSuccess = { mapRetention.close() }, - ) - } + fun attachVulkanOwnedTexture( + map: MapHandle, + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment( + map, + NativeAccess.attachVulkanOwnedTexture(map.nativeHandle(), descriptor, options), + ) - internal fun nativeHandleId(): Long = handle.raw + fun attachVulkanBorrowedTexture( + map: MapHandle, + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment( + map, + NativeAccess.attachVulkanBorrowedTexture(map.nativeHandle(), descriptor, options), + ) - internal fun releaseMetalFrame(frame: java.lang.foreign.MemorySegment) { - NativeAccess.releaseMetalOwnedTextureFrame(requireLiveHandle(), frame) - } + fun attachOpenGLOwnedTexture( + map: MapHandle, + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment( + map, + NativeAccess.attachOpenGLOwnedTexture(map.nativeHandle(), descriptor, options), + ) - internal fun releaseVulkanFrame(frame: java.lang.foreign.MemorySegment) { - NativeAccess.releaseVulkanOwnedTextureFrame(requireLiveHandle(), frame) + fun attachOpenGLBorrowedTexture( + map: MapHandle, + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment( + map, + NativeAccess.attachOpenGLBorrowedTexture(map.nativeHandle(), descriptor, options), + ) + + fun attachMetalSurface( + map: MapHandle, + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment(map, NativeAccess.attachMetalSurface(map.nativeHandle(), descriptor, options)) + + fun attachVulkanSurface( + map: MapHandle, + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment(map, NativeAccess.attachVulkanSurface(map.nativeHandle(), descriptor, options)) + + fun attachOpenGLSurface( + map: MapHandle, + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + attachment(map, NativeAccess.attachOpenGLSurface(map.nativeHandle(), descriptor, options)) + + private fun attachment( + map: MapHandle, + native: Pair>, + ): RenderSessionAttachment { + NativeAccess.ensureLoaded() + val session = RenderSessionHandle(map, native.first) + return try { + RenderSessionAttachment(session, retainSessionUntilComplete(session, native.second)) + } catch (error: Throwable) { + runCatching { session.abandon() } + runCatching { session.close() } + throw error + } + } } +} - internal fun releaseOpenGLFrame(frame: java.lang.foreign.MemorySegment) { - NativeAccess.releaseOpenGLOwnedTextureFrame(requireLiveHandle(), frame) +/** JVM FFM acquired-frame lease. */ +public actual class AcquiredFrameHandle +internal constructor( + private val session: RenderSessionHandle, + frame: Long, + private val scope: FrameScope, +) { + @Volatile private var nativeFrame: Long = frame + + public actual val isReleased: Boolean + get() = nativeFrame == 0L + + public actual fun result(): RenderFrameResult = + NativeAccess.acquiredFrameResult(requireAccessibleFrame()) + + public actual fun producerSync(): GpuSync = + NativeAccess.acquiredFrameProducerSync(requireAccessibleFrame()) + + public actual fun metalTexture(): MetalOwnedTextureFrame = + NativeAccess.acquiredMetalTexture(requireAccessibleFrame(), scope) + + public actual fun vulkanTexture(): VulkanOwnedTextureFrame = + NativeAccess.acquiredVulkanTexture(requireAccessibleFrame(), scope) + + public actual fun openGLTexture(): OpenGLOwnedTextureFrame = + NativeAccess.acquiredOpenGLTexture(requireAccessibleFrame(), scope) + + @Synchronized + public actual fun release(consumerCompletion: GpuSync) { + val frame = requireFrame() + val remainingFrame = NativeAccess.releaseAcquiredFrame(frame, consumerCompletion) + check(remainingFrame == 0L) { "native frame release did not consume the handle" } + nativeFrame = 0L + scope.close() + session.frameReleased(scope) } - internal fun finishFrameBorrow() { - activeFrame.endBorrow() + private fun requireAccessibleFrame(): Long { + scope.ensureActive() + return requireFrame() } - private fun requireLiveHandle(): NativeRenderSession { - core.requireLive() - return handle + private fun requireFrame(): Long { + val frame = nativeFrame + if (frame == 0L) throw Status.invalidState("AcquiredFrameHandle is released") + return frame } } diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt deleted file mode 100644 index 9260bf029..000000000 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.maplibre.nativeffi.render - -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner -import org.maplibre.nativeffi.internal.loader.NativeAccess - -/** Explicit handle for a Vulkan session-owned texture frame. */ -public actual class VulkanOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val frameSegment: NativeAccess.OwnedTextureFrameSegment, - private val scope: FrameScope, - private val frameValue: VulkanOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("VulkanOwnedTextureFrameHandle") - - init { - HandleLeakCleaner.registerFrame(this, core) - } - - public actual fun frame(): VulkanOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseVulkanFrame(frameSegment.segment) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - frameSegment.close() - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt index f893c2f3c..8a25833e4 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandle.kt @@ -62,9 +62,8 @@ internal constructor( /** * Releases the native request once the wrapper becomes unreachable. * - * Request handles carry no owner-thread affinity, so the cleanup thread may reclaim one. This - * holds the ownership state alone; holding the wrapper would keep it reachable and suppress every - * reclaim. + * Request handles carry no thread affinity, so the cleanup thread may reclaim one. This holds the + * ownership state alone; holding the wrapper would keep it reachable and suppress every reclaim. */ private class CloseWhenUnreachableAction(private val core: ResourceRequestHandleCore) : Runnable { override fun run() { diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt deleted file mode 100644 index 0903115b2..000000000 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner - -/** Owner-thread offline database operation backed by the JVM FFM bridge. */ -public actual class OfflineOperationHandle -internal constructor( - private val runtime: RuntimeHandle, - id: Long, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, -) : AutoCloseable { - private val runtimeRetention = runtime.retainChild("OfflineOperationHandle") - private val core = - OfflineOperationHandleCore(runtime, id, kind, resultKind, runtimeRetention::close) - - init { - HandleLeakCleaner.registerOfflineOperation(this, core.leakReport) - } - - public actual val id: Long - get() = core.id - - public actual val kind: OfflineOperationKind - get() = core.kind - - public actual val resultKind: OfflineOperationResultKind - get() = core.resultKind - - public actual val isClosed: Boolean - get() = core.isClosed - - internal fun requireLive(expectedRuntime: RuntimeHandle): Long = core.requireLive(expectedRuntime) - - internal fun requireLive( - expectedRuntime: RuntimeHandle, - expectedKind: OfflineOperationKind, - expectedResultKind: OfflineOperationResultKind, - ): Long = core.requireLive(expectedRuntime, expectedKind, expectedResultKind) - - internal fun markConsumed() { - core.markConsumed() - } - - public actual override fun close() { - if (!isClosed) runtime.discardOfflineOperation(this) - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt index 6a31d1f6d..b9ab81c83 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt @@ -1,8 +1,8 @@ package org.maplibre.nativeffi.runtime -import java.lang.foreign.Arena import java.lang.ref.WeakReference -import org.maplibre.nativeffi.error.InvalidStateException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.internal.callback.HttpHeaderTransformState import org.maplibre.nativeffi.internal.callback.ResourceProviderState import org.maplibre.nativeffi.internal.callback.ResourceTransformState @@ -22,338 +22,145 @@ import org.maplibre.nativeffi.resource.ResourceProviderCallback import org.maplibre.nativeffi.resource.ResourceTransformCallback /** Owned runtime handle backed by the JVM FFM bridge. */ -public actual class RuntimeHandle private constructor(private val handle: NativeRuntime) : - AutoCloseable { +public actual class RuntimeHandle private constructor(private val handle: NativeRuntime) { private val core = HandleStateCore("RuntimeHandle", handle.raw) init { HandleLeakCleaner.register(this, core.leakReport) } - private var resourceProviderState: ResourceProviderState? = null - private var resourceTransformState: ResourceTransformState? = null - private var httpHeaderTransformState: HttpHeaderTransformState? = null private val liveMaps = mutableMapOf>() - // One batch struct per handle, reused by every drain and freed by close(). - private val batchArena = Arena.ofShared() - private val batchSegment = NativeAccess.allocateRuntimeEventBatch(batchArena) + + /** Teardown report of the close that consumed this handle. */ + @Volatile private var tornDown: Deferred? = null public actual val isClosed: Boolean get() = core.isReleased() - public actual fun pump(timeoutMillis: Long, budgetMillis: Long) { - NativeAccess.ensureLoaded() - core.requireLive() - NativeAccess.pumpRuntime(handle, timeoutMillis, budgetMillis) - } - - public actual fun acquireWakeSource(): WakeSource { + public actual fun barrier(): Deferred { NativeAccess.ensureLoaded() - core.requireLive() - return WakeSource.fromNative(NativeAccess.acquireWakeSource(handle)) + return NativeAccess.runtimeBarrier(requireLiveHandle()) } - public actual fun startAmbientCacheOperation( - operation: AmbientCacheOperation - ): OfflineOperationHandle { + public actual fun runAmbientCacheOperation(operation: AmbientCacheOperation): Deferred { NativeAccess.ensureLoaded() - val operationId = - NativeAccess.startAmbientCacheOperation(requireLiveHandle(), operation.nativeValue) - return offlineOperation( - operationId, - OfflineOperationKind.AMBIENT_CACHE, - OfflineOperationResultKind.NONE, - ) + return NativeAccess.runAmbientCacheOperation(requireLiveHandle(), operation.nativeValue) } - public actual fun startSetMaximumAmbientCacheSize(size: Long): OfflineOperationHandle { + public actual fun setMaximumAmbientCacheSize(size: Long): Deferred { NativeAccess.ensureLoaded() Status.requireArgument(size >= 0) { "size must be non-negative" } - val operationId = NativeAccess.startSetMaximumAmbientCacheSize(requireLiveHandle(), size) - return offlineOperation( - operationId, - OfflineOperationKind.SET_MAXIMUM_AMBIENT_CACHE_SIZE, - OfflineOperationResultKind.NONE, - ) + return NativeAccess.setMaximumAmbientCacheSize(requireLiveHandle(), size) } - public actual fun startCreateOfflineRegion( + public actual fun createOfflineRegion( definition: OfflineRegionDefinition, metadata: ByteArray, - ): OfflineOperationHandle = - offlineOperation( - NativeAccess.startCreateOfflineRegion(requireLiveHandle(), definition, metadata), - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ) + ): Deferred = + NativeAccess.createOfflineRegion(requireLiveHandle(), definition, metadata) - public actual fun startOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - NativeAccess.startOfflineRegion(requireLiveHandle(), id), - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) + public actual fun offlineRegion(id: Long): Deferred = + NativeAccess.offlineRegion(requireLiveHandle(), id) - public actual fun startOfflineRegions(): OfflineOperationHandle> = - offlineOperation( - NativeAccess.startOfflineRegions(requireLiveHandle()), - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, - ) + public actual fun offlineRegions(): Deferred> = + NativeAccess.offlineRegions(requireLiveHandle()) - public actual fun startMergeOfflineRegionsDatabase( - path: String - ): OfflineOperationHandle> = - offlineOperation( - NativeAccess.startMergeOfflineRegionsDatabase(requireLiveHandle(), path), - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) + public actual fun mergeOfflineRegionsDatabase(path: String): Deferred> = + NativeAccess.mergeOfflineRegionsDatabase(requireLiveHandle(), path) - public actual fun startUpdateOfflineRegionMetadata( + public actual fun updateOfflineRegionMetadata( id: Long, metadata: ByteArray, - ): OfflineOperationHandle = - offlineOperation( - NativeAccess.startUpdateOfflineRegionMetadata(requireLiveHandle(), id, metadata), - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, - ) + ): Deferred = + NativeAccess.updateOfflineRegionMetadata(requireLiveHandle(), id, metadata) - public actual fun startOfflineRegionStatus( - id: Long - ): OfflineOperationHandle = - offlineOperation( - NativeAccess.startOfflineRegionStatus(requireLiveHandle(), id), - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, - ) + public actual fun offlineRegionStatus(id: Long): Deferred = + NativeAccess.offlineRegionStatus(requireLiveHandle(), id) - public actual fun startSetOfflineRegionObserved( - id: Long, - observed: Boolean, - ): OfflineOperationHandle = - offlineOperation( - NativeAccess.startSetOfflineRegionObserved(requireLiveHandle(), id, observed), - OfflineOperationKind.REGION_SET_OBSERVED, - OfflineOperationResultKind.NONE, - ) + public actual fun setOfflineRegionObserved(id: Long, observed: Boolean): Deferred = + NativeAccess.setOfflineRegionObserved(requireLiveHandle(), id, observed) - public actual fun startSetOfflineRegionDownloadState( + public actual fun setOfflineRegionDownloadState( id: Long, downloadState: OfflineRegionDownloadState, - ): OfflineOperationHandle = run { + ): Deferred { Status.requireArgument(downloadState.isKnown) { "Unknown offline region download state cannot be used as input: ${downloadState.nativeValue}" } - offlineOperation( - NativeAccess.startSetOfflineRegionDownloadState( - requireLiveHandle(), - id, - downloadState.nativeValue, - ), - OfflineOperationKind.REGION_SET_DOWNLOAD_STATE, - OfflineOperationResultKind.NONE, - ) - } - - public actual fun startInvalidateOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - NativeAccess.startInvalidateOfflineRegion(requireLiveHandle(), id), - OfflineOperationKind.REGION_INVALIDATE, - OfflineOperationResultKind.NONE, - ) - - public actual fun startDeleteOfflineRegion(id: Long): OfflineOperationHandle = - offlineOperation( - NativeAccess.startDeleteOfflineRegion(requireLiveHandle(), id), - OfflineOperationKind.REGION_DELETE, - OfflineOperationResultKind.NONE, - ) - - public actual fun takeCreateOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ) - return NativeAccess.takeCreateOfflineRegionResult( + return NativeAccess.setOfflineRegionDownloadState( requireLiveHandle(), - operationId, - operation::markConsumed, + id, + downloadState.nativeValue, ) } - public actual fun takeOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo? { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) - return NativeAccess.takeOfflineRegionResult( - requireLiveHandle(), - operationId, - operation::markConsumed, - ) - } + public actual fun invalidateOfflineRegion(id: Long): Deferred = + NativeAccess.invalidateOfflineRegion(requireLiveHandle(), id) - public actual fun takeOfflineRegionsResult( - operation: OfflineOperationHandle> - ): List { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, - ) - return NativeAccess.takeOfflineRegionsResult( - requireLiveHandle(), - operationId, - operation::markConsumed, - ) - } + public actual fun deleteOfflineRegion(id: Long): Deferred = + NativeAccess.deleteOfflineRegion(requireLiveHandle(), id) - public actual fun takeMergeOfflineRegionsDatabaseResult( - operation: OfflineOperationHandle> - ): List { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) - return NativeAccess.takeMergeOfflineRegionsDatabaseResult( - requireLiveHandle(), - operationId, - operation::markConsumed, - ) - } - - public actual fun takeUpdateOfflineRegionMetadataResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, - ) - return NativeAccess.takeUpdateOfflineRegionMetadataResult( - requireLiveHandle(), - operationId, - operation::markConsumed, - ) - } - - public actual fun takeOfflineRegionStatusResult( - operation: OfflineOperationHandle - ): OfflineRegionStatus { - val operationId = - operation.requireLive( - this, - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, - ) - return NativeAccess.takeOfflineRegionStatusResult( - requireLiveHandle(), - operationId, - operation::markConsumed, - ) - } - - public actual fun setResourceProvider(callback: ResourceProviderCallback) { + public actual fun setResourceProvider( + callback: ResourceProviderCallback + ): Deferred { NativeAccess.ensureLoaded() val replacement = ResourceProviderState(callback) - val previous: ResourceProviderState? - try { - resourceProviderState?.checkCanClose() - Status.check(NativeAccess.setResourceProvider(requireLiveHandle(), replacement.descriptor())) - previous = resourceProviderState - resourceProviderState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return try { + NativeAccess.setResourceProvider(requireLiveHandle(), replacement.descriptor()) } catch (error: Throwable) { - closeAndSuppress(error, replacement) + releaseCallbackRoot(replacement) throw error } - releaseCallbackRoot(previous) } - public actual fun clearResourceProvider() { + public actual fun clearResourceProvider(): Deferred { NativeAccess.ensureLoaded() - resourceProviderState?.checkCanClose() - Status.check(NativeAccess.clearResourceProvider(requireLiveHandle())) - val previous = resourceProviderState - resourceProviderState = null - // The install path retained this as a strong leak-cleaner root, so closing - // alone would keep it and everything its callback captured reachable. - releaseCallbackRoot(previous) + return NativeAccess.clearResourceProvider(requireLiveHandle()) } - public actual fun setResourceTransform(callback: ResourceTransformCallback) { + public actual fun setResourceTransform( + callback: ResourceTransformCallback + ): Deferred { NativeAccess.ensureLoaded() val replacement = ResourceTransformState(callback) - val previous: ResourceTransformState? - try { - resourceTransformState?.checkCanClose() - Status.check(NativeAccess.setResourceTransform(requireLiveHandle(), replacement.descriptor())) - previous = resourceTransformState - resourceTransformState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return try { + NativeAccess.setResourceTransform(requireLiveHandle(), replacement.descriptor()) } catch (error: Throwable) { - closeAndSuppress(error, replacement) + releaseCallbackRoot(replacement) throw error } - releaseCallbackRoot(previous) } - public actual fun clearResourceTransform() { + public actual fun clearResourceTransform(): Deferred { NativeAccess.ensureLoaded() - resourceTransformState?.checkCanClose() - Status.check(NativeAccess.clearResourceTransform(requireLiveHandle())) - val previous = resourceTransformState - resourceTransformState = null - releaseCallbackRoot(previous) + return NativeAccess.clearResourceTransform(requireLiveHandle()) } - public actual fun setHttpHeaderTransform(callback: HttpHeaderTransformCallback) { + public actual fun setHttpHeaderTransform( + callback: HttpHeaderTransformCallback + ): Deferred { NativeAccess.ensureLoaded() - httpHeaderTransformState?.checkCanClose() val replacement = HttpHeaderTransformState(callback) - try { - Status.check( - NativeAccess.setHttpHeaderTransform(requireLiveHandle(), replacement.descriptor()) - ) + HandleLeakCleaner.retainNativeCallbackRoot(replacement) + return try { + NativeAccess.setHttpHeaderTransform(requireLiveHandle(), replacement.descriptor()) } catch (error: Throwable) { - closeAndSuppress(error, replacement) + releaseCallbackRoot(replacement) throw error } - val previous = httpHeaderTransformState - httpHeaderTransformState = replacement - HandleLeakCleaner.retainNativeCallbackRoot(replacement) - releaseCallbackRoot(previous) } - public actual fun clearHttpHeaderTransform() { + public actual fun clearHttpHeaderTransform(): Deferred { NativeAccess.ensureLoaded() - httpHeaderTransformState?.checkCanClose() - Status.check(NativeAccess.clearHttpHeaderTransform(requireLiveHandle())) - val previous = httpHeaderTransformState - httpHeaderTransformState = null - releaseCallbackRoot(previous) + return NativeAccess.clearHttpHeaderTransform(requireLiveHandle()) } - public actual fun drainEvents(maxEvents: Int): RuntimeEventBatch { + public actual fun drainEvents(): RuntimeEventBatch { NativeAccess.ensureLoaded() - Status.requireArgument(maxEvents >= 0) { "maxEvents must be non-negative" } - val batch = - NativeAccess.drainRuntimeEvents(requireLiveHandle(), maxEvents.toLong(), batchSegment) - return RuntimeEventBatch(batch.events.map { it.toRuntimeEvent() }, batch.remainingCount) + val batch = NativeAccess.drainRuntimeEvents(requireLiveHandle()) + return RuntimeEventBatch(batch.events.map { it.toRuntimeEvent() }) } public actual var eventMask: RuntimeEventMask @@ -366,23 +173,18 @@ public actual class RuntimeHandle private constructor(private val handle: Native NativeAccess.setRuntimeEventMask(requireLiveHandle(), value.nativeValue) } - public actual override fun close() { - resourceProviderState?.checkCanClose() - resourceTransformState?.checkCanClose() - httpHeaderTransformState?.checkCanClose() - core.closeOnce( - destroy = { NativeAccess.destroyRuntime(handle) }, - afterSuccess = { - releaseCallbackRoot(resourceProviderState) - resourceProviderState = null - releaseCallbackRoot(resourceTransformState) - resourceTransformState = null - releaseCallbackRoot(httpHeaderTransformState) - httpHeaderTransformState = null - liveMaps.clear() - batchArena.close() - }, - ) + public actual fun close(): Deferred { + if (!core.beginClose()) return tornDown ?: CompletableDeferred(Unit) + val completed = + try { + NativeAccess.releaseRuntime(handle) + } catch (error: Throwable) { + core.abortClose() + throw error + } + tornDown = completed + core.completeClose { liveMaps.clear() } + return completed } public actual companion object { @@ -392,29 +194,6 @@ public actual class RuntimeHandle private constructor(private val handle: Native } } - private fun offlineOperation( - operationId: Long, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, - ): OfflineOperationHandle = OfflineOperationHandle(this, operationId, kind, resultKind) - - internal fun discardOfflineOperation(operation: OfflineOperationHandle<*>) { - if (operation.isClosed) return - val operationId = operation.requireLive(this) - val runtime = - try { - requireLiveHandle() - } catch (error: InvalidStateException) { - operation.markConsumed() - throw error - } - Status.check(NativeAccess.discardOfflineOperation(runtime, operationId)) - operation.markConsumed() - } - - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - core.retainChild(childTypeName) - internal fun nativeHandle(): NativeRuntime = requireLiveHandle() internal fun registerMap(map: MapHandle) { @@ -478,11 +257,3 @@ private fun closeQuietly(closeable: AutoCloseable?) { closeable?.close() } catch (_: RuntimeException) {} } - -private fun closeAndSuppress(error: Throwable, closeable: AutoCloseable?) { - try { - closeable?.close() - } catch (cleanup: Throwable) { - error.addSuppressed(cleanup) - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt deleted file mode 100644 index 0e43d423a..000000000 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.internal.lifecycle.HandleLeakCleaner -import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore -import org.maplibre.nativeffi.internal.lifecycle.NativeWakeSource -import org.maplibre.nativeffi.internal.loader.NativeAccess - -/** Wake source backed by the JVM FFM bridge. */ -public actual class WakeSource private constructor(private val source: NativeWakeSource) : - AutoCloseable { - private val core = HandleStateCore("WakeSource", source.raw) - - init { - HandleLeakCleaner.register(this, core.leakReport) - } - - public actual val isClosed: Boolean - get() = core.isReleased() - - public actual fun signal() { - NativeAccess.ensureLoaded() - core.withLive { NativeAccess.signalWakeSource(source) } - } - - public actual override fun close() { - core.closeOnce( - destroy = { - NativeAccess.destroyWakeSource(source) - MaplibreStatus.OK.nativeCode - } - ) - } - - internal companion object { - fun fromNative(source: NativeWakeSource): WakeSource = WakeSource(source) - } -} diff --git a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt index 0ecd23491..aa1435516 100644 --- a/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt +++ b/bindings/kotlin/src/jvmMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt @@ -27,7 +27,7 @@ internal constructor(private val handle: NativeGeoJsonSourceData) : AutoCloseabl ) } - /** Runs [block] with the live native handle and release held off for the borrow window. */ + /** Runs [block] after checking that this wrapper still owns the native handle. */ internal fun withNativeHandle(block: (NativeGeoJsonSourceData) -> R): R = core.withLive { block(handle) } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateJvmTest.kt index 61dc5af21..1ee4894ec 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateJvmTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/CallbackGateJvmTest.kt @@ -11,41 +11,42 @@ import kotlin.test.assertTrue class CallbackGateJvmTest { @Test - fun closeWaitsForActiveCallbackOnAnotherThread() { - var closes = 0 - val gate = CallbackGate("test callbacks") { closes++ } - val entered = CountDownLatch(1) - val releaseCallback = CountDownLatch(1) - val closeReturned = AtomicBoolean(false) - - val callbackThread = Thread { - val lease = assertNotNull(gate.enter()) - entered.countDown() - assertTrue(releaseCallback.await(5, TimeUnit.SECONDS)) - lease.close() + fun closeWaitsForActiveCallbackOnAnotherThread(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var closes = 0 + val gate = CallbackGate("test callbacks") { closes++ } + val entered = CountDownLatch(1) + val releaseCallback = CountDownLatch(1) + val closeReturned = AtomicBoolean(false) + + val callbackThread = Thread { + val lease = assertNotNull(gate.enter()) + entered.countDown() + assertTrue(releaseCallback.await(5, TimeUnit.SECONDS)) + lease.close() + } + callbackThread.start() + assertTrue(entered.await(5, TimeUnit.SECONDS)) + + val closeThread = Thread { + gate.close() + closeReturned.set(true) + } + closeThread.start() + + assertTrue(waitForClosing(gate)) + assertFalse(closeReturned.get()) + assertFalse(gate.isClosedForTesting()) + assertEquals(0, closes) + + releaseCallback.countDown() + closeThread.join(5_000) + callbackThread.join(5_000) + + assertTrue(closeReturned.get()) + assertTrue(gate.isClosedForTesting()) + assertEquals(1, closes) } - callbackThread.start() - assertTrue(entered.await(5, TimeUnit.SECONDS)) - - val closeThread = Thread { - gate.close() - closeReturned.set(true) - } - closeThread.start() - - assertTrue(waitForClosing(gate)) - assertFalse(closeReturned.get()) - assertFalse(gate.isClosedForTesting()) - assertEquals(0, closes) - - releaseCallback.countDown() - closeThread.join(5_000) - callbackThread.join(5_000) - - assertTrue(closeReturned.get()) - assertTrue(gate.isClosedForTesting()) - assertEquals(1, closes) - } private fun waitForClosing(gate: CallbackGate): Boolean { val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt index f157bf8f4..90f3533fe 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackStateTest.kt @@ -2,16 +2,10 @@ package org.maplibre.nativeffi.internal.callback import java.lang.foreign.Arena import java.lang.foreign.MemorySegment -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame import kotlin.test.assertTrue +import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.log.LogEvent import org.maplibre.nativeffi.log.LogRecord @@ -19,110 +13,42 @@ import org.maplibre.nativeffi.log.LogSeverity class LogCallbackStateTest { @Test - fun logCallbackCopiesRecordAndReturnsConsumedFlag() { - var copiedRecord: LogRecord? = null - - try { - LogCallbackState.set( + fun callbackCopiesRecordsContainsFailuresAndStopsAfterClose() { + var copied: LogRecord? = null + val state = + LogCallbackState.createForTesting( LogCallback { record -> - copiedRecord = record + copied = record true } ) - val state = assertNotNull(LogCallbackState.currentForTesting()) - - Arena.ofConfined().use { arena -> - val result = - state.invoke( - MemorySegment.NULL, - LogSeverity.WARNING.nativeValue, - LogEvent.RENDER.nativeValue, - 42L, - arena.allocateFrom("render warning"), - ) - - assertEquals(1, result) - } - + Arena.ofConfined().use { arena -> assertEquals( - LogRecord(LogSeverity.WARNING, LogEvent.RENDER, 42L, "render warning"), - copiedRecord, - ) - } finally { - LogCallbackState.clear() - } - } - - @Test - fun callbackClosureWaitsForEnteredCallbacksAndSupportsClosureFromCallback() { - val entered = CountDownLatch(1) - val release = CountDownLatch(1) - lateinit var state: LogCallbackState - state = - LogCallbackState.setForTestingAndGet( - LogCallback { - entered.countDown() - release.await(5, TimeUnit.SECONDS) - true - } + 1, + state.invoke( + MemorySegment.NULL, + LogSeverity.WARNING.nativeValue, + LogEvent.RENDER.nativeValue, + 42, + arena.allocateFrom("warning"), + ), ) - val invocation = thread { state.invoke(MemorySegment.NULL, 0, 0, 0, MemorySegment.NULL) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - val closed = CountDownLatch(1) - val closer = thread { - state.close() - closed.countDown() } - assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) - release.countDown() - invocation.join() - closer.join() + assertEquals(LogRecord(LogSeverity.WARNING, LogEvent.RENDER, 42, "warning"), copied) + state.close() + assertEquals(0, state.invoke(MemorySegment.NULL, 1, 0, 0, MemorySegment.NULL)) assertTrue(state.isClosedForTesting()) - lateinit var reentrant: LogCallbackState - reentrant = - LogCallbackState.setForTestingAndGet( - LogCallback { - reentrant.close() - true - } - ) - assertEquals(1, reentrant.invoke(MemorySegment.NULL, 0, 0, 0, MemorySegment.NULL)) - assertTrue(reentrant.isClosedForTesting()) - LogCallbackState.clearForTesting() + val throwing = LogCallbackState.createForTesting(LogCallback { error("contained") }) + assertEquals(0, throwing.invoke(MemorySegment.NULL, 1, 0, 0, MemorySegment.NULL)) + throwing.close() } @Test - fun replacementPreservesRawEnumsAndFailedReplacementKeepsPreviousState() { - var copied: LogRecord? = null - try { - LogCallbackState.setForTesting( - LogCallback { - copied = it - false - } - ) - val previous = assertNotNull(LogCallbackState.currentForTesting()) - assertFailsWith { - LogCallbackState.setForTesting(LogCallback { true }) { error("registration failed") } - } - assertSame(previous, LogCallbackState.currentForTesting()) - assertFalse(previous.isClosedForTesting()) - previous.invoke(MemorySegment.NULL, 991, 992, 7, MemorySegment.NULL) - assertEquals(991, copied?.severity?.nativeValue) - assertEquals(992, copied?.event?.nativeValue) - - LogCallbackState.setForTesting(LogCallback { true }) - assertTrue(previous.isClosedForTesting()) - } finally { - LogCallbackState.clearForTesting() - } - } - - private fun LogCallbackState.Companion.setForTestingAndGet( - callback: LogCallback - ): LogCallbackState { - setForTesting(callback) - return assertNotNull(currentForTesting()) + fun nativeRegistrationReplacesAndClearsOwnedState() { + Maplibre.setLogCallback(LogCallback { true }) + Maplibre.setLogCallback(LogCallback { false }) + Maplibre.clearLogCallback() + Maplibre.clearLogCallback() } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderStateTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderStateTest.kt index 28d182a3e..38645381e 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderStateTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderStateTest.kt @@ -23,120 +23,123 @@ import org.maplibre.nativeffi.resource.ResourceUsage class ResourceProviderStateTest { @Test - fun providerCallbackCopiesRequestAndReturnsDecision() { - var copied: ResourceRequest? = null - ResourceProviderState( - ResourceProviderCallback { request, _ -> - copied = request - ResourceProviderDecision.PASS_THROUGH - } - ) - .use { state -> - Arena.ofConfined().use { arena -> - val request = resourceRequest(arena) - val fakeHandle = SyntheticHandles.resourceRequest().raw + fun providerCallbackCopiesRequestAndReturnsDecision(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copied: ResourceRequest? = null + ResourceProviderState( + ResourceProviderCallback { request, _ -> + copied = request + ResourceProviderDecision.PASS_THROUGH + } + ) + .use { state -> + Arena.ofConfined().use { arena -> + val request = resourceRequest(arena) + val fakeHandle = SyntheticHandles.resourceRequest().raw - assertEquals( - ResourceProviderDecision.PASS_THROUGH.nativeValue, - state.invoke(MemorySegment.NULL, request, fakeHandle), - ) + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue, + state.invoke(MemorySegment.NULL, request, fakeHandle), + ) + } } - } - - assertEquals(ResourceKind(900), copied?.kind) - assertEquals(ResourceLoadingMethod(901), copied?.loadingMethod) - assertEquals(ResourcePriority(902), copied?.priority) - assertEquals(ResourceUsage(903), copied?.usage) - assertEquals(ResourceStoragePolicy(904), copied?.storagePolicy) - assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) - assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) - assertEquals(ResourceRequest.ByteRange(7, 11), copied?.range) - assertEquals(123L, copied?.priorModifiedUnixMs) - assertEquals(456L, copied?.priorExpiresUnixMs) - assertEquals("etag", copied?.priorEtag) - val priorData = copied?.priorData ?: ByteArray(0) - assertContentEquals(byteArrayOf(1, 2, 3), priorData) - priorData[0] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), copied?.priorData) - } - @Test - fun hostLanguageFailureTellsNativeNotToPassThrough() { - ResourceProviderState( - ResourceProviderCallback { _, _ -> throw IllegalStateException("contained") } - ) - .use { state -> - Arena.ofConfined().use { arena -> - assertEquals( - UNKNOWN_DECISION, - state.invoke( - MemorySegment.NULL, - resourceRequest(arena), - SyntheticHandles.resourceRequest().raw, - ), - ) - } - } - } + assertEquals(ResourceKind(900), copied?.kind) + assertEquals(ResourceLoadingMethod(901), copied?.loadingMethod) + assertEquals(ResourcePriority(902), copied?.priority) + assertEquals(ResourceUsage(903), copied?.usage) + assertEquals(ResourceStoragePolicy(904), copied?.storagePolicy) + assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) + assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) + assertEquals(ResourceRequest.ByteRange(7, 11), copied?.range) + assertEquals(123L, copied?.priorModifiedUnixMs) + assertEquals(456L, copied?.priorExpiresUnixMs) + assertEquals("etag", copied?.priorEtag) + val priorData = copied?.priorData ?: ByteArray(0) + assertContentEquals(byteArrayOf(1, 2, 3), priorData) + priorData[0] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), copied?.priorData) + } @Test - fun providerClosureDuringAndConcurrentWithCallbacksRejectsLaterEntry() { - lateinit var reentrant: ResourceProviderState - reentrant = + fun hostLanguageFailureTellsNativeNotToPassThrough(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { ResourceProviderState( - ResourceProviderCallback { _, _ -> - reentrant.close() - ResourceProviderDecision.PASS_THROUGH + ResourceProviderCallback { _, _ -> throw IllegalStateException("contained") } + ) + .use { state -> + Arena.ofConfined().use { arena -> + assertEquals( + UNKNOWN_DECISION, + state.invoke( + MemorySegment.NULL, + resourceRequest(arena), + SyntheticHandles.resourceRequest().raw, + ), + ) + } } - ) - Arena.ofConfined().use { arena -> - assertEquals( - ResourceProviderDecision.PASS_THROUGH.nativeValue, - reentrant.invoke( - MemorySegment.NULL, - resourceRequest(arena), - SyntheticHandles.resourceRequest().raw, - ), - ) - assertTrue(reentrant.isClosedForTesting()) - assertEquals( - UNKNOWN_DECISION, - reentrant.invoke( - MemorySegment.NULL, - resourceRequest(arena), - SyntheticHandles.resourceRequest().raw, - ), - ) } - val entered = CountDownLatch(1) - val release = CountDownLatch(1) - val state = - ResourceProviderState( - ResourceProviderCallback { _, _ -> - entered.countDown() - release.await(5, TimeUnit.SECONDS) - ResourceProviderDecision.PASS_THROUGH - } - ) - Arena.ofShared().use { arena -> - val request = resourceRequest(arena) - val invocation = thread { - state.invoke(MemorySegment.NULL, request, SyntheticHandles.resourceRequest().raw) + @Test + fun providerClosureDuringAndConcurrentWithCallbacksRejectsLaterEntry(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + lateinit var reentrant: ResourceProviderState + reentrant = + ResourceProviderState( + ResourceProviderCallback { _, _ -> + reentrant.close() + ResourceProviderDecision.PASS_THROUGH + } + ) + Arena.ofConfined().use { arena -> + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue, + reentrant.invoke( + MemorySegment.NULL, + resourceRequest(arena), + SyntheticHandles.resourceRequest().raw, + ), + ) + assertTrue(reentrant.isClosedForTesting()) + assertEquals( + UNKNOWN_DECISION, + reentrant.invoke( + MemorySegment.NULL, + resourceRequest(arena), + SyntheticHandles.resourceRequest().raw, + ), + ) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - val closed = CountDownLatch(1) - val closer = thread { - state.close() - closed.countDown() + + val entered = CountDownLatch(1) + val release = CountDownLatch(1) + val state = + ResourceProviderState( + ResourceProviderCallback { _, _ -> + entered.countDown() + release.await(5, TimeUnit.SECONDS) + ResourceProviderDecision.PASS_THROUGH + } + ) + Arena.ofShared().use { arena -> + val request = resourceRequest(arena) + val invocation = thread { + state.invoke(MemorySegment.NULL, request, SyntheticHandles.resourceRequest().raw) + } + assertTrue(entered.await(5, TimeUnit.SECONDS)) + val closed = CountDownLatch(1) + val closer = thread { + state.close() + closed.countDown() + } + assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) + release.countDown() + invocation.join() + closer.join() + assertTrue(state.isClosedForTesting()) } - assertFalse(closed.await(50, TimeUnit.MILLISECONDS)) - release.countDown() - invocation.join() - closer.join() - assertTrue(state.isClosedForTesting()) } - } private fun resourceRequest(arena: Arena): MemorySegment { val request = arena.allocate(RESOURCE_REQUEST_SIZE) diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformStateTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformStateTest.kt index 71d4fe364..f1132423b 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformStateTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformStateTest.kt @@ -11,90 +11,93 @@ import org.maplibre.nativeffi.resource.ResourceTransformCallback class ResourceTransformStateTest { @Test - fun transformCallbackCopiesRequestAndKeepsOriginalUrlWhenNullIsReturned() { - var copiedKind: ResourceKind? = null - var copiedUrl: String? = null - ResourceTransformState( - ResourceTransformCallback { request -> - copiedKind = request.kind - copiedUrl = request.url - null + fun transformCallbackCopiesRequestAndKeepsOriginalUrlWhenNullIsReturned(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copiedKind: ResourceKind? = null + var copiedUrl: String? = null + ResourceTransformState( + ResourceTransformCallback { request -> + copiedKind = request.kind + copiedUrl = request.url + null + } + ) + .use { state -> + Arena.ofConfined().use { arena -> + val response = arena.allocate(24) + val status = + state.invoke( + MemorySegment.NULL, + ResourceKind.TILE.nativeValue, + arena.allocateFrom("https://example.com/tile.pbf"), + response, + ) + + assertEquals(MaplibreStatus.OK.nativeCode, status) + assertEquals(ResourceKind.TILE, copiedKind) + assertEquals("https://example.com/tile.pbf", copiedUrl) + assertEquals(24, response.get(ValueLayout.JAVA_INT, 0)) + assertEquals(MemorySegment.NULL, response.get(ValueLayout.ADDRESS, 8)) + } } - ) - .use { state -> + } + + @Test + fun invalidRewriteUrlReturnsInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + ResourceTransformState(ResourceTransformCallback { "bad\u0000url" }).use { state -> Arena.ofConfined().use { arena -> val response = arena.allocate(24) val status = state.invoke( MemorySegment.NULL, - ResourceKind.TILE.nativeValue, - arena.allocateFrom("https://example.com/tile.pbf"), + ResourceKind.STYLE.nativeValue, + arena.allocateFrom("https://example.com/style.json"), response, ) - assertEquals(MaplibreStatus.OK.nativeCode, status) - assertEquals(ResourceKind.TILE, copiedKind) - assertEquals("https://example.com/tile.pbf", copiedUrl) + assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, status) assertEquals(24, response.get(ValueLayout.JAVA_INT, 0)) assertEquals(MemorySegment.NULL, response.get(ValueLayout.ADDRESS, 8)) } } - } + } @Test - fun invalidRewriteUrlReturnsInvalidArgument() { - ResourceTransformState(ResourceTransformCallback { "bad\u0000url" }).use { state -> + fun unknownKindsHostFailuresAndClosureDuringCallbackAreContained(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copiedKind: ResourceKind? = null + lateinit var state: ResourceTransformState + state = + ResourceTransformState( + ResourceTransformCallback { request -> + copiedKind = request.kind + state.close() + throw IllegalStateException("contained") + } + ) Arena.ofConfined().use { arena -> val response = arena.allocate(24) - val status = + assertEquals( + MaplibreStatus.NATIVE_ERROR.nativeCode, state.invoke( MemorySegment.NULL, - ResourceKind.STYLE.nativeValue, + 991, arena.allocateFrom("https://example.com/style.json"), response, - ) - - assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, status) - assertEquals(24, response.get(ValueLayout.JAVA_INT, 0)) - assertEquals(MemorySegment.NULL, response.get(ValueLayout.ADDRESS, 8)) + ), + ) + assertEquals(991, copiedKind?.nativeValue) + assertEquals( + MaplibreStatus.INVALID_ARGUMENT.nativeCode, + state.invoke( + MemorySegment.NULL, + 991, + arena.allocateFrom("https://example.com/style.json"), + response, + ), + ) + assertEquals(true, state.isClosedForTesting()) } } - } - - @Test - fun unknownKindsHostFailuresAndClosureDuringCallbackAreContained() { - var copiedKind: ResourceKind? = null - lateinit var state: ResourceTransformState - state = - ResourceTransformState( - ResourceTransformCallback { request -> - copiedKind = request.kind - state.close() - throw IllegalStateException("contained") - } - ) - Arena.ofConfined().use { arena -> - val response = arena.allocate(24) - assertEquals( - MaplibreStatus.NATIVE_ERROR.nativeCode, - state.invoke( - MemorySegment.NULL, - 991, - arena.allocateFrom("https://example.com/style.json"), - response, - ), - ) - assertEquals(991, copiedKind?.nativeValue) - assertEquals( - MaplibreStatus.INVALID_ARGUMENT.nativeCode, - state.invoke( - MemorySegment.NULL, - 991, - arena.allocateFrom("https://example.com/style.json"), - response, - ), - ) - assertEquals(true, state.isClosedForTesting()) - } - } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleanerTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleanerTest.kt index cbcafaa1a..26bf3f3ee 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleanerTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleLeakCleanerTest.kt @@ -6,75 +6,56 @@ import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -import org.maplibre.nativeffi.runtime.OfflineOperationKind -import org.maplibre.nativeffi.runtime.OfflineOperationLeakReport -import org.maplibre.nativeffi.runtime.OfflineOperationResultKind class HandleLeakCleanerTest { - // BND-044. Destruction is owner-thread-bound, so the cleaner thread only reports. + // BND-044. The cleaner reports; explicit close keeps native failures observable. @Test - fun unreachableHandleReportsLeakWithoutExplicitRelease() { - val reports = CopyOnWriteArrayList() + fun unreachableHandleReportsLeakWithoutExplicitRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val reports = CopyOnWriteArrayList() - registerUnreachableHandle(reports) + registerUnreachableHandle(reports) - assertTrue(awaitReport(reports), "expected the cleaner to report the unreachable handle") - assertEquals( - "Leaked RuntimeHandle native handle 0x1234; close handles explicitly.", - reports.single(), - ) - } - - @Test - fun releasedHandleStaysSilentWhenCollected() { - val reports = CopyOnWriteArrayList() - val liveReports = CopyOnWriteArrayList() - - registerReleasedHandle(reports) - // A second unreleased registration proves the cleaner ran, so the silence above is a - // real result. - registerUnreachableHandle(liveReports) - - assertTrue(awaitReport(liveReports), "expected the cleaner to run") - assertEquals(emptyList(), reports) - } + assertTrue(awaitReport(reports), "expected the cleaner to report the unreachable handle") + assertEquals( + "Leaked RuntimeHandle native handle 0x1234; close it explicitly.", + reports.single(), + ) + } @Test - fun unreachableOfflineOperationReportsLeak() { - val reports = CopyOnWriteArrayList() - val kind = OfflineOperationKind.AMBIENT_CACHE - val resultKind = OfflineOperationResultKind.NONE - - HandleLeakCleaner.registerOfflineOperation( - Any(), - OfflineOperationLeakReport(42L, kind, resultKind, reports::add), - ) - - assertTrue(awaitReport(reports), "expected the cleaner to report the offline operation") - assertEquals( - "Leaked OfflineOperationHandle id=42 kind=$kind resultKind=$resultKind; " + - "take or discard operations explicitly on the runtime owner thread.", - reports.single(), - ) - } + fun releasedHandleStaysSilentWhenCollected(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val reports = CopyOnWriteArrayList() + val liveReports = CopyOnWriteArrayList() + + registerReleasedHandle(reports) + // A second unreleased registration proves the cleaner ran, so the silence above is a + // real result. + registerUnreachableHandle(liveReports) + + assertTrue(awaitReport(liveReports), "expected the cleaner to run") + assertEquals(emptyList(), reports) + } @Test - fun blockedLeakReportDoesNotBlockNativeReclamationWorker() { - val reportStarted = CountDownLatch(1) - val unblockReport = CountDownLatch(1) - val reclaimed = CountDownLatch(1) - - registerBlockingLeakReport(reportStarted, unblockReport) - UnreachableActions.register(Any(), reclaimed::countDown) - - try { - assertTrue(awaitAction(reportStarted), "expected the leak-report worker to start") - assertTrue(awaitAction(reclaimed), "expected native reclamation to use another worker") - } finally { - unblockReport.countDown() + fun blockedLeakReportDoesNotBlockNativeReclamationWorker(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val reportStarted = CountDownLatch(1) + val unblockReport = CountDownLatch(1) + val reclaimed = CountDownLatch(1) + + registerBlockingLeakReport(reportStarted, unblockReport) + UnreachableActions.register(Any(), reclaimed::countDown) + + try { + assertTrue(awaitAction(reportStarted), "expected the leak-report worker to start") + assertTrue(awaitAction(reclaimed), "expected native reclamation to use another worker") + } finally { + unblockReport.countDown() + } } - } /** Registers a handle that is unreachable once this call returns. */ private fun registerUnreachableHandle(reports: MutableList) { diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreJvmTest.kt deleted file mode 100644 index 4c609c1f8..000000000 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateCoreJvmTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.maplibre.nativeffi.internal.lifecycle - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import org.maplibre.nativeffi.error.MaplibreStatus - -class HandleStateCoreJvmTest { - // BND-197. - @Test - fun closeWaitsForAUseInFlightOnAnotherThread() { - val state = HandleStateCore("TestHandle", 0x1234) - var destroys = 0 - val entered = CountDownLatch(1) - val releaseUse = CountDownLatch(1) - val closeReturned = AtomicBoolean(false) - val destroyedDuringUse = AtomicBoolean(false) - - val useThread = Thread { - state.withLive { - entered.countDown() - assertTrue(releaseUse.await(5, TimeUnit.SECONDS)) - if (destroys != 0) destroyedDuringUse.set(true) - } - } - useThread.start() - assertTrue(entered.await(5, TimeUnit.SECONDS)) - - val closeThread = Thread { - state.closeOnce( - destroy = { - destroys += 1 - MaplibreStatus.OK.nativeCode - } - ) - closeReturned.set(true) - } - closeThread.start() - - Thread.sleep(50) - assertFalse(closeReturned.get(), "close should wait for the use in flight") - assertEquals(0, destroys, "the handle should still be live during the use") - - releaseUse.countDown() - closeThread.join(5_000) - useThread.join(5_000) - - assertTrue(closeReturned.get()) - assertEquals(1, destroys) - assertFalse(destroyedDuringUse.get(), "the use should never observe a destroyed handle") - assertTrue(state.isReleased()) - } -} diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccessTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccessTest.kt index 414374882..c2e41c459 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccessTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeAccessTest.kt @@ -15,81 +15,87 @@ import org.maplibre.nativeffi.runtime.RuntimeOptions class NativeAccessTest { @Test - fun abiVersionMismatchReportsActualAndExpectedVersions() { - val error = - assertFailsWith { - NativeAccess.checkAbiVersion(NativeAccess.EXPECTED_C_ABI_VERSION + 1) - } + fun abiVersionMismatchReportsActualAndExpectedVersions(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val error = + assertFailsWith { + NativeAccess.checkAbiVersion(NativeAccess.EXPECTED_C_ABI_VERSION + 1) + } - assertEquals(NativeAccess.EXPECTED_C_ABI_VERSION + 1, error.actualVersion) - assertEquals(NativeAccess.EXPECTED_C_ABI_VERSION, error.expectedVersion) - } + assertEquals(NativeAccess.EXPECTED_C_ABI_VERSION + 1, error.actualVersion) + assertEquals(NativeAccess.EXPECTED_C_ABI_VERSION, error.expectedVersion) + } @Test - fun nativeAccessFailureIsWrappedWithJvmFlagGuidance() { - val error = - assertFailsWith { - NativeAccess.checkNativeAccessAndAbi { throw IllegalCallerException("native access") } - } + fun nativeAccessFailureIsWrappedWithJvmFlagGuidance(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val error = + assertFailsWith { + NativeAccess.checkNativeAccessAndAbi { throw IllegalCallerException("native access") } + } - assertTrue(error.message.orEmpty().contains("--enable-native-access=ALL-UNNAMED")) - } + assertTrue(error.message.orEmpty().contains("--enable-native-access=ALL-UNNAMED")) + } @Test - fun missingSymbolFailureIsWrappedAsUnsatisfiedLinkError() { - val error = - assertFailsWith { - NativeAccess.checkNativeAccessAndAbi { throw NoSuchElementException("mln_c_version") } - } + fun missingSymbolFailureIsWrappedAsUnsatisfiedLinkError(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val error = + assertFailsWith { + NativeAccess.checkNativeAccessAndAbi { throw NoSuchElementException("mln_c_version") } + } - assertTrue(error.message.orEmpty().contains("Maplibre C ABI symbols")) - } + assertTrue(error.message.orEmpty().contains("Maplibre C ABI symbols")) + } @Test - fun resourceResponseRejectsEmbeddedNulWithBindingInvalidArgument() { - NativeAccess.ensureLoaded() - val response = - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason.OTHER - errorMessage = "bad\u0000message" - } + fun resourceResponseRejectsEmbeddedNulWithBindingInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeAccess.ensureLoaded() + val response = + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason.OTHER + errorMessage = "bad\u0000message" + } - val error = - assertFailsWith { - NativeAccess.completeResourceRequest(SyntheticHandles.resourceRequest(), response) - } + val error = + assertFailsWith { + NativeAccess.completeResourceRequest(SyntheticHandles.resourceRequest(), response) + } - assertEquals("error message contains embedded NUL", error.diagnostic) - } + assertEquals("error message contains embedded NUL", error.diagnostic) + } @Test - fun resourceResponseRejectsUnknownErrorReasonWithBindingInvalidArgument() { - NativeAccess.ensureLoaded() - val response = - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason(999) - } + fun resourceResponseRejectsUnknownErrorReasonWithBindingInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeAccess.ensureLoaded() + val response = + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason(999) + } - val error = - assertFailsWith { - NativeAccess.completeResourceRequest(SyntheticHandles.resourceRequest(), response) - } + val error = + assertFailsWith { + NativeAccess.completeResourceRequest(SyntheticHandles.resourceRequest(), response) + } - assertEquals("Unknown resource error reason cannot be used as input: 999", error.diagnostic) - } + assertEquals("Unknown resource error reason cannot be used as input: 999", error.diagnostic) + } @Test - fun runtimeOptionsRejectEmbeddedNulWithBindingInvalidArgument() { - val error = - assertFailsWith { - RuntimeHandle.create( - RuntimeOptions().apply { - assetPath = "bad\u0000path" - cachePath = ":memory:" - } - ) - } + fun runtimeOptionsRejectEmbeddedNulWithBindingInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val error = + assertFailsWith { + RuntimeHandle.create( + RuntimeOptions().apply { + assetPath = "bad\u0000path" + cachePath = ":memory:" + } + ) + } - assertEquals("C string inputs cannot contain embedded NUL characters", error.diagnostic) - } + assertEquals("C string inputs cannot contain embedded NUL characters", error.diagnostic) + } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeLibraryTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeLibraryTest.kt index ec404bfb2..74d440e2c 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeLibraryTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/loader/NativeLibraryTest.kt @@ -8,19 +8,20 @@ import kotlin.test.assertTrue class NativeLibraryTest { @Test - fun explicitMissingPathReportsSourceAndPath() { - val missingPath = Path.of("build", "missing-maplibre-native-c") + fun explicitMissingPathReportsSourceAndPath(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val missingPath = Path.of("build", "missing-maplibre-native-c") - if (NativeLibrary.isLoaded()) { - NativeLibrary.load(missingPath) - assertTrue(NativeLibrary.isLoaded()) - return - } + if (NativeLibrary.isLoaded()) { + NativeLibrary.load(missingPath) + assertTrue(NativeLibrary.isLoaded()) + return@runSuspendTest + } - val error = assertFailsWith { NativeLibrary.load(missingPath) } + val error = assertFailsWith { NativeLibrary.load(missingPath) } - assertTrue(error.message.orEmpty().contains("explicit path")) - assertTrue(error.message.orEmpty().contains(missingPath.toString())) - assertFalse(NativeLibrary.isLoaded()) - } + assertTrue(error.message.orEmpty().contains("explicit path")) + assertTrue(error.message.orEmpty().contains(missingPath.toString())) + assertFalse(NativeLibrary.isLoaded()) + } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/struct/JvmStructsTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/struct/JvmStructsTest.kt index 5e5a2f3be..6c52759b8 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/struct/JvmStructsTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/internal/struct/JvmStructsTest.kt @@ -23,7 +23,6 @@ import org.maplibre.nativeffi.map.TileOptions import org.maplibre.nativeffi.map.ViewportMode import org.maplibre.nativeffi.map.ViewportOptions import org.maplibre.nativeffi.offline.OfflineRegionDefinition -import org.maplibre.nativeffi.query.FeatureStateSelector import org.maplibre.nativeffi.query.RenderedQueryGeometry import org.maplibre.nativeffi.render.EglContextDescriptor import org.maplibre.nativeffi.render.MetalBorrowedTextureDescriptor @@ -39,246 +38,231 @@ import org.maplibre.nativeffi.style.StyleImageOptions class JvmStructsTest { @Test - fun stringViewsPreserveEmbeddedNulBytes() { - assertEquals("value\u0000suffix", JvmStructs.stringViewRoundTrip("value\u0000suffix")) - } + fun stringViewsPreserveEmbeddedNulBytes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertEquals("value\u0000suffix", JvmStructs.stringViewRoundTrip("value\u0000suffix")) + } @Test - fun mapOptionsAndFieldMasksPreservePresentZeroValues() { - val camera = - CameraOptions().apply { - center = LatLng(0.0, 0.0) - zoom = 0.0 - anchor = ScreenPoint(0.0, 0.0) - } - val (fields, cameraCopy) = JvmStructs.cameraOptionsRoundTrip(camera) - assertNotEquals(0, fields) - assertEquals(camera, cameraCopy) - - val animation = - JvmStructs.animationOptionsSnapshot( - AnimationOptions().apply { - durationMs = 0.0 - velocity = 0.0 - transitionId = -1 + fun mapOptionsAndFieldMasksPreservePresentZeroValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val camera = + CameraOptions().apply { + center = LatLng(0.0, 0.0) + zoom = 0.0 + anchor = ScreenPoint(0.0, 0.0) } - ) - assertNotEquals(0, animation.fields) - assertEquals(0.0, animation.durationMs) - assertEquals(0.0, animation.velocity) - assertEquals(-1L, animation.transitionId) + val (fields, cameraCopy) = JvmStructs.cameraOptionsRoundTrip(camera) + assertNotEquals(0, fields) + assertEquals(camera, cameraCopy) - val viewport = - ViewportOptions().apply { - northOrientation = NorthOrientation.LEFT - constrainMode = ConstrainMode.SCREEN - viewportMode = ViewportMode.FLIPPED_Y - } - assertEquals(viewport, JvmStructs.viewportOptionsRoundTrip(viewport)) + val animation = + JvmStructs.animationOptionsSnapshot( + AnimationOptions().apply { + durationMs = 0.0 + velocity = 0.0 + transitionId = -1 + } + ) + assertNotEquals(0, animation.fields) + assertEquals(0.0, animation.durationMs) + assertEquals(0.0, animation.velocity) + assertEquals(-1L, animation.transitionId) - val tile = TileOptions().apply { lodMode = TileLodMode.DISTANCE } - assertEquals(tile, JvmStructs.tileOptionsRoundTrip(tile)) + val viewport = + ViewportOptions().apply { + northOrientation = NorthOrientation.LEFT + constrainMode = ConstrainMode.SCREEN + viewportMode = ViewportMode.FLIPPED_Y + } + assertEquals(viewport, JvmStructs.viewportOptionsRoundTrip(viewport)) - val projection = - ProjectionModeOptions().apply { - axonometric = false - xSkew = 0.0 - ySkew = 0.0 - } - assertEquals(projection, JvmStructs.projectionModeOptionsRoundTrip(projection)) - } + val tile = TileOptions().apply { lodMode = TileLodMode.DISTANCE } + assertEquals(tile, JvmStructs.tileOptionsRoundTrip(tile)) + + val projection = + ProjectionModeOptions().apply { + axonometric = false + xSkew = 0.0 + ySkew = 0.0 + } + assertEquals(projection, JvmStructs.projectionModeOptionsRoundTrip(projection)) + } @Test - fun queryGeometryMaterializesEachDiscriminator() { - val point = - JvmStructs.renderedQueryGeometryType(RenderedQueryGeometry.Point(ScreenPoint(1.0, 2.0))) - val box = - JvmStructs.renderedQueryGeometryType( - RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) - ) - val line = - JvmStructs.renderedQueryGeometryType( - RenderedQueryGeometry.LineString(listOf(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) - ) - assertEquals(3, setOf(point, box, line).size) - } + fun queryGeometryMaterializesEachDiscriminator(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val point = + JvmStructs.renderedQueryGeometryType(RenderedQueryGeometry.Point(ScreenPoint(1.0, 2.0))) + val box = + JvmStructs.renderedQueryGeometryType( + RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) + ) + val line = + JvmStructs.renderedQueryGeometryType( + RenderedQueryGeometry.LineString(listOf(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))) + ) + assertEquals(3, setOf(point, box, line).size) + } @Test - fun featureStateSelectorEmbedsStringViewsByValue() { - val minimal = JvmStructs.featureStateSelectorSnapshot(FeatureStateSelector("point")) - assertEquals("point", minimal.sourceId) - assertEquals(0, minimal.fields) - assertEquals(null, minimal.sourceLayerId) - assertEquals(null, minimal.featureId) - assertEquals(null, minimal.stateKey) + fun offlineDefinitionsRoundTripBothVariants(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val tile = + OfflineRegionDefinition.TilePyramid( + "asset://style.json", + LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), + 1.0, + 5.0, + 2.0f, + true, + ) + assertEquals(tile, JvmStructs.offlineRegionDefinitionRoundTrip(tile)) + val info = JvmStructs.offlineRegionInfoSnapshot(7, tile, byteArrayOf(1, 2, 3)) + assertEquals(7, info.id) + assertEquals(tile, info.definition) + assertContentEquals(byteArrayOf(1, 2, 3), info.metadata) - val full = - JvmStructs.featureStateSelectorSnapshot( - FeatureStateSelector("point").apply { - sourceLayerId = "layer" - featureId = "feature-1" - stateKey = "hover" - } - ) - assertEquals("point", full.sourceId) - assertEquals("layer", full.sourceLayerId) - assertEquals("feature-1", full.featureId) - assertEquals("hover", full.stateKey) - assertNotEquals(0, full.fields) - } + val geometry = + OfflineRegionDefinition.GeometryRegion( + "asset://style.json", + """{"type":"Point","coordinates":[6.0,5.0]}""".encodeToByteArray(), + 2.0, + 6.0, + 1.0f, + false, + ) + assertEquals(geometry, JvmStructs.offlineRegionDefinitionRoundTrip(geometry)) + } @Test - fun offlineDefinitionsRoundTripBothVariants() { - val tile = - OfflineRegionDefinition.TilePyramid( - "asset://style.json", - LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), - 1.0, - 5.0, - 2.0f, - true, - ) - assertEquals(tile, JvmStructs.offlineRegionDefinitionRoundTrip(tile)) - val info = JvmStructs.offlineRegionInfoSnapshot(7, tile, byteArrayOf(1, 2, 3)) - assertEquals(7, info.id) - assertEquals(tile, info.definition) - assertContentEquals(byteArrayOf(1, 2, 3), info.metadata) + fun styleAndImageMetadataPreserveFieldsAndLengths(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val style = + JvmStructs.styleImageOptionsSnapshot( + StyleImageOptions().apply { + pixelRatio = 0.0f + sdf = false + } + ) + assertNotEquals(0, style.fields) + assertEquals(0.0f, style.pixelRatio) + assertEquals(false, style.sdf) - val geometry = - OfflineRegionDefinition.GeometryRegion( - "asset://style.json", - """{"type":"Point","coordinates":[6.0,5.0]}""".encodeToByteArray(), - 2.0, - 6.0, - 1.0f, - false, + assertEquals( + org.maplibre.nativeffi.render.TextureImageInfo(2, 3, 8, 24), + JvmStructs.textureImageInfoSnapshot(2, 3, 8, 24), ) - assertEquals(geometry, JvmStructs.offlineRegionDefinitionRoundTrip(geometry)) - } + assertFailsWith { JvmStructs.textureImageInfoSnapshot(2, 3, 8, -1) } + assertFailsWith { JvmStructs.styleImageInfoSnapshot(-1) } + assertEquals(1, JvmStructs.styleStringListCleanupAfterCopyFailure()) + val source = JvmStructs.sourceInfoSnapshot(999, true) + assertEquals(999, source.type.nativeValue) + assertTrue(source.volatileSource) + } @Test - fun styleAndImageMetadataPreserveFieldsAndLengths() { - val style = - JvmStructs.styleImageOptionsSnapshot( - StyleImageOptions().apply { - pixelRatio = 0.0f - sdf = false - } - ) - assertNotEquals(0, style.fields) - assertEquals(0.0f, style.pixelRatio) - assertEquals(false, style.sdf) - - assertEquals( - org.maplibre.nativeffi.render.TextureImageInfo(2, 3, 8, 24), - JvmStructs.textureImageInfoSnapshot(2, 3, 8, 24), - ) - assertFailsWith { JvmStructs.textureImageInfoSnapshot(2, 3, 8, -1) } - assertFailsWith { JvmStructs.styleImageInfoSnapshot(-1) } - assertEquals(1, JvmStructs.styleStringListCleanupAfterCopyFailure()) - val source = JvmStructs.sourceInfoSnapshot(999, true) - assertEquals(999, source.type.nativeValue) - assertTrue(source.volatileSource) - } + fun mapCreationMaterializesFastPforAndMode(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val snapshot = + JvmStructs.mapOptionsSnapshot( + MapOptions().apply { + width = 320 + height = 240 + scaleFactor = 2.0 + mapMode = MapMode.STATIC + fastPforEnabled = true + } + ) + assertEquals(320, snapshot.width) + assertEquals(240, snapshot.height) + assertEquals(2.0, snapshot.scaleFactor) + assertEquals(MapMode.STATIC.nativeValue, snapshot.mapMode) + assertTrue(snapshot.fastPforEnabled) + } @Test - fun mapCreationMaterializesFastPforAndMode() { - val snapshot = - JvmStructs.mapOptionsSnapshot( - MapOptions().apply { - width = 320 - height = 240 - scaleFactor = 2.0 - mapMode = MapMode.STATIC - fastPforEnabled = true + fun resourceResponsesSnapshotOptionalFieldsAndBytes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val response = + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = byteArrayOf(1, 2, 3) + mustRevalidate = true + modifiedUnixMs = 10 + expiresUnixMs = 20 + retryAfterUnixMs = 30 } - ) - assertEquals(320, snapshot.width) - assertEquals(240, snapshot.height) - assertEquals(2.0, snapshot.scaleFactor) - assertEquals(MapMode.STATIC.nativeValue, snapshot.mapMode) - assertTrue(snapshot.fastPforEnabled) - } - - @Test - fun resourceResponsesSnapshotOptionalFieldsAndBytes() { - val response = - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = byteArrayOf(1, 2, 3) - mustRevalidate = true - modifiedUnixMs = 10 - expiresUnixMs = 20 - retryAfterUnixMs = 30 - } - val snapshot = JvmStructs.resourceResponseSnapshot(response) + val snapshot = JvmStructs.resourceResponseSnapshot(response) - assertEquals(ResourceResponseStatus.OK.nativeValue, snapshot.status) - assertContentEquals(byteArrayOf(1, 2, 3), snapshot.bytes) - assertTrue(snapshot.mustRevalidate) - assertTrue(snapshot.hasModified) - assertTrue(snapshot.hasExpires) - assertTrue(snapshot.hasRetryAfter) - } + assertEquals(ResourceResponseStatus.OK.nativeValue, snapshot.status) + assertContentEquals(byteArrayOf(1, 2, 3), snapshot.bytes) + assertTrue(snapshot.mustRevalidate) + assertTrue(snapshot.hasModified) + assertTrue(snapshot.hasExpires) + assertTrue(snapshot.hasRetryAfter) + } @Test - fun ownedBufferCleanupAndRuntimePayloadCopiesUseOwnedAdapterPaths() { - assertEquals(1, JvmStructs.ownedBufferCleanupAfterCopyFailure()) - assertEquals(1, JvmStructs.offlineRegionListCleanupAfterCopyFailure()) - val payload = - JvmStructs.unknownRuntimePayload(999, byteArrayOf(1, 2, 3)) as RuntimeEventPayload.Unknown - assertEquals(999, payload.rawPayloadType) - assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) - } + fun ownedBufferCleanupAndRuntimePayloadCopiesUseOwnedAdapterPaths(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertEquals(1, JvmStructs.ownedBufferCleanupAfterCopyFailure()) + assertEquals(1, JvmStructs.offlineRegionListCleanupAfterCopyFailure()) + val payload = + JvmStructs.unknownRuntimePayload(999, byteArrayOf(1, 2, 3)) as RuntimeEventPayload.Unknown + assertEquals(999, payload.rawPayloadType) + assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) + } @Test - fun backendDescriptorsPreserveExtentsPointersAndBackendFields() { - val extent = RenderTargetExtent(640, 480, 2.0) - val metal = - JvmStructs.metalSnapshot( - MetalBorrowedTextureDescriptor(extent, 65, 33, NativePointer.ofAddress(0x20)) - ) - assertEquals(640, metal.width) - assertEquals(0x20, metal.firstPointer) + fun backendDescriptorsPreserveExtentsPointersAndBackendFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val extent = RenderTargetExtent(640, 480, 2.0) + val metal = + JvmStructs.metalSnapshot( + MetalBorrowedTextureDescriptor(extent, 65, 33, NativePointer.ofAddress(0x20)) + ) + assertEquals(640, metal.width) + assertEquals(0x20, metal.firstPointer) - val context = - VulkanContextDescriptor( - NativePointer.ofAddress(0x10), - NativePointer.ofAddress(0x20), - NativePointer.ofAddress(0x30), - NativePointer.ofAddress(0x40), - 7, - NativePointer.ofAddress(0x41), - NativePointer.ofAddress(0x42), - ) - val vulkan = - JvmStructs.vulkanSnapshot( - VulkanBorrowedTextureDescriptor( - extent, - 65, - 33, - context, - NativePointer.ofAddress(0x50), - NativePointer.ofAddress(0x60), - 44, - 1, - ) - .apply { finalLayout = 2 } - ) - assertEquals(0x50, vulkan.firstPointer) - assertEquals(0x60, vulkan.secondPointer) - assertEquals(2, vulkan.extra) + val context = + VulkanContextDescriptor( + NativePointer.ofAddress(0x10), + NativePointer.ofAddress(0x20), + NativePointer.ofAddress(0x30), + NativePointer.ofAddress(0x40), + 7, + NativePointer.ofAddress(0x41), + NativePointer.ofAddress(0x42), + ) + val vulkan = + JvmStructs.vulkanSnapshot( + VulkanBorrowedTextureDescriptor( + extent, + 65, + 33, + context, + NativePointer.ofAddress(0x50), + NativePointer.ofAddress(0x60), + 44, + 1, + ) + .apply { finalLayout = 2 } + ) + assertEquals(0x50, vulkan.firstPointer) + assertEquals(0x60, vulkan.secondPointer) + assertEquals(2, vulkan.extra) - val egl = - EglContextDescriptor( - NativePointer.ofAddress(0x70), - NativePointer.ofAddress(0x71), - NativePointer.ofAddress(0x72), - NativePointer.ofAddress(0x73), - ) - val openGl = - JvmStructs.openGlSnapshot(OpenGLBorrowedTextureDescriptor(extent, 65, 33, egl, 17, 3553)) - assertEquals(17, openGl.firstPointer) - assertEquals(0x70, openGl.secondPointer) - assertEquals(3553, openGl.extra) - } + val egl = + EglContextDescriptor( + NativePointer.ofAddress(0x70), + NativePointer.ofAddress(0x71), + NativePointer.ofAddress(0x72), + NativePointer.ofAddress(0x73), + ) + val openGl = + JvmStructs.openGlSnapshot(OpenGLBorrowedTextureDescriptor(extent, 65, 33, egl, 17, 3553)) + assertEquals(17, openGl.firstPointer) + assertEquals(0x70, openGl.secondPointer) + assertEquals(3553, openGl.extra) + } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt index a4f5deb12..ba4b8a406 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt @@ -9,26 +9,27 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions class CustomGeometrySourceStateTest { @Test - fun callbacksCopyTileIdsContainFailuresAndStopAfterClosureDuringCallback() { - val received = mutableListOf() - lateinit var state: CustomGeometrySourceState - state = - CustomGeometrySourceState( - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) { - received += tileId - state.close() - throw IllegalStateException("contained") + fun callbacksCopyTileIdsContainFailuresAndStopAfterClosureDuringCallback(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val received = mutableListOf() + lateinit var state: CustomGeometrySourceState + state = + CustomGeometrySourceState( + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) { + received += tileId + state.close() + throw IllegalStateException("contained") + } } - } - ) - ) {} + ) + ) {} - state.fetchTileForTesting(CanonicalTileId(4, 5, 6)) - state.fetchTileForTesting(CanonicalTileId(7, 8, 9)) + state.fetchTileForTesting(CanonicalTileId(4, 5, 6)) + state.fetchTileForTesting(CanonicalTileId(7, 8, 9)) - assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) - assertTrue(state.isClosedForTesting()) - } + assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) + assertTrue(state.isClosedForTesting()) + } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/NativeBufferJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/NativeBufferJvmTest.kt index 63002eee5..b55214039 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/NativeBufferJvmTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/NativeBufferJvmTest.kt @@ -7,23 +7,25 @@ import org.maplibre.nativeffi.error.InvalidArgumentException class NativeBufferJvmTest { @Test - fun capacityValidationUsesTheFfmSegmentLength() { - NativeBuffer.allocate(4L).use { buffer -> - buffer.ensureCapacity(4L) - assertFailsWith { buffer.ensureCapacity(5L) } + fun capacityValidationUsesTheFfmSegmentLength(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeBuffer.allocate(4L).use { buffer -> + buffer.ensureCapacity(4L) + assertFailsWith { buffer.ensureCapacity(5L) } + } } - } @Test - fun closeDuringFfmBorrowDefersReleaseUntilBorrowReturns() { - val buffer = NativeBuffer.allocate(4L) + fun closeDuringFfmBorrowDefersReleaseUntilBorrowReturns(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val buffer = NativeBuffer.allocate(4L) - buffer.borrow { _, length -> - assertEquals(4L, length) - buffer.close() - assertFailsWith { buffer.byteLength() } - } + buffer.borrow { _, length -> + assertEquals(4L, length) + buffer.close() + assertFailsWith { buffer.byteLength() } + } - assertFailsWith { buffer.toByteArray() } - } + assertFailsWith { buffer.toByteArray() } + } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.jvm.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.jvm.kt index 9e308ce27..997be2a4c 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.jvm.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.jvm.kt @@ -24,9 +24,9 @@ private class JvmEglOwnedTextureSession( private val config: Long, private val surface: Long, private val context: Long, - override val session: RenderSessionHandle, + override val attachment: RenderSessionAttachment, ) : OwnedTextureTestSession { - override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionHandle { + override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionAttachment { val descriptor = EglContextDescriptor( NativePointer.ofAddress(display), @@ -37,32 +37,19 @@ private class JvmEglOwnedTextureSession( return session .map() .attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) } - override fun acquireFrame(): OwnedTextureTestFrame { - val handle = session.acquireOpenGLOwnedTextureFrame() - val frame = handle.frame() - return object : OwnedTextureTestFrame { - override val width: Int - get() = frame.width() - - override val height: Int - get() = frame.height() - - override val isClosed: Boolean - get() = handle.isClosed - - override fun close() { - handle.close() - } - } + override fun frameSize(frame: AcquiredFrameHandle): OwnedTextureFrameSize { + val texture = frame.openGLTexture() + return OwnedTextureFrameSize(texture.width(), texture.height()) } override fun close() { try { - session.close() + session.abandonAndClose() } finally { releaseEgl(display, surface, context) } @@ -132,11 +119,12 @@ private fun createEglSession( NativePointer.ofAddress(context), NativePointer.NULL_POINTER, ) - val session = + val attachment = map.attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) - return JvmEglOwnedTextureSession(display, config, surface, context, session) + return JvmEglOwnedTextureSession(display, config, surface, context, attachment) } catch (error: Throwable) { releaseEgl(display, surface, context) throw error diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleJvmTest.kt index ae42b1abf..d101b544e 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleJvmTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/resource/ResourceRequestHandleJvmTest.kt @@ -18,74 +18,76 @@ import org.maplibre.nativeffi.internal.loader.NativeAccess class ResourceRequestHandleJvmTest { @Test - fun unreachableProviderOwnedHandleReleasesNativeRequest() { - val released = CountDownLatch(1) + fun unreachableProviderOwnedHandleReleasesNativeRequest(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val released = CountDownLatch(1) - registerUnreachableProviderOwnedHandle(released) + registerUnreachableProviderOwnedHandle(released) - assertTrue(awaitRelease(released), "expected unreachable request cleanup to release native") - } + assertTrue(awaitRelease(released), "expected unreachable request cleanup to release native") + } @Test - fun completionFailureIsTerminalAndCopiesDiagnosticBeforeReleaseCleanup() { - NativeAccess.ensureLoaded() - val releases = AtomicInteger(0) - val handle = - ResourceRequestHandle( - SyntheticHandles.resourceRequest(), - completer = { _, _ -> MapLibreNativeC.mln_network_status_set(999_999) }, - releaser = { - releases.incrementAndGet() - MapLibreNativeC.mln_runtime_destroy(0L) - }, + fun completionFailureIsTerminalAndCopiesDiagnosticBeforeReleaseCleanup(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeAccess.ensureLoaded() + val releases = AtomicInteger(0) + val handle = + ResourceRequestHandle( + SyntheticHandles.resourceRequest(), + completer = { _, _ -> MapLibreNativeC.mln_network_status_set(999_999) }, + releaser = { releases.incrementAndGet() }, + ) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue, + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue, - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - val failure = - assertFailsWith { + val failure = + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } + assertTrue(failure.diagnostic.contains("network status")) + assertFalse(failure.diagnostic.contains("runtime")) + assertEquals(1, releases.get()) + assertFailsWith { handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - assertTrue(failure.diagnostic.contains("network status")) - assertFalse(failure.diagnostic.contains("runtime")) - assertEquals(1, releases.get()) - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - } @Test - fun concurrentCloseDefersReleaseUntilCompletionAndCancellationUsesInjectedCheck() { - val entered = CountDownLatch(1) - val continueCompletion = CountDownLatch(1) - val releases = AtomicInteger(0) - val handle = - ResourceRequestHandle( - SyntheticHandles.resourceRequest(), - completer = { _, _ -> - entered.countDown() - continueCompletion.await(5, TimeUnit.SECONDS) - MaplibreStatus.OK.nativeCode - }, - cancellationChecker = { true }, - releaser = { releases.incrementAndGet() }, + fun concurrentCloseDefersReleaseUntilCompletionAndCancellationUsesInjectedCheck(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val entered = CountDownLatch(1) + val continueCompletion = CountDownLatch(1) + val releases = AtomicInteger(0) + val handle = + ResourceRequestHandle( + SyntheticHandles.resourceRequest(), + completer = { _, _ -> + entered.countDown() + continueCompletion.await(5, TimeUnit.SECONDS) + MaplibreStatus.OK.nativeCode + }, + cancellationChecker = { true }, + releaser = { releases.incrementAndGet() }, + ) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue, + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue, - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - assertTrue(handle.isCancelled()) - val completion = thread { handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - assertTrue(entered.await(5, TimeUnit.SECONDS)) - handle.close() - assertEquals(0, releases.get()) - continueCompletion.countDown() - completion.join() - assertEquals(1, releases.get()) - assertFailsWith { handle.isCancelled() } - } + assertTrue(handle.isCancelled()) + val completion = thread { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } + assertTrue(entered.await(5, TimeUnit.SECONDS)) + handle.close() + assertEquals(0, releases.get()) + continueCompletion.countDown() + completion.join() + assertEquals(1, releases.get()) + assertFailsWith { handle.isCancelled() } + } private fun registerUnreachableProviderOwnedHandle(released: CountDownLatch) { val handle = diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/LocalFileStyleJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/LocalFileStyleJvmTest.kt index b7bd6999e..6e4f8a0f4 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/LocalFileStyleJvmTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/LocalFileStyleJvmTest.kt @@ -8,38 +8,40 @@ import org.maplibre.nativeffi.map.MapOptions class LocalFileStyleJvmTest { @Test - fun localFileStyleLoadsFromCanonicalEncodedUri() { - val tempDirectory = Files.createTempDirectory("maplibre resources ké地図") - try { - val styleFile = - Files.createDirectories(tempDirectory.resolve("style sheets").resolve("スタイル")) - .resolve("style.json") - Files.writeString(styleFile, STYLE_JSON) + fun localFileStyleLoadsFromCanonicalEncodedUri(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val tempDirectory = Files.createTempDirectory("maplibre resources ké地図") + try { + val styleFile = + Files.createDirectories(tempDirectory.resolve("style sheets").resolve("スタイル")) + .resolve("style.json") + Files.writeString(styleFile, STYLE_JSON) - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - try { - map.setStyleUrl(styleFile.toUri().toASCIIString()) - assertTrue(waitForStyleLoaded(runtime, map)) - } finally { - map.close() + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + try { + map.setStyleUrl(styleFile.toUri().toASCIIString()).await() + assertTrue(waitForStyleLoaded(runtime, map)) + } finally { + map.close() + } } + } finally { + tempDirectory.toFile().deleteRecursively() } - } finally { - tempDirectory.toFile().deleteRecursively() } - } - private fun waitForStyleLoaded(runtime: RuntimeHandle, map: MapHandle): Boolean { + private suspend fun waitForStyleLoaded(runtime: RuntimeHandle, map: MapHandle): Boolean { repeat(10_000) { - runtime.pump(1) + runtime.barrier().await() if ( runtime.drainEvents().events.any { it.type == RuntimeEventType.MAP_STYLE_LOADED && it.mapSource == map diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventJvmTest.kt index f0d904ebf..8d6f5f420 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventJvmTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventJvmTest.kt @@ -10,52 +10,54 @@ import org.maplibre.nativeffi.map.MapOptions class RuntimeEventJvmTest { @Test - fun unknownDomainsAndEventsFromReleasedMapsCopyWithoutResurrectingHandles() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - val mapId = map.nativeHandleId() - map.close() - try { - val orphan = - runtime.copyEventForTesting( - RuntimeEventType.MAP_STYLE_LOADED.nativeValue, - RuntimeEventSourceType.MAP.nativeValue, - mapId, - 0, - RuntimeEventPayload.None, - "", - ) - assertNull(orphan.mapSource) - assertNull(orphan.runtimeSource) - assertEquals(mapId, orphan.sourceId) + fun unknownDomainsAndEventsFromReleasedMapsCopyWithoutResurrectingHandles(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + val mapId = map.nativeHandleId() + map.close() + try { + val orphan = + runtime.copyEventForTesting( + RuntimeEventType.MAP_STYLE_LOADED.nativeValue, + RuntimeEventSourceType.MAP.nativeValue, + mapId, + 0, + RuntimeEventPayload.None, + "", + ) + assertNull(orphan.mapSource) + assertNull(orphan.runtimeSource) + assertEquals(mapId, orphan.sourceId) - val unknown = - runtime.copyEventForTesting( - 900, - 901, - 0x5AL, - 902, - JvmStructs.unknownRuntimePayload(903, byteArrayOf(1, 2, 3)), - "future event", - ) - assertEquals(900, unknown.type.nativeValue) - assertEquals(901, unknown.sourceType.nativeValue) - assertEquals(0x5AL, unknown.sourceId) - assertNull(unknown.mapSource) - assertNull(unknown.runtimeSource) - assertEquals("future event", unknown.message) - val payload = unknown.payload as RuntimeEventPayload.Unknown - assertEquals(903, payload.rawPayloadType) - assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) - } finally { - runtime.close() + val unknown = + runtime.copyEventForTesting( + 900, + 901, + 0x5AL, + 902, + JvmStructs.unknownRuntimePayload(903, byteArrayOf(1, 2, 3)), + "future event", + ) + assertEquals(900, unknown.type.nativeValue) + assertEquals(901, unknown.sourceType.nativeValue) + assertEquals(0x5AL, unknown.sourceId) + assertNull(unknown.mapSource) + assertNull(unknown.runtimeSource) + assertEquals("future event", unknown.message) + val payload = unknown.payload as RuntimeEventPayload.Unknown + assertEquals(903, payload.rawPayloadType) + assertContentEquals(byteArrayOf(1, 2, 3), payload.payloadBytes) + } finally { + runtime.close() + } } - } } diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt index a562f186d..399a7b8de 100644 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt @@ -9,7 +9,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertTrue import org.maplibre.nativeffi.error.InvalidArgumentException -import org.maplibre.nativeffi.error.WrongThreadException import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.internal.c.mln_runtime_event import org.maplibre.nativeffi.internal.c.mln_runtime_event_payload @@ -25,223 +24,240 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions class RuntimeEventsTest { @Test - fun oneDrainAfterStyleLoadReturnsEveryQueuedEvent() { - withMap { runtime, map -> - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - // Nothing drains while the style parses, so one drain reports the whole run. - repeat(20) { runtime.pump(1) } + fun oneDrainAfterStyleLoadReturnsEveryQueuedEvent(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + // Nothing drains while the style parses, so one drain reports the whole run. + repeat(20) { runtime.barrier().await() } - val batch = runtime.drainEvents() - assertEquals(0L, batch.remainingCount) - assertTrue(batch.events.size > 1, "expected several events, got ${batch.events}") - assertTrue(batch.events.any { it.type == RuntimeEventType.MAP_STYLE_LOADED }) - assertTrue(batch.events.all { it.mapSource == map }) + val batch = runtime.drainEvents() + assertTrue(batch.events.size > 1, "expected several events, got ${batch.events}") + assertTrue(batch.events.any { it.type == RuntimeEventType.MAP_STYLE_LOADED }) + assertTrue(batch.events.all { it.mapSource == map }) + } } - } @Test - fun narrowedMapMaskDropsOneTypeAndKeepsAnother() { - withMap { runtime, map -> - // One style load produces both types, so both are driven after this write. - map.eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED - map.setStyleJson(STYLE_JSON.encodeToByteArray()) + fun narrowedMapMaskDropsOneTypeAndKeepsAnother(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + // One style load produces both types, so both are driven after this write. + map.setEventMask(RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED).await() + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() - val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } - assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") + val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } + assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") + } } - } @Test - fun creationMaskNarrowsTheMapBeforeItsFirstStyleLoad() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val map = - MapHandle.create( - runtime, - mapOptions().apply { - eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED - }, - ) - try { - assertEquals(RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED, map.eventMask) - map.setStyleJson(STYLE_JSON.encodeToByteArray()) + fun creationMaskNarrowsTheMapBeforeItsFirstStyleLoad(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val map = + MapHandle.create( + runtime, + mapOptions().apply { + eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED + }, + ) + .await() + try { + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_LOADING_STARTED, + map.eventMask, + ) + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() - val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } - assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") - } finally { - map.close() + val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } + assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") + } finally { + map.close() + } } } - } @Test - fun bothHandlesReportEveryTypeUntilNarrowedAndKeepUnrelatedBitsOnAWrite() { - withMap { runtime, map -> - // Neither options struct names a mask, so both default fields select every type. - assertEquals(RuntimeEventMask.ALL, runtime.eventMask) - assertEquals(RuntimeEventMask.ALL, map.eventMask) + fun bothHandlesReportEveryTypeUntilNarrowedAndKeepUnrelatedBitsOnAWrite(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + // The runtime reports its global queue mask; the map reports its map-originated subset. + assertEquals(RuntimeEventMask.ALL, runtime.eventMask) + assertEquals(RuntimeEventMask.ALL_MAP_EVENTS, map.eventMask) - runtime.eventMask = RuntimeEventMask.ALL - map.eventMask = RuntimeEventMask.ALL - assertEquals(RuntimeEventMask.ALL, runtime.eventMask) - assertEquals(RuntimeEventMask.ALL, map.eventMask) + runtime.eventMask = RuntimeEventMask.ALL + map.setEventMask(RuntimeEventMask.ALL).await() + runtime.barrier().await() + assertEquals(RuntimeEventMask.ALL, runtime.eventMask) + assertEquals(RuntimeEventMask.ALL_MAP_EVENTS, map.eventMask) - // Read, clear one bit, write back: every other bit survives. - map.eventMask = map.eventMask - RuntimeEventMask.MAP_TILE_ACTION - assertEquals(RuntimeEventMask.ALL - RuntimeEventMask.MAP_TILE_ACTION, map.eventMask) - assertTrue(RuntimeEventType.MAP_STYLE_LOADED in map.eventMask) - - runtime.eventMask = runtime.eventMask - RuntimeEventMask.OFFLINE_OPERATION_COMPLETED - assertEquals( - RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_OPERATION_COMPLETED, - runtime.eventMask, - ) - } - } + // Read, clear one bit, write back: every other bit survives. + map.setEventMask(map.eventMask - RuntimeEventMask.MAP_TILE_ACTION).await() + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_TILE_ACTION, + map.eventMask, + ) + assertTrue(RuntimeEventType.MAP_STYLE_LOADED in map.eventMask) - @Test - fun maskBitOutsideEveryKnownTypeFailsEverySetterAndBothCreations() { - // The bit sits above the low 32, so a mask this binding narrowed on the way - // out would reach native as a value it accepts. - val unknownBit = RuntimeEventMask.ALL + RuntimeEventMask(1L shl 63) - assertFailsWith { - RuntimeHandle.create(RuntimeOptions().apply { eventMask = unknownBit }) - } - withMap { runtime, map -> - assertFailsWith { runtime.eventMask = unknownBit } - assertFailsWith { map.eventMask = unknownBit } - assertFailsWith { - MapHandle.create(runtime, mapOptions().apply { eventMask = unknownBit }) + runtime.eventMask = runtime.eventMask - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED + assertEquals( + RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED, + runtime.eventMask, + ) } } - } @Test - fun boundedDrainReportsWhatStaysQueuedAndTheNextDrainReachesZero() { - withMap { runtime, map -> - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - repeat(20) { runtime.pump(1) } - - val bounded = runtime.drainEvents(maxEvents = 1) - assertEquals(1, bounded.events.size) - assertTrue(bounded.remainingCount > 0, "a bounded drain reported nothing left") - - val rest = runtime.drainEvents() - assertEquals(0L, rest.remainingCount) - assertEquals(bounded.remainingCount, rest.events.size.toLong()) - assertFailsWith { runtime.drainEvents(maxEvents = -1) } - } - } + fun maskBitOutsideEveryKnownTypeFailsEverySetterAndBothCreations(): Unit = + org.maplibre.nativeffi.runtime + .runSuspendTest { // The bit sits above the low 32, so a mask this binding narrowed on the + // way + // out would reach native as a value it accepts. + val unknownBit = RuntimeEventMask.ALL + RuntimeEventMask(1L shl 63) + assertFailsWith { + runSuspendTest { RuntimeHandle.create(RuntimeOptions().apply { eventMask = unknownBit }) } + } + withMap { runtime, map -> + assertFailsWith { runtime.eventMask = unknownBit } + assertFailsWith { map.setEventMask(unknownBit).await() } + assertFailsWith { + runSuspendTest { + MapHandle.create(runtime, mapOptions().apply { eventMask = unknownBit }).await() + } + } + } + } @Test - fun unknownPayloadTypeKeepsItsRawValueAndCopiesTheWholeWindow() { - Arena.ofConfined().use { arena -> - val window = mln_runtime_event.sizeof() - mln_runtime_event.`payload$offset`() - val payload = arena.allocate(window) - payload.set(ValueLayout.JAVA_BYTE, 0, 1) - payload.set(ValueLayout.JAVA_BYTE, 1, 2) - payload.set(ValueLayout.JAVA_BYTE, 2, 3) + fun unknownPayloadTypeKeepsItsRawValueAndCopiesTheWholeWindow(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + Arena.ofConfined().use { arena -> + val window = mln_runtime_event.sizeof() - mln_runtime_event.`payload$offset`() + val payload = arena.allocate(window) + payload.set(ValueLayout.JAVA_BYTE, 0, 1) + payload.set(ValueLayout.JAVA_BYTE, 1, 2) + payload.set(ValueLayout.JAVA_BYTE, 2, 3) - val decoded = NativeAccess.runtimeEventPayloadForTesting(999, payload) + val decoded = NativeAccess.runtimeEventPayloadForTesting(999, payload) - val unknown = assertIs(decoded) - assertEquals(999, unknown.rawPayloadType) - assertEquals(mln_runtime_event_payload.sizeof().toInt(), unknown.payloadBytes.size) - assertContentEquals(byteArrayOf(1, 2, 3), unknown.payloadBytes.take(3).toByteArray()) + val unknown = assertIs(decoded) + assertEquals(999, unknown.rawPayloadType) + assertEquals(mln_runtime_event_payload.sizeof().toInt(), unknown.payloadBytes.size) + assertContentEquals(byteArrayOf(1, 2, 3), unknown.payloadBytes.take(3).toByteArray()) - // The copy survives a write through the source and a write into a copy. - payload.set(ValueLayout.JAVA_BYTE, 0, 9) - unknown.payloadBytes[1] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), unknown.payloadBytes.take(3).toByteArray()) + // The copy survives a write through the source and a write into a copy. + payload.set(ValueLayout.JAVA_BYTE, 0, 9) + unknown.payloadBytes[1] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), unknown.payloadBytes.take(3).toByteArray()) + } } - } @Test - fun styleReplacementReleasesADroppedSourceWithNoStyleLoadedEvent() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != SERVED_STYLE_URL) { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() - } - ) - ResourceProviderDecision.HANDLE - } - ) - val map = - MapHandle.create( + fun styleReplacementReleasesADroppedSourceWithNoStyleLoadedEvent(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + assertCommandCommitted( runtime, - mapOptions().apply { - eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED - }, - ) - try { - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - map.addCustomGeometrySource("custom", customGeometrySourceOptions()) - assertEquals(1, map.customGeometrySourceCountForTesting()) - // The binding adds no subscription of its own, so the mask reads back as written. - assertEquals( - RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED, - map.eventMask, - "the host's own mask changed", + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != SERVED_STYLE_URL) { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) + ResourceProviderDecision.HANDLE + } + ) + .await(), ) + val map = + MapHandle.create( + runtime, + mapOptions().apply { + eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED + }, + ) + .await() + try { + assertCommandCommitted(runtime, map.setStyleJson(STYLE_JSON.encodeToByteArray()).await()) + assertCommandCommitted( + runtime, + map.addCustomGeometrySource("custom", customGeometrySourceOptions()).await(), + ) + assertEquals(1, map.customGeometrySourceCountForTesting()) + // The binding adds no subscription of its own, so the mask reads back as written. + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_STYLE_LOADED, + map.eventMask, + "the host's own mask changed", + ) - // A URL load drops the source in the pump that completes it, and native - // reports that through the release callback rather than through an event. - map.setStyleUrl(SERVED_STYLE_URL) - val types = drainUntil(runtime) { map.customGeometrySourceCountForTesting() == 0 } - assertTrue( - RuntimeEventType.MAP_STYLE_LOADED !in types, - "a cleared style-loaded event reached the host", - ) - } finally { - map.close() + // A URL load drops the source when the asynchronous load completes, and native + // reports that through the release callback rather than through an event. + assertCommandCommitted(runtime, map.setStyleUrl(SERVED_STYLE_URL).await()) + val types = drainUntil(runtime) { map.customGeometrySourceCountForTesting() == 0 } + assertTrue( + RuntimeEventType.MAP_STYLE_LOADED !in types, + "a cleared style-loaded event reached the host", + ) + } finally { + map.close() + } } } - } @Test - fun inlineStyleLoadAndRemovalReleaseTheirSourcesBeforeReturning() { - withMap { _, map -> - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - map.addCustomGeometrySource("removed", customGeometrySourceOptions()) - map.addCustomGeometrySource("dropped", customGeometrySourceOptions()) - assertEquals(2, map.customGeometrySourceCountForTesting()) + fun inlineStyleCommandsReleaseTheirSourcesAfterTheBarrier(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + map.addCustomGeometrySource("removed", customGeometrySourceOptions()).await() + map.addCustomGeometrySource("dropped", customGeometrySourceOptions()).await() + runtime.barrier().await() + assertEquals(2, map.customGeometrySourceCountForTesting()) - assertTrue(map.removeStyleSource("removed")) - assertEquals(1, map.customGeometrySourceCountForTesting()) + map.removeStyleSource("removed").await() + runtime.barrier().await() + assertEquals(1, map.customGeometrySourceCountForTesting()) - // An inline load replaces the style before it returns, so the source it - // dropped is released by then too. - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - assertEquals(0, map.customGeometrySourceCountForTesting()) + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(0, map.customGeometrySourceCountForTesting()) - // Closing the map releases the source it still owns, before the callbacks - // it closes could be called again. - map.addCustomGeometrySource("surviving", customGeometrySourceOptions()) - map.close() - assertEquals(0, map.customGeometrySourceCountForTesting()) + // Closing the map quiesces and releases the source it still owns. + map.addCustomGeometrySource("surviving", customGeometrySourceOptions()).await() + runtime.barrier().await() + map.close() + runtime.barrier().await() + assertEquals(0, map.customGeometrySourceCountForTesting()) + } } - } @Test - fun drainAndBothMaskSettersReportTheWrongThread() { - withMap { runtime, map -> - val failures = mutableListOf() - val thread = Thread { - failures += assertFailsWith { runtime.drainEvents() } - failures += - assertFailsWith { runtime.eventMask = RuntimeEventMask.ALL } - failures += assertFailsWith { map.eventMask = RuntimeEventMask.ALL } + fun drainAndBothMaskSettersAreAnyThread(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + val failures = mutableListOf() + val thread = Thread { + try { + runtime.drainEvents() + runtime.eventMask = RuntimeEventMask.ALL + kotlinx.coroutines.runBlocking { map.setEventMask(RuntimeEventMask.ALL).await() } + } catch (error: Throwable) { + failures += error + } + } + thread.start() + thread.join() + assertTrue(failures.isEmpty(), "any-thread APIs failed: $failures") } - thread.start() - thread.join() - assertEquals(3, failures.size) } - } private fun customGeometrySourceOptions(): CustomGeometrySourceOptions = CustomGeometrySourceOptions( @@ -256,9 +272,9 @@ class RuntimeEventsTest { height = 64 } - private fun withMap(body: (RuntimeHandle, MapHandle) -> Unit) { + private suspend fun withMap(body: suspend (RuntimeHandle, MapHandle) -> Unit) { RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val map = MapHandle.create(runtime, mapOptions()) + val map = MapHandle.create(runtime, mapOptions()).await() try { body(runtime, map) } finally { @@ -267,14 +283,21 @@ class RuntimeEventsTest { } } - /** Pumps and drains until [done] holds for the event types seen so far. */ - private fun drainUntil( + private fun assertCommandCommitted( + @Suppress("UNUSED_PARAMETER") runtime: RuntimeHandle, + completion: CommandCompletion, + ) { + assertEquals(CommandDisposition.COMMITTED, completion.disposition) + } + + /** Drains until [done] holds for the event types seen so far. */ + private suspend fun drainUntil( runtime: RuntimeHandle, done: (Set) -> Boolean, ): Set { val types = mutableSetOf() repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() types += runtime.drainEvents().events.map { it.type } if (done(types)) return types Thread.sleep(1) diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorJvmTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorJvmTest.kt new file mode 100644 index 000000000..5632940ff --- /dev/null +++ b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeExecutorJvmTest.kt @@ -0,0 +1,28 @@ +package org.maplibre.nativeffi.runtime + +import java.util.concurrent.CompletableFuture +import kotlin.test.Test +import kotlin.test.assertTrue +import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.map.MapOptions + +class RuntimeExecutorJvmTest { + @Test + fun runtimeAndMapProgressAcrossCoroutineResumptionAndHostThreads(): Unit = runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + val map = + MapHandle.create( + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, + ) + .await() + val generation = CompletableFuture.supplyAsync { map.snapshot().generation }.get() + runtime.barrier().await() + assertTrue(map.queryCamera().await().generation >= generation) + map.close() + runtime.close() + } +} diff --git a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt b/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt deleted file mode 100644 index 23a057b2b..000000000 --- a/bindings/kotlin/src/jvmTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.test.Test -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue -import kotlin.time.TimeSource -import org.maplibre.nativeffi.error.InvalidStateException -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapOptions - -class WakeSourceTest { - // Pumps until the runtime is idle, so a later park is released only by the test's signal. - private fun quiesce(runtime: RuntimeHandle) { - repeat(100) { - runtime.pump(0) - if (runtime.drainEvents().events.isEmpty()) { - return - } - } - error("the runtime kept producing events while idle") - } - - @Test - fun parkedOwnerThreadWakesForNativeWorkAndForAWakeSource() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 512 - height = 512 - }, - ) - - quiesce(runtime) - - // Native reports the load failure from its own threads; it must reach the parked - // owner thread. - map.setStyleUrl("unsupported://style.json") - var loadingFailed = false - val loadStarted = TimeSource.Monotonic.markNow() - repeat(20) { - if (!loadingFailed) { - runtime.pump(10_000) - assertTrue( - loadStarted.elapsedNow().inWholeMilliseconds < 5_000, - "parks sat out their timeouts while the style load was pending", - ) - if (runtime.drainEvents().events.any { it.type == RuntimeEventType.MAP_LOADING_FAILED }) { - loadingFailed = true - } - } - } - assertTrue(loadingFailed) - - // Nothing else can end this park, so only the cross-thread signal releases it. - val source = runtime.acquireWakeSource() - quiesce(runtime) - val signaller = Thread { - Thread.sleep(20) - source.signal() - } - signaller.start() - val parkStarted = TimeSource.Monotonic.markNow() - runtime.pump(10_000) - assertTrue( - parkStarted.elapsedNow().inWholeMilliseconds < 5_000, - "the parked owner thread timed out instead of taking the signal", - ) - signaller.join() - - // A wake source stays usable after its runtime closes, in either teardown order. - map.close() - runtime.close() - source.signal() - source.close() - assertTrue(source.isClosed) - assertFailsWith { source.signal() } - } - - @Test - fun pumpClearsTheWakeFlagItReturnsOn() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - runtime.acquireWakeSource().use { source -> - quiesce(runtime) - - source.signal() - val signalledStarted = TimeSource.Monotonic.markNow() - runtime.pump(10_000) - assertTrue( - signalledStarted.elapsedNow().inWholeMilliseconds < 5_000, - "a pump waited even though the wake flag was set", - ) - - // The pump above cleared the wake flag, so this one waits its full timeout. - val idleStarted = TimeSource.Monotonic.markNow() - runtime.pump(200) - assertTrue( - idleStarted.elapsedNow().inWholeMilliseconds >= 100, - "the first pump left the wake flag set", - ) - } - } - } -} diff --git a/bindings/kotlin/src/linuxTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.linux.kt b/bindings/kotlin/src/linuxTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.linux.kt index 11bb79496..81f0cd277 100644 --- a/bindings/kotlin/src/linuxTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.linux.kt +++ b/bindings/kotlin/src/linuxTest/kotlin/org/maplibre/nativeffi/render/OwnedTextureTestSupport.linux.kt @@ -58,9 +58,9 @@ private class LinuxEglOwnedTextureSession( private val config: COpaquePointer, private val surface: COpaquePointer, private val context: COpaquePointer, - override val session: RenderSessionHandle, + override val attachment: RenderSessionAttachment, ) : OwnedTextureTestSession { - override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionHandle { + override fun attachAnotherOwnedTexture(width: Int, height: Int): RenderSessionAttachment { val descriptor = EglContextDescriptor( NativePointer.ofAddress(display.toLong()), @@ -71,32 +71,19 @@ private class LinuxEglOwnedTextureSession( return session .map() .attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) } - override fun acquireFrame(): OwnedTextureTestFrame { - val handle = session.acquireOpenGLOwnedTextureFrame() - val frame = handle.frame() - return object : OwnedTextureTestFrame { - override val width: Int - get() = frame.width() - - override val height: Int - get() = frame.height() - - override val isClosed: Boolean - get() = handle.isClosed - - override fun close() { - handle.close() - } - } + override fun frameSize(frame: AcquiredFrameHandle): OwnedTextureFrameSize { + val texture = frame.openGLTexture() + return OwnedTextureFrameSize(texture.width(), texture.height()) } override fun close() { try { - session.close() + session.abandonAndClose() } finally { releaseEgl(display, surface, context) } @@ -129,11 +116,12 @@ private fun createEglSession(map: MapHandle, width: Int, height: Int): OwnedText NativePointer.ofAddress(liveContext.toLong()), NativePointer.NULL_POINTER, ) - val session = + val attachment = map.attachOpenGLOwnedTexture( - OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor) + OpenGLOwnedTextureDescriptor(RenderTargetExtent(width, height, 1.0), descriptor), + OWNED_TEXTURE_ATTACH_OPTIONS, ) - return LinuxEglOwnedTextureSession(display, config, liveSurface, liveContext, session) + return LinuxEglOwnedTextureSession(display, config, liveSurface, liveContext, attachment) } catch (error: Throwable) { releaseEgl(display, surface, context) throw error diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt new file mode 100644 index 000000000..3e9eb3da6 --- /dev/null +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/async/CompletionBridge.kt @@ -0,0 +1,125 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package org.maplibre.nativeffi.internal.async + +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.alloc +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.readBytes +import kotlinx.cinterop.sizeOf +import kotlinx.cinterop.staticCFunction +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.error.MaplibreException +import org.maplibre.nativeffi.error.MaplibreStatus +import org.maplibre.nativeffi.internal.c.mln_completion +import org.maplibre.nativeffi.internal.c.mln_completion_result +import org.maplibre.nativeffi.internal.status.Status +import org.maplibre.nativeffi.runtime.CommandCompletion +import org.maplibre.nativeffi.runtime.CommandDisposition + +@OptIn(ExperimentalForeignApi::class) +private interface CompletionState { + fun complete(result: CPointer) +} + +internal object CompletionBridge { + + private class State( + private val convert: (CPointer) -> T, + private val acceptErrorStatus: Boolean, + ) : CompletionState { + val deferred = CompletableDeferred() + + override fun complete(result: CPointer) { + val raw = result.pointed + try { + val status = raw.status + if (status == MaplibreStatus.OK.nativeCode || acceptErrorStatus) { + deferred.complete(convert(result)) + } else { + val message = + raw.diagnostic.data?.readBytes(raw.diagnostic.size.toInt())?.decodeToString().orEmpty() + deferred.completeExceptionally( + MaplibreException.forStatus(MaplibreStatus.fromNative(status), status, message) + ) + } + } catch (failure: Throwable) { + deferred.completeExceptionally(failure) + } + } + } + + fun submit( + convert: (CPointer) -> T, + call: (CPointer) -> Int, + ): Deferred = submitInternal(convert, false, false, call) + + private fun submitInternal( + convert: (CPointer) -> T, + rejectSynchronously: Boolean, + acceptErrorStatus: Boolean, + call: (CPointer) -> Int, + ): Deferred { + val state = State(convert, acceptErrorStatus) + val reference = StableRef.create(state) + var handedOff = false + try { + memScoped { + val completion = alloc() + completion.size = sizeOf().toUInt() + completion.callback = staticCFunction(::completeNative) + completion.user_data = reference.asCPointer() + completion.release_user_data = staticCFunction(::releaseNative) + val status = call(completion.ptr) + handedOff = status == MaplibreStatus.OK.nativeCode + Status.check(status) + } + } catch (failure: Throwable) { + state.deferred.completeExceptionally(failure) + if (!handedOff) reference.dispose() + if (rejectSynchronously) throw failure + } + return state.deferred + } + + fun unit(call: (CPointer) -> Int): Deferred = submit({ _ -> }, call) + + fun unitChecked(call: (CPointer) -> Int): Deferred = + submitInternal({ _ -> }, true, false, call) + + fun command(call: (CPointer) -> Int): Deferred = + submitInternal( + { result -> + CommandCompletion( + CommandDisposition.fromNative(result.pointed.disposition.toInt()), + result.pointed.generation, + MaplibreStatus.fromNative(result.pointed.status), + result.pointed.diagnostic.data + ?.readBytes(result.pointed.diagnostic.size.toInt()) + ?.decodeToString() + .orEmpty(), + ) + }, + false, + true, + call, + ) +} + +@OptIn(ExperimentalForeignApi::class) +private fun completeNative(userData: COpaquePointer?, result: CPointer?) { + if (userData != null && result != null) + userData.asStableRef().get().complete(result) +} + +@OptIn(ExperimentalForeignApi::class) +private fun releaseNative(userData: COpaquePointer?) { + userData?.asStableRef()?.dispose() +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt index be294ac4b..2c73a5854 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/HttpHeaderTransformState.kt @@ -32,6 +32,7 @@ internal class HttpHeaderTransformState(private val callback: HttpHeaderTransfor descriptor.size = kotlinx.cinterop.sizeOf().toUInt() descriptor.callback = staticCFunction(::httpHeaderTransformCallback) descriptor.user_data = selfRef.asCPointer() + descriptor.release_user_data = staticCFunction(::releaseHttpHeaderTransform) } fun descriptor(): CPointer = descriptor.ptr @@ -96,3 +97,8 @@ private fun httpHeaderTransformCallback( ): Int = userData?.asStableRef()?.get()?.invoke(kind, url, response) ?: MaplibreStatus.INVALID_ARGUMENT.nativeCode + +@OptIn(ExperimentalForeignApi::class) +private fun releaseHttpHeaderTransform(userData: COpaquePointer?) { + userData?.asStableRef()?.get()?.close() +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt index 55a815d95..e05e05b2b 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/LogCallbackState.kt @@ -4,10 +4,13 @@ import kotlinx.cinterop.ByteVar import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.asStableRef import kotlinx.cinterop.staticCFunction import org.maplibre.nativeffi.internal.c.mln_log_clear_callback import org.maplibre.nativeffi.internal.c.mln_log_set_callback import org.maplibre.nativeffi.internal.memory.MemoryUtil +import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.log.LogCallback import org.maplibre.nativeffi.log.LogEvent import org.maplibre.nativeffi.log.LogRecord @@ -15,9 +18,9 @@ import org.maplibre.nativeffi.log.LogSeverity /** Owns process-global logging callback state. */ @OptIn(ExperimentalForeignApi::class) -internal class LogCallbackState private constructor(private val callback: LogCallback) : - AutoCloseable { - private val gate = CallbackGate("log callbacks") +internal class LogCallbackState(private val callback: LogCallback) : AutoCloseable { + private val selfRef = StableRef.create(this) + private val gate = CallbackGate("log callbacks") { selfRef.dispose() } fun invoke(severity: UInt, event: UInt, code: Long, message: CPointer?): UInt { val lease = gate.enter() ?: return 0U @@ -39,44 +42,33 @@ internal class LogCallbackState private constructor(private val callback: LogCal override fun close() = gate.close() - internal fun checkCanClose() = gate.checkCanClose() - internal fun isClosedForTesting(): Boolean = gate.isClosedForTesting() - internal fun isClosingForTesting(): Boolean = gate.isClosingForTesting() - internal companion object { - private val registry = LogCallbackRegistry() - fun set(callback: LogCallback) { - registry.current()?.checkCanClose() - registry.set(LogCallbackState(callback)) { - mln_log_set_callback(staticCFunction(::logCallback), null) + val replacement = LogCallbackState(callback) + try { + Status.check( + mln_log_set_callback( + staticCFunction(::logCallback), + replacement.selfRef.asCPointer(), + staticCFunction(::releaseLogCallback), + ) + ) + } catch (error: Throwable) { + replacement.close() + throw error } } - fun setForTesting( - callback: LogCallback, - install: () -> Int, - captureReplacement: (LogCallbackState) -> Unit, - ) { - registry.current()?.checkCanClose() - registry.set(LogCallbackState(callback).also(captureReplacement), install) - } - fun clear() { - registry.current()?.checkCanClose() - registry.clear(::mln_log_clear_callback) + Status.check(mln_log_clear_callback()) } - fun currentForTesting(): LogCallbackState? = registry.current() - - fun invokeCurrent(severity: UInt, event: UInt, code: Long, message: CPointer?): UInt = - registry.current()?.invoke(severity, event, code, message) ?: 0U + fun createForTesting(callback: LogCallback): LogCallbackState = LogCallbackState(callback) } } -@Suppress("UNUSED_PARAMETER") @OptIn(ExperimentalForeignApi::class) private fun logCallback( userData: COpaquePointer?, @@ -84,4 +76,10 @@ private fun logCallback( event: UInt, code: Long, message: CPointer?, -): UInt = LogCallbackState.invokeCurrent(severity, event, code, message) +): UInt = + userData?.asStableRef()?.get()?.invoke(severity, event, code, message) ?: 0U + +@OptIn(ExperimentalForeignApi::class) +private fun releaseLogCallback(userData: COpaquePointer?) { + userData?.asStableRef()?.get()?.close() +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt index ad3a6b1e3..277e65cdb 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceProviderState.kt @@ -28,6 +28,7 @@ internal class ResourceProviderState(private val callback: ResourceProviderCallb descriptor.size = kotlinx.cinterop.sizeOf().toUInt() descriptor.callback = staticCFunction(::resourceProviderCallback) descriptor.user_data = selfRef.asCPointer() + descriptor.release_user_data = staticCFunction(::releaseResourceProvider) } fun descriptor(): CPointer = descriptor.ptr @@ -75,3 +76,8 @@ private fun resourceProviderCallback( ): UInt = userData?.asStableRef()?.get()?.invoke(request, rawHandle) ?: UInt.MAX_VALUE + +@OptIn(ExperimentalForeignApi::class) +private fun releaseResourceProvider(userData: COpaquePointer?) { + userData?.asStableRef()?.get()?.close() +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt index 4de106ca6..048795183 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/callback/ResourceTransformState.kt @@ -33,6 +33,7 @@ internal class ResourceTransformState(private val callback: ResourceTransformCal descriptor.size = kotlinx.cinterop.sizeOf().toUInt() descriptor.callback = staticCFunction(::resourceTransformCallback) descriptor.user_data = selfRef.asCPointer() + descriptor.release_user_data = staticCFunction(::releaseResourceTransform) } fun descriptor(): CPointer = descriptor.ptr @@ -99,3 +100,8 @@ private fun resourceTransformCallback( ): Int = userData?.asStableRef()?.get()?.invoke(kind, url, outResponse) ?: MaplibreStatus.INVALID_ARGUMENT.nativeCode + +@OptIn(ExperimentalForeignApi::class) +private fun releaseResourceTransform(userData: COpaquePointer?) { + userData?.asStableRef()?.get()?.close() +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleState.kt index 8be16b3c0..871296403 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleState.kt @@ -23,7 +23,7 @@ internal class HandleState( return handle ?: throw org.maplibre.nativeffi.internal.status.Status.released(typeName) } - /** Runs [block] with the live handle and release held off. See [HandleStateCore.withLive]. */ + /** Runs [block] after checking that this wrapper still owns the handle. */ fun withLive(block: (T) -> R): R = core.withLive { val live = handle ?: throw org.maplibre.nativeffi.internal.status.Status.released(typeName) block(live) @@ -33,8 +33,19 @@ internal class HandleState( fun handleId(): Long = core.handleId() - fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - core.retainChild(childTypeName) + fun beginClose(): Boolean = core.beginClose() + + fun handleForClose(): T = + handle ?: throw org.maplibre.nativeffi.internal.status.Status.released(typeName) + + fun abortClose() = core.abortClose() + + fun completeClose(afterSuccess: () -> Unit = {}) { + core.completeClose { + handle = null + afterSuccess() + } + } fun closeOnce(destroy: (T) -> Int) { closeOnce(destroy) {} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandleValues.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandleValues.kt index 25082ed7b..52a40d86c 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandleValues.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/lifecycle/NativeHandleValues.kt @@ -33,8 +33,6 @@ internal fun styleStringListHandle(value: ULong): NativeStyleStringList = internal fun queriedFeatureListHandle(value: ULong): NativeQueriedFeatureList = NativeQueriedFeatureList(value.toLong()) -internal fun wakeSourceHandle(value: ULong): NativeWakeSource = NativeWakeSource(value.toLong()) - internal fun resourceRequestHandle(value: ULong): NativeResourceRequest = NativeResourceRequest(value.toLong()) diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/MapStructs.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/MapStructs.kt index f4aec1fa7..d2b2733a1 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/MapStructs.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/MapStructs.kt @@ -67,6 +67,7 @@ import org.maplibre.nativeffi.internal.c.mln_projection_mode import org.maplibre.nativeffi.internal.c.mln_projection_mode_default import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.map.ConstrainMode +import org.maplibre.nativeffi.map.DebugOption import org.maplibre.nativeffi.map.NorthOrientation import org.maplibre.nativeffi.map.ProjectionModeOptions import org.maplibre.nativeffi.map.TileLodMode @@ -77,6 +78,14 @@ import org.maplibre.nativeffi.map.ViewportOptions /** Materializes map and camera descriptors at the C boundary. */ @OptIn(ExperimentalForeignApi::class) internal object MapStructs { + fun debugOptions(mask: UInt): Set = + DebugOption.entries.filterTo(mutableSetOf()) { option -> + (mask and option.nativeMask.toUInt()) != 0u + } + + fun debugOptionMask(options: Set): UInt = + options.fold(0u) { mask, option -> mask or option.nativeMask.toUInt() } + fun animationOptions(value: AnimationOptions, scope: MemScope): CPointer { val native = scope.alloc() mln_animation_options_default().place(native.ptr) diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructs.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructs.kt index 1f70ba88f..e38e0798b 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructs.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructs.kt @@ -137,6 +137,13 @@ internal object QueryStructs { destroyer(list) } + fun queriedFeatures(values: CPointer?, count: ULong): List { + val size = checkedInt(count, "queried feature count") + if (size == 0) return emptyList() + requireNotNull(values) { "native completion omitted its queried features" } + return List(size) { index -> queriedFeature(values[index]) } + } + fun featureStateSelector( value: FeatureStateSelector, scope: MemScope, diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeStructs.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeStructs.kt index 5a7614ada..b4ea964f1 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeStructs.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeStructs.kt @@ -20,7 +20,6 @@ import org.maplibre.nativeffi.internal.c.MLN_OFFLINE_REGION_DEFINITION_GEOMETRY import org.maplibre.nativeffi.internal.c.MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_NONE -import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT @@ -37,7 +36,6 @@ import org.maplibre.nativeffi.internal.c.mln_offline_region_snapshot_get import org.maplibre.nativeffi.internal.c.mln_offline_region_status import org.maplibre.nativeffi.internal.c.mln_runtime_event import org.maplibre.nativeffi.internal.c.mln_runtime_event_camera_transition_finished -import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_operation_completed import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_response_error import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_status import org.maplibre.nativeffi.internal.c.mln_runtime_event_offline_region_tile_count_limit @@ -57,8 +55,6 @@ import org.maplibre.nativeffi.offline.OfflineRegionInfo import org.maplibre.nativeffi.offline.OfflineRegionStatus import org.maplibre.nativeffi.render.RenderMode import org.maplibre.nativeffi.resource.ResourceErrorReason -import org.maplibre.nativeffi.runtime.OfflineOperationKind -import org.maplibre.nativeffi.runtime.OfflineOperationResultKind import org.maplibre.nativeffi.runtime.RuntimeEventPayload /** Copies runtime event payloads out of native event storage. */ @@ -89,8 +85,6 @@ internal object RuntimeStructs { offlineRegionResponseError(event.payload.offline_region_response_error) MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT -> offlineRegionTileCountLimit(event.payload.offline_region_tile_count_limit) - MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED -> - offlineOperationCompleted(event.payload.offline_operation_completed) MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED -> cameraTransitionFinished(event.payload.camera_transition_finished) else -> unknownPayload(event, eventSize) @@ -167,17 +161,6 @@ internal object RuntimeStructs { checkedLong(value.limit, "offline tile count limit"), ) - private fun offlineOperationCompleted( - value: mln_runtime_event_offline_operation_completed - ): RuntimeEventPayload.OfflineOperationCompleted = - RuntimeEventPayload.OfflineOperationCompleted( - uint64BitsToLong(value.operation_id), - OfflineOperationKind.fromNative(value.operation_kind), - OfflineOperationResultKind.fromNative(value.result_kind), - value.result_status, - value.found, - ) - private fun cameraTransitionFinished( value: mln_runtime_event_camera_transition_finished ): RuntimeEventPayload.CameraTransitionFinished = diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt index 256f01969..2b8a199e6 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceState.kt @@ -28,8 +28,8 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions /** * Owns map/style-scoped custom geometry source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ @OptIn(ExperimentalForeignApi::class) internal class CustomGeometrySourceState( @@ -62,7 +62,7 @@ internal class CustomGeometrySourceState( } } - /** Runs when native stops referencing this state, on the map owner thread. */ + /** Runs when native stops referencing this state. */ internal fun released() { onReleased() } diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt index e5e0c3492..fd31ff07d 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/CustomMvtVectorSourceState.kt @@ -23,8 +23,8 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions /** * Owns map/style-scoped custom MVT vector source callback state. * - * [onReleased] runs on the map owner thread when native stops referencing this state, which is what - * drops it from its map's registry and closes it. + * Native invokes [onReleased] after it stops referencing this state, which drops it from its map's + * registry and closes it. */ @OptIn(ExperimentalForeignApi::class) internal class CustomMvtVectorSourceState( @@ -57,7 +57,7 @@ internal class CustomMvtVectorSourceState( } } - /** Runs when native stops referencing this state, on the map owner thread. */ + /** Runs after native stops referencing this state. */ internal fun released() { onReleased() } diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt index 2ac7414d7..fcca2891b 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapHandle.kt @@ -1,14 +1,11 @@ package org.maplibre.nativeffi.map -import kotlinx.cinterop.BooleanVar import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.CValue -import kotlinx.cinterop.DoubleVar import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.MemScope import kotlinx.cinterop.UByteVar -import kotlinx.cinterop.UIntVar import kotlinx.cinterop.ULongVar import kotlinx.cinterop.alloc import kotlinx.cinterop.allocArray @@ -17,27 +14,33 @@ import kotlinx.cinterop.memScoped import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr import kotlinx.cinterop.readBytes +import kotlinx.cinterop.readValue import kotlinx.cinterop.reinterpret import kotlinx.cinterop.sizeOf import kotlinx.cinterop.value -import org.maplibre.nativeffi.camera.AnimationOptions +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.camera.BoundOptions -import org.maplibre.nativeffi.camera.CameraFitOptions -import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraSnapshot +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.camera.FreeCameraOptions import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.LatLngBounds -import org.maplibre.nativeffi.geo.ScreenPoint -import org.maplibre.nativeffi.internal.c.MLN_STYLE_SOURCE_INFO_TILEJSON -import org.maplibre.nativeffi.internal.c.MLN_STYLE_SOURCE_INFO_URL -import org.maplibre.nativeffi.internal.c.mln_bound_options_default +import org.maplibre.nativeffi.internal.async.CompletionBridge +import org.maplibre.nativeffi.internal.async.mapDeferred +import org.maplibre.nativeffi.internal.c.MLN_STYLE_LAYER_INFO_SOURCE_ID +import org.maplibre.nativeffi.internal.c.MLN_STYLE_LAYER_INFO_SOURCE_LAYER import org.maplibre.nativeffi.internal.c.mln_buffer_view +import org.maplibre.nativeffi.internal.c.mln_camera_delta_default import org.maplibre.nativeffi.internal.c.mln_camera_options_default -import org.maplibre.nativeffi.internal.c.mln_free_camera_options_default +import org.maplibre.nativeffi.internal.c.mln_camera_query_result +import org.maplibre.nativeffi.internal.c.mln_camera_update_default +import org.maplibre.nativeffi.internal.c.mln_completion +import org.maplibre.nativeffi.internal.c.mln_completion_result import org.maplibre.nativeffi.internal.c.mln_image_stretch import org.maplibre.nativeffi.internal.c.mln_lat_lng -import org.maplibre.nativeffi.internal.c.mln_lat_lng_bounds +import org.maplibre.nativeffi.internal.c.mln_logical_extent import org.maplibre.nativeffi.internal.c.mln_map_add_color_relief_layer import org.maplibre.nativeffi.internal.c.mln_map_add_custom_geometry_source import org.maplibre.nativeffi.internal.c.mln_map_add_custom_mvt_vector_source @@ -55,80 +58,41 @@ import org.maplibre.nativeffi.internal.c.mln_map_add_style_layer_json import org.maplibre.nativeffi.internal.c.mln_map_add_style_source_json import org.maplibre.nativeffi.internal.c.mln_map_add_vector_source_tiles import org.maplibre.nativeffi.internal.c.mln_map_add_vector_source_url -import org.maplibre.nativeffi.internal.c.mln_map_camera_for_geometry -import org.maplibre.nativeffi.internal.c.mln_map_camera_for_lat_lng_bounds -import org.maplibre.nativeffi.internal.c.mln_map_camera_for_lat_lngs -import org.maplibre.nativeffi.internal.c.mln_map_cancel_transitions +import org.maplibre.nativeffi.internal.c.mln_map_apply_camera_delta +import org.maplibre.nativeffi.internal.c.mln_map_camera_query +import org.maplibre.nativeffi.internal.c.mln_map_camera_snapshot_get import org.maplibre.nativeffi.internal.c.mln_map_copy_layer_source_id import org.maplibre.nativeffi.internal.c.mln_map_copy_layer_source_layer -import org.maplibre.nativeffi.internal.c.mln_map_copy_loaded_style_json -import org.maplibre.nativeffi.internal.c.mln_map_copy_style_image_premultiplied_rgba8 import org.maplibre.nativeffi.internal.c.mln_map_copy_style_image_stretches -import org.maplibre.nativeffi.internal.c.mln_map_copy_style_source_attribution -import org.maplibre.nativeffi.internal.c.mln_map_copy_style_source_url -import org.maplibre.nativeffi.internal.c.mln_map_copy_style_url import org.maplibre.nativeffi.internal.c.mln_map_create -import org.maplibre.nativeffi.internal.c.mln_map_destroy -import org.maplibre.nativeffi.internal.c.mln_map_dump_debug_logs -import org.maplibre.nativeffi.internal.c.mln_map_ease_to -import org.maplibre.nativeffi.internal.c.mln_map_fly_to -import org.maplibre.nativeffi.internal.c.mln_map_get_bounds -import org.maplibre.nativeffi.internal.c.mln_map_get_camera -import org.maplibre.nativeffi.internal.c.mln_map_get_debug_options -import org.maplibre.nativeffi.internal.c.mln_map_get_event_mask import org.maplibre.nativeffi.internal.c.mln_map_get_feature_state -import org.maplibre.nativeffi.internal.c.mln_map_get_free_camera_options import org.maplibre.nativeffi.internal.c.mln_map_get_image_source_coordinates import org.maplibre.nativeffi.internal.c.mln_map_get_layer_filter -import org.maplibre.nativeffi.internal.c.mln_map_get_layer_max_zoom -import org.maplibre.nativeffi.internal.c.mln_map_get_layer_min_zoom import org.maplibre.nativeffi.internal.c.mln_map_get_layer_property -import org.maplibre.nativeffi.internal.c.mln_map_get_layer_visibility -import org.maplibre.nativeffi.internal.c.mln_map_get_projection_mode -import org.maplibre.nativeffi.internal.c.mln_map_get_rendering_stats_view_enabled -import org.maplibre.nativeffi.internal.c.mln_map_get_size import org.maplibre.nativeffi.internal.c.mln_map_get_style_image_info +import org.maplibre.nativeffi.internal.c.mln_map_get_style_layer_info import org.maplibre.nativeffi.internal.c.mln_map_get_style_layer_json -import org.maplibre.nativeffi.internal.c.mln_map_get_style_layer_type import org.maplibre.nativeffi.internal.c.mln_map_get_style_light_property import org.maplibre.nativeffi.internal.c.mln_map_get_style_source_info -import org.maplibre.nativeffi.internal.c.mln_map_get_style_source_tile_urls -import org.maplibre.nativeffi.internal.c.mln_map_get_style_source_type import org.maplibre.nativeffi.internal.c.mln_map_get_style_transition_options -import org.maplibre.nativeffi.internal.c.mln_map_get_tile_options -import org.maplibre.nativeffi.internal.c.mln_map_get_viewport_options import org.maplibre.nativeffi.internal.c.mln_map_invalidate_custom_geometry_source_region import org.maplibre.nativeffi.internal.c.mln_map_invalidate_custom_geometry_source_tile import org.maplibre.nativeffi.internal.c.mln_map_invalidate_custom_mvt_vector_source_tile -import org.maplibre.nativeffi.internal.c.mln_map_is_fully_loaded -import org.maplibre.nativeffi.internal.c.mln_map_is_gesture_in_progress -import org.maplibre.nativeffi.internal.c.mln_map_jump_to -import org.maplibre.nativeffi.internal.c.mln_map_lat_lng_bounds_for_camera -import org.maplibre.nativeffi.internal.c.mln_map_lat_lng_bounds_for_camera_unwrapped -import org.maplibre.nativeffi.internal.c.mln_map_lat_lng_for_pixel -import org.maplibre.nativeffi.internal.c.mln_map_lat_lngs_for_pixels import org.maplibre.nativeffi.internal.c.mln_map_list_style_layer_ids import org.maplibre.nativeffi.internal.c.mln_map_list_style_source_ids -import org.maplibre.nativeffi.internal.c.mln_map_move_by -import org.maplibre.nativeffi.internal.c.mln_map_move_by_animated +import org.maplibre.nativeffi.internal.c.mln_map_loaded_style_json import org.maplibre.nativeffi.internal.c.mln_map_move_style_layer import org.maplibre.nativeffi.internal.c.mln_map_options import org.maplibre.nativeffi.internal.c.mln_map_options_default -import org.maplibre.nativeffi.internal.c.mln_map_pitch_by -import org.maplibre.nativeffi.internal.c.mln_map_pitch_by_animated -import org.maplibre.nativeffi.internal.c.mln_map_pixel_for_lat_lng -import org.maplibre.nativeffi.internal.c.mln_map_pixels_for_lat_lngs import org.maplibre.nativeffi.internal.c.mln_map_projection_create +import org.maplibre.nativeffi.internal.c.mln_map_release import org.maplibre.nativeffi.internal.c.mln_map_remove_feature_state import org.maplibre.nativeffi.internal.c.mln_map_remove_style_image import org.maplibre.nativeffi.internal.c.mln_map_remove_style_layer import org.maplibre.nativeffi.internal.c.mln_map_remove_style_source import org.maplibre.nativeffi.internal.c.mln_map_request_repaint import org.maplibre.nativeffi.internal.c.mln_map_request_still_image -import org.maplibre.nativeffi.internal.c.mln_map_rotate_by -import org.maplibre.nativeffi.internal.c.mln_map_rotate_by_animated -import org.maplibre.nativeffi.internal.c.mln_map_scale_by -import org.maplibre.nativeffi.internal.c.mln_map_scale_by_animated +import org.maplibre.nativeffi.internal.c.mln_map_resize import org.maplibre.nativeffi.internal.c.mln_map_set_bounds import org.maplibre.nativeffi.internal.c.mln_map_set_custom_geometry_source_tile_data import org.maplibre.nativeffi.internal.c.mln_map_set_custom_mvt_vector_source_tile_data @@ -140,7 +104,6 @@ import org.maplibre.nativeffi.internal.c.mln_map_set_free_camera_options import org.maplibre.nativeffi.internal.c.mln_map_set_geojson_source_data import org.maplibre.nativeffi.internal.c.mln_map_set_geojson_source_synchronous_tiling import org.maplibre.nativeffi.internal.c.mln_map_set_geojson_source_url -import org.maplibre.nativeffi.internal.c.mln_map_set_gesture_in_progress import org.maplibre.nativeffi.internal.c.mln_map_set_image_source_coordinates import org.maplibre.nativeffi.internal.c.mln_map_set_image_source_image import org.maplibre.nativeffi.internal.c.mln_map_set_image_source_url @@ -155,7 +118,6 @@ import org.maplibre.nativeffi.internal.c.mln_map_set_location_indicator_accuracy import org.maplibre.nativeffi.internal.c.mln_map_set_location_indicator_bearing import org.maplibre.nativeffi.internal.c.mln_map_set_location_indicator_image_name import org.maplibre.nativeffi.internal.c.mln_map_set_location_indicator_location -import org.maplibre.nativeffi.internal.c.mln_map_set_projection_mode import org.maplibre.nativeffi.internal.c.mln_map_set_rendering_stats_view_enabled import org.maplibre.nativeffi.internal.c.mln_map_set_style_image import org.maplibre.nativeffi.internal.c.mln_map_set_style_json @@ -165,26 +127,20 @@ import org.maplibre.nativeffi.internal.c.mln_map_set_style_transition_options import org.maplibre.nativeffi.internal.c.mln_map_set_style_url import org.maplibre.nativeffi.internal.c.mln_map_set_tile_options import org.maplibre.nativeffi.internal.c.mln_map_set_viewport_options -import org.maplibre.nativeffi.internal.c.mln_map_style_image_exists -import org.maplibre.nativeffi.internal.c.mln_map_style_layer_exists -import org.maplibre.nativeffi.internal.c.mln_map_style_source_exists -import org.maplibre.nativeffi.internal.c.mln_map_tile_options_default -import org.maplibre.nativeffi.internal.c.mln_map_viewport_options_default -import org.maplibre.nativeffi.internal.c.mln_projection_mode_default -import org.maplibre.nativeffi.internal.c.mln_screen_point -import org.maplibre.nativeffi.internal.c.mln_style_image_info_default -import org.maplibre.nativeffi.internal.c.mln_style_source_info -import org.maplibre.nativeffi.internal.c.mln_style_transition_options_default +import org.maplibre.nativeffi.internal.c.mln_map_snapshot +import org.maplibre.nativeffi.internal.c.mln_map_snapshot_get +import org.maplibre.nativeffi.internal.c.mln_map_style_url +import org.maplibre.nativeffi.internal.c.mln_map_update_camera +import org.maplibre.nativeffi.internal.c.mln_style_image_result +import org.maplibre.nativeffi.internal.c.mln_style_image_stretches_result +import org.maplibre.nativeffi.internal.c.mln_style_layer_result +import org.maplibre.nativeffi.internal.c.mln_style_source_result import org.maplibre.nativeffi.internal.lifecycle.HandleState -import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.lifecycle.NativeMap import org.maplibre.nativeffi.internal.lifecycle.asHandle import org.maplibre.nativeffi.internal.lifecycle.mapHandle import org.maplibre.nativeffi.internal.lifecycle.mapProjectionHandle -import org.maplibre.nativeffi.internal.lifecycle.ownedBufferHandle import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue -import org.maplibre.nativeffi.internal.lifecycle.styleIdListHandle -import org.maplibre.nativeffi.internal.lifecycle.styleStringListHandle import org.maplibre.nativeffi.internal.memory.MemoryUtil import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.internal.struct.ByteStructs @@ -200,10 +156,13 @@ import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLOwnedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor import org.maplibre.nativeffi.render.PremultipliedRgba8Image +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor import org.maplibre.nativeffi.render.VulkanOwnedTextureDescriptor import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor +import org.maplibre.nativeffi.runtime.CommandCompletion import org.maplibre.nativeffi.runtime.RuntimeEventMask import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @@ -211,9 +170,9 @@ import org.maplibre.nativeffi.style.CustomMvtVectorSourceOptions import org.maplibre.nativeffi.style.GeoJsonSourceDataHandle import org.maplibre.nativeffi.style.GeoJsonSourceOptions import org.maplibre.nativeffi.style.ImageStretch +import org.maplibre.nativeffi.style.LayerInfo import org.maplibre.nativeffi.style.LocationIndicatorImageKind import org.maplibre.nativeffi.style.SourceInfo -import org.maplibre.nativeffi.style.SourceType import org.maplibre.nativeffi.style.StyleImage import org.maplibre.nativeffi.style.StyleImageInfo import org.maplibre.nativeffi.style.StyleImageOptions @@ -221,170 +180,185 @@ import org.maplibre.nativeffi.style.StyleLayerVisibility import org.maplibre.nativeffi.style.StyleTransitionOptions import org.maplibre.nativeffi.style.TileSourceOptions -/** Owned native map handle. Close it on the map owner thread. */ +/** Owned any-thread native map handle. */ @OptIn(ExperimentalForeignApi::class) public actual class MapHandle -private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : AutoCloseable { - private val runtimeRetention = runtime.retainChild("MapHandle") +private constructor( + private val runtime: RuntimeHandle, + handle: NativeMap, + cachedEventMask: RuntimeEventMask, +) { private val state = HandleState("MapHandle", handle, runtime) + private var cachedEventMask = + RuntimeEventMask(cachedEventMask.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) private val customGeometrySources = CustomGeometrySourceRegistry { it.close() } private val customMvtVectorSources = CustomGeometrySourceRegistry { it.close() } - public actual var eventMask: RuntimeEventMask - get() = memScoped { - val outMask = alloc() - Status.check(mln_map_get_event_mask(state.requireLive().rawHandleValue, outMask.ptr)) - RuntimeEventMask(outMask.value.toLong()) - } - set(value) { - Status.check( - mln_map_set_event_mask(state.requireLive().rawHandleValue, value.nativeValue.toULong()) - ) - } + public actual val eventMask: RuntimeEventMask + get() = cachedEventMask + + public actual fun setEventMask(value: RuntimeEventMask): Deferred = + command { completion -> + Status.check( + mln_map_set_event_mask( + state.requireLive().rawHandleValue, + value.nativeValue.toULong(), + completion, + ) + ) + } + .mapDeferred { + cachedEventMask = + RuntimeEventMask(value.nativeValue and RuntimeEventMask.ALL_MAP_EVENTS.nativeValue) + it + } - public actual fun setStyleUrl(url: String) { + public actual fun setStyleUrl(url: String): Deferred = command { completion -> MemoryUtil.requireValidCString(url) - Status.check(mln_map_set_style_url(state.requireLive().rawHandleValue, url)) + Status.check(mln_map_set_style_url(state.requireLive().rawHandleValue, url, completion)) } - public actual fun setStyleJson(json: ByteArray) { - memScoped { - Status.check( - mln_map_set_style_json( - state.requireLive().rawHandleValue, - ByteStructs.bufferView(json, this), + public actual fun setStyleJson(json: ByteArray): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_set_style_json( + state.requireLive().rawHandleValue, + ByteStructs.bufferView(json, this), + completion, + ) ) - ) + } } - } - public actual fun setFeatureState(selector: FeatureStateSelector, value: ByteArray) { + public actual fun setFeatureState( + selector: FeatureStateSelector, + value: ByteArray, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_feature_state( state.requireLive().rawHandleValue, QueryStructs.featureStateSelector(selector, this), ByteStructs.bufferView(value, this), + completion, ) ) } } - public actual fun getFeatureState(selector: FeatureStateSelector): ByteArray = memScoped { - val outState = alloc() - outState.value = 0uL - Status.check( - mln_map_get_feature_state( - state.requireLive().rawHandleValue, - QueryStructs.featureStateSelector(selector, this), - outState.ptr, + public actual fun getFeatureState(selector: FeatureStateSelector): Deferred = + memScoped { + CompletionBridge.submit( + { result -> checkNotNull(bufferCompletion(result)) }, + { completion -> + mln_map_get_feature_state( + state.requireLive().rawHandleValue, + QueryStructs.featureStateSelector(selector, this), + completion, + ) + }, ) - ) - ByteStructs.ownedBuffer(outState.value.asHandle("mln_buffer", ::ownedBufferHandle)) - } + } - public actual fun removeFeatureState(selector: FeatureStateSelector) { + public actual fun removeFeatureState( + selector: FeatureStateSelector + ): Deferred = command { completion -> memScoped { Status.check( mln_map_remove_feature_state( state.requireLive().rawHandleValue, QueryStructs.featureStateSelector(selector, this), + completion, ) ) } } - public actual fun loadedStyleJson(): ByteArray = copyMapBytes { handle, text, capacity, outSize -> - mln_map_copy_loaded_style_json(handle, text, capacity, outSize) - } + public actual fun loadedStyleJson(): Deferred = + CompletionBridge.submit( + { result -> checkNotNull(bufferCompletion(result)) }, + { completion -> mln_map_loaded_style_json(state.requireLive().rawHandleValue, completion) }, + ) - public actual fun styleUrl(): String = copyMapText { handle, text, capacity, outSize -> - mln_map_copy_style_url(handle, text, capacity, outSize) - } + public actual fun styleUrl(): Deferred = + CompletionBridge.submit( + { result -> checkNotNull(bufferCompletion(result)).decodeToString() }, + { completion -> mln_map_style_url(state.requireLive().rawHandleValue, completion) }, + ) - public actual fun addStyleSourceJson(sourceId: String, sourceJson: ByteArray) { + public actual fun addStyleSourceJson( + sourceId: String, + sourceJson: ByteArray, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_style_source_json( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), ByteStructs.bufferView(sourceJson, this), + completion, ) ) } } - public actual fun removeStyleSource(sourceId: String): Boolean = memScoped { - val outRemoved = alloc() - Status.check( - mln_map_remove_style_source( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outRemoved.ptr, - ) - ) - outRemoved.value - } - - public actual fun styleSourceExists(sourceId: String): Boolean = memScoped { - val outExists = alloc() - Status.check( - mln_map_style_source_exists( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outExists.ptr, - ) - ) - outExists.value - } + public actual fun removeStyleSource(sourceId: String): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_remove_style_source( + state.requireLive().rawHandleValue, + CoreStructs.stringView(sourceId, this), + completion, + ) + ) + } + } - public actual fun styleSourceType(sourceId: String): SourceType? = memScoped { - val outType = alloc() - val outFound = alloc() - Status.check( - mln_map_get_style_source_type( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outType.ptr, - outFound.ptr, - ) + public actual fun styleSourceInfo(sourceId: String): Deferred = memScoped { + CompletionBridge.submit( + { result -> + if (result.pointed.value_count == 0uL) null + else { + val value = result.pointed.value!!.reinterpret().pointed + val tileUrls = + List(value.tile_url_count.toInt()) { index -> + CoreStructs.stringView(value.tile_urls!![index]) + } + StyleStructs.sourceInfo( + value.info, + if (value.info.has_attribution) CoreStructs.stringView(value.attribution) else null, + CoreStructs.stringView(value.url), + tileUrls, + ) + } + }, + { completion -> + mln_map_get_style_source_info( + state.requireLive().rawHandleValue, + CoreStructs.stringView(sourceId, this), + completion, + ) + }, ) - if (outFound.value) SourceType.fromNative(outType.value) else null } - public actual fun styleSourceInfo(sourceId: String): SourceInfo? = memScoped { - val outInfo = alloc() - outInfo.size = sizeOf().toUInt() - val outFound = alloc() - Status.check( - mln_map_get_style_source_info( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outInfo.ptr, - outFound.ptr, - ) + public actual fun styleSourceIds(): Deferred> = + CompletionBridge.submit( + ::stringViewsCompletion, + { completion -> + mln_map_list_style_source_ids(state.requireLive().rawHandleValue, completion) + }, ) - if (!outFound.value) return@memScoped null - val attribution = copyStyleSourceAttribution(sourceId, outInfo) - val url = copyStyleSourceUrl(sourceId, outInfo) - val tileUrls = copyStyleSourceTileUrls(sourceId, outInfo) - StyleStructs.sourceInfo(outInfo, attribution, url, tileUrls) - } - - public actual fun styleSourceIds(): List = memScoped { - val outList = alloc() - outList.value = 0uL - Status.check(mln_map_list_style_source_ids(state.requireLive().rawHandleValue, outList.ptr)) - StyleStructs.styleIdList(outList.value.asHandle("mln_map_list_style_ids", ::styleIdListHandle)) - } public actual fun addGeoJsonSourceUrl( sourceId: String, url: String, options: GeoJsonSourceOptions?, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_geojson_source_url( @@ -392,12 +366,16 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), CoreStructs.stringView(url, this), StyleStructs.geoJsonSourceOptions(options, this), + completion, ) ) } } - public actual fun addGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun addGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { completion -> data.withNativeHandle { nativeData -> memScoped { Status.check( @@ -405,25 +383,33 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), nativeData.rawHandleValue, + completion, ) ) } } } - public actual fun setGeoJsonSourceUrl(sourceId: String, url: String) { + public actual fun setGeoJsonSourceUrl( + sourceId: String, + url: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_geojson_source_url( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), CoreStructs.stringView(url, this), + completion, ) ) } } - public actual fun setGeoJsonSourceData(sourceId: String, data: GeoJsonSourceDataHandle) { + public actual fun setGeoJsonSourceData( + sourceId: String, + data: GeoJsonSourceDataHandle, + ): Deferred = command { completion -> data.withNativeHandle { nativeData -> memScoped { Status.check( @@ -431,19 +417,24 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), nativeData.rawHandleValue, + completion, ) ) } } } - public actual fun setGeoJsonSourceSynchronousTiling(sourceId: String, enabled: Boolean) { + public actual fun setGeoJsonSourceSynchronousTiling( + sourceId: String, + enabled: Boolean, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_geojson_source_synchronous_tiling( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), enabled, + completion, ) ) } @@ -452,29 +443,31 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut public actual fun addCustomGeometrySource( sourceId: String, options: CustomGeometrySourceOptions, - ) { - // The release callback captures the registry rather than this map, so a map a - // host leaks with a live source still reports as leaked. - val registry = customGeometrySources - val sourceState = CustomGeometrySourceState(options) { registry.remove(sourceId) } - registry.install(sourceId, sourceState) { - memScoped { - Status.check( - mln_map_add_custom_geometry_source( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - sourceState.descriptor(), + ): Deferred = + command { completion + -> // The release callback captures the registry rather than this map, so a map a + // host leaks with a live source still reports as leaked. + val registry = customGeometrySources + val sourceState = CustomGeometrySourceState(options) { registry.remove(sourceId) } + registry.install(sourceId, sourceState) { + memScoped { + Status.check( + mln_map_add_custom_geometry_source( + state.requireLive().rawHandleValue, + CoreStructs.stringView(sourceId, this), + sourceState.descriptor(), + completion, + ) ) - ) + } } } - } public actual fun setCustomGeometrySourceTileData( sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_custom_geometry_source_tile_data( @@ -482,30 +475,39 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), StyleStructs.canonicalTileId(tileId), ByteStructs.bufferView(data, this), + completion, ) ) } } - public actual fun invalidateCustomGeometrySourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomGeometrySourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_invalidate_custom_geometry_source_tile( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), StyleStructs.canonicalTileId(tileId), + completion, ) ) } } - public actual fun invalidateCustomGeometrySourceRegion(sourceId: String, bounds: LatLngBounds) { + public actual fun invalidateCustomGeometrySourceRegion( + sourceId: String, + bounds: LatLngBounds, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_invalidate_custom_geometry_source_region( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), CoreStructs.latLngBounds(bounds), + completion, ) ) } @@ -514,7 +516,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut public actual fun addCustomMvtVectorSource( sourceId: String, options: CustomMvtVectorSourceOptions, - ) { + ): Deferred = command { completion -> val registry = customMvtVectorSources val sourceState = CustomMvtVectorSourceState(options) { registry.remove(sourceId) } registry.install(sourceId, sourceState) { @@ -524,6 +526,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), sourceState.descriptor(), + completion, ) ) } @@ -534,7 +537,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, tileId: CanonicalTileId, data: ByteArray, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_custom_mvt_vector_source_tile_data( @@ -542,6 +545,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), StyleStructs.canonicalTileId(tileId), ByteStructs.bufferView(data, this), + completion, ) ) } @@ -551,7 +555,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, tileId: CanonicalTileId, message: String, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_custom_mvt_vector_source_tile_error( @@ -559,18 +563,23 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), StyleStructs.canonicalTileId(tileId), CoreStructs.stringView(message, this), + completion, ) ) } } - public actual fun invalidateCustomMvtVectorSourceTile(sourceId: String, tileId: CanonicalTileId) { + public actual fun invalidateCustomMvtVectorSourceTile( + sourceId: String, + tileId: CanonicalTileId, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_invalidate_custom_mvt_vector_source_tile( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), StyleStructs.canonicalTileId(tileId), + completion, ) ) } @@ -578,7 +587,11 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut internal fun customGeometrySourceCountForTesting(): Int = customGeometrySources.size - public actual fun addVectorSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addVectorSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_vector_source_url( @@ -586,6 +599,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), CoreStructs.stringView(url, this), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } @@ -595,7 +609,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> val tileSnapshot = tiles.toList() memScoped { Status.check( @@ -605,12 +619,17 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut StyleStructs.stringViewArray(tileSnapshot, this), tileSnapshot.size.toULong(), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } } - public actual fun addRasterSourceUrl(sourceId: String, url: String, options: TileSourceOptions?) { + public actual fun addRasterSourceUrl( + sourceId: String, + url: String, + options: TileSourceOptions?, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_raster_source_url( @@ -618,6 +637,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), CoreStructs.stringView(url, this), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } @@ -627,7 +647,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> val tileSnapshot = tiles.toList() memScoped { Status.check( @@ -637,6 +657,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut StyleStructs.stringViewArray(tileSnapshot, this), tileSnapshot.size.toULong(), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } @@ -646,7 +667,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, url: String, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_raster_dem_source_url( @@ -654,6 +675,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), CoreStructs.stringView(url, this), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } @@ -663,7 +685,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, tiles: List, options: TileSourceOptions?, - ) { + ): Deferred = command { completion -> val tileSnapshot = tiles.toList() memScoped { Status.check( @@ -673,6 +695,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut StyleStructs.stringViewArray(tileSnapshot, this), tileSnapshot.size.toULong(), StyleStructs.tileSourceOptions(options, this), + completion, ) ) } @@ -682,7 +705,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut imageId: String, image: PremultipliedRgba8Image, options: StyleImageOptions, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_style_image( @@ -690,126 +713,104 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(imageId, this), StyleStructs.premultipliedRgba8Image(image, this), StyleStructs.styleImageOptions(options, this), + completion, ) ) } } - public actual fun removeStyleImage(imageId: String): Boolean = memScoped { - val outRemoved = alloc() - Status.check( - mln_map_remove_style_image( - state.requireLive().rawHandleValue, - CoreStructs.stringView(imageId, this), - outRemoved.ptr, - ) - ) - outRemoved.value - } - - public actual fun styleImageExists(imageId: String): Boolean = memScoped { - val outExists = alloc() - Status.check( - mln_map_style_image_exists( - state.requireLive().rawHandleValue, - CoreStructs.stringView(imageId, this), - outExists.ptr, - ) - ) - outExists.value - } + public actual fun removeStyleImage(imageId: String): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_remove_style_image( + state.requireLive().rawHandleValue, + CoreStructs.stringView(imageId, this), + completion, + ) + ) + } + } - public actual fun styleImageInfo(imageId: String): StyleImageInfo? = memScoped { - val outInfo = mln_style_image_info_default().getPointer(this) - val outFound = alloc() - Status.check( - mln_map_get_style_image_info( - state.requireLive().rawHandleValue, - CoreStructs.stringView(imageId, this), - outInfo, - outFound.ptr, - ) + public actual fun styleImageInfo(imageId: String): Deferred = memScoped { + CompletionBridge.submit( + { result -> + if (result.pointed.value_count == 0uL) null + else + StyleStructs.styleImageInfo( + result.pointed.value!!.reinterpret().pointed.info + ) + }, + { completion -> + mln_map_get_style_image_info( + state.requireLive().rawHandleValue, + CoreStructs.stringView(imageId, this), + completion, + ) + }, ) - if (outFound.value) StyleStructs.styleImageInfo(outInfo.pointed) else null } public actual fun styleImageStretches( imageId: String - ): Pair, List>? = memScoped { - val handle = state.requireLive().rawHandleValue - val outXCount = alloc() - val outYCount = alloc() - val outFound = alloc() - Status.check( - mln_map_copy_style_image_stretches( - handle, - CoreStructs.stringView(imageId, this), - null, - 0UL, - outXCount.ptr, - null, - 0UL, - outYCount.ptr, - outFound.ptr, - ) - ) - if (!outFound.value) return@memScoped null - - val xCount = checkedInt(outXCount.value, "style image stretch x count") - val yCount = checkedInt(outYCount.value, "style image stretch y count") - val rawX = if (xCount == 0) null else allocArray(xCount) - val rawY = if (yCount == 0) null else allocArray(yCount) - Status.check( - mln_map_copy_style_image_stretches( - handle, - CoreStructs.stringView(imageId, this), - rawX, - xCount.toULong(), - outXCount.ptr, - rawY, - yCount.toULong(), - outYCount.ptr, - outFound.ptr, - ) + ): Deferred, List>?> = memScoped { + CompletionBridge.submit( + { result -> + if (result.pointed.value_count == 0uL) null + else { + val value = result.pointed.value!!.reinterpret().pointed + val copy = { values: CPointer?, count: ULong -> + List(count.toInt()) { index -> ImageStretch(values!![index].from, values[index].to) } + } + copy(value.stretch_x, value.stretch_x_count) to + copy(value.stretch_y, value.stretch_y_count) + } + }, + { completion -> + mln_map_copy_style_image_stretches( + state.requireLive().rawHandleValue, + CoreStructs.stringView(imageId, this), + completion, + ) + }, ) - val toList = { array: CPointer?, count: Int -> - List(count) { index -> ImageStretch(array!![index].from, array[index].to) } - } - toList(rawX, xCount) to toList(rawY, yCount) } - public actual fun copyStyleImagePremultipliedRgba8(imageId: String): StyleImage? = memScoped { - val info = styleImageInfo(imageId) ?: return@memScoped null - val outPixels = - allocArray(checkedInt(info.byteLength.toULong(), "style image byte length")) - val outByteLength = alloc() - val outFound = alloc() - Status.check( - mln_map_copy_style_image_premultiplied_rgba8( - state.requireLive().rawHandleValue, - CoreStructs.stringView(imageId, this), - outPixels, - info.byteLength.toULong(), - outByteLength.ptr, - outFound.ptr, + public actual fun copyStyleImagePremultipliedRgba8(imageId: String): Deferred = + memScoped { + CompletionBridge.submit( + { result -> + if (result.pointed.value_count == 0uL) null + else { + val value = result.pointed.value!!.reinterpret().pointed + val info = StyleStructs.styleImageInfo(value.info) + StyleImage( + PremultipliedRgba8Image( + info.width, + info.height, + info.stride, + ByteStructs.copyBufferView(value.pixels), + ), + info.pixelRatio, + info.sdf, + ) + } + }, + { completion -> + mln_map_get_style_image_info( + state.requireLive().rawHandleValue, + CoreStructs.stringView(imageId, this), + completion, + ) + }, ) - ) - if (!outFound.value) return@memScoped null - StyleImage( - PremultipliedRgba8Image( - info.width, - info.height, - info.stride, - outPixels - .reinterpret() - .readBytes(checkedInt(outByteLength.value, "style image copied byte length")), - ), - info.pixelRatio, - info.sdf, - ) - } + } - public actual fun addImageSourceUrl(sourceId: String, coordinates: List, url: String) { + public actual fun addImageSourceUrl( + sourceId: String, + coordinates: List, + url: String, + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() memScoped { Status.check( @@ -819,6 +820,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.latLngArray(coordinateSnapshot, this), coordinateSnapshot.size.toULong(), CoreStructs.stringView(url, this), + completion, ) ) } @@ -828,7 +830,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut sourceId: String, coordinates: List, image: PremultipliedRgba8Image, - ) { + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() memScoped { Status.check( @@ -838,36 +840,46 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.latLngArray(coordinateSnapshot, this), coordinateSnapshot.size.toULong(), StyleStructs.premultipliedRgba8Image(image, this), + completion, ) ) } } - public actual fun setImageSourceUrl(sourceId: String, url: String) { - memScoped { - Status.check( - mln_map_set_image_source_url( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - CoreStructs.stringView(url, this), + public actual fun setImageSourceUrl(sourceId: String, url: String): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_set_image_source_url( + state.requireLive().rawHandleValue, + CoreStructs.stringView(sourceId, this), + CoreStructs.stringView(url, this), + completion, + ) ) - ) + } } - } - public actual fun setImageSourceImage(sourceId: String, image: PremultipliedRgba8Image) { + public actual fun setImageSourceImage( + sourceId: String, + image: PremultipliedRgba8Image, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_image_source_image( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), StyleStructs.premultipliedRgba8Image(image, this), + completion, ) ) } } - public actual fun setImageSourceCoordinates(sourceId: String, coordinates: List) { + public actual fun setImageSourceCoordinates( + sourceId: String, + coordinates: List, + ): Deferred = command { completion -> val coordinateSnapshot = coordinates.toList() memScoped { Status.check( @@ -876,121 +888,51 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(sourceId, this), CoreStructs.latLngArray(coordinateSnapshot, this), coordinateSnapshot.size.toULong(), + completion, ) ) } } - public actual fun imageSourceCoordinates(sourceId: String): List? = memScoped { - val outCoordinates = allocArray(4) - val outCoordinateCount = alloc() - val outFound = alloc() - Status.check( - mln_map_get_image_source_coordinates( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outCoordinates, - 4UL, - outCoordinateCount.ptr, - outFound.ptr, - ) - ) - if (outFound.value) - CoreStructs.latLngArray( - outCoordinates, - checkedInt(outCoordinateCount.value, "image source coordinate count"), - ) - else null - } - - private fun copyStyleSourceAttribution(sourceId: String, info: mln_style_source_info): String? { - if (!info.has_attribution) return null - if (info.attribution_size == 0UL) return "" - return memScoped { - val outAttribution = - allocArray(checkedInt(info.attribution_size, "style source attribution size")) - val outAttributionSize = alloc() - val outFound = alloc() - Status.check( - mln_map_copy_style_source_attribution( + public actual fun imageSourceCoordinates(sourceId: String): Deferred?> = memScoped { + CompletionBridge.submit( + { result -> + val count = result.pointed.value_count + if (count == 0uL) null + else + CoreStructs.latLngArray(result.pointed.value!!.reinterpret(), count.toInt()) + }, + { completion -> + mln_map_get_image_source_coordinates( state.requireLive().rawHandleValue, CoreStructs.stringView(sourceId, this), - outAttribution, - info.attribution_size, - outAttributionSize.ptr, - outFound.ptr, + completion, ) - ) - if (outFound.value) - outAttribution - .readBytes(checkedInt(outAttributionSize.value, "style source copied attribution size")) - .decodeToString() - else null - } - } - - private fun copyStyleSourceUrl(sourceId: String, info: mln_style_source_info): String? { - if (info.fields and MLN_STYLE_SOURCE_INFO_URL == 0u) return null - if (info.url_size == 0UL) return "" - return memScoped { - val outUrl = allocArray(checkedInt(info.url_size, "style source URL size")) - val outUrlSize = alloc() - val outFound = alloc() - Status.check( - mln_map_copy_style_source_url( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outUrl, - info.url_size, - outUrlSize.ptr, - outFound.ptr, - ) - ) - if (outFound.value) - outUrl - .readBytes(checkedInt(outUrlSize.value, "style source copied URL size")) - .decodeToString() - else null - } - } - - private fun copyStyleSourceTileUrls( - sourceId: String, - info: mln_style_source_info, - ): List? { - if (info.fields and MLN_STYLE_SOURCE_INFO_TILEJSON == 0u) return null - return memScoped { - val outList = alloc() - outList.value = 0uL - val outFound = alloc() - Status.check( - mln_map_get_style_source_tile_urls( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - outList.ptr, - outFound.ptr, - ) - ) - check(outFound.value) { "style source disappeared while its metadata was copied" } - StyleStructs.styleStringList( - outList.value.asHandle("mln_map_get_style_source_tile_urls", ::styleStringListHandle) - ) - } + }, + ) } - public actual fun addStyleLayerJson(layerJson: ByteArray, beforeLayerId: String) { + public actual fun addStyleLayerJson( + layerJson: ByteArray, + beforeLayerId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_style_layer_json( state.requireLive().rawHandleValue, ByteStructs.bufferView(layerJson, this), CoreStructs.stringView(beforeLayerId, this), + completion, ) ) } } - public actual fun addHillshadeLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addHillshadeLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_hillshade_layer( @@ -998,12 +940,17 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(layerId, this), CoreStructs.stringView(sourceId, this), CoreStructs.stringView(beforeLayerId, this), + completion, ) ) } } - public actual fun addColorReliefLayer(layerId: String, sourceId: String, beforeLayerId: String) { + public actual fun addColorReliefLayer( + layerId: String, + sourceId: String, + beforeLayerId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_color_relief_layer( @@ -1011,18 +958,23 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(layerId, this), CoreStructs.stringView(sourceId, this), CoreStructs.stringView(beforeLayerId, this), + completion, ) ) } } - public actual fun addLocationIndicatorLayer(layerId: String, beforeLayerId: String) { + public actual fun addLocationIndicatorLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_add_location_indicator_layer( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), CoreStructs.stringView(beforeLayerId, this), + completion, ) ) } @@ -1032,7 +984,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut layerId: String, coordinate: LatLng, altitude: Double, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_location_indicator_location( @@ -1040,30 +992,39 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(layerId, this), CoreStructs.latLng(coordinate), altitude, + completion, ) ) } } - public actual fun setLocationIndicatorBearing(layerId: String, bearing: Double) { + public actual fun setLocationIndicatorBearing( + layerId: String, + bearing: Double, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_location_indicator_bearing( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), bearing, + completion, ) ) } } - public actual fun setLocationIndicatorAccuracyRadius(layerId: String, radius: Double) { + public actual fun setLocationIndicatorAccuracyRadius( + layerId: String, + radius: Double, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_location_indicator_accuracy_radius( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), radius, + completion, ) ) } @@ -1073,7 +1034,7 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut layerId: String, imageKind: LocationIndicatorImageKind, imageId: String, - ) { + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_location_indicator_image_name( @@ -1081,142 +1042,167 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(layerId, this), imageKind.nativeValue.toUInt(), CoreStructs.stringView(imageId, this), + completion, ) ) } } - public actual fun removeStyleLayer(layerId: String): Boolean = memScoped { - val outRemoved = alloc() - Status.check( - mln_map_remove_style_layer( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outRemoved.ptr, - ) - ) - outRemoved.value - } + public actual fun removeStyleLayer(layerId: String): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_remove_style_layer( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + ) + } + } - public actual fun styleLayerExists(layerId: String): Boolean = memScoped { - val outExists = alloc() - Status.check( - mln_map_style_layer_exists( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outExists.ptr, - ) + public actual fun styleLayerInfo(layerId: String): Deferred = memScoped { + CompletionBridge.submit( + { result -> + if (result.pointed.value_count == 0uL) null + else { + val raw = result.pointed.value!!.reinterpret().pointed + val info = raw.info + val fields = info.fields + LayerInfo( + ByteStructs.copyBufferView(info.type).decodeToString(), + info.min_zoom, + info.max_zoom, + StyleLayerVisibility.fromNative(info.visibility), + if ((fields and MLN_STYLE_LAYER_INFO_SOURCE_ID) != 0u) + ByteStructs.copyBufferView(raw.source_id).decodeToString() + else null, + if ((fields and MLN_STYLE_LAYER_INFO_SOURCE_LAYER) != 0u) + ByteStructs.copyBufferView(raw.source_layer).decodeToString() + else null, + ) + } + }, + { completion -> + mln_map_get_style_layer_info( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + }, ) - outExists.value } - public actual fun styleLayerType(layerId: String): String? = memScoped { - val outType = alloc() - val outFound = alloc() - Status.check( - mln_map_get_style_layer_type( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outType.ptr, - outFound.ptr, - ) + public actual fun styleLayerIds(): Deferred> = + CompletionBridge.submit( + ::stringViewsCompletion, + { completion -> mln_map_list_style_layer_ids(state.requireLive().rawHandleValue, completion) }, ) - if (outFound.value) CoreStructs.stringView(outType) else null - } - - public actual fun styleLayerIds(): List = memScoped { - val outList = alloc() - outList.value = 0uL - Status.check(mln_map_list_style_layer_ids(state.requireLive().rawHandleValue, outList.ptr)) - StyleStructs.styleIdList(outList.value.asHandle("mln_map_list_style_ids", ::styleIdListHandle)) - } - public actual fun moveStyleLayer(layerId: String, beforeLayerId: String) { + public actual fun moveStyleLayer( + layerId: String, + beforeLayerId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_move_style_layer( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), CoreStructs.stringView(beforeLayerId, this), + completion, ) ) } } - public actual fun styleLayerJson(layerId: String): ByteArray? = memScoped { - val outLayer = alloc() - val outFound = alloc() - outLayer.value = 0uL - Status.check( - mln_map_get_style_layer_json( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outLayer.ptr, - outFound.ptr, - ) + public actual fun styleLayerJson(layerId: String): Deferred = memScoped { + CompletionBridge.submit( + ::bufferCompletion, + { completion -> + mln_map_get_style_layer_json( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + }, ) - if (outFound.value) - ByteStructs.ownedBuffer(outLayer.value.asHandle("mln_buffer", ::ownedBufferHandle)) - else null } - public actual fun setStyleLightJson(lightJson: ByteArray) { - memScoped { - Status.check( - mln_map_set_style_light_json( - state.requireLive().rawHandleValue, - ByteStructs.bufferView(lightJson, this), + public actual fun setStyleLightJson(lightJson: ByteArray): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_set_style_light_json( + state.requireLive().rawHandleValue, + ByteStructs.bufferView(lightJson, this), + completion, + ) ) - ) + } } - } - public actual fun setStyleLightProperty(propertyName: String, value: ByteArray) { + public actual fun setStyleLightProperty( + propertyName: String, + value: ByteArray, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_style_light_property( state.requireLive().rawHandleValue, CoreStructs.stringView(propertyName, this), ByteStructs.bufferView(value, this), + completion, ) ) } } - public actual fun styleLightProperty(propertyName: String): ByteArray? = memScoped { - val outValue = alloc() - outValue.value = 0uL - Status.check( - mln_map_get_style_light_property( - state.requireLive().rawHandleValue, - CoreStructs.stringView(propertyName, this), - outValue.ptr, - ) + public actual fun styleLightProperty(propertyName: String): Deferred = memScoped { + CompletionBridge.submit( + ::bufferCompletion, + { completion -> + mln_map_get_style_light_property( + state.requireLive().rawHandleValue, + CoreStructs.stringView(propertyName, this), + completion, + ) + }, ) - if (outValue.value == 0uL) null - else ByteStructs.ownedBuffer(outValue.value.asHandle("mln_buffer", ::ownedBufferHandle)) } - public actual fun setStyleTransitionOptions(options: StyleTransitionOptions) { + public actual fun setStyleTransitionOptions( + options: StyleTransitionOptions + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_style_transition_options( state.requireLive().rawHandleValue, StyleStructs.styleTransitionOptions(options, this), + completion, ) ) } } - public actual fun styleTransitionOptions(): StyleTransitionOptions = memScoped { - val outOptions = mln_style_transition_options_default().getPointer(this) - Status.check( - mln_map_get_style_transition_options(state.requireLive().rawHandleValue, outOptions) + public actual fun styleTransitionOptions(): Deferred = + CompletionBridge.submit( + { result -> + StyleStructs.styleTransitionOptions( + result.pointed.value!! + .reinterpret() + .pointed + ) + }, + { completion -> + mln_map_get_style_transition_options(state.requireLive().rawHandleValue, completion) + }, ) - StyleStructs.styleTransitionOptions(outOptions.pointed) - } - public actual fun setLayerProperty(layerId: String, propertyName: String, value: ByteArray) { + public actual fun setLayerProperty( + layerId: String, + propertyName: String, + value: ByteArray, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_layer_property( @@ -1224,115 +1210,126 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut CoreStructs.stringView(layerId, this), CoreStructs.stringView(propertyName, this), ByteStructs.bufferView(value, this), + completion, ) ) } } - public actual fun layerProperty(layerId: String, propertyName: String): ByteArray? = memScoped { - val outValue = alloc() - outValue.value = 0uL - Status.check( - mln_map_get_layer_property( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - CoreStructs.stringView(propertyName, this), - outValue.ptr, + public actual fun layerProperty(layerId: String, propertyName: String): Deferred = + memScoped { + CompletionBridge.submit( + ::bufferCompletion, + { completion -> + mln_map_get_layer_property( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + CoreStructs.stringView(propertyName, this), + completion, + ) + }, ) - ) - if (outValue.value == 0uL) null - else ByteStructs.ownedBuffer(outValue.value.asHandle("mln_buffer", ::ownedBufferHandle)) - } + } - public actual fun setLayerFilter(layerId: String, filter: ByteArray) { + public actual fun setLayerFilter( + layerId: String, + filter: ByteArray, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_layer_filter( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), ByteStructs.bufferViewPointer(filter, this), + completion, ) ) } } - public actual fun clearLayerFilter(layerId: String) { - memScoped { - Status.check( - mln_map_set_layer_filter( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - null, + public actual fun clearLayerFilter(layerId: String): Deferred = + command { completion -> + memScoped { + Status.check( + mln_map_set_layer_filter( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + null, + completion, + ) ) - ) + } } - } - public actual fun layerFilter(layerId: String): ByteArray? = memScoped { - val outFilter = alloc() - outFilter.value = 0uL - Status.check( - mln_map_get_layer_filter( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outFilter.ptr, - ) + public actual fun layerFilter(layerId: String): Deferred = memScoped { + CompletionBridge.submit( + ::bufferCompletion, + { completion -> + mln_map_get_layer_filter( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + }, ) - if (outFilter.value == 0uL) null - else ByteStructs.ownedBuffer(outFilter.value.asHandle("mln_buffer", ::ownedBufferHandle)) } - public actual fun setLayerSourceLayer(layerId: String, sourceLayer: String) { + public actual fun setLayerSourceLayer( + layerId: String, + sourceLayer: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_layer_source_layer( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), CoreStructs.stringView(sourceLayer, this), + completion, ) ) } } - public actual fun layerSourceLayer(layerId: String): String = - copyLayerText(layerId) { handle, layerView, text, capacity, outSize -> - mln_map_copy_layer_source_layer(handle, layerView, text, capacity, outSize) - } + public actual fun layerSourceLayer(layerId: String): Deferred = memScoped { + CompletionBridge.submit( + { result -> checkNotNull(bufferCompletion(result)).decodeToString() }, + { completion -> + mln_map_copy_layer_source_layer( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + }, + ) + } - public actual fun setLayerSourceId(layerId: String, sourceId: String) { + public actual fun setLayerSourceId( + layerId: String, + sourceId: String, + ): Deferred = command { completion -> memScoped { Status.check( mln_map_set_layer_source_id( state.requireLive().rawHandleValue, CoreStructs.stringView(layerId, this), CoreStructs.stringView(sourceId, this), + completion, ) ) } } - public actual fun layerSourceId(layerId: String): String = - copyLayerText(layerId) { handle, layerView, text, capacity, outSize -> - mln_map_copy_layer_source_id(handle, layerView, text, capacity, outSize) - } - - /** - * Probes the required length, then copies. A null buffer with zero capacity is a size probe the C - * API answers with OK. - */ - private fun copyMapText( - copy: (ULong, CPointer?, ULong, CPointer) -> Int - ): String = memScoped { - val handle = state.requireLive().rawHandleValue - val outSize = alloc() - Status.check(copy(handle, null, 0UL, outSize.ptr)) - val required = checkedInt(outSize.value, "map text size") - if (required == 0) return@memScoped "" - - val buffer = allocArray(required) - val outCopied = alloc() - Status.check(copy(handle, buffer, required.toULong(), outCopied.ptr)) - buffer.readBytes(checkedInt(outCopied.value, "map copied text size")).decodeToString() + public actual fun layerSourceId(layerId: String): Deferred = memScoped { + CompletionBridge.submit( + { result -> checkNotNull(bufferCompletion(result)).decodeToString() }, + { completion -> + mln_map_copy_layer_source_id( + state.requireLive().rawHandleValue, + CoreStructs.stringView(layerId, this), + completion, + ) + }, + ) } private fun copyMapBytes( @@ -1368,543 +1365,328 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut buffer.readBytes(checkedInt(outCopied.value, "layer copied text size")).decodeToString() } - public actual fun setLayerMinZoom(layerId: String, minZoom: Double) { - memScoped { - Status.check( - mln_map_set_layer_min_zoom( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - minZoom, - ) - ) - } - } - - public actual fun layerMinZoom(layerId: String): Double = memScoped { - val outZoom = alloc() - Status.check( - mln_map_get_layer_min_zoom( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outZoom.ptr, - ) - ) - outZoom.value - } - - public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double) { - memScoped { - Status.check( - mln_map_set_layer_max_zoom( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - maxZoom, - ) - ) - } - } - - public actual fun layerMaxZoom(layerId: String): Double = memScoped { - val outZoom = alloc() - Status.check( - mln_map_get_layer_max_zoom( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outZoom.ptr, - ) - ) - outZoom.value - } - - public actual fun setLayerVisibility(layerId: String, visibility: StyleLayerVisibility) { - memScoped { - Status.check( - mln_map_set_layer_visibility( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - visibility.nativeValue.toUInt(), - ) - ) - } - } - - public actual fun layerVisibility(layerId: String): StyleLayerVisibility = memScoped { - val outVisibility = alloc() - Status.check( - mln_map_get_layer_visibility( - state.requireLive().rawHandleValue, - CoreStructs.stringView(layerId, this), - outVisibility.ptr, - ) - ) - StyleLayerVisibility.fromNative(outVisibility.value) - } - - public actual fun requestRepaint() { - Status.check(mln_map_request_repaint(state.requireLive().rawHandleValue)) - } - - public actual fun requestStillImage() { - Status.check(mln_map_request_still_image(state.requireLive().rawHandleValue)) - } - - public actual var debugOptions: Set - get() = memScoped { - val outOptions = alloc() - Status.check(mln_map_get_debug_options(state.requireLive().rawHandleValue, outOptions.ptr)) - DebugOption.entries.filterTo(mutableSetOf()) { - (outOptions.value.toInt() and it.nativeMask) != 0 - } - } - set(options) { - val mask = options.fold(0) { acc, option -> acc or option.nativeMask } - Status.check(mln_map_set_debug_options(state.requireLive().rawHandleValue, mask.toUInt())) - } - - public actual var isRenderingStatsViewEnabled: Boolean - get() = memScoped { - val outEnabled = alloc() - Status.check( - mln_map_get_rendering_stats_view_enabled(state.requireLive().rawHandleValue, outEnabled.ptr) - ) - outEnabled.value - } - set(enabled) { - Status.check( - mln_map_set_rendering_stats_view_enabled(state.requireLive().rawHandleValue, enabled) - ) - } - - public actual val isFullyLoaded: Boolean - get() = memScoped { - val outLoaded = alloc() - Status.check(mln_map_is_fully_loaded(state.requireLive().rawHandleValue, outLoaded.ptr)) - outLoaded.value - } - - public actual fun dumpDebugLogs() { - Status.check(mln_map_dump_debug_logs(state.requireLive().rawHandleValue)) - } - - public actual val size: MapSize - get() = memScoped { - val outWidth = alloc() - val outHeight = alloc() - val outScaleFactor = alloc() - Status.check( - mln_map_get_size( - state.requireLive().rawHandleValue, - outWidth.ptr, - outHeight.ptr, - outScaleFactor.ptr, - ) - ) - MapSize(outWidth.value.toInt(), outHeight.value.toInt(), outScaleFactor.value) - } - - public actual var viewportOptions: ViewportOptions - get() = memScoped { - val outOptions = mln_map_viewport_options_default().getPointer(this) - Status.check(mln_map_get_viewport_options(state.requireLive().rawHandleValue, outOptions)) - MapStructs.viewportOptions(outOptions.pointed) - } - set(options) { + public actual fun setLayerMinZoom(layerId: String, minZoom: Double): Deferred = + command { completion -> memScoped { Status.check( - mln_map_set_viewport_options( + mln_map_set_layer_min_zoom( state.requireLive().rawHandleValue, - MapStructs.viewportOptions(options, this), + CoreStructs.stringView(layerId, this), + minZoom, + completion, ) ) } } - public actual var tileOptions: TileOptions - get() = memScoped { - val outOptions = mln_map_tile_options_default().getPointer(this) - Status.check(mln_map_get_tile_options(state.requireLive().rawHandleValue, outOptions)) - MapStructs.tileOptions(outOptions.pointed) - } - set(options) { + public actual fun setLayerMaxZoom(layerId: String, maxZoom: Double): Deferred = + command { completion -> memScoped { Status.check( - mln_map_set_tile_options( + mln_map_set_layer_max_zoom( state.requireLive().rawHandleValue, - MapStructs.tileOptions(options, this), + CoreStructs.stringView(layerId, this), + maxZoom, + completion, ) ) } } - public actual val camera: CameraOptions - get() = memScoped { - val outCamera = mln_camera_options_default().getPointer(this) - Status.check(mln_map_get_camera(state.requireLive().rawHandleValue, outCamera)) - MapStructs.cameraOptions(outCamera.pointed) - } - - public actual fun jumpTo(camera: CameraOptions) { - memScoped { - Status.check( - mln_map_jump_to(state.requireLive().rawHandleValue, MapStructs.cameraOptions(camera, this)) - ) - } - } - - public actual fun easeTo(camera: CameraOptions, animation: AnimationOptions?) { + public actual fun setLayerVisibility( + layerId: String, + visibility: StyleLayerVisibility, + ): Deferred = command { completion -> memScoped { Status.check( - mln_map_ease_to( + mln_map_set_layer_visibility( state.requireLive().rawHandleValue, - MapStructs.cameraOptions(camera, this), - animation?.let { MapStructs.animationOptions(it, this) }, + CoreStructs.stringView(layerId, this), + visibility.nativeValue.toUInt(), + completion, ) ) } } - public actual fun flyTo(camera: CameraOptions, animation: AnimationOptions?) { - memScoped { - Status.check( - mln_map_fly_to( - state.requireLive().rawHandleValue, - MapStructs.cameraOptions(camera, this), - animation?.let { MapStructs.animationOptions(it, this) }, - ) - ) - } + public actual fun requestRepaint(): Deferred = command { completion -> + Status.check(mln_map_request_repaint(state.requireLive().rawHandleValue, completion)) } - public actual fun moveBy(deltaX: Double, deltaY: Double) { - Status.check(mln_map_move_by(state.requireLive().rawHandleValue, deltaX, deltaY)) + public actual fun requestStillImage(): Deferred = command { completion -> + Status.check(mln_map_request_still_image(state.requireLive().rawHandleValue, completion)) } - public actual fun moveByAnimated(deltaX: Double, deltaY: Double, animation: AnimationOptions?) { - memScoped { - Status.check( - mln_map_move_by_animated( - state.requireLive().rawHandleValue, - deltaX, - deltaY, - animation?.let { MapStructs.animationOptions(it, this) }, - ) - ) - } - } - - public actual fun scaleBy(scale: Double, anchor: ScreenPoint?) { - Status.check( - mln_map_scale_by( - state.requireLive().rawHandleValue, - scale, - anchor?.let { CoreStructs.screenPoint(it) }, - ) + public actual fun snapshot(): MapSnapshot = memScoped { + val value = alloc() + value.size = sizeOf().toUInt() + Status.check(mln_map_snapshot_get(state.requireLive().rawHandleValue, value.ptr)) + val extent = value.logical_extent + MapSnapshot( + value.generation.toLong(), + MapStructs.debugOptions(value.debug_options), + MapStructs.cameraOptions(value.camera), + MapSize(extent.width.toInt(), extent.height.toInt(), extent.scale_factor), + MapStructs.projectionModeOptions(value.projection_mode), + MapStructs.viewportOptions(value.viewport), + value.fully_loaded, + value.rendering_stats_view_enabled, + value.repaint_demand, + value.latest_render_update_generation.toLong(), + MapStructs.tileOptions(value.tile), + MapStructs.boundOptions(value.bounds), + MapStructs.freeCameraOptions(value.free_camera), ) } - public actual fun scaleByAnimated( - scale: Double, - anchor: ScreenPoint?, - animation: AnimationOptions?, - ) { - memScoped { + public actual fun setDebugOptions(options: Set): Deferred = + command { completion -> Status.check( - mln_map_scale_by_animated( + mln_map_set_debug_options( state.requireLive().rawHandleValue, - scale, - anchor?.let { CoreStructs.screenPoint(it) }, - animation?.let { MapStructs.animationOptions(it, this) }, + MapStructs.debugOptionMask(options), + completion, ) ) } - } - - public actual fun rotateBy(first: ScreenPoint, second: ScreenPoint) { - Status.check( - mln_map_rotate_by( - state.requireLive().rawHandleValue, - CoreStructs.screenPoint(first), - CoreStructs.screenPoint(second), - ) - ) - } - public actual fun rotateByAnimated( - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions?, - ) { - memScoped { + public actual fun setRenderingStatsViewEnabled(enabled: Boolean): Deferred = + command { completion -> Status.check( - mln_map_rotate_by_animated( + mln_map_set_rendering_stats_view_enabled( state.requireLive().rawHandleValue, - CoreStructs.screenPoint(first), - CoreStructs.screenPoint(second), - animation?.let { MapStructs.animationOptions(it, this) }, + enabled, + completion, ) ) } - } - - public actual fun pitchBy(pitch: Double) { - Status.check(mln_map_pitch_by(state.requireLive().rawHandleValue, pitch)) - } - public actual fun pitchByAnimated(pitch: Double, animation: AnimationOptions?) { + public actual fun setViewportOptions(options: ViewportOptions): Deferred = memScoped { - Status.check( - mln_map_pitch_by_animated( - state.requireLive().rawHandleValue, - pitch, - animation?.let { MapStructs.animationOptions(it, this) }, + command { completion -> + Status.check( + mln_map_set_viewport_options( + state.requireLive().rawHandleValue, + MapStructs.viewportOptions(options, this), + completion, + ) ) - ) + } } - } - - public actual fun cancelTransitions() { - Status.check(mln_map_cancel_transitions(state.requireLive().rawHandleValue)) - } - public actual var isGestureInProgress: Boolean - get() = memScoped { - val outInProgress = alloc() + public actual fun setTileOptions(options: TileOptions): Deferred = memScoped { + command { completion -> Status.check( - mln_map_is_gesture_in_progress(state.requireLive().rawHandleValue, outInProgress.ptr) + mln_map_set_tile_options( + state.requireLive().rawHandleValue, + MapStructs.tileOptions(options, this), + completion, + ) ) - outInProgress.value - } - set(inProgress) { - Status.check(mln_map_set_gesture_in_progress(state.requireLive().rawHandleValue, inProgress)) } - - public actual fun cameraForLatLngBounds( - bounds: LatLngBounds, - fitOptions: CameraFitOptions?, - ): CameraOptions = memScoped { - val outCamera = mln_camera_options_default().getPointer(this) - Status.check( - mln_map_camera_for_lat_lng_bounds( - state.requireLive().rawHandleValue, - CoreStructs.latLngBounds(bounds), - fitOptions?.let { MapStructs.cameraFitOptions(it, this) }, - outCamera, - ) - ) - MapStructs.cameraOptions(outCamera.pointed) } - public actual fun cameraForLatLngs( - coordinates: List, - fitOptions: CameraFitOptions?, - ): CameraOptions = memScoped { - val coordinateSnapshot = coordinates.toList() - val outCamera = mln_camera_options_default().getPointer(this) - Status.check( - mln_map_camera_for_lat_lngs( - state.requireLive().rawHandleValue, - CoreStructs.latLngArray(coordinateSnapshot, this), - coordinateSnapshot.size.toULong(), - fitOptions?.let { MapStructs.cameraFitOptions(it, this) }, - outCamera, - ) - ) - MapStructs.cameraOptions(outCamera.pointed) - } - - public actual fun cameraForGeometry( - geometry: ByteArray, - fitOptions: CameraFitOptions?, - ): CameraOptions = memScoped { - val outCamera = mln_camera_options_default().getPointer(this) - Status.check( - mln_map_camera_for_geometry( - state.requireLive().rawHandleValue, - ByteStructs.bufferView(geometry, this), - fitOptions?.let { MapStructs.cameraFitOptions(it, this) }, - outCamera, - ) - ) - MapStructs.cameraOptions(outCamera.pointed) - } - - public actual fun latLngBoundsForCamera(camera: CameraOptions): LatLngBounds = memScoped { - val outBounds = alloc() - Status.check( - mln_map_lat_lng_bounds_for_camera( - state.requireLive().rawHandleValue, - MapStructs.cameraOptions(camera, this), - outBounds.ptr, - ) - ) - CoreStructs.latLngBounds(outBounds) - } - - public actual fun latLngBoundsForCameraUnwrapped(camera: CameraOptions): LatLngBounds = - memScoped { - val outBounds = alloc() + public actual fun setBounds(options: BoundOptions): Deferred = memScoped { + command { completion -> Status.check( - mln_map_lat_lng_bounds_for_camera_unwrapped( + mln_map_set_bounds( state.requireLive().rawHandleValue, - MapStructs.cameraOptions(camera, this), - outBounds.ptr, + MapStructs.boundOptions(options, this), + completion, ) ) - CoreStructs.latLngBounds(outBounds) - } - - public actual var bounds: BoundOptions - get() = memScoped { - val outOptions = mln_bound_options_default().getPointer(this) - Status.check(mln_map_get_bounds(state.requireLive().rawHandleValue, outOptions)) - MapStructs.boundOptions(outOptions.pointed) - } - set(options) { - memScoped { - Status.check( - mln_map_set_bounds( - state.requireLive().rawHandleValue, - MapStructs.boundOptions(options, this), - ) - ) - } } + } - public actual var freeCameraOptions: FreeCameraOptions - get() = memScoped { - val outOptions = mln_free_camera_options_default().getPointer(this) - Status.check(mln_map_get_free_camera_options(state.requireLive().rawHandleValue, outOptions)) - MapStructs.freeCameraOptions(outOptions.pointed) - } - set(options) { - memScoped { + public actual fun setFreeCameraOptions(options: FreeCameraOptions): Deferred = + memScoped { + command { completion -> Status.check( mln_map_set_free_camera_options( state.requireLive().rawHandleValue, MapStructs.freeCameraOptions(options, this), + completion, ) ) } } - public actual var projectionMode: ProjectionModeOptions - get() = memScoped { - val outMode = mln_projection_mode_default().getPointer(this) - Status.check(mln_map_get_projection_mode(state.requireLive().rawHandleValue, outMode)) - MapStructs.projectionModeOptions(outMode.pointed) - } - set(mode) { - memScoped { - Status.check( - mln_map_set_projection_mode( - state.requireLive().rawHandleValue, - MapStructs.projectionModeOptions(mode, this), - ) - ) - } - } - - public actual fun pixelForLatLng(coordinate: LatLng): ScreenPoint = memScoped { - val outPoint = alloc() - Status.check( - mln_map_pixel_for_lat_lng( - state.requireLive().rawHandleValue, - CoreStructs.latLng(coordinate), - outPoint.ptr, + public actual fun resize(size: MapSize): Deferred = memScoped { + val extent = alloc() + extent.width = size.width.toUInt() + extent.height = size.height.toUInt() + extent.scale_factor = size.scaleFactor + command { completion -> + Status.check( + mln_map_resize(state.requireLive().rawHandleValue, extent.readValue(), completion) ) - ) - CoreStructs.screenPoint(outPoint) + } } - public actual fun latLngForPixel(point: ScreenPoint): LatLng = memScoped { - val outCoordinate = alloc() + public actual fun cameraSnapshot(): CameraSnapshot = memScoped { + val outCamera = mln_camera_options_default().getPointer(this) + val outGeneration = alloc() Status.check( - mln_map_lat_lng_for_pixel( - state.requireLive().rawHandleValue, - CoreStructs.screenPoint(point), - outCoordinate.ptr, - ) + mln_map_camera_snapshot_get(state.requireLive().rawHandleValue, outCamera, outGeneration.ptr) ) - CoreStructs.latLng(outCoordinate) - } - - public actual fun pixelsForLatLngs(coordinates: List): List = memScoped { - val coordinateSnapshot = coordinates.toList() - if (coordinateSnapshot.isEmpty()) return@memScoped emptyList() - val outPoints = allocArray(coordinateSnapshot.size) - Status.check( - mln_map_pixels_for_lat_lngs( - state.requireLive().rawHandleValue, - CoreStructs.latLngArray(coordinateSnapshot, this), - coordinateSnapshot.size.toULong(), - outPoints, + CameraSnapshot(outGeneration.value.toLong(), MapStructs.cameraOptions(outCamera.pointed)) + } + + public actual fun updateCamera(update: CameraUpdate): Deferred = memScoped { + val nativeUpdate = mln_camera_update_default().getPointer(this) + nativeUpdate.pointed.mode = update.mode.nativeValue.toUInt() + val camera = MapStructs.cameraOptions(update.camera, this).pointed + nativeUpdate.pointed.camera.size = camera.size + nativeUpdate.pointed.camera.fields = camera.fields + nativeUpdate.pointed.camera.latitude = camera.latitude + nativeUpdate.pointed.camera.longitude = camera.longitude + nativeUpdate.pointed.camera.center_altitude = camera.center_altitude + nativeUpdate.pointed.camera.padding.top = camera.padding.top + nativeUpdate.pointed.camera.padding.left = camera.padding.left + nativeUpdate.pointed.camera.padding.bottom = camera.padding.bottom + nativeUpdate.pointed.camera.padding.right = camera.padding.right + nativeUpdate.pointed.camera.anchor.x = camera.anchor.x + nativeUpdate.pointed.camera.anchor.y = camera.anchor.y + nativeUpdate.pointed.camera.zoom = camera.zoom + nativeUpdate.pointed.camera.bearing = camera.bearing + nativeUpdate.pointed.camera.pitch = camera.pitch + nativeUpdate.pointed.camera.roll = camera.roll + nativeUpdate.pointed.camera.field_of_view = camera.field_of_view + val animation = MapStructs.animationOptions(update.animation, this).pointed + nativeUpdate.pointed.animation.size = animation.size + nativeUpdate.pointed.animation.fields = animation.fields + nativeUpdate.pointed.animation.duration_ms = animation.duration_ms + nativeUpdate.pointed.animation.velocity = animation.velocity + nativeUpdate.pointed.animation.min_zoom = animation.min_zoom + nativeUpdate.pointed.animation.easing.x1 = animation.easing.x1 + nativeUpdate.pointed.animation.easing.y1 = animation.easing.y1 + nativeUpdate.pointed.animation.easing.x2 = animation.easing.x2 + nativeUpdate.pointed.animation.easing.y2 = animation.easing.y2 + nativeUpdate.pointed.animation.transition_id = animation.transition_id + nativeUpdate.pointed.gesture_phase = update.gesturePhase.nativeValue.toUInt() + command { completion -> + Status.check( + mln_map_update_camera(state.requireLive().rawHandleValue, nativeUpdate, completion) + ) + } + } + + public actual fun applyCameraDelta(delta: CameraDelta): Deferred = memScoped { + val nativeDelta = mln_camera_delta_default().getPointer(this) + nativeDelta.pointed.kind = delta.kind.nativeValue.toUInt() + nativeDelta.pointed.offset.x = delta.offset.x + nativeDelta.pointed.offset.y = delta.offset.y + nativeDelta.pointed.amount = delta.amount + nativeDelta.pointed.has_anchor = delta.anchor != null + delta.anchor?.let { + nativeDelta.pointed.anchor.x = it.x + nativeDelta.pointed.anchor.y = it.y + } + val animation = MapStructs.animationOptions(delta.animation, this).pointed + nativeDelta.pointed.animation.size = animation.size + nativeDelta.pointed.animation.fields = animation.fields + nativeDelta.pointed.animation.duration_ms = animation.duration_ms + nativeDelta.pointed.animation.velocity = animation.velocity + nativeDelta.pointed.animation.min_zoom = animation.min_zoom + nativeDelta.pointed.animation.easing.x1 = animation.easing.x1 + nativeDelta.pointed.animation.easing.y1 = animation.easing.y1 + nativeDelta.pointed.animation.easing.x2 = animation.easing.x2 + nativeDelta.pointed.animation.easing.y2 = animation.easing.y2 + nativeDelta.pointed.animation.transition_id = animation.transition_id + command { completion -> + Status.check( + mln_map_apply_camera_delta(state.requireLive().rawHandleValue, nativeDelta, completion) ) - ) - CoreStructs.screenPointArray(outPoints, coordinateSnapshot.size) + } } - public actual fun latLngsForPixels(points: List): List = memScoped { - val pointSnapshot = points.toList() - if (pointSnapshot.isEmpty()) return@memScoped emptyList() - val outCoordinates = allocArray(pointSnapshot.size) - Status.check( - mln_map_lat_lngs_for_pixels( - state.requireLive().rawHandleValue, - CoreStructs.screenPointArray(pointSnapshot, this), - pointSnapshot.size.toULong(), - outCoordinates, - ) + public actual fun queryCamera(): Deferred = + CompletionBridge.submit( + { completion -> + val result = completion.pointed.value!!.reinterpret().pointed + CameraSnapshot(result.generation.toLong(), MapStructs.cameraOptions(result.camera)) + }, + { completion -> mln_map_camera_query(state.requireLive().rawHandleValue, completion) }, ) - CoreStructs.latLngArray(outCoordinates, pointSnapshot.size) - } public actual fun attachMetalOwnedTexture( - descriptor: MetalOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachMetalOwnedTexture(this, descriptor) + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalOwnedTexture(this, descriptor, options) public actual fun attachMetalBorrowedTexture( - descriptor: MetalBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachMetalBorrowedTexture(this, descriptor) + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachMetalBorrowedTexture(this, descriptor, options) public actual fun attachVulkanOwnedTexture( - descriptor: VulkanOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachVulkanOwnedTexture(this, descriptor) + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanOwnedTexture(this, descriptor, options) public actual fun attachVulkanBorrowedTexture( - descriptor: VulkanBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachVulkanBorrowedTexture(this, descriptor) + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachVulkanBorrowedTexture(this, descriptor, options) public actual fun attachOpenGLOwnedTexture( - descriptor: OpenGLOwnedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachOpenGLOwnedTexture(this, descriptor) + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLOwnedTexture(this, descriptor, options) public actual fun attachOpenGLBorrowedTexture( - descriptor: OpenGLBorrowedTextureDescriptor - ): RenderSessionHandle = RenderSessionHandle.attachOpenGLBorrowedTexture(this, descriptor) - - public actual fun attachMetalSurface(descriptor: MetalSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachMetalSurface(this, descriptor) - - public actual fun attachVulkanSurface(descriptor: VulkanSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachVulkanSurface(this, descriptor) - - public actual fun attachOpenGLSurface(descriptor: OpenGLSurfaceDescriptor): RenderSessionHandle = - RenderSessionHandle.attachOpenGLSurface(this, descriptor) - - public actual fun createProjection(): MapProjectionHandle = memScoped { - val outProjection = alloc() - outProjection.value = 0uL - Status.check(mln_map_projection_create(state.requireLive().rawHandleValue, outProjection.ptr)) - MapProjectionHandle( - outProjection.value.asHandle("mln_map_projection_create", ::mapProjectionHandle) + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = + RenderSessionHandle.attachOpenGLBorrowedTexture(this, descriptor, options) + + public actual fun attachMetalSurface( + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachMetalSurface(this, descriptor, options) + + public actual fun attachVulkanSurface( + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachVulkanSurface(this, descriptor, options) + + public actual fun attachOpenGLSurface( + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ): RenderSessionAttachment = RenderSessionHandle.attachOpenGLSurface(this, descriptor, options) + + public actual fun createProjection(): Deferred = + CompletionBridge.submit( + { result -> + MapProjectionHandle( + result.pointed.value!! + .reinterpret() + .pointed + .value + .asHandle("mln_map_projection_create", ::mapProjectionHandle) + ) + }, + { completion -> mln_map_projection_create(state.requireLive().rawHandleValue, completion) }, ) - } - public actual override fun close() { - state.closeOnce({ handle -> mln_map_destroy(handle.rawHandleValue) }) { - runtime.unregisterMap(this) - runtimeRetention.close() - } + public actual fun close(): Deferred { + if (!state.beginClose()) return kotlinx.coroutines.CompletableDeferred(Unit) + val completed = + try { + CompletionBridge.unitChecked { completion -> + mln_map_release(state.handleForClose().rawHandleValue, completion) + } + } catch (error: Throwable) { + state.abortClose() + throw error + } + state.completeClose { runtime.unregisterMap(this) } + return completed } public actual val isClosed: Boolean @@ -1916,25 +1698,36 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut internal fun nativeHandleId(): Long = state.handleId() - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - state.retainChild(childTypeName) - private fun checkedInt(value: ULong, name: String): Int { require(value <= Int.MAX_VALUE.toULong()) { "$name exceeds Int.MAX_VALUE" } return value.toInt() } public actual companion object { - public actual fun create(runtime: RuntimeHandle, options: MapOptions): MapHandle = memScoped { - val outMap = alloc() - outMap.value = 0uL - Status.check( - mln_map_create(runtime.nativeHandle().rawHandleValue, mapOptions(options, this), outMap.ptr) - ) - val map = MapHandle(runtime, outMap.value.asHandle("mln_map_create", ::mapHandle)) - runtime.registerMap(map) - map - } + public actual fun create(runtime: RuntimeHandle, options: MapOptions): Deferred = + memScoped { + CompletionBridge.submit( + { result -> + MapHandle( + runtime, + result.pointed.value!! + .reinterpret() + .pointed + .value + .asHandle("mln_map_create", ::mapHandle), + options.eventMask, + ) + .also(runtime::registerMap) + }, + { completion -> + mln_map_create( + runtime.nativeHandle().rawHandleValue, + mapOptions(options, this), + completion, + ) + }, + ) + } internal fun mapOptionsForTesting(options: MapOptions, inspect: (mln_map_options) -> Unit) { memScoped { inspect(mapOptions(options, this).pointed) } @@ -1945,13 +1738,13 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut mln_map_options_default().place(nativeOptions.ptr) options.width?.let { Status.requireArgument(it >= 0) { "width must be non-negative" } - nativeOptions.width = it.toUInt() + nativeOptions.initial_extent.width = it.toUInt() } options.height?.let { Status.requireArgument(it >= 0) { "height must be non-negative" } - nativeOptions.height = it.toUInt() + nativeOptions.initial_extent.height = it.toUInt() } - options.scaleFactor?.let { nativeOptions.scale_factor = it } + options.scaleFactor?.let { nativeOptions.initial_extent.scale_factor = it } options.mapMode?.let { Status.requireArgument(it.isKnown) { "Unknown map mode cannot be used as input: ${it.nativeValue}" @@ -1964,3 +1757,26 @@ private constructor(private val runtime: RuntimeHandle, handle: NativeMap) : Aut } } } + +@OptIn(ExperimentalForeignApi::class) +private fun bufferCompletion(result: CPointer): ByteArray? { + if (result.pointed.value_count == 0uL) return null + return ByteStructs.copyBufferView(result.pointed.value!!.reinterpret().pointed) +} + +@OptIn(ExperimentalForeignApi::class) +private fun stringViewsCompletion(result: CPointer): List { + val count = result.pointed.value_count + if (count == 0uL) return emptyList() + require(count <= Int.MAX_VALUE.toULong()) { "string count exceeds Int.MAX_VALUE" } + val values = result.pointed.value!!.reinterpret() + return List(count.toInt()) { index -> CoreStructs.stringView(values[index]) } +} + +@OptIn(ExperimentalForeignApi::class) +private inline fun command( + crossinline call: (CPointer) -> Unit +): Deferred = CompletionBridge.command { + call(it) + 0 +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt index c1967581e..48d95ce00 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/map/MapProjectionHandle.kt @@ -11,7 +11,7 @@ import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.internal.c.mln_camera_options_default import org.maplibre.nativeffi.internal.c.mln_lat_lng -import org.maplibre.nativeffi.internal.c.mln_map_projection_destroy +import org.maplibre.nativeffi.internal.c.mln_map_projection_close import org.maplibre.nativeffi.internal.c.mln_map_projection_get_camera import org.maplibre.nativeffi.internal.c.mln_map_projection_lat_lng_for_pixel import org.maplibre.nativeffi.internal.c.mln_map_projection_pixel_for_lat_lng @@ -27,32 +27,30 @@ import org.maplibre.nativeffi.internal.struct.ByteStructs import org.maplibre.nativeffi.internal.struct.CoreStructs import org.maplibre.nativeffi.internal.struct.MapStructs -/** Any-thread standalone projection snapshot created from a map. */ +/** + * Owned standalone projection snapshot created from a map. + * + * Every call is synchronous, runs on the calling thread, is internally serialized, and may be made + * from any thread. A projection copies the map's transform state at creation and never observes map + * changes made after that and remains usable after its source map and runtime close. + */ @OptIn(ExperimentalForeignApi::class) -public actual class MapProjectionHandle internal constructor(handle: NativeMapProjection) : - AutoCloseable { +public actual class MapProjectionHandle internal constructor(handle: NativeMapProjection) { private val state = HandleState("MapProjectionHandle", handle) - public actual val camera: CameraOptions - get() = memScoped { - val outCamera = mln_camera_options_default().getPointer(this) - state.withLive { handle -> - Status.check(mln_map_projection_get_camera(handle.rawHandleValue, outCamera)) - } - MapStructs.cameraOptions(outCamera.pointed) - } + public actual fun camera(): CameraOptions = memScoped { + val outCamera = mln_camera_options_default().getPointer(this) + Status.check(mln_map_projection_get_camera(state.requireLive().rawHandleValue, outCamera)) + MapStructs.cameraOptions(outCamera.pointed) + } - public actual fun setCamera(camera: CameraOptions) { - memScoped { - state.withLive { handle -> - Status.check( - mln_map_projection_set_camera( - handle.rawHandleValue, - MapStructs.cameraOptions(camera, this), - ) - ) - } - } + public actual fun setCamera(camera: CameraOptions): Unit = memScoped { + Status.check( + mln_map_projection_set_camera( + state.requireLive().rawHandleValue, + MapStructs.cameraOptions(camera, this), + ) + ) } public actual fun setVisibleCoordinates(coordinates: List, padding: EdgeInsets) { @@ -71,18 +69,14 @@ public actual class MapProjectionHandle internal constructor(handle: NativeMapPr } } - public actual fun setVisibleGeometry(geometry: ByteArray, padding: EdgeInsets) { - memScoped { - state.withLive { handle -> - Status.check( - mln_map_projection_set_visible_geometry( - handle.rawHandleValue, - ByteStructs.bufferView(geometry, this), - CoreStructs.edgeInsets(padding), - ) - ) - } - } + public actual fun setVisibleGeometry(geometry: ByteArray, padding: EdgeInsets): Unit = memScoped { + Status.check( + mln_map_projection_set_visible_geometry( + state.requireLive().rawHandleValue, + ByteStructs.bufferView(geometry, this), + CoreStructs.edgeInsets(padding), + ) + ) } public actual fun pixelForLatLng(coordinate: LatLng): ScreenPoint = memScoped { @@ -113,14 +107,17 @@ public actual class MapProjectionHandle internal constructor(handle: NativeMapPr CoreStructs.latLng(outCoordinate) } - public actual override fun close() { - state.closeOnce { handle -> mln_map_projection_destroy(handle.rawHandleValue) } + public actual fun close() { + if (!state.beginClose()) return + try { + Status.check(mln_map_projection_close(state.handleForClose().rawHandleValue)) + } catch (error: Throwable) { + state.abortClose() + throw error + } + state.completeClose() } public actual val isClosed: Boolean get() = state.isReleased() - - internal fun nativeHandle(): NativeMapProjection = state.requireLive() - - internal fun nativeHandleId(): Long = state.handleId() } diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt deleted file mode 100644 index ec81b25f2..000000000 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/MetalOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.maplibre.nativeffi.render - -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.Cleaner -import kotlin.native.ref.createCleaner -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.nativeHeap -import kotlinx.cinterop.rawValue -import org.maplibre.nativeffi.internal.c.mln_metal_owned_texture_frame - -/** Explicit handle for a Metal session-owned texture frame. */ -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -public actual class MetalOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val framePointer: CPointer, - private val scope: FrameScope, - private val frameValue: MetalOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("MetalOwnedTextureFrameHandle") - @Suppress("unused") private val cleaner: Cleaner = createCleaner(core) { it.reportLeak() } - - public actual fun frame(): MetalOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseMetalFrame(framePointer) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeHeap.free(framePointer.rawValue) - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt deleted file mode 100644 index f3c6b4c97..000000000 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/OpenGLOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.maplibre.nativeffi.render - -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.Cleaner -import kotlin.native.ref.createCleaner -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.nativeHeap -import kotlinx.cinterop.rawValue -import org.maplibre.nativeffi.internal.c.mln_opengl_owned_texture_frame - -/** Explicit handle for an OpenGL session-owned texture frame. */ -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -public actual class OpenGLOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val framePointer: CPointer, - private val scope: FrameScope, - private val frameValue: OpenGLOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("OpenGLOwnedTextureFrameHandle") - @Suppress("unused") private val cleaner: Cleaner = createCleaner(core) { it.reportLeak() } - - public actual fun frame(): OpenGLOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseOpenGLFrame(framePointer) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeHeap.free(framePointer.rawValue) - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt index 6efa3509f..ee6e121e4 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/RenderSessionHandle.kt @@ -1,243 +1,250 @@ package org.maplibre.nativeffi.render -import kotlinx.cinterop.BooleanVar -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.UByteVar -import kotlinx.cinterop.UIntVar -import kotlinx.cinterop.ULongVar -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.nativeHeap -import kotlinx.cinterop.pointed -import kotlinx.cinterop.ptr -import kotlinx.cinterop.rawValue -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.sizeOf -import kotlinx.cinterop.toLong -import kotlinx.cinterop.value -import org.maplibre.nativeffi.internal.c.mln_metal_borrowed_texture_attach -import org.maplibre.nativeffi.internal.c.mln_metal_borrowed_texture_set_target -import org.maplibre.nativeffi.internal.c.mln_metal_owned_texture_acquire_frame -import org.maplibre.nativeffi.internal.c.mln_metal_owned_texture_attach -import org.maplibre.nativeffi.internal.c.mln_metal_owned_texture_frame -import org.maplibre.nativeffi.internal.c.mln_metal_owned_texture_release_frame -import org.maplibre.nativeffi.internal.c.mln_metal_surface_attach -import org.maplibre.nativeffi.internal.c.mln_metal_surface_set_target -import org.maplibre.nativeffi.internal.c.mln_opengl_borrowed_texture_attach -import org.maplibre.nativeffi.internal.c.mln_opengl_borrowed_texture_set_target -import org.maplibre.nativeffi.internal.c.mln_opengl_owned_texture_acquire_frame -import org.maplibre.nativeffi.internal.c.mln_opengl_owned_texture_attach -import org.maplibre.nativeffi.internal.c.mln_opengl_owned_texture_frame -import org.maplibre.nativeffi.internal.c.mln_opengl_owned_texture_release_frame -import org.maplibre.nativeffi.internal.c.mln_opengl_surface_attach -import org.maplibre.nativeffi.internal.c.mln_opengl_surface_set_target -import org.maplibre.nativeffi.internal.c.mln_render_session_clear_data -import org.maplibre.nativeffi.internal.c.mln_render_session_destroy -import org.maplibre.nativeffi.internal.c.mln_render_session_detach -import org.maplibre.nativeffi.internal.c.mln_render_session_dump_debug_logs -import org.maplibre.nativeffi.internal.c.mln_render_session_query_feature_extensions -import org.maplibre.nativeffi.internal.c.mln_render_session_query_rendered_features -import org.maplibre.nativeffi.internal.c.mln_render_session_query_source_features -import org.maplibre.nativeffi.internal.c.mln_render_session_reduce_memory_use -import org.maplibre.nativeffi.internal.c.mln_render_session_render_update -import org.maplibre.nativeffi.internal.c.mln_render_session_resize -import org.maplibre.nativeffi.internal.c.mln_texture_image_info_default -import org.maplibre.nativeffi.internal.c.mln_texture_read_premultiplied_rgba8 -import org.maplibre.nativeffi.internal.c.mln_vulkan_borrowed_texture_attach -import org.maplibre.nativeffi.internal.c.mln_vulkan_borrowed_texture_set_target -import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_acquire_frame -import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_attach -import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_frame -import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_release_frame -import org.maplibre.nativeffi.internal.c.mln_vulkan_surface_attach -import org.maplibre.nativeffi.internal.c.mln_vulkan_surface_set_target -import org.maplibre.nativeffi.internal.lifecycle.HandleState -import org.maplibre.nativeffi.internal.lifecycle.NativeRenderSession -import org.maplibre.nativeffi.internal.lifecycle.asHandle -import org.maplibre.nativeffi.internal.lifecycle.ownedBufferHandle -import org.maplibre.nativeffi.internal.lifecycle.queriedFeatureListHandle -import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue -import org.maplibre.nativeffi.internal.lifecycle.renderSessionHandle +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlinx.cinterop.* +import kotlinx.coroutines.Deferred +import org.maplibre.nativeffi.internal.async.CompletionBridge +import org.maplibre.nativeffi.internal.c.* +import org.maplibre.nativeffi.internal.lifecycle.* import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.internal.struct.ByteStructs -import org.maplibre.nativeffi.internal.struct.CoreStructs import org.maplibre.nativeffi.internal.struct.QueryStructs import org.maplibre.nativeffi.internal.struct.RenderStructs import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.query.QueriedFeature -import org.maplibre.nativeffi.query.RenderedFeatureQueryOptions -import org.maplibre.nativeffi.query.RenderedQueryGeometry -import org.maplibre.nativeffi.query.SourceFeatureQueryOptions +import org.maplibre.nativeffi.query.* +import org.maplibre.nativeffi.runtime.* -/** Owned native render session handle. Close it on the thread that attached it. */ -@OptIn(ExperimentalForeignApi::class) +@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) public actual class RenderSessionHandle private constructor(private val map: MapHandle, handle: NativeRenderSession) : AutoCloseable { - private val mapRetention = map.retainChild("RenderSessionHandle") private val state = HandleState("RenderSessionHandle", handle, map) - private val activeFrame = ActiveFrameState() - - public actual fun resize(width: Int, height: Int, scaleFactor: Double) { - activeFrame.ensureInactive("resize") - Status.requireArgument(width >= 0) { "width must be non-negative" } - Status.requireArgument(height >= 0) { "height must be non-negative" } - Status.check( - mln_render_session_resize( - state.requireLive().rawHandleValue, - width.toUInt(), - height.toUInt(), - scaleFactor, - ) + private val acquiredFrameScopes = AtomicReference>(emptyList()) + + public actual val isClosed: Boolean + get() = state.isReleased() + + public actual fun map(): MapHandle = map + + public actual fun capabilities(): RenderSessionCapabilities = memScoped { + val value = alloc() + value.size = sizeOf().toUInt() + Status.check(mln_render_session_get_capabilities(id(), value.ptr)) + RenderSessionCapabilities( + RenderDriver.fromNative(value.driver.toInt()), + value.texture_ring_depth.toInt(), + value.flags and MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION != 0u, + value.flags and MLN_RENDER_SESSION_CAPABILITY_READBACK != 0u, + value.flags and MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC != 0u, + value.flags and MLN_RENDER_SESSION_CAPABILITY_PRESENTATION != 0u, + ) + } + + public actual fun snapshot(): RenderSessionSnapshot = memScoped { + val value = alloc() + value.size = sizeOf().toUInt() + Status.check(mln_render_session_get_snapshot(id(), value.ptr)) + RenderSessionSnapshot( + RenderSessionState.fromNative(value.state.toInt()), + RenderDriver.fromNative(value.driver.toInt()), + RenderResult.fromNative(value.latest_result), + RenderTargetExtent( + value.extent.width.toInt(), + value.extent.height.toInt(), + value.extent.scale_factor, + ), + value.generation, + value.map_update_generation, + value.rendered_update_generation, + value.extent_generation, + value.frame_generation, + value.latest_demand_token, + value.pending_demand_count.toInt(), + value.acquired_frame_count.toInt(), + value.target_ready, + value.pending_changes, ) } - public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor) { - activeFrame.ensureInactive("set target") + public actual fun requestFrame(demand: FrameDemand) = memScoped { + val value = alloc() + mln_frame_demand_default().place(value.ptr) + value.flags = + (if (demand.ifNeeded) MLN_FRAME_DEMAND_IF_NEEDED else 0u) or + (if (demand.present) MLN_FRAME_DEMAND_PRESENT else 0u) + value.token = demand.token + value.coalescing_boundary = demand.coalescingBoundary + value.timeout_ns = demand.timeoutNanoseconds + Status.check(mln_render_session_request_frame(id(), value.ptr)) + } + + public actual fun drainFrameResults(): List = memScoped { + val outBatch = alloc() + outBatch.value = 0u + Status.check(mln_render_session_drain_frame_results(id(), outBatch.ptr)) + val batch = outBatch.value + try { + val outCount = alloc() + Status.check(mln_render_frame_batch_count(batch, outCount.ptr)) + List(outCount.value.toInt()) { index -> + val value = alloc() + value.size = sizeOf().toUInt() + Status.check(mln_render_frame_batch_get(batch, index.toULong(), value.ptr)) + frameResult(value) + } + } finally { + if (batch != 0uL) mln_render_frame_batch_release(batch) + } + } + + public actual fun serviceDriverWork(maxWork: Int): Int = memScoped { + Status.requireArgument(maxWork >= 0) { "maxWork must be non-negative" } + val serviced = alloc() + Status.check(mln_render_session_service_driver_work(id(), maxWork.toULong(), serviced.ptr)) + serviced.value.toInt() + } + + public actual fun acquireFrame(): AcquiredFrameHandle? = memScoped { + val outFrame = alloc() + outFrame.value = 0u + val status = mln_render_session_acquire_frame(id(), outFrame.ptr) + if (status == MLN_STATUS_NOT_READY) return@memScoped null + Status.check(status) + val scope = FrameScope() + retainFrameScope(scope) + AcquiredFrameHandle(this@RenderSessionHandle, outFrame.value, scope) + } + + public actual fun resize(extent: RenderTargetExtent): Deferred = memScoped { + val native = extent(extent, this) + unit { mln_render_session_resize(id(), native, it) } + } + + public actual fun setMetalSurfaceTarget(descriptor: MetalSurfaceDescriptor): Deferred = memScoped { - Status.check( + unit { mln_metal_surface_set_target( - state.requireLive().rawHandleValue, + id(), RenderStructs.metalSurfaceDescriptor(descriptor, this), + it, ) - ) + } } - } - public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor) { - activeFrame.ensureInactive("set target") + public actual fun setVulkanSurfaceTarget(descriptor: VulkanSurfaceDescriptor): Deferred = memScoped { - Status.check( + unit { mln_vulkan_surface_set_target( - state.requireLive().rawHandleValue, + id(), RenderStructs.vulkanSurfaceDescriptor(descriptor, this), + it, ) - ) + } } - } - public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor) { - activeFrame.ensureInactive("set target") + public actual fun setOpenGLSurfaceTarget(descriptor: OpenGLSurfaceDescriptor): Deferred = memScoped { - Status.check( + unit { mln_opengl_surface_set_target( - state.requireLive().rawHandleValue, + id(), RenderStructs.openglSurfaceDescriptor(descriptor, this), + it, ) - ) + } } - } - public actual fun setMetalBorrowedTextureTarget(descriptor: MetalBorrowedTextureDescriptor) { - activeFrame.ensureInactive("set target") - memScoped { - Status.check( - mln_metal_borrowed_texture_set_target( - state.requireLive().rawHandleValue, - RenderStructs.metalBorrowedTextureDescriptor(descriptor, this), - ) + public actual fun setMetalBorrowedTextureTarget( + descriptor: MetalBorrowedTextureDescriptor + ): Deferred = memScoped { + unit { + mln_metal_borrowed_texture_set_target( + id(), + RenderStructs.metalBorrowedTextureDescriptor(descriptor, this), + it, ) } } - public actual fun setVulkanBorrowedTextureTarget(descriptor: VulkanBorrowedTextureDescriptor) { - activeFrame.ensureInactive("set target") - memScoped { - Status.check( - mln_vulkan_borrowed_texture_set_target( - state.requireLive().rawHandleValue, - RenderStructs.vulkanBorrowedTextureDescriptor(descriptor, this), - ) + public actual fun setVulkanBorrowedTextureTarget( + descriptor: VulkanBorrowedTextureDescriptor + ): Deferred = memScoped { + unit { + mln_vulkan_borrowed_texture_set_target( + id(), + RenderStructs.vulkanBorrowedTextureDescriptor(descriptor, this), + it, ) } } - public actual fun setOpenGLBorrowedTextureTarget(descriptor: OpenGLBorrowedTextureDescriptor) { - activeFrame.ensureInactive("set target") - memScoped { - Status.check( - mln_opengl_borrowed_texture_set_target( - state.requireLive().rawHandleValue, - RenderStructs.openglBorrowedTextureDescriptor(descriptor, this), - ) + public actual fun setOpenGLBorrowedTextureTarget( + descriptor: OpenGLBorrowedTextureDescriptor + ): Deferred = memScoped { + unit { + mln_opengl_borrowed_texture_set_target( + id(), + RenderStructs.openglBorrowedTextureDescriptor(descriptor, this), + it, ) } } - public actual fun renderUpdate(): RenderUpdate = memScoped { - activeFrame.ensureInactive("render") - val outResult = alloc() - outResult.value = 0u - val outNeedsRepaint = alloc() - outNeedsRepaint.value = false - Status.check( - mln_render_session_render_update( - state.requireLive().rawHandleValue, - outResult.ptr, - outNeedsRepaint.ptr, - ) - ) - RenderUpdate(RenderResult.fromNative(outResult.value), outNeedsRepaint.value) + public actual fun reduceMemoryUse(): Deferred = unit { + mln_render_session_reduce_memory_use(id(), it) } - public actual fun detach() { - activeFrame.ensureInactive("detach") - Status.check(mln_render_session_detach(state.requireLive().rawHandleValue)) - mapRetention.close() - } + public actual fun clearData(): Deferred = unit { mln_render_session_clear_data(id(), it) } - public actual fun reduceMemoryUse() { - activeFrame.ensureInactive("reduce memory use") - Status.check(mln_render_session_reduce_memory_use(state.requireLive().rawHandleValue)) + public actual fun dumpDebugLogs(): Deferred = unit { + mln_render_session_dump_debug_logs(id(), it) } - public actual fun clearData() { - activeFrame.ensureInactive("clear data") - Status.check(mln_render_session_clear_data(state.requireLive().rawHandleValue)) - } + public actual fun barrier(): Deferred = unit { mln_render_session_barrier(id(), it) } - public actual fun dumpDebugLogs() { - activeFrame.ensureInactive("dump debug logs") - Status.check(mln_render_session_dump_debug_logs(state.requireLive().rawHandleValue)) - } + public actual fun detach(): Deferred = unit { mln_render_session_detach(id(), it) } public actual fun queryRenderedFeatures( geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions?, - ): List = memScoped { - activeFrame.ensureInactive("query rendered features") - val outResult = alloc() - outResult.value = 0uL - Status.check( - mln_render_session_query_rendered_features( - state.requireLive().rawHandleValue, - QueryStructs.renderedQueryGeometry(geometry, this), - QueryStructs.renderedFeatureQueryOptions(options, this), - outResult.ptr, - ) - ) - QueryStructs.queriedFeatureList( - outResult.value.asHandle("mln_queried_feature_list", ::queriedFeatureListHandle) + ): Deferred> = memScoped { + CompletionBridge.submit( + { result -> + QueryStructs.queriedFeatures( + result.pointed.value?.reinterpret(), + result.pointed.value_count, + ) + }, + { completion -> + mln_render_session_query_rendered_features( + id(), + QueryStructs.renderedQueryGeometry(geometry, this), + QueryStructs.renderedFeatureQueryOptions(options, this), + completion, + ) + }, ) } public actual fun querySourceFeatures( sourceId: String, options: SourceFeatureQueryOptions?, - ): List = memScoped { - activeFrame.ensureInactive("query source features") - val outResult = alloc() - outResult.value = 0uL - Status.check( - mln_render_session_query_source_features( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - QueryStructs.sourceFeatureQueryOptions(options, this), - outResult.ptr, - ) - ) - QueryStructs.queriedFeatureList( - outResult.value.asHandle("mln_queried_feature_list", ::queriedFeatureListHandle) + ): Deferred> = memScoped { + CompletionBridge.submit( + { result -> + QueryStructs.queriedFeatures( + result.pointed.value?.reinterpret(), + result.pointed.value_count, + ) + }, + { completion -> + mln_render_session_query_source_features( + id(), + ByteStructs.bufferView(sourceId.encodeToByteArray(), this), + QueryStructs.sourceFeatureQueryOptions(options, this), + completion, + ) + }, ) } @@ -247,422 +254,387 @@ private constructor(private val map: MapHandle, handle: NativeRenderSession) : A extension: String, extensionField: String, arguments: ByteArray?, - ): ByteArray = memScoped { - activeFrame.ensureInactive("query feature extension") - val outResult = alloc() - outResult.value = 0uL - Status.check( - mln_render_session_query_feature_extensions( - state.requireLive().rawHandleValue, - CoreStructs.stringView(sourceId, this), - ByteStructs.bufferView(feature, this), - CoreStructs.stringView(extension, this), - CoreStructs.stringView(extensionField, this), - arguments?.let { ByteStructs.bufferViewPointer(it, this) }, - outResult.ptr, - ) + ): Deferred = memScoped { + val argument = arguments?.let { ByteStructs.bufferView(it, this) } + CompletionBridge.submit( + { result -> requiredBuffer(result) }, + { completion -> + mln_render_session_query_feature_extensions( + id(), + ByteStructs.bufferView(sourceId.encodeToByteArray(), this), + ByteStructs.bufferView(feature, this), + ByteStructs.bufferView(extension.encodeToByteArray(), this), + ByteStructs.bufferView(extensionField.encodeToByteArray(), this), + argument?.getPointer(this), + completion, + ) + }, ) - ByteStructs.ownedBuffer(outResult.value.asHandle("mln_buffer", ::ownedBufferHandle)) } - public actual fun textureImageInfo(): TextureImageInfo = memScoped { - activeFrame.ensureInactive("read texture data") - val outInfo = mln_texture_image_info_default().getPointer(this) - val status = - mln_texture_read_premultiplied_rgba8(state.requireLive().rawHandleValue, null, 0UL, outInfo) - val info = RenderStructs.textureImageInfo(outInfo.pointed) - if (status == 0 || (status == -1 && info.byteLength > 0L)) { - info - } else { - Status.check(status) - error("unreachable") - } - } - - public actual fun readPremultipliedRgba8(buffer: NativeBuffer): TextureImageInfo = memScoped { - activeFrame.ensureInactive("read texture data") - val outInfo = mln_texture_image_info_default().getPointer(this) - buffer.borrow { pointer, capacity -> - Status.check( - mln_texture_read_premultiplied_rgba8( - state.requireLive().rawHandleValue, - pointer?.reinterpret(), - capacity.toULong(), - outInfo, + public actual fun readPremultipliedRgba8(): Deferred = + CompletionBridge.submit( + { result -> + val raw = result.pointed.value!!.reinterpret().pointed + TextureReadback( + ByteStructs.copyBufferView(raw.data), + RenderStructs.textureImageInfo(raw.info), ) - ) - } - val info = RenderStructs.textureImageInfo(outInfo.pointed) - // Native reads an empty destination as a size probe rather than a copy, so the - // capacity is rechecked here. - buffer.ensureCapacity(info.byteLength.toULong()) - info - } + }, + { completion -> mln_texture_read_premultiplied_rgba8(id(), completion) }, + ) - public actual fun acquireMetalOwnedTextureFrame(): MetalOwnedTextureFrameHandle { - val frame = nativeHeap.alloc() - var acquired = false - var borrowStarted = false - try { - activeFrame.beginAcquire() - borrowStarted = true - frame.size = sizeOf().toUInt() - Status.check( - mln_metal_owned_texture_acquire_frame(state.requireLive().rawHandleValue, frame.ptr) - ) - acquired = true - val scope = FrameScope() - return MetalOwnedTextureFrameHandle( - this, - frame.ptr, - scope, - metalOwnedTextureFrame(frame, scope), - ) - } catch (error: Throwable) { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired, - releaseNative = { releaseMetalFrame(frame.ptr) }, - closeLocal = { - if (borrowStarted) activeFrame.endBorrow() - nativeHeap.free(frame.rawPtr) - }, - failure = error, - ) - } + public actual fun abandon(): RenderAbandonResult = memScoped { + val value = alloc() + value.size = sizeOf().toUInt() + Status.check(mln_render_session_abandon(id(), value.ptr)) + acquiredFrameScopes.load().forEach(FrameScope::close) + RenderAbandonResult( + RenderAbandonDisposition.fromNative(value.disposition.toInt()), + value.quarantined_resource_count.toInt(), + ) } - public actual fun acquireVulkanOwnedTextureFrame(): VulkanOwnedTextureFrameHandle { - val frame = nativeHeap.alloc() - var acquired = false - var borrowStarted = false - try { - activeFrame.beginAcquire() - borrowStarted = true - frame.size = sizeOf().toUInt() - Status.check( - mln_vulkan_owned_texture_acquire_frame(state.requireLive().rawHandleValue, frame.ptr) - ) - acquired = true - val scope = FrameScope() - return VulkanOwnedTextureFrameHandle( - this, - frame.ptr, - scope, - vulkanOwnedTextureFrame(frame, scope), - ) - } catch (error: Throwable) { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired, - releaseNative = { releaseVulkanFrame(frame.ptr) }, - closeLocal = { - if (borrowStarted) activeFrame.endBorrow() - nativeHeap.free(frame.rawPtr) - }, - failure = error, - ) - } + public actual override fun close() { + state.closeOnce(destroy = { mln_render_session_destroy(it.rawHandleValue) }) } - public actual fun acquireOpenGLOwnedTextureFrame(): OpenGLOwnedTextureFrameHandle { - val frame = nativeHeap.alloc() - var acquired = false - var borrowStarted = false - try { - activeFrame.beginAcquire() - borrowStarted = true - frame.size = sizeOf().toUInt() - Status.check( - mln_opengl_owned_texture_acquire_frame(state.requireLive().rawHandleValue, frame.ptr) - ) - acquired = true - val scope = FrameScope() - return OpenGLOwnedTextureFrameHandle( - this, - frame.ptr, - scope, - openglOwnedTextureFrame(frame, scope), - ) - } catch (error: Throwable) { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired, - releaseNative = { releaseOpenGLFrame(frame.ptr) }, - closeLocal = { - if (borrowStarted) activeFrame.endBorrow() - nativeHeap.free(frame.rawPtr) - }, - failure = error, - ) + internal fun frameReleased(scope: FrameScope) { + while (true) { + val scopes = acquiredFrameScopes.load() + if (acquiredFrameScopes.compareAndSet(scopes, scopes - scope)) return } } - public actual override fun close() { - activeFrame.ensureInactive("destroy") - state.closeOnce({ handle -> mln_render_session_destroy(handle.rawHandleValue) }) { - mapRetention.close() + private fun retainFrameScope(scope: FrameScope) { + while (true) { + val scopes = acquiredFrameScopes.load() + if (acquiredFrameScopes.compareAndSet(scopes, scopes + scope)) return } } - public actual val isClosed: Boolean - get() = state.isReleased() - - public actual fun map(): MapHandle = map - - internal fun nativeHandle(): NativeRenderSession = state.requireLive() + private fun id(): ULong = state.requireLive().rawHandleValue - internal fun nativeHandleId(): Long = state.handleId() + private fun unit(start: (CPointer) -> Int): Deferred = + CompletionBridge.unit(start) - internal fun releaseMetalFrame(frame: CPointer) { - Status.check(mln_metal_owned_texture_release_frame(state.requireLive().rawHandleValue, frame)) + private fun requiredBuffer(result: CPointer): ByteArray { + require(result.pointed.value_count == 1uL) { "native completion omitted its byte result" } + return ByteStructs.copyBufferView(result.pointed.value!!.reinterpret().pointed) } - internal fun releaseVulkanFrame(frame: CPointer) { - Status.check(mln_vulkan_owned_texture_release_frame(state.requireLive().rawHandleValue, frame)) - } - - internal fun releaseOpenGLFrame(frame: CPointer) { - Status.check(mln_opengl_owned_texture_release_frame(state.requireLive().rawHandleValue, frame)) - } - - internal fun finishFrameBorrow() { - activeFrame.endBorrow() - } - - private fun metalOwnedTextureFrame( - value: mln_metal_owned_texture_frame, - scope: FrameScope, - ): MetalOwnedTextureFrame = - MetalOwnedTextureFrame( - scope, - uint64BitsToLong(value.generation), - checkedInt(value.width, "Metal frame width"), - checkedInt(value.height, "Metal frame height"), - value.scale_factor, - uint64BitsToLong(value.frame_id), - scopedPointer(value.texture, scope), - scopedPointer(value.device, scope), - uint64BitsToLong(value.pixel_format), - ) - - private fun vulkanOwnedTextureFrame( - value: mln_vulkan_owned_texture_frame, - scope: FrameScope, - ): VulkanOwnedTextureFrame = - VulkanOwnedTextureFrame( - scope, - uint64BitsToLong(value.generation), - checkedInt(value.width, "Vulkan frame width"), - checkedInt(value.height, "Vulkan frame height"), - value.scale_factor, - uint64BitsToLong(value.frame_id), - scopedPointer(value.image, scope), - scopedPointer(value.image_view, scope), - scopedPointer(value.device, scope), - value.format.toInt(), - value.layout.toInt(), - ) - - private fun openglOwnedTextureFrame( - value: mln_opengl_owned_texture_frame, - scope: FrameScope, - ): OpenGLOwnedTextureFrame = - OpenGLOwnedTextureFrame( - scope, - uint64BitsToLong(value.generation), - checkedInt(value.width, "OpenGL frame width"), - checkedInt(value.height, "OpenGL frame height"), - value.scale_factor, - uint64BitsToLong(value.frame_id), - value.texture.toInt(), - value.target.toInt(), - value.internal_format.toInt(), - value.format.toInt(), - value.type.toInt(), - ) - - private fun checkedInt(value: UInt, name: String): Int { - require(value <= Int.MAX_VALUE.toUInt()) { "$name exceeds Int.MAX_VALUE" } - return value.toInt() - } - - private fun uint64BitsToLong(value: ULong): Long = value.toLong() - - private fun scopedPointer( - pointer: kotlinx.cinterop.COpaquePointer?, - scope: FrameScope, - ): NativePointer = - pointer?.rawValue?.toLong()?.let { NativePointer.scoped(it, scope) } - ?: NativePointer.NULL_POINTER - internal companion object { + private fun attach( + map: MapHandle, + options: RenderSessionAttachOptions, + call: + ( + CPointer, CPointer, CPointer, + ) -> Int, + ): RenderSessionAttachment = memScoped { + val nativeOptions = alloc() + mln_render_session_attach_options_default().place(nativeOptions.ptr) + nativeOptions.driver = options.driver.nativeValue.toUInt() + nativeOptions.requested_texture_ring_depth = options.requestedTextureRingDepth.toUInt() + val session = alloc() + session.value = 0u + val completed = CompletionBridge.unitChecked { completion -> + call(nativeOptions.ptr, session.ptr, completion) + } + val handle = + RenderSessionHandle( + map, + session.value.asHandle("mln_render_session", ::renderSessionHandle), + ) + try { + RenderSessionAttachment(handle, retainSessionUntilComplete(handle, completed)) + } catch (error: Throwable) { + runCatching { handle.abandon() } + runCatching { handle.close() } + throw error + } + } + internal fun attachMetalOwnedTexture( map: MapHandle, descriptor: MetalOwnedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_metal_owned_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.metalOwnedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachMetalBorrowedTexture( map: MapHandle, descriptor: MetalBorrowedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_metal_borrowed_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.metalBorrowedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachVulkanOwnedTexture( map: MapHandle, descriptor: VulkanOwnedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_vulkan_owned_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.vulkanOwnedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachVulkanBorrowedTexture( map: MapHandle, descriptor: VulkanBorrowedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_vulkan_borrowed_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.vulkanBorrowedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachOpenGLOwnedTexture( map: MapHandle, descriptor: OpenGLOwnedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_opengl_owned_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.openglOwnedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachOpenGLBorrowedTexture( map: MapHandle, descriptor: OpenGLBorrowedTextureDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_opengl_borrowed_texture_attach( map.nativeHandle().rawHandleValue, RenderStructs.openglBorrowedTextureDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachMetalSurface( map: MapHandle, descriptor: MetalSurfaceDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_metal_surface_attach( map.nativeHandle().rawHandleValue, RenderStructs.metalSurfaceDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachVulkanSurface( map: MapHandle, descriptor: VulkanSurfaceDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_vulkan_surface_attach( map.nativeHandle().rawHandleValue, RenderStructs.vulkanSurfaceDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } } internal fun attachOpenGLSurface( map: MapHandle, descriptor: OpenGLSurfaceDescriptor, - ): RenderSessionHandle = memScoped { - val outSession = alloc() - outSession.value = 0uL - Status.check( + options: RenderSessionAttachOptions, + ) = memScoped { + attach(map, options) { o, s, p -> mln_opengl_surface_attach( map.nativeHandle().rawHandleValue, RenderStructs.openglSurfaceDescriptor(descriptor, this), - outSession.ptr, + o, + s, + p, ) - ) - RenderSessionHandle( - map, - outSession.value.asHandle("mln_render_session", ::renderSessionHandle), - ) + } + } + } +} + +@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) +public actual class AcquiredFrameHandle +internal constructor( + private val session: RenderSessionHandle, + private var frame: ULong, + private val scope: FrameScope, +) { + private val released = AtomicInt(0) + public actual val isReleased: Boolean + get() = released.load() != 0 + + public actual fun result(): RenderFrameResult = memScoped { + val v = alloc() + v.size = sizeOf().toUInt() + Status.check(mln_acquired_frame_get_result(requireFrame(), v.ptr)) + frameResult(v) + } + + public actual fun producerSync(): GpuSync = memScoped { + val v = alloc() + mln_gpu_sync_default().place(v.ptr) + Status.check(mln_acquired_frame_get_producer_sync(requireFrame(), v.ptr)) + GpuSync( + GpuSyncKind.fromNative(v.kind.toInt()), + v.`object`?.rawValue?.toLong()?.toULong() ?: 0u, + v.value, + ) + } + + public actual fun metalTexture(): MetalOwnedTextureFrame = memScoped { + val v = alloc() + v.size = sizeOf().toUInt() + Status.check(mln_acquired_frame_get_metal_texture(requireFrame(), v.ptr)) + MetalOwnedTextureFrame( + scope, + v.generation.toLong(), + v.width.toInt(), + v.height.toInt(), + v.scale_factor, + v.frame_id.toLong(), + scoped(v.texture), + scoped(v.device), + v.pixel_format.toLong(), + ) + } + + public actual fun vulkanTexture(): VulkanOwnedTextureFrame = memScoped { + val v = alloc() + v.size = sizeOf().toUInt() + Status.check(mln_acquired_frame_get_vulkan_texture(requireFrame(), v.ptr)) + VulkanOwnedTextureFrame( + scope, + v.generation.toLong(), + v.width.toInt(), + v.height.toInt(), + v.scale_factor, + v.frame_id.toLong(), + scoped(v.image), + scoped(v.image_view), + scoped(v.device), + v.format.toInt(), + v.layout.toInt(), + ) + } + + public actual fun openGLTexture(): OpenGLOwnedTextureFrame = memScoped { + val v = alloc() + v.size = sizeOf().toUInt() + Status.check(mln_acquired_frame_get_opengl_texture(requireFrame(), v.ptr)) + OpenGLOwnedTextureFrame( + scope, + v.generation.toLong(), + v.width.toInt(), + v.height.toInt(), + v.scale_factor, + v.frame_id.toLong(), + v.texture.toInt(), + v.target.toInt(), + v.internal_format.toInt(), + v.format.toInt(), + v.type.toInt(), + ) + } + + public actual fun release(consumerCompletion: GpuSync): Unit = memScoped { + check(released.compareAndSet(0, 1)) { "AcquiredFrameHandle is already released" } + val native = alloc() + mln_gpu_sync_default().place(native.ptr) + native.kind = consumerCompletion.kind.nativeValue.toUInt() + native.`object` = consumerCompletion.objectHandle.toLong().toCPointer() + native.value = consumerCompletion.value + val holder = alloc() + holder.value = frame + try { + Status.check(mln_acquired_frame_release(holder.ptr, native.ptr)) + frame = 0u + scope.close() + session.frameReleased(scope) + } catch (e: Throwable) { + released.store(0) + throw e } } + + private fun requireFrame(): ULong { + scope.ensureActive() + check(!isReleased) { "AcquiredFrameHandle is already released" } + return frame + } + + private fun scoped(pointer: COpaquePointer?): NativePointer = + pointer?.rawValue?.toLong()?.let { NativePointer.scoped(it, scope) } + ?: NativePointer.NULL_POINTER } + +@OptIn(ExperimentalForeignApi::class) +private fun extent(value: RenderTargetExtent, scope: MemScope): CPointer = + scope + .alloc() + .apply { + size = sizeOf().toUInt() + width = value.width.toUInt() + height = value.height.toUInt() + scale_factor = value.scaleFactor + } + .ptr + +@OptIn(ExperimentalForeignApi::class) +private fun frameResult(value: mln_render_frame_result) = + RenderFrameResult( + RenderResult.fromNative(value.disposition), + value.token, + value.map_update_generation, + value.extent_generation, + value.frame_generation, + value.needs_repaint, + ) diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt deleted file mode 100644 index 03437a676..000000000 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/render/VulkanOwnedTextureFrameHandle.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.maplibre.nativeffi.render - -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.Cleaner -import kotlin.native.ref.createCleaner -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.nativeHeap -import kotlinx.cinterop.rawValue -import org.maplibre.nativeffi.internal.c.mln_vulkan_owned_texture_frame - -/** Explicit handle for a Vulkan session-owned texture frame. */ -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -public actual class VulkanOwnedTextureFrameHandle -internal constructor( - private val session: RenderSessionHandle, - private val framePointer: CPointer, - private val scope: FrameScope, - private val frameValue: VulkanOwnedTextureFrame, -) : AutoCloseable { - private val core = OwnedTextureFrameHandleCore("VulkanOwnedTextureFrameHandle") - @Suppress("unused") private val cleaner: Cleaner = createCleaner(core) { it.reportLeak() } - - public actual fun frame(): VulkanOwnedTextureFrame { - core.ensureOpen() - return frameValue - } - - public actual val isClosed: Boolean - get() = core.isClosed() - - public actual override fun close() { - core.close( - releaseNative = { session.releaseVulkanFrame(framePointer) }, - ownerClosed = { session.isClosed }, - releaseLocal = ::releaseLocal, - ) - } - - private fun releaseLocal() { - try { - scope.close() - } finally { - try { - nativeHeap.free(framePointer.rawValue) - } finally { - session.finishFrameBorrow() - } - } - } -} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt deleted file mode 100644 index d688af30b..000000000 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/OfflineOperationHandle.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.Cleaner -import kotlin.native.ref.createCleaner - -@OptIn(ExperimentalNativeApi::class) -public actual class OfflineOperationHandle -internal constructor( - private val runtime: RuntimeHandle, - nativeId: ULong, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, -) : AutoCloseable { - private val runtimeRetention = runtime.retainChild("OfflineOperationHandle") - private val core = - OfflineOperationHandleCore( - runtime, - nativeId.toLong(), - kind, - resultKind, - runtimeRetention::close, - ) - @Suppress("unused") private val cleaner: Cleaner = createCleaner(core.leakReport) { it.report() } - - public actual val id: Long - get() = core.id - - public actual val kind: OfflineOperationKind - get() = core.kind - - public actual val resultKind: OfflineOperationResultKind - get() = core.resultKind - - public actual val isClosed: Boolean - get() = core.isClosed - - internal fun requireLive(expectedRuntime: RuntimeHandle): ULong = - core.requireLive(expectedRuntime).toULong() - - internal fun requireLive( - expectedRuntime: RuntimeHandle, - expectedKind: OfflineOperationKind, - expectedResultKind: OfflineOperationResultKind, - ): ULong = core.requireLive(expectedRuntime, expectedKind, expectedResultKind).toULong() - - internal fun markConsumed() { - core.markConsumed() - } - - public actual override fun close() { - if (!isClosed) runtime.discardOfflineOperation(this) - } -} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt index 0ebd9ecd4..d26d77c85 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandle.kt @@ -1,72 +1,69 @@ package org.maplibre.nativeffi.runtime +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.experimental.ExperimentalNativeApi import kotlin.native.ref.WeakReference -import kotlinx.cinterop.Arena -import kotlinx.cinterop.BooleanVar import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ULongVar import kotlinx.cinterop.alloc +import kotlinx.cinterop.get import kotlinx.cinterop.interpretCPointer import kotlinx.cinterop.memScoped import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret import kotlinx.cinterop.sizeOf import kotlinx.cinterop.toLong import kotlinx.cinterop.value +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.Maplibre -import org.maplibre.nativeffi.error.InvalidStateException +import org.maplibre.nativeffi.internal.async.CompletionBridge +import org.maplibre.nativeffi.internal.c.mln_completion +import org.maplibre.nativeffi.internal.c.mln_completion_result +import org.maplibre.nativeffi.internal.c.mln_event_batch_get +import org.maplibre.nativeffi.internal.c.mln_event_batch_release +import org.maplibre.nativeffi.internal.c.mln_offline_region_info import org.maplibre.nativeffi.internal.c.mln_offline_region_status +import org.maplibre.nativeffi.internal.c.mln_runtime_barrier import org.maplibre.nativeffi.internal.c.mln_runtime_clear_http_header_transform import org.maplibre.nativeffi.internal.c.mln_runtime_clear_resource_provider import org.maplibre.nativeffi.internal.c.mln_runtime_clear_resource_transform import org.maplibre.nativeffi.internal.c.mln_runtime_create -import org.maplibre.nativeffi.internal.c.mln_runtime_destroy import org.maplibre.nativeffi.internal.c.mln_runtime_drain_events import org.maplibre.nativeffi.internal.c.mln_runtime_event -import org.maplibre.nativeffi.internal.c.mln_runtime_event_batch +import org.maplibre.nativeffi.internal.c.mln_runtime_event_batch_view import org.maplibre.nativeffi.internal.c.mln_runtime_get_event_mask -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_operation_discard -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_create_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_create_take_result -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_delete_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get_status_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get_status_take_result -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get_take_result -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_invalidate_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_set_download_state_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_set_observed_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_update_metadata_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_update_metadata_take_result -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_list_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_list_take_result -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_merge_database_start -import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_merge_database_take_result +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_create +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_delete +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_get_status +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_invalidate +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_set_download_state +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_set_observed +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_region_update_metadata +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_list +import org.maplibre.nativeffi.internal.c.mln_runtime_offline_regions_merge_database import org.maplibre.nativeffi.internal.c.mln_runtime_options import org.maplibre.nativeffi.internal.c.mln_runtime_options_default -import org.maplibre.nativeffi.internal.c.mln_runtime_pump -import org.maplibre.nativeffi.internal.c.mln_runtime_run_ambient_cache_operation_start +import org.maplibre.nativeffi.internal.c.mln_runtime_release +import org.maplibre.nativeffi.internal.c.mln_runtime_run_ambient_cache_operation import org.maplibre.nativeffi.internal.c.mln_runtime_set_event_mask import org.maplibre.nativeffi.internal.c.mln_runtime_set_http_header_transform -import org.maplibre.nativeffi.internal.c.mln_runtime_set_maximum_ambient_cache_size_start +import org.maplibre.nativeffi.internal.c.mln_runtime_set_maximum_ambient_cache_size import org.maplibre.nativeffi.internal.c.mln_runtime_set_resource_provider import org.maplibre.nativeffi.internal.c.mln_runtime_set_resource_transform -import org.maplibre.nativeffi.internal.c.mln_runtime_wake_source_acquire import org.maplibre.nativeffi.internal.callback.HttpHeaderTransformState import org.maplibre.nativeffi.internal.callback.ResourceProviderState import org.maplibre.nativeffi.internal.callback.ResourceTransformState import org.maplibre.nativeffi.internal.lifecycle.HandleState -import org.maplibre.nativeffi.internal.lifecycle.HandleStateCore import org.maplibre.nativeffi.internal.lifecycle.NativeRuntime import org.maplibre.nativeffi.internal.lifecycle.asHandle -import org.maplibre.nativeffi.internal.lifecycle.offlineRegionListHandle -import org.maplibre.nativeffi.internal.lifecycle.offlineRegionSnapshotHandle import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue import org.maplibre.nativeffi.internal.lifecycle.runtimeHandle -import org.maplibre.nativeffi.internal.lifecycle.wakeSourceHandle import org.maplibre.nativeffi.internal.memory.MemoryUtil import org.maplibre.nativeffi.internal.status.Status import org.maplibre.nativeffi.internal.struct.RuntimeStructs @@ -79,557 +76,250 @@ import org.maplibre.nativeffi.resource.HttpHeaderTransformCallback import org.maplibre.nativeffi.resource.ResourceProviderCallback import org.maplibre.nativeffi.resource.ResourceTransformCallback -/** Owned native runtime handle. Close it on the owner thread. */ -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -public actual class RuntimeHandle -internal constructor( - handle: NativeRuntime, - private val destroyer: (ULong) -> Int = ::mln_runtime_destroy, -) : AutoCloseable { +/** Owned native runtime handle. */ +@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class, ExperimentalNativeApi::class) +public actual class RuntimeHandle internal constructor(handle: NativeRuntime) { private val state = HandleState("RuntimeHandle", handle) private val liveMaps = mutableMapOf>() - // One batch struct per handle, reused by every drain and freed by close(). - private val batchArena = Arena() - private val batch = batchArena.alloc() - private var resourceTransformState: ResourceTransformState? = null - private var httpHeaderTransformState: HttpHeaderTransformState? = null - private var resourceProviderState: ResourceProviderState? = null - - public actual fun pump(timeoutMillis: Long, budgetMillis: Long) { - Status.check(mln_runtime_pump(state.requireLive().rawHandleValue, timeoutMillis, budgetMillis)) - } - public actual fun acquireWakeSource(): WakeSource = memScoped { - val outSource = alloc() - outSource.value = 0uL - Status.check(mln_runtime_wake_source_acquire(state.requireLive().rawHandleValue, outSource.ptr)) - WakeSource(outSource.value.asHandle("mln_runtime_wake_source_acquire", ::wakeSourceHandle)) + /** Teardown report of the close that consumed this handle. */ + private val tornDown = AtomicReference?>(null) + + public actual fun barrier(): Deferred = CompletionBridge.unit { completion -> + mln_runtime_barrier(state.requireLive().rawHandleValue, completion) } - public actual fun startAmbientCacheOperation( - operation: AmbientCacheOperation - ): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_run_ambient_cache_operation_start( + public actual fun runAmbientCacheOperation(operation: AmbientCacheOperation): Deferred = + CompletionBridge.unit { completion -> + mln_runtime_run_ambient_cache_operation( state.requireLive().rawHandleValue, operation.nativeValue.toUInt(), - outOperationId.ptr, + completion, ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.AMBIENT_CACHE, - OfflineOperationResultKind.NONE, - ) - } + } - public actual fun startSetMaximumAmbientCacheSize(size: Long): OfflineOperationHandle = - memScoped { - Status.requireArgument(size >= 0) { "size must be non-negative" } - val outOperationId = alloc() - Status.check( - mln_runtime_set_maximum_ambient_cache_size_start( - state.requireLive().rawHandleValue, - size.toULong(), - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.SET_MAXIMUM_AMBIENT_CACHE_SIZE, - OfflineOperationResultKind.NONE, + public actual fun setMaximumAmbientCacheSize(size: Long): Deferred { + Status.requireArgument(size >= 0) { "size must be non-negative" } + return CompletionBridge.unit { completion -> + mln_runtime_set_maximum_ambient_cache_size( + state.requireLive().rawHandleValue, + size.toULong(), + completion, ) } + } - public actual fun startCreateOfflineRegion( + public actual fun createOfflineRegion( definition: OfflineRegionDefinition, metadata: ByteArray, - ): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_create_start( - state.requireLive().rawHandleValue, - RuntimeStructs.offlineRegionDefinition(definition, this), - RuntimeStructs.metadata(metadata, this), - metadata.size.toULong(), - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, + ): Deferred = memScoped { + CompletionBridge.submit( + { result -> offlineRegionInfo(result)!! }, + { completion -> + mln_runtime_offline_region_create( + state.requireLive().rawHandleValue, + RuntimeStructs.offlineRegionDefinition(definition, this), + RuntimeStructs.metadata(metadata, this), + metadata.size.toULong(), + completion, + ) + }, ) } - public actual fun startOfflineRegion(id: Long): OfflineOperationHandle = - memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_get_start( - state.requireLive().rawHandleValue, - id, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) - } + public actual fun offlineRegion(id: Long): Deferred = + CompletionBridge.submit( + ::offlineRegionInfo, + { completion -> + mln_runtime_offline_region_get(state.requireLive().rawHandleValue, id, completion) + }, + ) + + public actual fun offlineRegions(): Deferred> = + CompletionBridge.submit( + ::offlineRegionList, + { completion -> + mln_runtime_offline_regions_list(state.requireLive().rawHandleValue, completion) + }, + ) - public actual fun startOfflineRegions(): OfflineOperationHandle> = + public actual fun mergeOfflineRegionsDatabase(path: String): Deferred> = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_regions_list_start( - state.requireLive().rawHandleValue, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, + MemoryUtil.requireValidCString(path) + CompletionBridge.submit( + ::offlineRegionList, + { completion -> + mln_runtime_offline_regions_merge_database( + state.requireLive().rawHandleValue, + path, + completion, + ) + }, ) } - public actual fun startMergeOfflineRegionsDatabase( - path: String - ): OfflineOperationHandle> = memScoped { - MemoryUtil.requireValidCString(path) - val outOperationId = alloc() - Status.check( - mln_runtime_offline_regions_merge_database_start( - state.requireLive().rawHandleValue, - path, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) - } - - public actual fun startUpdateOfflineRegionMetadata( + public actual fun updateOfflineRegionMetadata( id: Long, metadata: ByteArray, - ): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_update_metadata_start( - state.requireLive().rawHandleValue, - id, - RuntimeStructs.metadata(metadata, this), - metadata.size.toULong(), - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, + ): Deferred = memScoped { + CompletionBridge.submit( + { result -> offlineRegionInfo(result)!! }, + { completion -> + mln_runtime_offline_region_update_metadata( + state.requireLive().rawHandleValue, + id, + RuntimeStructs.metadata(metadata, this), + metadata.size.toULong(), + completion, + ) + }, ) } - public actual fun startOfflineRegionStatus( - id: Long - ): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_get_status_start( - state.requireLive().rawHandleValue, - id, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, + public actual fun offlineRegionStatus(id: Long): Deferred = + CompletionBridge.submit( + { result -> + RuntimeStructs.offlineRegionStatus( + result.pointed.value!!.reinterpret().pointed + ) + }, + { completion -> + mln_runtime_offline_region_get_status(state.requireLive().rawHandleValue, id, completion) + }, ) - } - public actual fun startSetOfflineRegionObserved( - id: Long, - observed: Boolean, - ): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_set_observed_start( + public actual fun setOfflineRegionObserved(id: Long, observed: Boolean): Deferred = + CompletionBridge.unit { completion -> + mln_runtime_offline_region_set_observed( state.requireLive().rawHandleValue, id, observed, - outOperationId.ptr, + completion, ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_SET_OBSERVED, - OfflineOperationResultKind.NONE, - ) - } + } - public actual fun startSetOfflineRegionDownloadState( + public actual fun setOfflineRegionDownloadState( id: Long, downloadState: OfflineRegionDownloadState, - ): OfflineOperationHandle = memScoped { + ): Deferred { Status.requireArgument(downloadState.isKnown) { "Unknown offline region download state cannot be used as input: ${downloadState.nativeValue}" } - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_set_download_state_start( + return CompletionBridge.unit { completion -> + mln_runtime_offline_region_set_download_state( state.requireLive().rawHandleValue, id, downloadState.nativeValue.toUInt(), - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_SET_DOWNLOAD_STATE, - OfflineOperationResultKind.NONE, - ) - } - - public actual fun startInvalidateOfflineRegion(id: Long): OfflineOperationHandle = - memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_invalidate_start( - state.requireLive().rawHandleValue, - id, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_INVALIDATE, - OfflineOperationResultKind.NONE, + completion, ) } - - public actual fun startDeleteOfflineRegion(id: Long): OfflineOperationHandle = memScoped { - val outOperationId = alloc() - Status.check( - mln_runtime_offline_region_delete_start( - state.requireLive().rawHandleValue, - id, - outOperationId.ptr, - ) - ) - offlineOperation( - outOperationId.value, - OfflineOperationKind.REGION_DELETE, - OfflineOperationResultKind.NONE, - ) - } - - public actual fun takeCreateOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo = memScoped { - val outRegion = alloc() - outRegion.value = 0uL - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGION_CREATE, - OfflineOperationResultKind.REGION, - ) - Status.check( - mln_runtime_offline_region_create_take_result( - state.requireLive().rawHandleValue, - operationId, - outRegion.ptr, - ) - ) - operation.markConsumed() - RuntimeStructs.offlineRegionSnapshot( - outRegion.value.asHandle("mln_offline_region_snapshot", ::offlineRegionSnapshotHandle) - ) - } - - public actual fun takeOfflineRegionResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo? = memScoped { - val outRegion = alloc() - val outFound = alloc() - outRegion.value = 0uL - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGION_GET, - OfflineOperationResultKind.OPTIONAL_REGION, - ) - Status.check( - mln_runtime_offline_region_get_take_result( - state.requireLive().rawHandleValue, - operationId, - outRegion.ptr, - outFound.ptr, - ) - ) - operation.markConsumed() - if (!outFound.value) null - else - RuntimeStructs.offlineRegionSnapshot( - outRegion.value.asHandle("mln_offline_region_snapshot", ::offlineRegionSnapshotHandle) - ) - } - - public actual fun takeOfflineRegionsResult( - operation: OfflineOperationHandle> - ): List = memScoped { - val outRegions = alloc() - outRegions.value = 0uL - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGIONS_LIST, - OfflineOperationResultKind.REGION_LIST, - ) - Status.check( - mln_runtime_offline_regions_list_take_result( - state.requireLive().rawHandleValue, - operationId, - outRegions.ptr, - ) - ) - operation.markConsumed() - RuntimeStructs.offlineRegionList( - outRegions.value.asHandle("mln_offline_region_list", ::offlineRegionListHandle) - ) } - public actual fun takeMergeOfflineRegionsDatabaseResult( - operation: OfflineOperationHandle> - ): List = memScoped { - val outRegions = alloc() - outRegions.value = 0uL - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGIONS_MERGE_DATABASE, - OfflineOperationResultKind.REGION_LIST, - ) - Status.check( - mln_runtime_offline_regions_merge_database_take_result( - state.requireLive().rawHandleValue, - operationId, - outRegions.ptr, - ) - ) - operation.markConsumed() - RuntimeStructs.offlineRegionList( - outRegions.value.asHandle("mln_offline_region_list", ::offlineRegionListHandle) - ) - } + public actual fun invalidateOfflineRegion(id: Long): Deferred = + CompletionBridge.unit { completion -> + mln_runtime_offline_region_invalidate(state.requireLive().rawHandleValue, id, completion) + } - public actual fun takeUpdateOfflineRegionMetadataResult( - operation: OfflineOperationHandle - ): OfflineRegionInfo = memScoped { - val outRegion = alloc() - outRegion.value = 0uL - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGION_UPDATE_METADATA, - OfflineOperationResultKind.REGION, - ) - Status.check( - mln_runtime_offline_region_update_metadata_take_result( - state.requireLive().rawHandleValue, - operationId, - outRegion.ptr, - ) - ) - operation.markConsumed() - RuntimeStructs.offlineRegionSnapshot( - outRegion.value.asHandle("mln_offline_region_snapshot", ::offlineRegionSnapshotHandle) - ) - } + public actual fun deleteOfflineRegion(id: Long): Deferred = + CompletionBridge.unit { completion -> + mln_runtime_offline_region_delete(state.requireLive().rawHandleValue, id, completion) + } - public actual fun takeOfflineRegionStatusResult( - operation: OfflineOperationHandle - ): OfflineRegionStatus = memScoped { - val outStatus = alloc() - outStatus.size = sizeOf().toUInt() - val operationId = - operation.requireLive( - this@RuntimeHandle, - OfflineOperationKind.REGION_GET_STATUS, - OfflineOperationResultKind.REGION_STATUS, - ) - Status.check( - mln_runtime_offline_region_get_status_take_result( - state.requireLive().rawHandleValue, - operationId, - outStatus.ptr, - ) + private fun offlineRegionInfo(result: CPointer): OfflineRegionInfo? { + if (result.pointed.value_count == 0uL) return null + return RuntimeStructs.offlineRegionInfo( + result.pointed.value!!.reinterpret().pointed ) - operation.markConsumed() - RuntimeStructs.offlineRegionStatus(outStatus) } - private fun offlineOperation( - operationId: ULong, - kind: OfflineOperationKind, - resultKind: OfflineOperationResultKind, - ): OfflineOperationHandle = OfflineOperationHandle(this, operationId, kind, resultKind) - - internal fun discardOfflineOperation(operation: OfflineOperationHandle<*>) { - if (operation.isClosed) return - val id = operation.requireLive(this) - val runtime = - try { - state.requireLive().rawHandleValue - } catch (error: InvalidStateException) { - operation.markConsumed() - throw error - } - Status.check(mln_runtime_offline_operation_discard(runtime, id)) - operation.markConsumed() + private fun offlineRegionList(result: CPointer): List { + val count = result.pointed.value_count + if (count == 0uL) return emptyList() + require(count <= Int.MAX_VALUE.toULong()) { "offline region count exceeds Int.MAX_VALUE" } + val regions = result.pointed.value!!.reinterpret() + return List(count.toInt()) { index -> RuntimeStructs.offlineRegionInfo(regions[index]) } } - public actual fun setResourceProvider(callback: ResourceProviderCallback) { - setResourceProvider(callback) { replacement -> + public actual fun setResourceProvider( + callback: ResourceProviderCallback + ): Deferred { + val replacement = ResourceProviderState(callback) + return command(replacement::close) { completion -> mln_runtime_set_resource_provider( state.requireLive().rawHandleValue, replacement.descriptor(), + completion, ) } } - internal fun setResourceProviderForTesting( - callback: ResourceProviderCallback, - install: (ResourceProviderState) -> Int, - ) { - setResourceProvider(callback, install) - } - - private fun setResourceProvider( - callback: ResourceProviderCallback, - install: (ResourceProviderState) -> Int, - ) { - resourceProviderState?.checkCanClose() - val replacement = ResourceProviderState(callback) - val previous: ResourceProviderState? - try { - Status.check(install(replacement)) - previous = resourceProviderState - resourceProviderState = replacement - } catch (error: Throwable) { - replacement.close() - throw error - } - previous?.close() - } - - public actual fun clearResourceProvider() { - resourceProviderState?.checkCanClose() - Status.check(mln_runtime_clear_resource_provider(state.requireLive().rawHandleValue)) - val previous = resourceProviderState - resourceProviderState = null - previous?.close() + public actual fun clearResourceProvider(): Deferred = command { completion -> + mln_runtime_clear_resource_provider(state.requireLive().rawHandleValue, completion) } - public actual fun setResourceTransform(callback: ResourceTransformCallback) { - setResourceTransform(callback) { replacement -> + public actual fun setResourceTransform( + callback: ResourceTransformCallback + ): Deferred { + val replacement = ResourceTransformState(callback) + return command(replacement::close) { completion -> mln_runtime_set_resource_transform( state.requireLive().rawHandleValue, replacement.descriptor(), + completion, ) } } - internal fun setResourceTransformForTesting( - callback: ResourceTransformCallback, - install: (ResourceTransformState) -> Int, - ) { - setResourceTransform(callback, install) + public actual fun clearResourceTransform(): Deferred = command { completion -> + mln_runtime_clear_resource_transform(state.requireLive().rawHandleValue, completion) } - private fun setResourceTransform( - callback: ResourceTransformCallback, - install: (ResourceTransformState) -> Int, - ) { - resourceTransformState?.checkCanClose() - val replacement = ResourceTransformState(callback) - val previous: ResourceTransformState? - try { - Status.check(install(replacement)) - previous = resourceTransformState - resourceTransformState = replacement - } catch (error: Throwable) { - replacement.close() - throw error - } - previous?.close() - } - - public actual fun clearResourceTransform() { - resourceTransformState?.checkCanClose() - Status.check(mln_runtime_clear_resource_transform(state.requireLive().rawHandleValue)) - val previous = resourceTransformState - resourceTransformState = null - previous?.close() - } - - public actual fun setHttpHeaderTransform(callback: HttpHeaderTransformCallback) { - httpHeaderTransformState?.checkCanClose() + public actual fun setHttpHeaderTransform( + callback: HttpHeaderTransformCallback + ): Deferred { val replacement = HttpHeaderTransformState(callback) - try { - Status.check( - mln_runtime_set_http_header_transform( - state.requireLive().rawHandleValue, - replacement.descriptor(), - ) + return command(replacement::close) { completion -> + mln_runtime_set_http_header_transform( + state.requireLive().rawHandleValue, + replacement.descriptor(), + completion, ) - } catch (error: Throwable) { - replacement.close() - throw error } - val previous = httpHeaderTransformState - httpHeaderTransformState = replacement - previous?.close() } - public actual fun clearHttpHeaderTransform() { - httpHeaderTransformState?.checkCanClose() - Status.check(mln_runtime_clear_http_header_transform(state.requireLive().rawHandleValue)) - val previous = httpHeaderTransformState - httpHeaderTransformState = null - previous?.close() - } + public actual fun clearHttpHeaderTransform(): Deferred = + command { completion -> + mln_runtime_clear_http_header_transform(state.requireLive().rawHandleValue, completion) + } - public actual fun drainEvents(maxEvents: Int): RuntimeEventBatch { - Status.requireArgument(maxEvents >= 0) { "maxEvents must be non-negative" } - val runtime = state.requireLive().rawHandleValue - batch.size = sizeOf().toUInt() - Status.check(mln_runtime_drain_events(runtime, maxEvents.toULong(), batch.ptr)) - val eventCount = batch.event_count - require(eventCount <= Int.MAX_VALUE.toULong()) { "event count exceeds Int.MAX_VALUE" } - val remainingCount = batch.remaining_count - require(remainingCount <= Long.MAX_VALUE.toULong()) { "remaining count exceeds Long.MAX_VALUE" } - if (eventCount == 0uL) { - return RuntimeEventBatch(emptyList(), remainingCount.toLong()) + public actual fun drainEvents(): RuntimeEventBatch = memScoped { + val outBatch = alloc() + outBatch.value = 0uL + Status.check(mln_runtime_drain_events(state.requireLive().rawHandleValue, outBatch.ptr)) + try { + val view = alloc() + view.size = sizeOf().toUInt() + Status.check(mln_event_batch_get(outBatch.value, view.ptr)) + val eventCount = view.event_count + require(eventCount <= Int.MAX_VALUE.toULong()) { "event count exceeds Int.MAX_VALUE" } + val copied = + if (eventCount == 0uL) { + emptyList() + } else { + // The stride the batch reports can exceed sizeOf(), so + // step through the array by it rather than indexing the typed array. + val eventSize = view.event_size.toLong() + val base = view.events!!.rawValue + val messages = view.messages + List(eventCount.toInt()) { index -> + val event = interpretCPointer(base + index * eventSize)!!.pointed + copyEvent(event, messages, eventSize) + } + } + RuntimeEventBatch(copied) + } finally { + mln_event_batch_release(outBatch.value) } - // The stride the batch reports can exceed sizeOf(), so - // step through the array by it rather than indexing the typed array. - val eventSize = batch.event_size.toLong() - val base = batch.events!!.rawValue - val messages = batch.messages - val copied = - List(eventCount.toInt()) { index -> - val event = interpretCPointer(base + index * eventSize)!!.pointed - copyEvent(event, messages, eventSize) - } - return RuntimeEventBatch(copied, remainingCount.toLong()) } public actual var eventMask: RuntimeEventMask @@ -644,19 +334,19 @@ internal constructor( ) } - public actual override fun close() { - resourceProviderState?.checkCanClose() - resourceTransformState?.checkCanClose() - httpHeaderTransformState?.checkCanClose() - state.closeOnce({ runtime -> destroyer(runtime.rawHandleValue) }) { - batchArena.clear() - resourceProviderState?.close() - resourceTransformState?.close() - httpHeaderTransformState?.close() - resourceProviderState = null - resourceTransformState = null - httpHeaderTransformState = null - } + public actual fun close(): Deferred { + if (!state.beginClose()) return tornDown.load() ?: CompletableDeferred(Unit) + val handle = state.handleForClose().rawHandleValue + val completed = + try { + CompletionBridge.unitChecked { completion -> mln_runtime_release(handle, completion) } + } catch (error: Throwable) { + state.abortClose() + throw error + } + tornDown.store(completed) + state.completeClose {} + return completed } public actual val isClosed: Boolean @@ -666,13 +356,6 @@ internal constructor( internal fun nativeHandleId(): Long = state.handleId() - internal fun retainChild(childTypeName: String): HandleStateCore.ChildRetention = - state.retainChild(childTypeName) - - internal fun resourceProviderStateForTesting(): ResourceProviderState? = resourceProviderState - - internal fun resourceTransformStateForTesting(): ResourceTransformState? = resourceTransformState - internal fun copyEventForTesting( event: mln_runtime_event, messages: CPointer?, @@ -734,7 +417,6 @@ internal constructor( options.assetPath?.let { nativeOptions.asset_path = MemoryUtil.cString(this, it) } options.cachePath?.let { nativeOptions.cache_path = MemoryUtil.cString(this, it) } nativeOptions.event_mask = options.eventMask.nativeValue.toULong() - val outRuntime = alloc() outRuntime.value = 0uL Status.check(creator(nativeOptions.ptr, outRuntime.ptr)) @@ -742,3 +424,11 @@ internal constructor( } } } + +@OptIn(ExperimentalForeignApi::class) +private inline fun command( + crossinline rejected: () -> Unit = {}, + crossinline call: (CPointer) -> Int, +): Deferred = CompletionBridge.command { + call(it).also { status -> if (status != 0) rejected() } +} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt deleted file mode 100644 index c9fb0dcff..000000000 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/runtime/WakeSource.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlinx.cinterop.ExperimentalForeignApi -import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.internal.c.mln_wake_source_destroy -import org.maplibre.nativeffi.internal.c.mln_wake_source_signal -import org.maplibre.nativeffi.internal.lifecycle.HandleState -import org.maplibre.nativeffi.internal.lifecycle.NativeWakeSource -import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue -import org.maplibre.nativeffi.internal.status.Status - -/** Owned wake source. Signal and close it from any thread. */ -@OptIn(ExperimentalForeignApi::class) -public actual class WakeSource internal constructor(handle: NativeWakeSource) : AutoCloseable { - private val state = HandleState("WakeSource", handle) - - public actual val isClosed: Boolean - get() = state.isReleased() - - public actual fun signal() { - state.withLive { handle -> Status.check(mln_wake_source_signal(handle.rawHandleValue)) } - } - - public actual override fun close() { - state.closeOnce { source -> - mln_wake_source_destroy(source.rawHandleValue) - MaplibreStatus.OK.nativeCode - } - } -} diff --git a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt index 6f9073b21..50eca6e76 100644 --- a/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt +++ b/bindings/kotlin/src/nativeMain/kotlin/org/maplibre/nativeffi/style/GeoJsonSourceDataHandle.kt @@ -34,7 +34,7 @@ public actual class GeoJsonSourceDataHandle internal constructor(handle: NativeG } } - /** Runs [block] with the live native handle and release held off for the borrow window. */ + /** Runs [block] after checking that this wrapper still owns the native handle. */ internal fun withNativeHandle(block: (NativeGeoJsonSourceData) -> R): R = state.withLive(block) diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/MaplibreNativeTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/MaplibreNativeTest.kt index 68afe3411..98da64d19 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/MaplibreNativeTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/MaplibreNativeTest.kt @@ -13,18 +13,19 @@ import org.maplibre.nativeffi.error.NativeErrorException @OptIn(ExperimentalForeignApi::class) class MaplibreNativeTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun abiVersionMismatchUsesStableBindingError() { - val error = - assertFailsWith { - Maplibre.checkCompatibleCAbi(Maplibre.EXPECTED_C_ABI_VERSION + 1L) - } + fun abiVersionMismatchUsesStableBindingError(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val error = + assertFailsWith { + Maplibre.checkCompatibleCAbi(Maplibre.EXPECTED_C_ABI_VERSION + 1L) + } - assertEquals(MaplibreStatus.NATIVE_ERROR, error.status) - assertIs(error) - assertEquals(MaplibreStatus.NATIVE_ERROR.nativeCode, error.nativeStatusCode) - assertEquals(Maplibre.EXPECTED_C_ABI_VERSION + 1L, error.actualVersion) - assertEquals(Maplibre.EXPECTED_C_ABI_VERSION, error.expectedVersion) - assertTrue(error.diagnostic.contains("C ABI version")) - assertTrue(error.diagnostic.contains("expected ${Maplibre.EXPECTED_C_ABI_VERSION}")) - } + assertEquals(MaplibreStatus.NATIVE_ERROR, error.status) + assertIs(error) + assertEquals(MaplibreStatus.NATIVE_ERROR.nativeCode, error.nativeStatusCode) + assertEquals(Maplibre.EXPECTED_C_ABI_VERSION + 1L, error.actualVersion) + assertEquals(Maplibre.EXPECTED_C_ABI_VERSION, error.expectedVersion) + assertTrue(error.diagnostic.contains("C ABI version")) + assertTrue(error.diagnostic.contains("expected ${Maplibre.EXPECTED_C_ABI_VERSION}")) + } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/NativeTestBase.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/NativeTestBase.kt index f3806851d..76a99d95a 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/NativeTestBase.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/NativeTestBase.kt @@ -17,7 +17,7 @@ open class NativeTestBase { fun installNativeTestLogCallback() { // Native logs share the process output stream with Kotlin/Native test events, so // consume MapLibre records to keep them out of Gradle's test report. - Status.check(mln_log_set_callback(staticCFunction(::consumeNativeTestLog), null)) + Status.check(mln_log_set_callback(staticCFunction(::consumeNativeTestLog), null, null)) } @AfterTest diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateTest.kt index e46d5d65e..b6c7bf93f 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/lifecycle/HandleStateTest.kt @@ -27,29 +27,30 @@ class HandleStateTest : org.maplibre.nativeffi.NativeTestBase() { // BND-040, BND-041, BND-046, BND-048. @Test - fun concurrentReleaseDuringNativeDestroyRejectsSecondCloseAndDestroysOnce() { - val handle = SyntheticHandles.map() - val state = HandleState("TestHandle", handle) - val phase = AtomicInt(0) - val concurrentCloseError = AtomicReference(null) - var attempts = 0 + fun concurrentReleaseDuringNativeDestroyRejectsSecondCloseAndDestroysOnce(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val handle = SyntheticHandles.map() + val state = HandleState("TestHandle", handle) + val phase = AtomicInt(0) + val concurrentCloseError = AtomicReference(null) + var attempts = 0 - runConcurrentClose(ConcurrentHandleClose(state, phase, concurrentCloseError)) { - state.closeOnce { - attempts += 1 - phase.store(PHASE_RELEASING) - waitForPhase(phase, PHASE_CONCURRENT_CLOSE_FINISHED) - MaplibreStatus.OK.nativeCode + runConcurrentClose(ConcurrentHandleClose(state, phase, concurrentCloseError)) { + state.closeOnce { + attempts += 1 + phase.store(PHASE_RELEASING) + waitForPhase(phase, PHASE_CONCURRENT_CLOSE_FINISHED) + MaplibreStatus.OK.nativeCode + } } - } - val error = concurrentCloseError.load() - assertTrue(error is InvalidStateException) - assertEquals(MaplibreStatus.INVALID_STATE, error.status) - assertEquals("TestHandle is currently releasing", error.diagnostic) - assertEquals(1, attempts) - assertTrue(state.isReleased()) - } + val error = concurrentCloseError.load() + assertTrue(error is InvalidStateException) + assertEquals(MaplibreStatus.INVALID_STATE, error.status) + assertEquals("TestHandle is currently releasing", error.diagnostic) + assertEquals(1, attempts) + assertTrue(state.isReleased()) + } private fun runConcurrentClose(close: ConcurrentHandleClose, block: () -> Unit) { memScoped { diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/memory/MemoryUtilTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/memory/MemoryUtilTest.kt index 1695771ed..f1b31c6c6 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/memory/MemoryUtilTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/memory/MemoryUtilTest.kt @@ -11,13 +11,14 @@ import kotlinx.cinterop.ptr @OptIn(ExperimentalForeignApi::class) class MemoryUtilTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun stringViewCopiesRejectOversizedNativeLengths() { - memScoped { - val byte = alloc() + fun stringViewCopiesRejectOversizedNativeLengths(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val byte = alloc() - assertFailsWith { - MemoryUtil.copyStringView(byte.ptr, Int.MAX_VALUE.toULong() + 1UL) + assertFailsWith { + MemoryUtil.copyStringView(byte.ptr, Int.MAX_VALUE.toULong() + 1UL) + } } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/status/StatusTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/status/StatusTest.kt index 15514a877..7090eea7f 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/status/StatusTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/status/StatusTest.kt @@ -24,70 +24,79 @@ class NativeStatusDiagnosticTest : org.maplibre.nativeffi.NativeTestBase() { // BND-020, BND-021, BND-022, BND-023, BND-024, BND-025, BND-026. @Test - fun deterministicNativeStatusProducersThrowMappedExceptionTypes() { - memScoped { - val invalidArgument = - assertFailsWith { Status.check(mln_network_status_set(999_999U)) } - assertEquals(MaplibreStatus.INVALID_ARGUMENT, invalidArgument.status) - assertTrue(invalidArgument.diagnostic.contains("network status")) + fun deterministicNativeStatusProducersThrowMappedExceptionTypes(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val invalidArgument = + assertFailsWith { + Status.check(mln_network_status_set(999_999U)) + } + assertEquals(MaplibreStatus.INVALID_ARGUMENT, invalidArgument.status) + assertTrue(invalidArgument.diagnostic.contains("network status")) - val response = alloc() - response.size = sizeOf().toUInt() - val replacement = "https://example.com/style.json" - val invalidState = - assertFailsWith { - Status.check( - mln_resource_transform_response_set_url( - response.ptr, - replacement, - replacement.length.toULong(), + val response = alloc() + response.size = sizeOf().toUInt() + val replacement = "https://example.com/style.json" + val invalidState = + assertFailsWith { + Status.check( + mln_resource_transform_response_set_url( + response.ptr, + replacement, + replacement.length.toULong(), + ) ) - ) - } - assertEquals(MaplibreStatus.INVALID_STATE, invalidState.status) - assertTrue(invalidState.diagnostic.contains("resource transform")) + } + assertEquals(MaplibreStatus.INVALID_STATE, invalidState.status) + assertTrue(invalidState.diagnostic.contains("resource transform")) + } } - } @Test - fun thrownExceptionCarriesNativeStatusCodeAndCopiedDiagnostic() { - val exception = assertFailsWith { Status.check(-1) } + fun thrownExceptionCarriesNativeStatusCodeAndCopiedDiagnostic(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val exception = assertFailsWith { Status.check(-1) } - assertEquals(MaplibreStatus.INVALID_ARGUMENT, exception.status) - assertEquals(-1, exception.nativeStatusCode) - assertEquals(Status.currentDiagnostic(), exception.diagnostic) - } + assertEquals(MaplibreStatus.INVALID_ARGUMENT, exception.status) + assertEquals(-1, exception.nativeStatusCode) + assertEquals(Status.currentDiagnostic(), exception.diagnostic) + } @Test - fun nativeStatusConversionCapturesDiagnosticImmediately() { - try { - val exception = - assertFailsWith { Status.check(mln_network_status_set(999_999U)) } - val diagnostic = exception.diagnostic + fun nativeStatusConversionCapturesDiagnosticImmediately(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + try { + val exception = + assertFailsWith { + Status.check(mln_network_status_set(999_999U)) + } + val diagnostic = exception.diagnostic - assertEquals(MaplibreStatus.INVALID_ARGUMENT, exception.status) - assertTrue(diagnostic.contains("network status")) + assertEquals(MaplibreStatus.INVALID_ARGUMENT, exception.status) + assertTrue(diagnostic.contains("network status")) - Status.check(mln_network_status_set(NetworkStatus.ONLINE.nativeValue.toUInt())) + Status.check(mln_network_status_set(NetworkStatus.ONLINE.nativeValue.toUInt())) - assertEquals("", Status.currentDiagnostic()) - assertEquals(diagnostic, exception.diagnostic) - } finally { - Status.check(mln_network_status_set(NetworkStatus.ONLINE.nativeValue.toUInt())) + assertEquals("", Status.currentDiagnostic()) + assertEquals(diagnostic, exception.diagnostic) + } finally { + Status.check(mln_network_status_set(NetworkStatus.ONLINE.nativeValue.toUInt())) + } } - } @Test - fun nullTerminatedStringsRejectEmbeddedNul() { - memScoped { - Status.exception(mln_network_status_set(999_999U)) + fun nullTerminatedStringsRejectEmbeddedNul(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + Status.exception(mln_network_status_set(999_999U)) - val error = assertFailsWith { MemoryUtil.cString(this, "a\u0000b") } + val error = + assertFailsWith { MemoryUtil.cString(this, "a\u0000b") } - assertEquals(MaplibreStatus.INVALID_ARGUMENT, error.status) - assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, error.nativeStatusCode) - assertEquals("C string inputs cannot contain embedded NUL characters", error.diagnostic) - assertFalse(error.diagnostic.contains("network status")) + assertEquals(MaplibreStatus.INVALID_ARGUMENT, error.status) + assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, error.nativeStatusCode) + assertEquals("C string inputs cannot contain embedded NUL characters", error.diagnostic) + assertFalse(error.diagnostic.contains("network status")) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/CoreStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/CoreStructsTest.kt index 3837e943c..05f1e08cb 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/CoreStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/CoreStructsTest.kt @@ -13,25 +13,27 @@ class CoreStructsTest : org.maplibre.nativeffi.NativeTestBase() { // BND-063. @Test - fun stringViewsPreserveEmbeddedNulWithExplicitLength() { - memScoped { - val native = alloc() - CoreStructs.stringView("a\u0000b", this).place(native.ptr) + fun stringViewsPreserveEmbeddedNulWithExplicitLength(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val native = alloc() + CoreStructs.stringView("a\u0000b", this).place(native.ptr) - assertEquals(3UL, native.size) - assertEquals("a\u0000b", CoreStructs.stringView(native)) + assertEquals(3UL, native.size) + assertEquals("a\u0000b", CoreStructs.stringView(native)) + } } - } @Test - fun setStringViewPreservesEmbeddedNulWithExplicitLength() { - memScoped { - val native = alloc() + fun setStringViewPreservesEmbeddedNulWithExplicitLength(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val native = alloc() - CoreStructs.setStringView(native, "a\u0000b", this) + CoreStructs.setStringView(native, "a\u0000b", this) - assertEquals(3UL, native.size) - assertEquals("a\u0000b", CoreStructs.stringView(native)) + assertEquals(3UL, native.size) + assertEquals("a\u0000b", CoreStructs.stringView(native)) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/MapStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/MapStructsTest.kt index 91849a844..ea2021768 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/MapStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/MapStructsTest.kt @@ -44,143 +44,150 @@ class MapStructsTest : org.maplibre.nativeffi.NativeTestBase() { // BND-060, BND-061. @Test - fun cameraAndAnimationOptionsInitializeDefaultsAndPresentZeroMasks() { - memScoped { - val absentCamera = MapStructs.cameraOptions(CameraOptions(), this).pointed - assertEquals(sizeOf().toUInt(), absentCamera.size) - assertEquals(0U, absentCamera.fields) - - val camera = - MapStructs.cameraOptions( - CameraOptions().apply { - center = LatLng(0.0, 0.0) - zoom = 0.0 - anchor = ScreenPoint(0.0, 0.0) - }, - this, - ) - .pointed - - assertEquals(sizeOf().toUInt(), camera.size) - assertEquals( - MLN_CAMERA_OPTION_CENTER or MLN_CAMERA_OPTION_ANCHOR or MLN_CAMERA_OPTION_ZOOM, - camera.fields, - ) - assertEquals(0.0, camera.latitude) - assertEquals(0.0, camera.longitude) - assertEquals(0.0, camera.zoom) - assertEquals(0.0, camera.anchor.x) - assertEquals(0.0, camera.anchor.y) - - val animation = - MapStructs.animationOptions( - AnimationOptions().apply { - durationMs = 0.0 - easing = UnitBezier(0.0, 0.0, 1.0, 1.0) - transitionId = 0L - }, - this, - ) - .pointed - - assertEquals(sizeOf().toUInt(), animation.size) - assertEquals( - MLN_ANIMATION_OPTION_DURATION or - MLN_ANIMATION_OPTION_EASING or - MLN_ANIMATION_OPTION_TRANSITION_ID, - animation.fields, - ) - assertEquals(0.0, animation.duration_ms) - assertEquals(0.0, animation.easing.x1) - assertEquals(1.0, animation.easing.x2) - assertEquals(0UL, animation.transition_id) + fun cameraAndAnimationOptionsInitializeDefaultsAndPresentZeroMasks(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val absentCamera = MapStructs.cameraOptions(CameraOptions(), this).pointed + assertEquals(sizeOf().toUInt(), absentCamera.size) + assertEquals(0U, absentCamera.fields) + + val camera = + MapStructs.cameraOptions( + CameraOptions().apply { + center = LatLng(0.0, 0.0) + zoom = 0.0 + anchor = ScreenPoint(0.0, 0.0) + }, + this, + ) + .pointed + + assertEquals(sizeOf().toUInt(), camera.size) + assertEquals( + MLN_CAMERA_OPTION_CENTER or MLN_CAMERA_OPTION_ANCHOR or MLN_CAMERA_OPTION_ZOOM, + camera.fields, + ) + assertEquals(0.0, camera.latitude) + assertEquals(0.0, camera.longitude) + assertEquals(0.0, camera.zoom) + assertEquals(0.0, camera.anchor.x) + assertEquals(0.0, camera.anchor.y) + + val animation = + MapStructs.animationOptions( + AnimationOptions().apply { + durationMs = 0.0 + easing = UnitBezier(0.0, 0.0, 1.0, 1.0) + transitionId = 0L + }, + this, + ) + .pointed + + assertEquals(sizeOf().toUInt(), animation.size) + assertEquals( + MLN_ANIMATION_OPTION_DURATION or + MLN_ANIMATION_OPTION_EASING or + MLN_ANIMATION_OPTION_TRANSITION_ID, + animation.fields, + ) + assertEquals(0.0, animation.duration_ms) + assertEquals(0.0, animation.easing.x1) + assertEquals(1.0, animation.easing.x2) + assertEquals(0UL, animation.transition_id) + } } - } @Test - fun cameraOptionsRoundTripOnlyPresentFields() { - val camera = - CameraOptions().apply { - center = LatLng(12.0, 34.0) - zoom = 5.0 - padding = EdgeInsets(1.0, 2.0, 3.0, 4.0) - anchor = ScreenPoint(8.0, 9.0) + fun cameraOptionsRoundTripOnlyPresentFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val camera = + CameraOptions().apply { + center = LatLng(12.0, 34.0) + zoom = 5.0 + padding = EdgeInsets(1.0, 2.0, 3.0, 4.0) + anchor = ScreenPoint(8.0, 9.0) + } + + val copy = memScoped { + MapStructs.cameraOptions(MapStructs.cameraOptions(camera, this).pointed) } - val copy = memScoped { - MapStructs.cameraOptions(MapStructs.cameraOptions(camera, this).pointed) + assertNotNull(copy.center) + assertEquals(12.0, copy.center?.latitude) + assertEquals(34.0, copy.center?.longitude) + assertNotNull(copy.zoom) + assertEquals(5.0, copy.zoom) + assertNotNull(copy.padding) + assertEquals(EdgeInsets(1.0, 2.0, 3.0, 4.0), copy.padding) + assertNotNull(copy.anchor) + assertEquals(ScreenPoint(8.0, 9.0), copy.anchor) + assertFalse(copy.bearing != null) } - assertNotNull(copy.center) - assertEquals(12.0, copy.center?.latitude) - assertEquals(34.0, copy.center?.longitude) - assertNotNull(copy.zoom) - assertEquals(5.0, copy.zoom) - assertNotNull(copy.padding) - assertEquals(EdgeInsets(1.0, 2.0, 3.0, 4.0), copy.padding) - assertNotNull(copy.anchor) - assertEquals(ScreenPoint(8.0, 9.0), copy.anchor) - assertFalse(copy.bearing != null) - } - @Test - fun viewportAndTileOptionSnapshotsPreserveUnknownEnumRawValues() { - memScoped { - val viewport = alloc() - viewport.size = sizeOf().toUInt() - viewport.fields = - MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION or - MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE or - MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE - viewport.north_orientation = 900U - viewport.constrain_mode = 901U - viewport.viewport_mode = 902U - val tile = alloc() - tile.size = sizeOf().toUInt() - tile.fields = MLN_MAP_TILE_OPTION_LOD_MODE - tile.lod_mode = 903U - - val viewportCopy = MapStructs.viewportOptions(viewport) - val tileCopy = MapStructs.tileOptions(tile) - - assertEquals(NorthOrientation(900), viewportCopy.northOrientation) - assertEquals(ConstrainMode(901), viewportCopy.constrainMode) - assertEquals(ViewportMode(902), viewportCopy.viewportMode) - assertEquals(TileLodMode(903), tileCopy.lodMode) + fun viewportAndTileOptionSnapshotsPreserveUnknownEnumRawValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val viewport = alloc() + viewport.size = sizeOf().toUInt() + viewport.fields = + MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION or + MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE or + MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE + viewport.north_orientation = 900U + viewport.constrain_mode = 901U + viewport.viewport_mode = 902U + val tile = alloc() + tile.size = sizeOf().toUInt() + tile.fields = MLN_MAP_TILE_OPTION_LOD_MODE + tile.lod_mode = 903U + + val viewportCopy = MapStructs.viewportOptions(viewport) + val tileCopy = MapStructs.tileOptions(tile) + + assertEquals(NorthOrientation(900), viewportCopy.northOrientation) + assertEquals(ConstrainMode(901), viewportCopy.constrainMode) + assertEquals(ViewportMode(902), viewportCopy.viewportMode) + assertEquals(TileLodMode(903), tileCopy.lodMode) + } } - } @Test - fun projectionModeOptionsInitializeDefaultsAndPresentZeroMasks() { - memScoped { - val absentProjection = MapStructs.projectionModeOptions(ProjectionModeOptions(), this).pointed - assertEquals(sizeOf().toUInt(), absentProjection.size) - assertEquals(0U, absentProjection.fields) - - val projection = - MapStructs.projectionModeOptions( - ProjectionModeOptions().apply { - axonometric = false - xSkew = 0.0 - ySkew = 0.0 - }, - this, - ) - .pointed - - assertEquals(sizeOf().toUInt(), projection.size) - assertEquals( - MLN_PROJECTION_MODE_AXONOMETRIC or MLN_PROJECTION_MODE_X_SKEW or MLN_PROJECTION_MODE_Y_SKEW, - projection.fields, - ) - assertEquals(false, projection.axonometric) - assertEquals(0.0, projection.x_skew) - assertEquals(0.0, projection.y_skew) - - val copy = MapStructs.projectionModeOptions(projection) - assertEquals(false, copy.axonometric) - assertEquals(0.0, copy.xSkew) - assertEquals(0.0, copy.ySkew) + fun projectionModeOptionsInitializeDefaultsAndPresentZeroMasks(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val absentProjection = + MapStructs.projectionModeOptions(ProjectionModeOptions(), this).pointed + assertEquals(sizeOf().toUInt(), absentProjection.size) + assertEquals(0U, absentProjection.fields) + + val projection = + MapStructs.projectionModeOptions( + ProjectionModeOptions().apply { + axonometric = false + xSkew = 0.0 + ySkew = 0.0 + }, + this, + ) + .pointed + + assertEquals(sizeOf().toUInt(), projection.size) + assertEquals( + MLN_PROJECTION_MODE_AXONOMETRIC or + MLN_PROJECTION_MODE_X_SKEW or + MLN_PROJECTION_MODE_Y_SKEW, + projection.fields, + ) + assertEquals(false, projection.axonometric) + assertEquals(0.0, projection.x_skew) + assertEquals(0.0, projection.y_skew) + + val copy = MapStructs.projectionModeOptions(projection) + assertEquals(false, copy.axonometric) + assertEquals(0.0, copy.xSkew) + assertEquals(0.0, copy.ySkew) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructsTest.kt index fb0b8e2bc..43fa60a72 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/QueryStructsTest.kt @@ -38,78 +38,85 @@ class QueryStructsTest : org.maplibre.nativeffi.NativeTestBase() { // BND-060, BND-061, BND-066, BND-106. @Test - fun renderedQueryGeometryMaterializesBoxesAndLineStrings() { - memScoped { - val box = - QueryStructs.renderedQueryGeometry( - RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))), - this, - ) - .pointed - assertEquals(MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX, box.type) - assertEquals(1.0, box.data.box.min.x) - assertEquals(4.0, box.data.box.max.y) + fun renderedQueryGeometryMaterializesBoxesAndLineStrings(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val box = + QueryStructs.renderedQueryGeometry( + RenderedQueryGeometry.Box(ScreenBox(ScreenPoint(1.0, 2.0), ScreenPoint(3.0, 4.0))), + this, + ) + .pointed + assertEquals(MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX, box.type) + assertEquals(1.0, box.data.box.min.x) + assertEquals(4.0, box.data.box.max.y) - val line = - QueryStructs.renderedQueryGeometry( - RenderedQueryGeometry.LineString(listOf(ScreenPoint(5.0, 6.0), ScreenPoint(7.0, 8.0))), - this, - ) - .pointed - assertEquals(MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING, line.type) - assertEquals(2UL, line.data.line_string.point_count) - assertEquals(7.0, line.data.line_string.points!![1].x) + val line = + QueryStructs.renderedQueryGeometry( + RenderedQueryGeometry.LineString( + listOf(ScreenPoint(5.0, 6.0), ScreenPoint(7.0, 8.0)) + ), + this, + ) + .pointed + assertEquals(MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING, line.type) + assertEquals(2UL, line.data.line_string.point_count) + assertEquals(7.0, line.data.line_string.points!![1].x) + } } - } @Test - fun queryOptionsTrackOptionalFieldsSeparatelyFromFilters() { - memScoped { - val rendered = - QueryStructs.renderedFeatureQueryOptions( - RenderedFeatureQueryOptions().apply { - layerIds = listOf("roads", "labels") - filter = "[\"has\",\"name\"]".encodeToByteArray() - }, - this, - )!! - .pointed - assertTrue((rendered.fields and MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS) != 0U) - assertEquals(2UL, rendered.layer_id_count) - assertNotNull(rendered.filter) + fun queryOptionsTrackOptionalFieldsSeparatelyFromFilters(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val rendered = + QueryStructs.renderedFeatureQueryOptions( + RenderedFeatureQueryOptions().apply { + layerIds = listOf("roads", "labels") + filter = "[\"has\",\"name\"]".encodeToByteArray() + }, + this, + )!! + .pointed + assertTrue((rendered.fields and MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS) != 0U) + assertEquals(2UL, rendered.layer_id_count) + assertNotNull(rendered.filter) - val source = - QueryStructs.sourceFeatureQueryOptions( - SourceFeatureQueryOptions().apply { sourceLayerIds = listOf("water") }, - this, - )!! - .pointed - assertTrue((source.fields and MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS) != 0U) - assertEquals(1UL, source.source_layer_id_count) + val source = + QueryStructs.sourceFeatureQueryOptions( + SourceFeatureQueryOptions().apply { sourceLayerIds = listOf("water") }, + this, + )!! + .pointed + assertTrue((source.fields and MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS) != 0U) + assertEquals(1UL, source.source_layer_id_count) + } } - } @Test - fun featureStateSelectorKeepsStateKeyDependentOnFeatureId() { - assertFailsWith { FeatureStateSelector("source").stateKey = "hover" } + fun featureStateSelectorKeepsStateKeyDependentOnFeatureId(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertFailsWith { + FeatureStateSelector("source").stateKey = "hover" + } - val selector = - FeatureStateSelector("source").apply { - sourceLayerId = "layer" - featureId = "feature-1" - stateKey = "hover" + val selector = + FeatureStateSelector("source").apply { + sourceLayerId = "layer" + featureId = "feature-1" + stateKey = "hover" + } + memScoped { + val native = QueryStructs.featureStateSelector(selector, this).pointed + assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID) != 0U) + assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_FEATURE_ID) != 0U) + assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_STATE_KEY) != 0U) } - memScoped { - val native = QueryStructs.featureStateSelector(selector, this).pointed - assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_SOURCE_LAYER_ID) != 0U) - assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_FEATURE_ID) != 0U) - assertTrue((native.fields and MLN_FEATURE_STATE_SELECTOR_STATE_KEY) != 0U) - } - selector.featureId = null - assertFalse(selector.featureId != null) - assertFalse(selector.stateKey != null) - } + selector.featureId = null + assertFalse(selector.featureId != null) + assertFalse(selector.stateKey != null) + } @Test fun queriedFeatureListCopiesHitsAndDestroysNativeHandle() { diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RenderStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RenderStructsTest.kt index e73ea4444..3f820c191 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RenderStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RenderStructsTest.kt @@ -30,222 +30,226 @@ import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor @OptIn(ExperimentalForeignApi::class) class RenderStructsTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun textureImageInfoCopiesMetadataAndRejectsOversizedLengths() { - memScoped { - val info = alloc() - info.width = 2U - info.height = 3U - info.stride = 8U - info.byte_length = 24UL + fun textureImageInfoCopiesMetadataAndRejectsOversizedLengths(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val info = alloc() + info.width = 2U + info.height = 3U + info.stride = 8U + info.byte_length = 24UL - assertEquals(TextureImageInfo(2, 3, 8, 24), RenderStructs.textureImageInfo(info)) + assertEquals(TextureImageInfo(2, 3, 8, 24), RenderStructs.textureImageInfo(info)) - info.byte_length = Long.MAX_VALUE.toULong() + 1UL - assertFailsWith { RenderStructs.textureImageInfo(info) } + info.byte_length = Long.MAX_VALUE.toULong() + 1UL + assertFailsWith { RenderStructs.textureImageInfo(info) } + } } - } @Test - fun metalDescriptorsMaterializeOpaquePointersAndExtents() { - memScoped { - val extent = RenderTargetExtent(640, 480, 2.0) - val owned = - RenderStructs.metalOwnedTextureDescriptor( - MetalOwnedTextureDescriptor( - extent, - MetalContextDescriptor(NativePointer.ofAddress(0x10L)), - ), - this, - ) - .pointed - assertEquals(640U, owned.extent.width) - assertEquals(2.0, owned.extent.scale_factor) - assertFalse(owned.context.device == null) + fun metalDescriptorsMaterializeOpaquePointersAndExtents(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val extent = RenderTargetExtent(640, 480, 2.0) + val owned = + RenderStructs.metalOwnedTextureDescriptor( + MetalOwnedTextureDescriptor( + extent, + MetalContextDescriptor(NativePointer.ofAddress(0x10L)), + ), + this, + ) + .pointed + assertEquals(640U, owned.extent.width) + assertEquals(2.0, owned.extent.scale_factor) + assertFalse(owned.context.device == null) - val borrowed = - RenderStructs.metalBorrowedTextureDescriptor( - MetalBorrowedTextureDescriptor( - extent, - physicalWidth = 65, - physicalHeight = 33, - texture = NativePointer.ofAddress(0x20L), - ), - this, - ) - .pointed - assertFalse(borrowed.texture == null) - assertEquals(65U, borrowed.physical_width) - assertEquals(33U, borrowed.physical_height) + val borrowed = + RenderStructs.metalBorrowedTextureDescriptor( + MetalBorrowedTextureDescriptor( + extent, + physicalWidth = 65, + physicalHeight = 33, + texture = NativePointer.ofAddress(0x20L), + ), + this, + ) + .pointed + assertFalse(borrowed.texture == null) + assertEquals(65U, borrowed.physical_width) + assertEquals(33U, borrowed.physical_height) - val surface = - RenderStructs.metalSurfaceDescriptor( - MetalSurfaceDescriptor( - extent, - MetalContextDescriptor(NativePointer.NULL_POINTER), - NativePointer.ofAddress(0x30L), - ), - this, - ) - .pointed - assertFalse(surface.layer == null) + val surface = + RenderStructs.metalSurfaceDescriptor( + MetalSurfaceDescriptor( + extent, + MetalContextDescriptor(NativePointer.NULL_POINTER), + NativePointer.ofAddress(0x30L), + ), + this, + ) + .pointed + assertFalse(surface.layer == null) + } } - } @Test - fun vulkanDescriptorsMaterializeContextPointersAndOptionalFinalLayout() { - memScoped { - val context = - VulkanContextDescriptor( - NativePointer.ofAddress(0x10L), - NativePointer.ofAddress(0x20L), - NativePointer.ofAddress(0x30L), - NativePointer.ofAddress(0x40L), - 7, - NativePointer.ofAddress(0x41L), - NativePointer.ofAddress(0x42L), - ) - val borrowed = - RenderStructs.vulkanBorrowedTextureDescriptor( - VulkanBorrowedTextureDescriptor( - RenderTargetExtent(256, 256, 1.0), - 65, - 33, - context, - NativePointer.ofAddress(0x50L), - NativePointer.ofAddress(0x60L), - 44, - 1, - ) - .apply { finalLayout = 2 }, - this, + fun vulkanDescriptorsMaterializeContextPointersAndOptionalFinalLayout(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val context = + VulkanContextDescriptor( + NativePointer.ofAddress(0x10L), + NativePointer.ofAddress(0x20L), + NativePointer.ofAddress(0x30L), + NativePointer.ofAddress(0x40L), + 7, + NativePointer.ofAddress(0x41L), + NativePointer.ofAddress(0x42L), ) - .pointed - assertFalse(borrowed.context.instance == null) - assertEquals(7U, borrowed.context.graphics_queue_family_index) - assertFalse(borrowed.context.get_instance_proc_addr == null) - assertFalse(borrowed.context.get_device_proc_addr == null) - assertEquals(44U, borrowed.format) - assertEquals(2U, borrowed.final_layout) + val borrowed = + RenderStructs.vulkanBorrowedTextureDescriptor( + VulkanBorrowedTextureDescriptor( + RenderTargetExtent(256, 256, 1.0), + 65, + 33, + context, + NativePointer.ofAddress(0x50L), + NativePointer.ofAddress(0x60L), + 44, + 1, + ) + .apply { finalLayout = 2 }, + this, + ) + .pointed + assertFalse(borrowed.context.instance == null) + assertEquals(7U, borrowed.context.graphics_queue_family_index) + assertFalse(borrowed.context.get_instance_proc_addr == null) + assertFalse(borrowed.context.get_device_proc_addr == null) + assertEquals(44U, borrowed.format) + assertEquals(2U, borrowed.final_layout) - val surface = - RenderStructs.vulkanSurfaceDescriptor( - VulkanSurfaceDescriptor( - RenderTargetExtent(256, 256, 1.0), - context, - NativePointer.ofAddress(0x70L), - ), - this, - ) - .pointed - assertFalse(surface.surface == null) + val surface = + RenderStructs.vulkanSurfaceDescriptor( + VulkanSurfaceDescriptor( + RenderTargetExtent(256, 256, 1.0), + context, + NativePointer.ofAddress(0x70L), + ), + this, + ) + .pointed + assertFalse(surface.surface == null) + } } - } @Test - fun openglDescriptorsMaterializeWglAndEglContexts() { - memScoped { - val extent = RenderTargetExtent(320, 240, 1.5) - val wglContext = - org.maplibre.nativeffi.render.WglContextDescriptor( - NativePointer.ofAddress(0x10L), - NativePointer.ofAddress(0x20L), - NativePointer.ofAddress(0x30L), - ) - val owned = - RenderStructs.openglOwnedTextureDescriptor( - OpenGLOwnedTextureDescriptor(extent, wglContext), - this, + fun openglDescriptorsMaterializeWglAndEglContexts(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val extent = RenderTargetExtent(320, 240, 1.5) + val wglContext = + org.maplibre.nativeffi.render.WglContextDescriptor( + NativePointer.ofAddress(0x10L), + NativePointer.ofAddress(0x20L), + NativePointer.ofAddress(0x30L), ) - .pointed - assertEquals(320U, owned.extent.width) - assertFalse(owned.context.data.wgl.device_context == null) - assertFalse(owned.context.data.wgl.share_context == null) - assertFalse(owned.context.data.wgl.get_proc_address == null) + val owned = + RenderStructs.openglOwnedTextureDescriptor( + OpenGLOwnedTextureDescriptor(extent, wglContext), + this, + ) + .pointed + assertEquals(320U, owned.extent.width) + assertFalse(owned.context.data.wgl.device_context == null) + assertFalse(owned.context.data.wgl.share_context == null) + assertFalse(owned.context.data.wgl.get_proc_address == null) - val borrowed = - RenderStructs.openglBorrowedTextureDescriptor( - OpenGLBorrowedTextureDescriptor( - extent, - physicalWidth = 65, - physicalHeight = 33, - context = wglContext, - texture = 17, - target = 3553, - ), - this, - ) - .pointed - assertEquals(17U, borrowed.texture) - assertEquals(3553U, borrowed.target) - assertEquals(65U, borrowed.physical_width) - assertEquals(33U, borrowed.physical_height) + val borrowed = + RenderStructs.openglBorrowedTextureDescriptor( + OpenGLBorrowedTextureDescriptor( + extent, + physicalWidth = 65, + physicalHeight = 33, + context = wglContext, + texture = 17, + target = 3553, + ), + this, + ) + .pointed + assertEquals(17U, borrowed.texture) + assertEquals(3553U, borrowed.target) + assertEquals(65U, borrowed.physical_width) + assertEquals(33U, borrowed.physical_height) - val eglContext = - EglContextDescriptor( - NativePointer.ofAddress(0x40L), - NativePointer.ofAddress(0x50L), - NativePointer.ofAddress(0x60L), - NativePointer.ofAddress(0x70L), - ) - val surface = - RenderStructs.openglSurfaceDescriptor( - OpenGLSurfaceDescriptor(extent, eglContext, NativePointer.ofAddress(0x80L)), - this, + val eglContext = + EglContextDescriptor( + NativePointer.ofAddress(0x40L), + NativePointer.ofAddress(0x50L), + NativePointer.ofAddress(0x60L), + NativePointer.ofAddress(0x70L), ) - .pointed - assertFalse(surface.context.data.egl.display == null) - assertFalse(surface.context.data.egl.config == null) - assertFalse(surface.context.data.egl.share_context == null) - assertFalse(surface.context.data.egl.get_proc_address == null) - assertFalse(surface.surface == null) - assertEquals(OpenGLContextOwnership.SHARED.nativeValue.toUInt(), surface.context.ownership) - assertEquals( - OpenGLClientApi.UNSPECIFIED.nativeValue.toUInt(), - surface.context.data.egl.client_api, - ) - - val dedicatedContext = - EglContextDescriptor( - NativePointer.ofAddress(0x40L), - NativePointer.ofAddress(0x50L), - NativePointer.NULL_POINTER, - NativePointer.ofAddress(0x70L), - clientApi = OpenGLClientApi.GLES, - ownership = OpenGLContextOwnership.DEDICATED, + val surface = + RenderStructs.openglSurfaceDescriptor( + OpenGLSurfaceDescriptor(extent, eglContext, NativePointer.ofAddress(0x80L)), + this, + ) + .pointed + assertFalse(surface.context.data.egl.display == null) + assertFalse(surface.context.data.egl.config == null) + assertFalse(surface.context.data.egl.share_context == null) + assertFalse(surface.context.data.egl.get_proc_address == null) + assertFalse(surface.surface == null) + assertEquals(OpenGLContextOwnership.SHARED.nativeValue.toUInt(), surface.context.ownership) + assertEquals( + OpenGLClientApi.UNSPECIFIED.nativeValue.toUInt(), + surface.context.data.egl.client_api, ) - val dedicatedSurface = - RenderStructs.openglSurfaceDescriptor( - OpenGLSurfaceDescriptor(extent, dedicatedContext, NativePointer.ofAddress(0x80L)), - this, - ) - .pointed - assertEquals( - OpenGLContextOwnership.DEDICATED.nativeValue.toUInt(), - dedicatedSurface.context.ownership, - ) - assertEquals( - OpenGLClientApi.GLES.nativeValue.toUInt(), - dedicatedSurface.context.data.egl.client_api, - ) - assertNull(dedicatedSurface.context.data.egl.share_context) - val unknownContext = - EglContextDescriptor( - NativePointer.ofAddress(0x40L), - NativePointer.ofAddress(0x50L), - NativePointer.NULL_POINTER, - NativePointer.ofAddress(0x70L), - clientApi = OpenGLClientApi(97), - ownership = OpenGLContextOwnership(98), + val dedicatedContext = + EglContextDescriptor( + NativePointer.ofAddress(0x40L), + NativePointer.ofAddress(0x50L), + NativePointer.NULL_POINTER, + NativePointer.ofAddress(0x70L), + clientApi = OpenGLClientApi.GLES, + ownership = OpenGLContextOwnership.DEDICATED, + ) + val dedicatedSurface = + RenderStructs.openglSurfaceDescriptor( + OpenGLSurfaceDescriptor(extent, dedicatedContext, NativePointer.ofAddress(0x80L)), + this, + ) + .pointed + assertEquals( + OpenGLContextOwnership.DEDICATED.nativeValue.toUInt(), + dedicatedSurface.context.ownership, ) - val unknownSurface = - RenderStructs.openglSurfaceDescriptor( - OpenGLSurfaceDescriptor(extent, unknownContext, NativePointer.ofAddress(0x80L)), - this, + assertEquals( + OpenGLClientApi.GLES.nativeValue.toUInt(), + dedicatedSurface.context.data.egl.client_api, + ) + assertNull(dedicatedSurface.context.data.egl.share_context) + + val unknownContext = + EglContextDescriptor( + NativePointer.ofAddress(0x40L), + NativePointer.ofAddress(0x50L), + NativePointer.NULL_POINTER, + NativePointer.ofAddress(0x70L), + clientApi = OpenGLClientApi(97), + ownership = OpenGLContextOwnership(98), ) - .pointed - assertEquals(98U, unknownSurface.context.ownership) - assertEquals(97U, unknownSurface.context.data.egl.client_api) + val unknownSurface = + RenderStructs.openglSurfaceDescriptor( + OpenGLSurfaceDescriptor(extent, unknownContext, NativePointer.ofAddress(0x80L)), + this, + ) + .pointed + assertEquals(98U, unknownSurface.context.ownership) + assertEquals(97U, unknownSurface.context.data.egl.client_api) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeOfflineStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeOfflineStructsTest.kt index 72cea0b45..b86423a3b 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeOfflineStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/RuntimeOfflineStructsTest.kt @@ -26,7 +26,6 @@ import org.maplibre.nativeffi.geo.LatLngBounds import org.maplibre.nativeffi.internal.c.MLN_OFFLINE_REGION_DEFINITION_GEOMETRY import org.maplibre.nativeffi.internal.c.MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED -import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP import org.maplibre.nativeffi.internal.c.MLN_RUNTIME_EVENT_PAYLOAD_TILE_ACTION import org.maplibre.nativeffi.internal.c.mln_offline_region_definition @@ -38,307 +37,297 @@ import org.maplibre.nativeffi.internal.lifecycle.SyntheticHandles import org.maplibre.nativeffi.map.TileOperation import org.maplibre.nativeffi.offline.OfflineRegionDefinition import org.maplibre.nativeffi.render.RenderMode -import org.maplibre.nativeffi.runtime.OfflineOperationKind -import org.maplibre.nativeffi.runtime.OfflineOperationResultKind import org.maplibre.nativeffi.runtime.RuntimeEventPayload @OptIn(ExperimentalForeignApi::class) class RuntimeOfflineStructsTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun offlineRegionDefinitionMaterializesTilePyramidAndGeometryVariants() { - memScoped { - val tilePyramid = - RuntimeStructs.offlineRegionDefinition( - OfflineRegionDefinition.TilePyramid( - "asset://style.json", - LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), - 1.0, - 5.0, - 2.0f, - true, - ), - this, - ) - .pointed - assertEquals(MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID, tilePyramid.type) - assertEquals(1.0, tilePyramid.data.tile_pyramid.bounds.southwest.latitude) - assertEquals(4.0, tilePyramid.data.tile_pyramid.bounds.northeast.longitude) - assertEquals(2.0f, tilePyramid.data.tile_pyramid.pixel_ratio) - - val geometry = - RuntimeStructs.offlineRegionDefinition( - OfflineRegionDefinition.GeometryRegion( - "asset://style.json", - "{\"type\":\"Point\",\"coordinates\":[6,5]}".encodeToByteArray(), - 2.0, - 6.0, - 1.0f, - false, - ), - this, - ) - .pointed - assertEquals(MLN_OFFLINE_REGION_DEFINITION_GEOMETRY, geometry.type) - assertEquals(36UL, geometry.data.geometry.geometry.size) + fun offlineRegionDefinitionMaterializesTilePyramidAndGeometryVariants(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val tilePyramid = + RuntimeStructs.offlineRegionDefinition( + OfflineRegionDefinition.TilePyramid( + "asset://style.json", + LatLngBounds(LatLng(1.0, 2.0), LatLng(3.0, 4.0)), + 1.0, + 5.0, + 2.0f, + true, + ), + this, + ) + .pointed + assertEquals(MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID, tilePyramid.type) + assertEquals(1.0, tilePyramid.data.tile_pyramid.bounds.southwest.latitude) + assertEquals(4.0, tilePyramid.data.tile_pyramid.bounds.northeast.longitude) + assertEquals(2.0f, tilePyramid.data.tile_pyramid.pixel_ratio) + + val geometry = + RuntimeStructs.offlineRegionDefinition( + OfflineRegionDefinition.GeometryRegion( + "asset://style.json", + "{\"type\":\"Point\",\"coordinates\":[6,5]}".encodeToByteArray(), + 2.0, + 6.0, + 1.0f, + false, + ), + this, + ) + .pointed + assertEquals(MLN_OFFLINE_REGION_DEFINITION_GEOMETRY, geometry.type) + assertEquals(36UL, geometry.data.geometry.geometry.size) + } } - } @Test - fun offlineMetadataUsesNullPointerOnlyForEmptyMetadata() { - memScoped { - assertEquals(null, RuntimeStructs.metadata(ByteArray(0), this)) - assertNotNull(RuntimeStructs.metadata(byteArrayOf(1, 2, 3), this)) + fun offlineMetadataUsesNullPointerOnlyForEmptyMetadata(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + assertEquals(null, RuntimeStructs.metadata(ByteArray(0), this)) + assertNotNull(RuntimeStructs.metadata(byteArrayOf(1, 2, 3), this)) + } } - } @Test - fun offlineRegionInfoPreservesUnknownDefinitionDiscriminator() { - memScoped { - val info = alloc() - info.size = sizeOf().toUInt() - info.id = 7 - info.definition.size = sizeOf().toUInt() - info.definition.type = 999U - info.metadata = null - info.metadata_size = 0UL - - val definition = RuntimeStructs.offlineRegionInfo(info).definition - - assertEquals( - OfflineRegionDefinition.Unknown(999, sizeOf().toInt()), - definition, - ) + fun offlineRegionInfoPreservesUnknownDefinitionDiscriminator(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val info = alloc() + info.size = sizeOf().toUInt() + info.id = 7 + info.definition.size = sizeOf().toUInt() + info.definition.type = 999U + info.metadata = null + info.metadata_size = 0UL + + val definition = RuntimeStructs.offlineRegionInfo(info).definition + + assertEquals( + OfflineRegionDefinition.Unknown(999, sizeOf().toInt()), + definition, + ) + } } - } @Test - fun unknownOfflineRegionDefinitionIsOutputOnly() { - memScoped { - assertFailsWith { - RuntimeStructs.offlineRegionDefinition(OfflineRegionDefinition.Unknown(999, 8), this) + fun unknownOfflineRegionDefinitionIsOutputOnly(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + assertFailsWith { + RuntimeStructs.offlineRegionDefinition(OfflineRegionDefinition.Unknown(999, 8), this) + } } } - } @Test - fun offlineRegionSnapshotCopiesInfoAndDestroysNativeHandle() { - memScoped { - var destroys = 0 - val metadata = allocArray(2) - metadata[0] = 4U - metadata[1] = 5U - val snapshot = SyntheticHandles.offlineRegionSnapshot() - val styleUrl = "asset://offline-style.json".cstr.getPointer(this) - - val info = - RuntimeStructs.offlineRegionSnapshot( - snapshot, - getter = { _, outInfo -> - fillValidOfflineRegionInfo(outInfo.pointed, styleUrl, metadata, 2UL) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) - - assertEquals(7, info.id) - assertEquals(validTilePyramidDefinition(), info.definition) - assertContentEquals(byteArrayOf(4, 5), info.metadata) - assertEquals(1, destroys) - } - } + fun offlineRegionSnapshotCopiesInfoAndDestroysNativeHandle(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val metadata = allocArray(2) + metadata[0] = 4U + metadata[1] = 5U + val snapshot = SyntheticHandles.offlineRegionSnapshot() + val styleUrl = "asset://offline-style.json".cstr.getPointer(this) + + val info = + RuntimeStructs.offlineRegionSnapshot( + snapshot, + getter = { _, outInfo -> + fillValidOfflineRegionInfo(outInfo.pointed, styleUrl, metadata, 2UL) + MaplibreStatus.OK.nativeCode + }, + destroyer = { destroys++ }, + ) - @Test - fun offlineRegionSnapshotDestroysNativeHandleWhenCopyFails() { - memScoped { - var destroys = 0 - val metadata = alloc() - val snapshot = SyntheticHandles.offlineRegionSnapshot() - - assertFailsWith { - RuntimeStructs.offlineRegionSnapshot( - snapshot, - getter = { _, outInfo -> - fillOfflineRegionInfoWithOversizedMetadata(outInfo.pointed, metadata.ptr) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) + assertEquals(7, info.id) + assertEquals(validTilePyramidDefinition(), info.definition) + assertContentEquals(byteArrayOf(4, 5), info.metadata) + assertEquals(1, destroys) } - - assertEquals(1, destroys) } - } @Test - fun offlineRegionListCopiesInfoAndDestroysNativeHandle() { - memScoped { - var destroys = 0 - val metadata = allocArray(1) - metadata[0] = 6U - val list = SyntheticHandles.offlineRegionList() - val styleUrl = "asset://offline-style.json".cstr.getPointer(this) - - val regions = - RuntimeStructs.offlineRegionList( - list, - counter = { _, outCount -> - outCount[0] = 1UL - MaplibreStatus.OK.nativeCode - }, - getter = { _, _, outInfo -> - fillValidOfflineRegionInfo(outInfo.pointed, styleUrl, metadata, 1UL) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) + fun offlineRegionSnapshotDestroysNativeHandleWhenCopyFails(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val metadata = alloc() + val snapshot = SyntheticHandles.offlineRegionSnapshot() + + assertFailsWith { + RuntimeStructs.offlineRegionSnapshot( + snapshot, + getter = { _, outInfo -> + fillOfflineRegionInfoWithOversizedMetadata(outInfo.pointed, metadata.ptr) + MaplibreStatus.OK.nativeCode + }, + destroyer = { destroys++ }, + ) + } - assertEquals(1, regions.size) - assertEquals(7, regions.single().id) - assertEquals(validTilePyramidDefinition(), regions.single().definition) - assertContentEquals(byteArrayOf(6), regions.single().metadata) - assertEquals(1, destroys) + assertEquals(1, destroys) + } } - } @Test - fun offlineRegionListDestroysNativeHandleWhenCopyFails() { - memScoped { - var destroys = 0 - val metadata = alloc() - val list = SyntheticHandles.offlineRegionList() - - assertFailsWith { - RuntimeStructs.offlineRegionList( - list, - counter = { _, outCount -> - outCount[0] = 1UL - MaplibreStatus.OK.nativeCode - }, - getter = { _, _, outInfo -> - fillOfflineRegionInfoWithOversizedMetadata(outInfo.pointed, metadata.ptr) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) - } + fun offlineRegionListCopiesInfoAndDestroysNativeHandle(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val metadata = allocArray(1) + metadata[0] = 6U + val list = SyntheticHandles.offlineRegionList() + val styleUrl = "asset://offline-style.json".cstr.getPointer(this) + + val regions = + RuntimeStructs.offlineRegionList( + list, + counter = { _, outCount -> + outCount[0] = 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, outInfo -> + fillValidOfflineRegionInfo(outInfo.pointed, styleUrl, metadata, 1UL) + MaplibreStatus.OK.nativeCode + }, + destroyer = { destroys++ }, + ) - assertEquals(1, destroys) + assertEquals(1, regions.size) + assertEquals(7, regions.single().id) + assertEquals(validTilePyramidDefinition(), regions.single().definition) + assertContentEquals(byteArrayOf(6), regions.single().metadata) + assertEquals(1, destroys) + } } - } @Test - fun unknownRuntimePayloadCopiesTheWholeUnionWindow() { - memScoped { - val event = alloc() - event.payload_type = 999U - val payload = event.payload.ptr.reinterpret() - for (index in 0 until payloadWindowSize) { - payload[index] = 0 + fun offlineRegionListDestroysNativeHandleWhenCopyFails(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val metadata = alloc() + val list = SyntheticHandles.offlineRegionList() + + assertFailsWith { + RuntimeStructs.offlineRegionList( + list, + counter = { _, outCount -> + outCount[0] = 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, outInfo -> + fillOfflineRegionInfoWithOversizedMetadata(outInfo.pointed, metadata.ptr) + MaplibreStatus.OK.nativeCode + }, + destroyer = { destroys++ }, + ) + } + + assertEquals(1, destroys) } - payload[0] = 1 - payload[1] = 2 - payload[2] = 3 - - val result = RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.Unknown - - assertEquals(999, result.rawPayloadType) - assertEquals(payloadWindowSize, result.payloadBytes.size) - assertContentEquals(byteArrayOf(1, 2, 3), result.payloadBytes.take(3).toByteArray()) - - // The copy survives both a write through the source and a write into a - // previously returned array. - payload[0] = 9 - val firstCopy = result.payloadBytes - firstCopy[1] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), result.payloadBytes.take(3).toByteArray()) } - } @Test - fun payloadTypeSelectsWhichUnionMemberOneWindowDecodesAs() { - memScoped { - val event = alloc() - event.payload.camera_transition_finished.transition_id = 0x0000_0007_0000_0384UL - - event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED - val transition = - RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.CameraTransitionFinished - assertEquals(0x0000_0007_0000_0384UL.toLong(), transition.transitionId) - - // The same window read as another member takes that member's own field. - event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP - val renderMap = RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.RenderMap - assertEquals(RenderMode(900), renderMap.mode) + fun unknownRuntimePayloadCopiesTheWholeUnionWindow(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val event = alloc() + event.payload_type = 999U + val payload = event.payload.ptr.reinterpret() + for (index in 0 until payloadWindowSize) { + payload[index] = 0 + } + payload[0] = 1 + payload[1] = 2 + payload[2] = 3 + + val result = RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.Unknown + + assertEquals(999, result.rawPayloadType) + assertEquals(payloadWindowSize, result.payloadBytes.size) + assertContentEquals(byteArrayOf(1, 2, 3), result.payloadBytes.take(3).toByteArray()) + + // The copy survives both a write through the source and a write into a + // previously returned array. + payload[0] = 9 + val firstCopy = result.payloadBytes + firstCopy[1] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), result.payloadBytes.take(3).toByteArray()) + } } - } @Test - fun typedRuntimePayloadsPreserveUnknownRawEnums() { - memScoped { - val renderMapEvent = alloc() - renderMapEvent.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP - renderMapEvent.payload.render_map.mode = 900U - - val renderMapResult = - RuntimeStructs.payload(renderMapEvent, eventSize) as RuntimeEventPayload.RenderMap - assertEquals(RenderMode(900), renderMapResult.mode) - assertEquals(900, renderMapResult.mode.nativeValue) - - val tileActionEvent = alloc() - tileActionEvent.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_TILE_ACTION - tileActionEvent.payload.tile_action.operation = 901U - tileActionEvent.payload.tile_action.tile_id.overscaled_z = 1U - tileActionEvent.payload.tile_action.tile_id.wrap = -1 - tileActionEvent.payload.tile_action.tile_id.canonical_z = 2U - tileActionEvent.payload.tile_action.tile_id.canonical_x = 3U - tileActionEvent.payload.tile_action.tile_id.canonical_y = 4U - - val tilePayload = - RuntimeStructs.payload(tileActionEvent, eventSize) as RuntimeEventPayload.TileAction - assertEquals(TileOperation(901), tilePayload.operation) - assertEquals(901, tilePayload.operation.nativeValue) - assertEquals(1L, tilePayload.tileId.overscaledZ) - assertEquals(-1, tilePayload.tileId.wrap) - assertEquals(2L, tilePayload.tileId.canonicalZ) - assertEquals(3L, tilePayload.tileId.canonicalX) - assertEquals(4L, tilePayload.tileId.canonicalY) + fun payloadTypeSelectsWhichUnionMemberOneWindowDecodesAs(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val event = alloc() + event.payload.camera_transition_finished.transition_id = 0x0000_0007_0000_0384UL + + event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED + val transition = + RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.CameraTransitionFinished + assertEquals(0x0000_0007_0000_0384UL.toLong(), transition.transitionId) + + // The same window read as another member takes that member's own field. + event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP + val renderMap = RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.RenderMap + assertEquals(RenderMode(900), renderMap.mode) + } } - } @Test - fun offlineOperationCompletedPreservesOperationIdBitPattern() { - memScoped { - val event = alloc() - event.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED - event.payload.offline_operation_completed.operation_id = Long.MAX_VALUE.toULong() + 1UL - event.payload.offline_operation_completed.operation_kind = 902U - event.payload.offline_operation_completed.result_kind = 903U - - val result = - RuntimeStructs.payload(event, eventSize) as RuntimeEventPayload.OfflineOperationCompleted - - assertEquals((Long.MAX_VALUE.toULong() + 1UL).toLong(), result.operationId) - assertEquals(OfflineOperationKind(902), result.operationKind) - assertEquals(902, result.operationKind.nativeValue) - assertEquals(OfflineOperationResultKind(903), result.resultKind) - assertEquals(903, result.resultKind.nativeValue) + fun typedRuntimePayloadsPreserveUnknownRawEnums(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val renderMapEvent = alloc() + renderMapEvent.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP + renderMapEvent.payload.render_map.mode = 900U + + val renderMapResult = + RuntimeStructs.payload(renderMapEvent, eventSize) as RuntimeEventPayload.RenderMap + assertEquals(RenderMode(900), renderMapResult.mode) + assertEquals(900, renderMapResult.mode.nativeValue) + + val tileActionEvent = alloc() + tileActionEvent.payload_type = MLN_RUNTIME_EVENT_PAYLOAD_TILE_ACTION + tileActionEvent.payload.tile_action.operation = 901U + tileActionEvent.payload.tile_action.tile_id.overscaled_z = 1U + tileActionEvent.payload.tile_action.tile_id.wrap = -1 + tileActionEvent.payload.tile_action.tile_id.canonical_z = 2U + tileActionEvent.payload.tile_action.tile_id.canonical_x = 3U + tileActionEvent.payload.tile_action.tile_id.canonical_y = 4U + + val tilePayload = + RuntimeStructs.payload(tileActionEvent, eventSize) as RuntimeEventPayload.TileAction + assertEquals(TileOperation(901), tilePayload.operation) + assertEquals(901, tilePayload.operation.nativeValue) + assertEquals(1L, tilePayload.tileId.overscaledZ) + assertEquals(-1, tilePayload.tileId.wrap) + assertEquals(2L, tilePayload.tileId.canonicalZ) + assertEquals(3L, tilePayload.tileId.canonicalX) + assertEquals(4L, tilePayload.tileId.canonicalY) + } } - } @Test - fun runtimeEventMessageComesFromItsOffsetInTheBatchArena() { - memScoped { - val arena = "firstsecond".encodeToByteArray().toCValues().getPointer(this) - val event = alloc() - event.message_offset = 5U - event.message_size = 6U - - assertEquals("second", RuntimeStructs.message(event, arena)) - - event.message_size = 0U - assertEquals("", RuntimeStructs.message(event, arena)) + fun runtimeEventMessageComesFromItsOffsetInTheBatchArena(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val arena = "firstsecond".encodeToByteArray().toCValues().getPointer(this) + val event = alloc() + event.message_offset = 5U + event.message_size = 6U + + assertEquals("second", RuntimeStructs.message(event, arena)) + + event.message_size = 0U + assertEquals("", RuntimeStructs.message(event, arena)) + } } - } private fun fillOfflineRegionInfoWithOversizedMetadata( info: mln_offline_region_info, diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/StyleStructsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/StyleStructsTest.kt index a9ecf82cd..c5b86414a 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/StyleStructsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/internal/struct/StyleStructsTest.kt @@ -42,209 +42,219 @@ class StyleStructsTest : org.maplibre.nativeffi.NativeTestBase() { // BND-060, BND-061, BND-062, BND-066. @Test - fun styleOptionsInitializeDefaultsAndPresentZeroMasks() { - memScoped { - val absentImage = StyleStructs.styleImageOptions(null, this).pointed - assertEquals(sizeOf().toUInt(), absentImage.size) - assertEquals(0U, absentImage.fields) - - val image = - StyleStructs.styleImageOptions( - StyleImageOptions().apply { - pixelRatio = 0.0f - sdf = false - }, - this, - ) - .pointed - assertEquals(MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO or MLN_STYLE_IMAGE_OPTION_SDF, image.fields) - assertEquals(0.0f, image.pixel_ratio) - assertEquals(false, image.sdf) - - val source = - StyleStructs.tileSourceOptions( - TileSourceOptions().apply { - minZoom = 0.0 - tileSize = 0 - bounds = LatLngBounds(LatLng(0.0, 0.0), LatLng(0.0, 0.0)) - }, - this, - )!! - .pointed - assertEquals(sizeOf().toUInt(), source.size) - assertEquals( - MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM or - MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS or - MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE, - source.fields, - ) - assertEquals(0.0, source.min_zoom) - assertEquals(0U, source.tile_size) - assertEquals(0.0, source.bounds.southwest.latitude) - assertEquals(0.0, source.bounds.northeast.longitude) - } - } + fun styleOptionsInitializeDefaultsAndPresentZeroMasks(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val absentImage = StyleStructs.styleImageOptions(null, this).pointed + assertEquals(sizeOf().toUInt(), absentImage.size) + assertEquals(0U, absentImage.fields) - @Test - fun geoJsonSourceOptionsKeepNativeDefaultsAndMaskPresentZeroValues() { - memScoped { - assertNull(StyleStructs.geoJsonSourceOptions(null, this)) - - val clusterProperties = "{\"weight\":[\"+\",[\"get\",\"weight\"]]}".encodeToByteArray() - val options = - StyleStructs.geoJsonSourceOptions( - GeoJsonSourceOptions().apply { - minZoom = 0.0 - clusterMinPoints = 0 - cluster = false - this.clusterProperties = clusterProperties - }, - this, - )!! - .pointed - - assertEquals(sizeOf().toUInt(), options.size) - assertEquals( - MLN_GEOJSON_SOURCE_OPTION_MIN_ZOOM or - MLN_GEOJSON_SOURCE_OPTION_CLUSTER_MIN_POINTS or - MLN_GEOJSON_SOURCE_OPTION_CLUSTER or - MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES, - options.fields, - ) - assertEquals(0.0, options.min_zoom) - assertEquals(0U, options.cluster_min_points) - assertEquals(false, options.cluster) - assertContentEquals(clusterProperties, ByteStructs.copyBufferView(options.cluster_properties)) - - // Fields left absent keep the values mln_geojson_source_options_default() wrote. - val defaults = alloc() - mln_geojson_source_options_default().place(defaults.ptr) - assertEquals(defaults.max_zoom, options.max_zoom) - assertEquals(defaults.tolerance, options.tolerance) - assertEquals(defaults.tile_size, options.tile_size) - assertEquals(defaults.cluster_radius, options.cluster_radius) + val image = + StyleStructs.styleImageOptions( + StyleImageOptions().apply { + pixelRatio = 0.0f + sdf = false + }, + this, + ) + .pointed + assertEquals(MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO or MLN_STYLE_IMAGE_OPTION_SDF, image.fields) + assertEquals(0.0f, image.pixel_ratio) + assertEquals(false, image.sdf) + + val source = + StyleStructs.tileSourceOptions( + TileSourceOptions().apply { + minZoom = 0.0 + tileSize = 0 + bounds = LatLngBounds(LatLng(0.0, 0.0), LatLng(0.0, 0.0)) + }, + this, + )!! + .pointed + assertEquals(sizeOf().toUInt(), source.size) + assertEquals( + MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM or + MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS or + MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE, + source.fields, + ) + assertEquals(0.0, source.min_zoom) + assertEquals(0U, source.tile_size) + assertEquals(0.0, source.bounds.southwest.latitude) + assertEquals(0.0, source.bounds.northeast.longitude) + } } - } @Test - fun styleIdListCopiesIdsAndDestroysNativeHandle() { - var destroys = 0 - val ids = memScoped { - val list = SyntheticHandles.styleIdList() - - StyleStructs.styleIdList( - list.rawHandleValue, - counter = { _, outCount -> - outCount[0] = 1UL - MaplibreStatus.OK.nativeCode - }, - getter = { _, _, outId -> - CoreStructs.setStringView(outId.pointed, "roads", this) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) - } + fun geoJsonSourceOptionsKeepNativeDefaultsAndMaskPresentZeroValues(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + assertNull(StyleStructs.geoJsonSourceOptions(null, this)) - assertEquals(listOf("roads"), ids) - assertEquals(1, destroys) - } + val clusterProperties = "{\"weight\":[\"+\",[\"get\",\"weight\"]]}".encodeToByteArray() + val options = + StyleStructs.geoJsonSourceOptions( + GeoJsonSourceOptions().apply { + minZoom = 0.0 + clusterMinPoints = 0 + cluster = false + this.clusterProperties = clusterProperties + }, + this, + )!! + .pointed + + assertEquals(sizeOf().toUInt(), options.size) + assertEquals( + MLN_GEOJSON_SOURCE_OPTION_MIN_ZOOM or + MLN_GEOJSON_SOURCE_OPTION_CLUSTER_MIN_POINTS or + MLN_GEOJSON_SOURCE_OPTION_CLUSTER or + MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES, + options.fields, + ) + assertEquals(0.0, options.min_zoom) + assertEquals(0U, options.cluster_min_points) + assertEquals(false, options.cluster) + assertContentEquals( + clusterProperties, + ByteStructs.copyBufferView(options.cluster_properties), + ) + + // Fields left absent keep the values mln_geojson_source_options_default() wrote. + val defaults = alloc() + mln_geojson_source_options_default().place(defaults.ptr) + assertEquals(defaults.max_zoom, options.max_zoom) + assertEquals(defaults.tolerance, options.tolerance) + assertEquals(defaults.tile_size, options.tile_size) + assertEquals(defaults.cluster_radius, options.cluster_radius) + } + } @Test - fun styleIdListDestroysNativeHandleWhenCopyFails() { - memScoped { + fun styleIdListCopiesIdsAndDestroysNativeHandle(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { var destroys = 0 - val list = SyntheticHandles.styleIdList() + val ids = memScoped { + val list = SyntheticHandles.styleIdList() - assertFailsWith { StyleStructs.styleIdList( list.rawHandleValue, counter = { _, outCount -> - outCount[0] = Int.MAX_VALUE.toULong() + 1UL + outCount[0] = 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, outId -> + CoreStructs.setStringView(outId.pointed, "roads", this) MaplibreStatus.OK.nativeCode }, - getter = { _, _, _ -> MaplibreStatus.OK.nativeCode }, destroyer = { destroys++ }, ) } + assertEquals(listOf("roads"), ids) assertEquals(1, destroys) } - } @Test - fun styleStringListCopiesValuesAndDestroysOnSuccessAndFailure() { - memScoped { - var destroys = 0 - val values = - StyleStructs.styleStringList( - 123UL, - counter = { _, outCount -> - outCount[0] = 1UL - MaplibreStatus.OK.nativeCode - }, - getter = { _, _, outValue -> - CoreStructs.setStringView(outValue.pointed, "tile", this) - MaplibreStatus.OK.nativeCode - }, - destroyer = { destroys++ }, - ) - assertEquals(listOf("tile"), values) - assertEquals(1, destroys) + fun styleIdListDestroysNativeHandleWhenCopyFails(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val list = SyntheticHandles.styleIdList() - assertFailsWith { - StyleStructs.styleStringList( - 456UL, - counter = { _, outCount -> - outCount[0] = Int.MAX_VALUE.toULong() + 1UL - MaplibreStatus.OK.nativeCode - }, - getter = { _, _, _ -> MaplibreStatus.OK.nativeCode }, - destroyer = { destroys++ }, - ) + assertFailsWith { + StyleStructs.styleIdList( + list.rawHandleValue, + counter = { _, outCount -> + outCount[0] = Int.MAX_VALUE.toULong() + 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, _ -> MaplibreStatus.OK.nativeCode }, + destroyer = { destroys++ }, + ) + } + + assertEquals(1, destroys) } - assertEquals(2, destroys) } - } @Test - fun sourceInfoPreservesUnknownTypeAndStableFields() { - memScoped { - val native = alloc() - native.type = 999U - native.is_volatile = true - - val info = StyleStructs.sourceInfo(native, "source attribution") - - assertEquals(SourceType(999), info.type) - assertEquals(999, info.type.nativeValue) - assertEquals(true, info.volatileSource) - assertEquals("source attribution", info.attribution) + fun styleStringListCopiesValuesAndDestroysOnSuccessAndFailure(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var destroys = 0 + val values = + StyleStructs.styleStringList( + 123UL, + counter = { _, outCount -> + outCount[0] = 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, outValue -> + CoreStructs.setStringView(outValue.pointed, "tile", this) + MaplibreStatus.OK.nativeCode + }, + destroyer = { destroys++ }, + ) + assertEquals(listOf("tile"), values) + assertEquals(1, destroys) + + assertFailsWith { + StyleStructs.styleStringList( + 456UL, + counter = { _, outCount -> + outCount[0] = Int.MAX_VALUE.toULong() + 1UL + MaplibreStatus.OK.nativeCode + }, + getter = { _, _, _ -> MaplibreStatus.OK.nativeCode }, + destroyer = { destroys++ }, + ) + } + assertEquals(2, destroys) + } } - } @Test - fun styleImageInfoCopiesMetadataAndRejectsOverflow() { - memScoped { - val native = alloc() - native.width = 1U - native.height = 2U - native.stride = 8U - native.byte_length = 16UL - native.pixel_ratio = 2.0f - native.sdf = true - - val info = StyleStructs.styleImageInfo(native) - - assertEquals(1, info.width) - assertEquals(2, info.height) - assertEquals(8, info.stride) - assertEquals(16L, info.byteLength) - assertEquals(2.0f, info.pixelRatio) - assertEquals(true, info.sdf) - - native.byte_length = Long.MAX_VALUE.toULong() + 1UL - assertFailsWith { StyleStructs.styleImageInfo(native) } + fun sourceInfoPreservesUnknownTypeAndStableFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val native = alloc() + native.type = 999U + native.is_volatile = true + + val info = StyleStructs.sourceInfo(native, "source attribution") + + assertEquals(SourceType(999), info.type) + assertEquals(999, info.type.nativeValue) + assertEquals(true, info.volatileSource) + assertEquals("source attribution", info.attribution) + } + } + + @Test + fun styleImageInfoCopiesMetadataAndRejectsOverflow(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val native = alloc() + native.width = 1U + native.height = 2U + native.stride = 8U + native.byte_length = 16UL + native.pixel_ratio = 2.0f + native.sdf = true + + val info = StyleStructs.styleImageInfo(native) + + assertEquals(1, info.width) + assertEquals(2, info.height) + assertEquals(8, info.stride) + assertEquals(16L, info.byteLength) + assertEquals(2.0f, info.pixelRatio) + assertEquals(true, info.sdf) + + native.byte_length = Long.MAX_VALUE.toULong() + 1UL + assertFailsWith { StyleStructs.styleImageInfo(native) } + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/log/LogCallbackStateTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/log/LogCallbackStateTest.kt index 8b9f1b3d1..b1091859f 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/log/LogCallbackStateTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/log/LogCallbackStateTest.kt @@ -1,339 +1,50 @@ package org.maplibre.nativeffi.log -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.asStableRef import kotlinx.cinterop.cstr -import kotlinx.cinterop.get import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.staticCFunction import org.maplibre.nativeffi.Maplibre -import org.maplibre.nativeffi.error.InvalidArgumentException -import org.maplibre.nativeffi.error.InvalidStateException -import org.maplibre.nativeffi.error.MaplibreException -import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.callback.LogCallbackState -import platform.posix.pthread_create -import platform.posix.pthread_join -import platform.posix.pthread_tVar -import platform.posix.usleep -@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) +@OptIn(ExperimentalForeignApi::class) class LogCallbackStateTest : org.maplibre.nativeffi.NativeTestBase() { - // BND-120, BND-121, BND-122, BND-123. - @Test - fun processGlobalLogCallbackCopiesRecordsContainsHostFailuresAndClearsState() { - val records = mutableListOf() - val replacementRecords = mutableListOf() - var initialState: LogCallbackState? = null - var replacementState: LogCallbackState? = null - try { - Maplibre.setLogCallback( - LogCallback { record -> - records += record - true - } - ) - initialState = LogCallbackState.currentForTesting() - memScoped { - assertEquals(1U, initialState?.invoke(1U, 3U, 7L, "hello".cstr.getPointer(this))) - } - assertEquals(LogSeverity.INFO, records.single().severity) - assertEquals(LogSeverity.INFO.nativeValue, records.single().severity.nativeValue) - assertEquals(LogEvent.PARSE_STYLE, records.single().event) - assertEquals(LogEvent.PARSE_STYLE.nativeValue, records.single().event.nativeValue) - assertEquals(7L, records.single().code) - assertEquals("hello", records.single().message) - - Maplibre.setLogCallback( + fun callbackCopiesRecordsContainsFailuresAndStopsAfterClose() = memScoped { + var copied: LogRecord? = null + val state = + LogCallbackState.createForTesting( LogCallback { record -> - replacementRecords += record - false - } - ) - replacementState = LogCallbackState.currentForTesting() - assertEquals(0U, initialState?.invoke(1U, 3U, 8L, null)) - memScoped { - assertEquals(0U, replacementState?.invoke(2U, 6U, 9L, "replacement".cstr.getPointer(this))) - } - assertEquals(1, records.size) - assertEquals("replacement", replacementRecords.single().message) - - Maplibre.setLogCallback(LogCallback { throw IllegalStateException("contained") }) - assertEquals(0U, replacementState?.invoke(2U, 6U, 10L, null)) - assertEquals(0U, LogCallbackState.currentForTesting()?.invoke(2U, 6U, 0L, null)) - } finally { - Maplibre.clearLogCallback() - assertNull(LogCallbackState.currentForTesting()) - assertEquals(0U, LogCallbackState.invokeCurrent(1U, 3U, 12L, null)) - assertEquals(0U, replacementState?.invoke(2U, 6U, 11L, null)) - } - } - - @Test - fun logCallbackCopiesMessageAndPreservesUnknownRawEnums() { - var record: LogRecord? = null - try { - Maplibre.setLogCallback( - LogCallback { - record = it - true - } - ) - val state = requireNotNull(LogCallbackState.currentForTesting()) - - memScoped { assertEquals(1U, state.invoke(900U, 901U, 12L, "future".cstr.getPointer(this))) } - - assertEquals(LogSeverity(900), record?.severity) - assertEquals(900, record?.severity?.nativeValue) - assertEquals(LogEvent(901), record?.event) - assertEquals(901, record?.event?.nativeValue) - assertEquals(12L, record?.code) - assertEquals("future", record?.message) - } finally { - Maplibre.clearLogCallback() - } - } - - @Test - fun failedInitialNativeLogInstallClosesReplacementState() { - var replacementState: LogCallbackState? = null - - val error = - assertFailsWith { - LogCallbackState.setForTesting( - LogCallback { true }, - install = { MaplibreStatus.NATIVE_ERROR.nativeCode }, - captureReplacement = { replacementState = it }, - ) - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, error.status) - assertNull(LogCallbackState.currentForTesting()) - assertTrue(replacementState?.isClosedForTesting() == true) - } - - @Test - fun closeDuringLogCallbackAllowsEnteredCallbackAndSuppressesLaterUpcalls() { - var accepted = 0 - lateinit var state: LogCallbackState - try { - Maplibre.setLogCallback( - LogCallback { - state.close() - accepted += 1 - true - } - ) - state = requireNotNull(LogCallbackState.currentForTesting()) - - assertEquals(1U, state.invoke(1U, 3U, 7L, null)) - assertEquals(1, accepted) - assertEquals(0U, state.invoke(1U, 3U, 8L, null)) - } finally { - Maplibre.clearLogCallback() - } - } - - @Test - fun clearDuringLogCallbackRejectsBeforeNativeClear() { - var clearError: Throwable? = null - lateinit var state: LogCallbackState - try { - Maplibre.setLogCallback( - LogCallback { - clearError = assertFailsWith { Maplibre.clearLogCallback() } + copied = record true } ) - state = requireNotNull(LogCallbackState.currentForTesting()) - - assertEquals(1U, state.invoke(1U, 3U, 7L, null)) - assertTrue(clearError is InvalidStateException) - assertTrue(LogCallbackState.currentForTesting() === state) - } finally { - Maplibre.clearLogCallback() - } - } - - @Test - fun setDuringLogCallbackRejectsBeforeNativeInstall() { - var setError: Throwable? = null - lateinit var state: LogCallbackState - try { - Maplibre.setLogCallback( - LogCallback { - setError = - assertFailsWith { Maplibre.setLogCallback(LogCallback { true }) } - true - } - ) - state = requireNotNull(LogCallbackState.currentForTesting()) - - assertEquals(1U, state.invoke(1U, 3U, 7L, null)) - assertTrue(setError is InvalidStateException) - assertTrue(LogCallbackState.currentForTesting() === state) - } finally { - Maplibre.clearLogCallback() - } - } - - @Test - fun concurrentCloseDuringLogCallbackAllowsEnteredCallbackAndSuppressesLaterUpcalls() { - val phase = AtomicInt(LOG_PHASE_READY) - val closeReturned = AtomicInt(0) - val error = AtomicReference(null) - val accepted = AtomicInt(0) - lateinit var state: LogCallbackState - try { - Maplibre.setLogCallback( - LogCallback { - accepted.addAndFetch(1) - phase.store(LOG_PHASE_ENTERED) - waitForLogPhase(phase, LOG_PHASE_RELEASE) - true - } - ) - state = requireNotNull(LogCallbackState.currentForTesting()) - - runLogInvokeOnNativeThread(ConcurrentLogInvoke(state, phase, error)) { - waitForLogPhase(phase, LOG_PHASE_ENTERED) - runNativeAction( - NativeAction(error) { - state.close() - closeReturned.store(1) - } - ) { - waitForLogClosing(state) - assertEquals(0, closeReturned.load()) - assertEquals(0U, state.invoke(1U, 3U, 8L, null)) - phase.store(LOG_PHASE_RELEASE) - } - } - - error.load()?.let { throw it } - assertEquals(1, accepted.load()) - assertEquals(1, closeReturned.load()) - assertTrue(state.isClosedForTesting()) - } finally { - phase.store(LOG_PHASE_RELEASE) - Maplibre.clearLogCallback() - } - } - - @Test - fun logSeverityMasksRejectUnknownInputs() { - assertEquals(1 shl 1, LogSeverity.INFO.nativeMask) - kotlin.test.assertFailsWith { LogSeverity(900).nativeMask } - } - - private fun runLogInvokeOnNativeThread(invocation: ConcurrentLogInvoke, block: () -> Unit) { - runNativeAction(invocation, block) - } - - private fun runNativeAction(action: NativeRunnable, block: () -> Unit) { - memScoped { - val selfRef = StableRef.create(action) - val thread = alloc() - val status = - pthread_create(thread.ptr, null, staticCFunction(::runNativeRunnable), selfRef.asCPointer()) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - try { - block() - } finally { - action.release() - pthread_join(thread.ptr[0], null) - } - } + assertEquals( + 1U, + state.invoke( + LogSeverity.WARNING.nativeValue.toUInt(), + LogEvent.RENDER.nativeValue.toUInt(), + 42, + "warning".cstr.getPointer(this), + ), + ) + assertEquals(LogRecord(LogSeverity.WARNING, LogEvent.RENDER, 42, "warning"), copied) + state.close() + assertEquals(0U, state.invoke(1U, 0U, 0, null)) + assertTrue(state.isClosedForTesting()) + + val throwing = LogCallbackState.createForTesting(LogCallback { error("contained") }) + assertEquals(0U, throwing.invoke(1U, 0U, 0, null)) + throwing.close() + } + + @Test + fun nativeRegistrationReplacesAndClearsOwnedState() { + Maplibre.setLogCallback(LogCallback { true }) + Maplibre.setLogCallback(LogCallback { false }) + Maplibre.clearLogCallback() + Maplibre.clearLogCallback() } } - -@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) -private class ConcurrentLogInvoke( - private val state: LogCallbackState, - private val phase: AtomicInt, - private val error: AtomicReference, -) : NativeRunnable { - override fun run() { - try { - val result = state.invoke(1U, 3U, 7L, null) - if (result != 1U) error.store(AssertionError("expected entered log callback to return 1")) - } catch (throwable: Throwable) { - error.store(throwable) - } finally { - phase.store(LOG_PHASE_FINISHED) - } - } - - override fun release() { - phase.store(LOG_PHASE_RELEASE) - } -} - -@OptIn(ExperimentalAtomicApi::class) -private class NativeAction( - private val error: AtomicReference, - private val block: () -> Unit, -) : NativeRunnable { - override fun run() { - try { - block() - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -private interface NativeRunnable { - fun run() - - fun release() {} -} - -@OptIn(ExperimentalForeignApi::class) -private fun runNativeRunnable(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() - } - return null -} - -@OptIn(ExperimentalAtomicApi::class) -private fun waitForLogPhase(phase: AtomicInt, expected: Int) { - repeat(10_000) { - if (phase.load() == expected) return - usleep(1_000U) - } - error("timed out waiting for log callback phase $expected") -} - -private fun waitForLogClosing(state: LogCallbackState) { - repeat(10_000) { - if (state.isClosingForTesting()) return - usleep(1_000U) - } - error("timed out waiting for log callback close") -} - -private const val LOG_PHASE_READY = 0 -private const val LOG_PHASE_ENTERED = 1 -private const val LOG_PHASE_RELEASE = 2 -private const val LOG_PHASE_FINISHED = 3 diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt index ec2c97e95..5dd1eadaa 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/CustomGeometrySourceStateTest.kt @@ -12,68 +12,70 @@ import org.maplibre.nativeffi.style.CustomGeometrySourceOptions @OptIn(ExperimentalForeignApi::class) class CustomGeometrySourceStateTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun callbacksCopyTileIdsContainHostFailuresAndStopAfterClose() { - val received = mutableListOf() - val state = - CustomGeometrySourceState( - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) { - received += tileId - throw IllegalStateException("contained") - } + fun callbacksCopyTileIdsContainHostFailuresAndStopAfterClose(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val received = mutableListOf() + val state = + CustomGeometrySourceState( + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) { + received += tileId + throw IllegalStateException("contained") + } - override fun cancelTile(tileId: CanonicalTileId) { - received += tileId + override fun cancelTile(tileId: CanonicalTileId) { + received += tileId + } } - } - ) - ) {} - val tile = - cValue { - z = 1U - x = 2U - y = 3U - } + ) + ) {} + val tile = + cValue { + z = 1U + x = 2U + y = 3U + } - state.fetch(tile) - state.cancel(tile) - state.close() - state.fetch(tile) + state.fetch(tile) + state.cancel(tile) + state.close() + state.fetch(tile) - assertEquals(listOf(CanonicalTileId(1, 2, 3), CanonicalTileId(1, 2, 3)), received) - } + assertEquals(listOf(CanonicalTileId(1, 2, 3), CanonicalTileId(1, 2, 3)), received) + } @Test - fun closeDuringInFlightCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls() { - val received = mutableListOf() - lateinit var state: CustomGeometrySourceState - state = - CustomGeometrySourceState( - CustomGeometrySourceOptions( - object : CustomGeometrySourceCallback { - override fun fetchTile(tileId: CanonicalTileId) { - received += tileId - state.close() - } + fun closeDuringInFlightCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val received = mutableListOf() + lateinit var state: CustomGeometrySourceState + state = + CustomGeometrySourceState( + CustomGeometrySourceOptions( + object : CustomGeometrySourceCallback { + override fun fetchTile(tileId: CanonicalTileId) { + received += tileId + state.close() + } - override fun cancelTile(tileId: CanonicalTileId) { - received += tileId + override fun cancelTile(tileId: CanonicalTileId) { + received += tileId + } } - } - ) - ) {} - val tile = - cValue { - z = 4U - x = 5U - y = 6U - } + ) + ) {} + val tile = + cValue { + z = 4U + x = 5U + y = 6U + } - state.fetch(tile) - state.cancel(tile) - state.close() + state.fetch(tile) + state.cancel(tile) + state.close() - assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) - } + assertEquals(listOf(CanonicalTileId(4, 5, 6)), received) + } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt index d39e9d2fd..20e548fe4 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/DescriptorValidationTest.kt @@ -16,48 +16,50 @@ import org.maplibre.nativeffi.resource.ResourceResponseStatus @OptIn(ExperimentalForeignApi::class) class NativeDescriptorValidationTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun enumInputsRejectUnknownSentinelsBeforeNativeCalls() { - assertFailsWith { - MapHandle.mapOptionsForTesting(MapOptions().apply { mapMode = MapMode(900) }) {} - } - memScoped { - assertFailsWith { - MapStructs.tileOptions(TileOptions().apply { lodMode = TileLodMode(901) }, this) - } - assertFailsWith { - MapStructs.viewportOptions( - ViewportOptions().apply { northOrientation = NorthOrientation(902) }, - this, - ) - } + fun enumInputsRejectUnknownSentinelsBeforeNativeCalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { assertFailsWith { - MapStructs.viewportOptions( - ViewportOptions().apply { constrainMode = ConstrainMode(903) }, - this, - ) + MapHandle.mapOptionsForTesting(MapOptions().apply { mapMode = MapMode(900) }) {} } - assertFailsWith { - MapStructs.viewportOptions( - ViewportOptions().apply { viewportMode = ViewportMode(904) }, - this, - ) - } - assertFailsWith { - ResourceStructs.resourceResponse( - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason(905) - errorMessage = "bad" - }, - this, - ) + memScoped { + assertFailsWith { + MapStructs.tileOptions(TileOptions().apply { lodMode = TileLodMode(901) }, this) + } + assertFailsWith { + MapStructs.viewportOptions( + ViewportOptions().apply { northOrientation = NorthOrientation(902) }, + this, + ) + } + assertFailsWith { + MapStructs.viewportOptions( + ViewportOptions().apply { constrainMode = ConstrainMode(903) }, + this, + ) + } + assertFailsWith { + MapStructs.viewportOptions( + ViewportOptions().apply { viewportMode = ViewportMode(904) }, + this, + ) + } + assertFailsWith { + ResourceStructs.resourceResponse( + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason(905) + errorMessage = "bad" + }, + this, + ) + } } } - } @Test - fun canonicalTileMaterializationRejectsOverflow() { - assertFailsWith { - StyleStructs.canonicalTileId(CanonicalTileId(0, UInt.MAX_VALUE.toLong() + 1, 0)) + fun canonicalTileMaterializationRejectsOverflow(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + assertFailsWith { + StyleStructs.canonicalTileId(CanonicalTileId(0, UInt.MAX_VALUE.toLong() + 1, 0)) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/MapHandleNativeTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/MapHandleNativeTest.kt index 173ce7a19..778926070 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/MapHandleNativeTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/map/MapHandleNativeTest.kt @@ -1,33 +1,20 @@ package org.maplibre.nativeffi.map -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.asStableRef -import kotlinx.cinterop.get -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.staticCFunction -import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.error.WrongThreadException +import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate import org.maplibre.nativeffi.runtime.RuntimeHandle -import platform.posix.pthread_create -import platform.posix.pthread_join -import platform.posix.pthread_tVar +import org.maplibre.nativeffi.runtime.RuntimeOptions +import org.maplibre.nativeffi.runtime.runSuspendTest -@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) +@OptIn(ExperimentalForeignApi::class) class MapHandleNativeTest : org.maplibre.nativeffi.NativeTestBase() { - // BND-042, BND-100, BND-190, BND-191. - @Test - fun mapCreationOptionsMaterializeExtentScaleAndMode() { + fun mapCreationOptionsMaterializeExtentScaleAndMode(): Unit = runSuspendTest { MapHandle.mapOptionsForTesting( MapOptions().apply { width = 320 @@ -37,165 +24,34 @@ class MapHandleNativeTest : org.maplibre.nativeffi.NativeTestBase() { fastPforEnabled = true } ) { native -> - assertEquals(320U, native.width) - assertEquals(240U, native.height) - assertEquals(2.0, native.scale_factor) + assertEquals(320U, native.initial_extent.width) + assertEquals(240U, native.initial_extent.height) + assertEquals(2.0, native.initial_extent.scale_factor) assertEquals(MapMode.STATIC.nativeValue.toUInt(), native.map_mode) assertTrue(native.fast_pfor_enabled) } } @Test - fun mapCreationLeavesFastPforDecodingOffByDefault() { - MapHandle.mapOptionsForTesting(MapOptions()) { native -> assertFalse(native.fast_pfor_enabled) } - } - - @Test - fun mapOwnerThreadCallFromAnotherNativeThreadReportsCopiedDiagnostic() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) + fun commandAndSnapshotProgressAcrossCoroutineResumption(): Unit = runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) val map = MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - val callError = AtomicReference(null) - try { - spawnMapCallOnNativeThread(map, callError) - val error = callError.load() - if (error !is WrongThreadException) - throw error ?: AssertionError("wrong-thread map call succeeded") - val diagnostic = error.diagnostic - assertEquals(MaplibreStatus.WRONG_THREAD, error.status) - assertEquals(MaplibreStatus.WRONG_THREAD.nativeCode, error.nativeStatusCode) - assertTrue(diagnostic.isNotBlank()) - - runtime.pump(0) - - assertEquals(diagnostic, error.diagnostic) - } finally { - map.close() - runtime.close() - } - } - - @Test - fun mapCloseFromAnotherNativeThreadReportsWrongThreadAndLeavesHandleLive() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 64 - height = 64 - }, - ) - val closeError = AtomicReference(null) - try { - spawnMapCloseOnNativeThread(map, closeError) - val error = closeError.load() - if (error !is WrongThreadException) - throw error ?: AssertionError("wrong-thread map close succeeded") - assertEquals(MaplibreStatus.WRONG_THREAD, error.status) - assertFalse(map.isClosed) - - map.setStyleJson("{\"version\":8,\"sources\":{},\"layers\":[]}".encodeToByteArray()) - } finally { - map.close() - runtime.close() - } - assertTrue(map.isClosed) - } - - private fun spawnMapCallOnNativeThread(map: MapHandle, callError: AtomicReference) { - memScoped { - val call = BackgroundMapCall(map, callError) - val selfRef = StableRef.create(call) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::runMapCallOnNativeThread), - selfRef.asCPointer(), - ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - pthread_join(thread.ptr[0], null) - } - } - - private fun spawnMapCloseOnNativeThread(map: MapHandle, closeError: AtomicReference) { - memScoped { - val close = BackgroundMapClose(map, closeError) - val selfRef = StableRef.create(close) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::closeMapOnNativeThread), - selfRef.asCPointer(), + runtime, + MapOptions().apply { + width = 64 + height = 64 + }, ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - pthread_join(thread.ptr[0], null) - } - } -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundMapCall( - private val map: MapHandle, - private val error: AtomicReference, -) { - fun run() { - try { - map.setStyleJson("{\"version\":8,\"sources\":{},\"layers\":[]}".encodeToByteArray()) - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun runMapCallOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() - } - return null -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundMapClose( - private val map: MapHandle, - private val error: AtomicReference, -) { - fun run() { - try { - map.close() - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun closeMapOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() + .await() + val command = + map.updateCamera(CameraUpdate(camera = CameraOptions().apply { zoom = 2.0 })).await() + assertTrue(command.generation > 0uL) + runtime.barrier().await() + assertEquals(2.0, map.queryCamera().await().camera.zoom) + map.close() + runtime.close() + assertTrue(map.isClosed) + assertFalse(runtime.isClosed.not()) } - return null } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt index 856482b33..08ae71ab1 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/FrameAcquirePolicyTest.kt @@ -21,37 +21,40 @@ class NativeFrameAcquirePolicyTest : org.maplibre.nativeffi.NativeTestBase() { // BND-026, BND-167, BND-169, BND-172. @Test - fun cleanupNativeFailureDoesNotReplaceOriginalNativeDiagnostic() { - memScoped { - val failure = - assertFailsWith { Status.check(mln_network_status_set(999_999U)) } - val originalDiagnostic = failure.diagnostic - val response = alloc() - response.size = sizeOf().toUInt() - val replacement = "https://example.com/style.json" + fun cleanupNativeFailureDoesNotReplaceOriginalNativeDiagnostic(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val failure = + assertFailsWith { + Status.check(mln_network_status_set(999_999U)) + } + val originalDiagnostic = failure.diagnostic + val response = alloc() + response.size = sizeOf().toUInt() + val replacement = "https://example.com/style.json" - val thrown = - assertFailsWith { - FrameAcquirePolicy.cleanupAfterWrapperFailure( - acquired = true, - releaseNative = { - Status.check( - mln_resource_transform_response_set_url( - response.ptr, - replacement, - replacement.length.toULong(), + val thrown = + assertFailsWith { + FrameAcquirePolicy.cleanupAfterWrapperFailure( + acquired = true, + releaseNative = { + Status.check( + mln_resource_transform_response_set_url( + response.ptr, + replacement, + replacement.length.toULong(), + ) ) - ) - }, - closeLocal = {}, - failure = failure, - ) - } + }, + closeLocal = {}, + failure = failure, + ) + } - assertSame(failure, thrown) - assertEquals(originalDiagnostic, thrown.diagnostic) - assertTrue(thrown.diagnostic.contains("network status")) - assertTrue(Status.currentDiagnostic().contains("resource transform")) + assertSame(failure, thrown) + assertEquals(originalDiagnostic, thrown.diagnostic) + assertTrue(thrown.diagnostic.contains("network status")) + assertTrue(Status.currentDiagnostic().contains("resource transform")) + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/NativeBufferNativeTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/NativeBufferNativeTest.kt index e8f968f1d..018621763 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/NativeBufferNativeTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/render/NativeBufferNativeTest.kt @@ -9,23 +9,25 @@ import org.maplibre.nativeffi.error.InvalidArgumentException @OptIn(ExperimentalForeignApi::class) class NativeBufferNativeTest { @Test - fun capacityValidationUsesTheNativeAllocationLength() { - NativeBuffer.allocate(4L).use { buffer -> - buffer.ensureCapacity(4UL) - assertFailsWith { buffer.ensureCapacity(5UL) } + fun capacityValidationUsesTheNativeAllocationLength(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + NativeBuffer.allocate(4L).use { buffer -> + buffer.ensureCapacity(4UL) + assertFailsWith { buffer.ensureCapacity(5UL) } + } } - } @Test - fun closeDuringPointerBorrowDefersReleaseUntilBorrowReturns() { - val buffer = NativeBuffer.allocate(4L) + fun closeDuringPointerBorrowDefersReleaseUntilBorrowReturns(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val buffer = NativeBuffer.allocate(4L) - buffer.borrow { _, length -> - assertEquals(4L, length) - buffer.close() - assertFailsWith { buffer.byteLength() } - } + buffer.borrow { _, length -> + assertEquals(4L, length) + buffer.close() + assertFailsWith { buffer.byteLength() } + } - assertFailsWith { buffer.toByteArray() } - } + assertFailsWith { buffer.toByteArray() } + } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceProviderStateTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceProviderStateTest.kt index c8a6393ba..b92e0ecae 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceProviderStateTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceProviderStateTest.kt @@ -29,7 +29,6 @@ import org.maplibre.nativeffi.error.InvalidStateException import org.maplibre.nativeffi.error.MaplibreStatus import org.maplibre.nativeffi.internal.c.mln_network_status_set import org.maplibre.nativeffi.internal.c.mln_resource_request -import org.maplibre.nativeffi.internal.c.mln_runtime_destroy import org.maplibre.nativeffi.internal.callback.ResourceProviderState import org.maplibre.nativeffi.internal.lifecycle.SyntheticHandles import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue @@ -44,342 +43,353 @@ class ResourceProviderStateTest : org.maplibre.nativeffi.NativeTestBase() { // BND-141, BND-069. @Test - fun providerCallbackCopiesRequestAndReturnsDecision() { - var copied: ResourceRequest? = null - val state = - ResourceProviderState( - ResourceProviderCallback { request, _ -> - copied = request - ResourceProviderDecision.PASS_THROUGH - } - ) - try { - memScoped { - val request = alloc() - request.requested_url = "maplibre://tiles/2/1/1.pbf".cstr.getPointer(this) - request.resolved_url = "https://example.com/tile.pbf".cstr.getPointer(this) - request.kind = 900U - request.loading_method = 901U - request.priority = 902U - request.usage = 903U - request.storage_policy = 904U - request.has_range = true - request.range_start = 7UL - request.range_end = 11UL - request.has_prior_modified = true - request.prior_modified_unix_ms = 123L - request.has_prior_expires = true - request.prior_expires_unix_ms = 456L - request.prior_etag = "etag".cstr.getPointer(this) - val priorData = allocArray(3) - priorData[0] = 1U - priorData[1] = 2U - priorData[2] = 3U - request.prior_data = priorData - request.prior_data_size = 3UL - val fakeHandle = SyntheticHandles.resourceRequest() - assertEquals( - ResourceProviderDecision.PASS_THROUGH.nativeValue.toUInt(), - state.invoke(request.ptr, fakeHandle.rawHandleValue), + fun providerCallbackCopiesRequestAndReturnsDecision(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copied: ResourceRequest? = null + val state = + ResourceProviderState( + ResourceProviderCallback { request, _ -> + copied = request + ResourceProviderDecision.PASS_THROUGH + } ) + try { + memScoped { + val request = alloc() + request.requested_url = "maplibre://tiles/2/1/1.pbf".cstr.getPointer(this) + request.resolved_url = "https://example.com/tile.pbf".cstr.getPointer(this) + request.kind = 900U + request.loading_method = 901U + request.priority = 902U + request.usage = 903U + request.storage_policy = 904U + request.has_range = true + request.range_start = 7UL + request.range_end = 11UL + request.has_prior_modified = true + request.prior_modified_unix_ms = 123L + request.has_prior_expires = true + request.prior_expires_unix_ms = 456L + request.prior_etag = "etag".cstr.getPointer(this) + val priorData = allocArray(3) + priorData[0] = 1U + priorData[1] = 2U + priorData[2] = 3U + request.prior_data = priorData + request.prior_data_size = 3UL + val fakeHandle = SyntheticHandles.resourceRequest() + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue.toUInt(), + state.invoke(request.ptr, fakeHandle.rawHandleValue), + ) + } + assertEquals(ResourceKind(900), copied?.kind) + assertEquals(900, copied?.kind?.nativeValue) + assertEquals(ResourceLoadingMethod(901), copied?.loadingMethod) + assertEquals(901, copied?.loadingMethod?.nativeValue) + assertEquals(ResourcePriority(902), copied?.priority) + assertEquals(902, copied?.priority?.nativeValue) + assertEquals(ResourceUsage(903), copied?.usage) + assertEquals(903, copied?.usage?.nativeValue) + assertEquals(ResourceStoragePolicy(904), copied?.storagePolicy) + assertEquals(904, copied?.storagePolicy?.nativeValue) + assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) + assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) + assertEquals(ResourceRequest.ByteRange(7, 11), copied?.range) + assertEquals(123L, copied?.priorModifiedUnixMs) + assertEquals(456L, copied?.priorExpiresUnixMs) + assertEquals("etag", copied?.priorEtag) + val firstPriorData = copied?.priorData ?: ByteArray(0) + assertContentEquals(byteArrayOf(1, 2, 3), firstPriorData) + firstPriorData[0] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), copied?.priorData) + } finally { + state.close() } - assertEquals(ResourceKind(900), copied?.kind) - assertEquals(900, copied?.kind?.nativeValue) - assertEquals(ResourceLoadingMethod(901), copied?.loadingMethod) - assertEquals(901, copied?.loadingMethod?.nativeValue) - assertEquals(ResourcePriority(902), copied?.priority) - assertEquals(902, copied?.priority?.nativeValue) - assertEquals(ResourceUsage(903), copied?.usage) - assertEquals(903, copied?.usage?.nativeValue) - assertEquals(ResourceStoragePolicy(904), copied?.storagePolicy) - assertEquals(904, copied?.storagePolicy?.nativeValue) - assertEquals("maplibre://tiles/2/1/1.pbf", copied?.requestedUrl) - assertEquals("https://example.com/tile.pbf", copied?.resolvedUrl) - assertEquals(ResourceRequest.ByteRange(7, 11), copied?.range) - assertEquals(123L, copied?.priorModifiedUnixMs) - assertEquals(456L, copied?.priorExpiresUnixMs) - assertEquals("etag", copied?.priorEtag) - val firstPriorData = copied?.priorData ?: ByteArray(0) - assertContentEquals(byteArrayOf(1, 2, 3), firstPriorData) - firstPriorData[0] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), copied?.priorData) - } finally { - state.close() } - } @Test - fun resourceResponseMaterializerSnapshotsOptionalFields() { - memScoped { - val response = - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = byteArrayOf(1, 2, 3) - etag = "abc" - modifiedUnixMs = 10L - expiresUnixMs = 20L - retryAfterUnixMs = 30L - mustRevalidate = true - } - val native = ResourceStructs.resourceResponse(response, this).pointed - assertEquals(ResourceResponseStatus.OK.nativeValue.toUInt(), native.status) - assertEquals(3UL, native.byte_count) - assertEquals(true, native.must_revalidate) - assertEquals(true, native.has_modified) - assertEquals(true, native.has_expires) - assertEquals(true, native.has_retry_after) - - val bytes = response.bytes - bytes[0] = 9 - assertContentEquals(byteArrayOf(1, 2, 3), response.bytes) + fun resourceResponseMaterializerSnapshotsOptionalFields(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val response = + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = byteArrayOf(1, 2, 3) + etag = "abc" + modifiedUnixMs = 10L + expiresUnixMs = 20L + retryAfterUnixMs = 30L + mustRevalidate = true + } + val native = ResourceStructs.resourceResponse(response, this).pointed + assertEquals(ResourceResponseStatus.OK.nativeValue.toUInt(), native.status) + assertEquals(3UL, native.byte_count) + assertEquals(true, native.must_revalidate) + assertEquals(true, native.has_modified) + assertEquals(true, native.has_expires) + assertEquals(true, native.has_retry_after) + + val bytes = response.bytes + bytes[0] = 9 + assertContentEquals(byteArrayOf(1, 2, 3), response.bytes) + } } - } @Test - fun resourceResponseMaterializerRejectsEmbeddedNulWithBindingInvalidArgument() { - memScoped { - val response = - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason.OTHER - etag = "bad\u0000etag" - } + fun resourceResponseMaterializerRejectsEmbeddedNulWithBindingInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val response = + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason.OTHER + etag = "bad\u0000etag" + } - val error = - assertFailsWith { - ResourceStructs.resourceResponse(response, this) - } + val error = + assertFailsWith { + ResourceStructs.resourceResponse(response, this) + } - assertEquals("ETag contains embedded NUL", error.diagnostic) + assertEquals("ETag contains embedded NUL", error.diagnostic) + } } - } @Test - fun resourceResponseMaterializerRejectsUnknownErrorReasonWithBindingInvalidArgument() { - memScoped { - val response = - ResourceResponse(ResourceResponseStatus.ERROR).apply { - errorReason = ResourceErrorReason(999) - } + fun resourceResponseMaterializerRejectsUnknownErrorReasonWithBindingInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val response = + ResourceResponse(ResourceResponseStatus.ERROR).apply { + errorReason = ResourceErrorReason(999) + } - val error = - assertFailsWith { - ResourceStructs.resourceResponse(response, this) - } + val error = + assertFailsWith { + ResourceStructs.resourceResponse(response, this) + } - assertEquals("Unknown resource error reason cannot be used as input: 999", error.diagnostic) + assertEquals("Unknown resource error reason cannot be used as input: 999", error.diagnostic) + } } - } // BND-142, BND-150, BND-151. // BND-121, BND-123. @Test - fun hostLanguageFailureTellsNativeNotToPassThrough() { - val state = - ResourceProviderState( - ResourceProviderCallback { _, _ -> throw IllegalStateException("contained") } - ) - try { + fun hostLanguageFailureTellsNativeNotToPassThrough(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = + ResourceProviderState( + ResourceProviderCallback { _, _ -> throw IllegalStateException("contained") } + ) + try { + memScoped { + val request = alloc() + request.requested_url = null + request.resolved_url = null + val fakeHandle = SyntheticHandles.resourceRequest() + assertEquals(UInt.MAX_VALUE, state.invoke(request.ptr, fakeHandle.rawHandleValue)) + } + } finally { + state.close() + } + } + + @Test + fun closeDuringProviderCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { memScoped { + var calls = 0 + lateinit var state: ResourceProviderState + state = + ResourceProviderState( + ResourceProviderCallback { _, _ -> + calls += 1 + state.close() + assertFalse(state.isClosedForTesting()) + ResourceProviderDecision.PASS_THROUGH + } + ) val request = alloc() request.requested_url = null request.resolved_url = null val fakeHandle = SyntheticHandles.resourceRequest() - assertEquals(UInt.MAX_VALUE, state.invoke(request.ptr, fakeHandle.rawHandleValue)) - } - } finally { - state.close() - } - } - @Test - fun closeDuringProviderCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls() { - memScoped { - var calls = 0 - lateinit var state: ResourceProviderState - state = - ResourceProviderState( - ResourceProviderCallback { _, _ -> - calls += 1 - state.close() - assertFalse(state.isClosedForTesting()) - ResourceProviderDecision.PASS_THROUGH - } + assertEquals( + ResourceProviderDecision.PASS_THROUGH.nativeValue.toUInt(), + state.invoke(request.ptr, fakeHandle.rawHandleValue), ) - val request = alloc() - request.requested_url = null - request.resolved_url = null - val fakeHandle = SyntheticHandles.resourceRequest() - - assertEquals( - ResourceProviderDecision.PASS_THROUGH.nativeValue.toUInt(), - state.invoke(request.ptr, fakeHandle.rawHandleValue), - ) - assertTrue(state.isClosedForTesting()) - assertEquals(UInt.MAX_VALUE, state.invoke(request.ptr, fakeHandle.rawHandleValue)) - assertEquals(1, calls) - state.close() + assertTrue(state.isClosedForTesting()) + assertEquals(UInt.MAX_VALUE, state.invoke(request.ptr, fakeHandle.rawHandleValue)) + assertEquals(1, calls) + state.close() + } } - } // BND-146, BND-147, BND-148, BND-152, BND-153. @Test - fun completionThatReachesNativeIsTerminalWhenNativeReturnsError() { - memScoped { - var completions = 0 - var releases = 0 - val fakeHandle = SyntheticHandles.resourceRequest() - val handle = - ResourceRequestHandle( - fakeHandle, - completer = { _, _ -> - completions++ - MaplibreStatus.INVALID_STATE.nativeCode - }, - releaser = { releases++ }, + fun completionThatReachesNativeIsTerminalWhenNativeReturnsError(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var completions = 0 + var releases = 0 + val fakeHandle = SyntheticHandles.resourceRequest() + val handle = + ResourceRequestHandle( + fakeHandle, + completer = { _, _ -> + completions++ + MaplibreStatus.INVALID_STATE.nativeCode + }, + releaser = { releases++ }, + ) + + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue.toUInt(), + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), ) + val nativeFailure = + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } + assertEquals(MaplibreStatus.INVALID_STATE.nativeCode, nativeFailure.nativeStatusCode) + assertEquals(1, completions) + assertEquals(1, releases) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue.toUInt(), - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - val nativeFailure = assertFailsWith { handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) } - assertEquals(MaplibreStatus.INVALID_STATE.nativeCode, nativeFailure.nativeStatusCode) - assertEquals(1, completions) - assertEquals(1, releases) - - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + assertEquals(1, completions) + assertEquals(1, releases) } - assertEquals(1, completions) - assertEquals(1, releases) } - } @Test - fun nativeCompletionFailureKeepsDiagnosticCapturedBeforeReleaseCleanup() { - memScoped { - val fakeHandle = SyntheticHandles.resourceRequest() - val handle = - ResourceRequestHandle( - fakeHandle, - completer = { _, _ -> mln_network_status_set(999_999U) }, - releaser = { mln_runtime_destroy(0uL) }, - ) + fun nativeCompletionFailureKeepsDiagnosticCapturedBeforeReleaseCleanup(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val fakeHandle = SyntheticHandles.resourceRequest() + val handle = + ResourceRequestHandle( + fakeHandle, + completer = { _, _ -> mln_network_status_set(999_999U) }, + releaser = {}, + ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue.toUInt(), - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - val nativeFailure = - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) - } + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue.toUInt(), + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + val nativeFailure = + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } - assertTrue(nativeFailure.diagnostic.contains("network status")) - assertFalse(nativeFailure.diagnostic.contains("runtime")) + assertTrue(nativeFailure.diagnostic.contains("network status")) + assertFalse(nativeFailure.diagnostic.contains("runtime")) + } } - } @Test - fun closeDuringCompletionWaitsForCompletionBeforeNativeRelease() { - memScoped { - var releases = 0 - val fakeHandle = SyntheticHandles.resourceRequest() - lateinit var handle: ResourceRequestHandle - handle = - ResourceRequestHandle( - fakeHandle, - completer = { _, _ -> - handle.close() - assertEquals(0, releases) - MaplibreStatus.OK.nativeCode - }, - releaser = { releases++ }, - ) + fun closeDuringCompletionWaitsForCompletionBeforeNativeRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var releases = 0 + val fakeHandle = SyntheticHandles.resourceRequest() + lateinit var handle: ResourceRequestHandle + handle = + ResourceRequestHandle( + fakeHandle, + completer = { _, _ -> + handle.close() + assertEquals(0, releases) + MaplibreStatus.OK.nativeCode + }, + releaser = { releases++ }, + ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue.toUInt(), - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue.toUInt(), + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) - assertEquals(1, releases) - handle.close() - assertEquals(1, releases) + assertEquals(1, releases) + handle.close() + assertEquals(1, releases) + } } - } @Test - fun concurrentCloseDuringCompletionWaitsForCompletionBeforeNativeRelease() { - memScoped { - val phase = AtomicInt(RESOURCE_PHASE_READY) - val error = AtomicReference(null) - val releases = AtomicInt(0) - val fakeHandle = SyntheticHandles.resourceRequest() - val handle = - ResourceRequestHandle( - fakeHandle, - completer = { _, _ -> - phase.store(RESOURCE_PHASE_ENTERED) - waitForResourcePhase(phase, RESOURCE_PHASE_RELEASE) - MaplibreStatus.OK.nativeCode - }, - releaser = { releases.addAndFetch(1) }, - ) + fun concurrentCloseDuringCompletionWaitsForCompletionBeforeNativeRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val phase = AtomicInt(RESOURCE_PHASE_READY) + val error = AtomicReference(null) + val releases = AtomicInt(0) + val fakeHandle = SyntheticHandles.resourceRequest() + val handle = + ResourceRequestHandle( + fakeHandle, + completer = { _, _ -> + phase.store(RESOURCE_PHASE_ENTERED) + waitForResourcePhase(phase, RESOURCE_PHASE_RELEASE) + MaplibreStatus.OK.nativeCode + }, + releaser = { releases.addAndFetch(1) }, + ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue.toUInt(), - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue.toUInt(), + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) - runResourceCompletionOnNativeThread(ConcurrentResourceCompletion(handle, phase, error)) { - waitForResourcePhase(phase, RESOURCE_PHASE_ENTERED) - handle.close() - assertEquals(0, releases.load()) - } + runResourceCompletionOnNativeThread(ConcurrentResourceCompletion(handle, phase, error)) { + waitForResourcePhase(phase, RESOURCE_PHASE_ENTERED) + handle.close() + assertEquals(0, releases.load()) + } - error.load()?.let { throw it } - assertEquals(1, releases.load()) - assertFailsWith { - handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + error.load()?.let { throw it } + assertEquals(1, releases.load()) + assertFailsWith { + handle.complete(ResourceResponse(ResourceResponseStatus.NO_CONTENT)) + } } } - } @Test - fun closeDuringCancellationCheckWaitsForCheckBeforeNativeRelease() { - memScoped { - var releases = 0 - val fakeHandle = SyntheticHandles.resourceRequest() - lateinit var handle: ResourceRequestHandle - handle = - ResourceRequestHandle( - fakeHandle, - cancellationChecker = { _, outCancelled -> - handle.close() - assertEquals(0, releases) - outCancelled.pointed.value = false - MaplibreStatus.OK.nativeCode - }, - releaser = { releases++ }, - ) + fun closeDuringCancellationCheckWaitsForCheckBeforeNativeRelease(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + var releases = 0 + val fakeHandle = SyntheticHandles.resourceRequest() + lateinit var handle: ResourceRequestHandle + handle = + ResourceRequestHandle( + fakeHandle, + cancellationChecker = { _, outCancelled -> + handle.close() + assertEquals(0, releases) + outCancelled.pointed.value = false + MaplibreStatus.OK.nativeCode + }, + releaser = { releases++ }, + ) - assertEquals( - ResourceProviderDecision.HANDLE.nativeValue.toUInt(), - handle.finishProviderDecision(ResourceProviderDecision.HANDLE), - ) - assertEquals(false, handle.isCancelled()) + assertEquals( + ResourceProviderDecision.HANDLE.nativeValue.toUInt(), + handle.finishProviderDecision(ResourceProviderDecision.HANDLE), + ) + assertEquals(false, handle.isCancelled()) - assertEquals(1, releases) - handle.close() - assertEquals(1, releases) + assertEquals(1, releases) + handle.close() + assertEquals(1, releases) + } } - } } @OptIn(ExperimentalAtomicApi::class) diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceTransformStateTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceTransformStateTest.kt index 05b11968e..4e1b5926f 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceTransformStateTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/resource/ResourceTransformStateTest.kt @@ -17,91 +17,95 @@ import org.maplibre.nativeffi.internal.callback.ResourceTransformState @OptIn(ExperimentalForeignApi::class) class ResourceTransformStateTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun transformCallbackCopiesRequestAndPreservesUnknownKindRawValue() { - var copiedRequest: ResourceTransformRequest? = null - val state = - ResourceTransformState( - ResourceTransformCallback { request -> - copiedRequest = request - null + fun transformCallbackCopiesRequestAndPreservesUnknownKindRawValue(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var copiedRequest: ResourceTransformRequest? = null + val state = + ResourceTransformState( + ResourceTransformCallback { request -> + copiedRequest = request + null + } + ) + try { + memScoped { + val out = alloc() + val status = + state.invoke(999U, "https://example.com/style.json".cstr.getPointer(this), out.ptr) + assertEquals(MaplibreStatus.OK.nativeCode, status) + assertNull(out.url) } - ) - try { - memScoped { - val out = alloc() - val status = - state.invoke(999U, "https://example.com/style.json".cstr.getPointer(this), out.ptr) - assertEquals(MaplibreStatus.OK.nativeCode, status) - assertNull(out.url) + assertEquals(ResourceKind(999), copiedRequest?.kind) + assertEquals(999, copiedRequest?.kind?.nativeValue) + assertEquals("https://example.com/style.json", copiedRequest?.url) + } finally { + state.close() } - assertEquals(ResourceKind(999), copiedRequest?.kind) - assertEquals(999, copiedRequest?.kind?.nativeValue) - assertEquals("https://example.com/style.json", copiedRequest?.url) - } finally { - state.close() } - } @Test - fun embeddedNulReplacementReturnsInvalidArgument() { - val state = - ResourceTransformState( - ResourceTransformCallback { "https://cdn.example.com/\u0000truncated" } - ) - try { - memScoped { - val out = alloc() - assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(1U, null, out.ptr)) + fun embeddedNulReplacementReturnsInvalidArgument(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + val state = + ResourceTransformState( + ResourceTransformCallback { "https://cdn.example.com/\u0000truncated" } + ) + try { + memScoped { + val out = alloc() + assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(1U, null, out.ptr)) + } + } finally { + state.close() } - } finally { - state.close() } - } @Test - fun nullEmptyAndThrowingTransformsProduceNoReplacement() { - memScoped { - val nullState = ResourceTransformState(ResourceTransformCallback { null }) - val nullOut = alloc() - assertEquals(MaplibreStatus.OK.nativeCode, nullState.invoke(3U, null, nullOut.ptr)) - assertNull(nullOut.url) - nullState.close() + fun nullEmptyAndThrowingTransformsProduceNoReplacement(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + memScoped { + val nullState = ResourceTransformState(ResourceTransformCallback { null }) + val nullOut = alloc() + assertEquals(MaplibreStatus.OK.nativeCode, nullState.invoke(3U, null, nullOut.ptr)) + assertNull(nullOut.url) + nullState.close() - // BND-121. - val throwingState = - ResourceTransformState( - ResourceTransformCallback { throw IllegalStateException("contained") } + // BND-121. + val throwingState = + ResourceTransformState( + ResourceTransformCallback { throw IllegalStateException("contained") } + ) + val throwingOut = alloc() + assertEquals( + MaplibreStatus.NATIVE_ERROR.nativeCode, + throwingState.invoke(3U, null, throwingOut.ptr), ) - val throwingOut = alloc() - assertEquals( - MaplibreStatus.NATIVE_ERROR.nativeCode, - throwingState.invoke(3U, null, throwingOut.ptr), - ) - throwingState.close() + throwingState.close() + } } - } @Test - fun closeDuringTransformCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls() { - var calls = 0 - lateinit var state: ResourceTransformState - state = - ResourceTransformState( - ResourceTransformCallback { - calls += 1 - state.close() - assertFalse(state.isClosedForTesting()) - null - } - ) - memScoped { - val out = alloc() + fun closeDuringTransformCallbackCompletesAfterCallbackAndSuppressesLaterUpcalls(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + var calls = 0 + lateinit var state: ResourceTransformState + state = + ResourceTransformState( + ResourceTransformCallback { + calls += 1 + state.close() + assertFalse(state.isClosedForTesting()) + null + } + ) + memScoped { + val out = alloc() - assertEquals(MaplibreStatus.OK.nativeCode, state.invoke(1U, null, out.ptr)) - assertTrue(state.isClosedForTesting()) - assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(1U, null, out.ptr)) - assertEquals(1, calls) - state.close() + assertEquals(MaplibreStatus.OK.nativeCode, state.invoke(1U, null, out.ptr)) + assertTrue(state.isClosedForTesting()) + assertEquals(MaplibreStatus.INVALID_ARGUMENT.nativeCode, state.invoke(1U, null, out.ptr)) + assertEquals(1, calls) + state.close() + } } - } } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt index fd67e2c22..59f2c31a7 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeEventsTest.kt @@ -16,7 +16,6 @@ import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import kotlinx.cinterop.staticCFunction import org.maplibre.nativeffi.error.InvalidArgumentException -import org.maplibre.nativeffi.error.WrongThreadException import org.maplibre.nativeffi.geo.CanonicalTileId import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapOptions @@ -34,178 +33,193 @@ import platform.posix.usleep @OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) class RuntimeEventsTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun narrowedMapMaskDropsOneTypeAndKeepsAnother() { - withMap { runtime, map -> - // One style load produces both types, so both are driven after this write. - map.eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - - val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } - assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") - } - } - - @Test - fun creationMaskNarrowsTheMapBeforeItsFirstStyleLoad() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val map = - MapHandle.create( - runtime, - mapOptions().apply { - eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED - }, - ) - try { - assertEquals(RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED, map.eventMask) - map.setStyleJson(STYLE_JSON.encodeToByteArray()) + fun narrowedMapMaskDropsOneTypeAndKeepsAnother(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + // One style load produces both types, so both are driven after this write. + map.setEventMask(RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED).await() + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") - } finally { - map.close() } } - } @Test - fun bothHandlesReportEveryTypeUntilNarrowedAndKeepUnrelatedBitsOnAWrite() { - withMap { runtime, map -> - // Neither options struct names a mask, so both default fields select every type. - assertEquals(RuntimeEventMask.ALL, runtime.eventMask) - assertEquals(RuntimeEventMask.ALL, map.eventMask) - - runtime.eventMask = RuntimeEventMask.ALL - map.eventMask = RuntimeEventMask.ALL - assertEquals(RuntimeEventMask.ALL, runtime.eventMask) - assertEquals(RuntimeEventMask.ALL, map.eventMask) - - // Read, clear one bit, write back: every other bit survives. - map.eventMask = map.eventMask - RuntimeEventMask.MAP_TILE_ACTION - assertEquals(RuntimeEventMask.ALL - RuntimeEventMask.MAP_TILE_ACTION, map.eventMask) - assertTrue(RuntimeEventType.MAP_STYLE_LOADED in map.eventMask) - - runtime.eventMask = runtime.eventMask - RuntimeEventMask.OFFLINE_OPERATION_COMPLETED - assertEquals( - RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_OPERATION_COMPLETED, - runtime.eventMask, - ) - } - } + fun creationMaskNarrowsTheMapBeforeItsFirstStyleLoad(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + val map = + MapHandle.create( + runtime, + mapOptions().apply { + eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_LOADING_STARTED + }, + ) + .await() + try { + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_LOADING_STARTED, + map.eventMask, + ) + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() - @Test - fun maskBitOutsideEveryKnownTypeFailsEverySetterAndBothCreations() { - // The bit sits above the low 32, so a mask this binding narrowed on the way - // out would reach native as a value it accepts. - val unknownBit = RuntimeEventMask.ALL + RuntimeEventMask(1L shl 63) - assertFailsWith { - RuntimeHandle.create(RuntimeOptions().apply { eventMask = unknownBit }) - } - withMap { runtime, map -> - assertFailsWith { runtime.eventMask = unknownBit } - assertFailsWith { map.eventMask = unknownBit } - assertFailsWith { - MapHandle.create(runtime, mapOptions().apply { eventMask = unknownBit }) + val types = drainUntil(runtime) { RuntimeEventType.MAP_STYLE_LOADED in it } + assertTrue(RuntimeEventType.MAP_LOADING_STARTED !in types, "cleared type was delivered") + } finally { + map.close() + } } } - } @Test - fun boundedDrainReportsWhatStaysQueuedAndTheNextDrainReachesZero() { - withMap { runtime, map -> - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - repeat(20) { runtime.pump(1) } + fun bothHandlesReportEveryTypeUntilNarrowedAndKeepUnrelatedBitsOnAWrite(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + // The runtime reports its global queue mask; the map reports its map-originated subset. + assertEquals(RuntimeEventMask.ALL, runtime.eventMask) + assertEquals(RuntimeEventMask.ALL_MAP_EVENTS, map.eventMask) - val bounded = runtime.drainEvents(maxEvents = 1) - assertEquals(1, bounded.events.size) - assertTrue(bounded.remainingCount > 0, "a bounded drain reported nothing left") + runtime.eventMask = RuntimeEventMask.ALL + map.setEventMask(RuntimeEventMask.ALL).await() + assertEquals(RuntimeEventMask.ALL, runtime.eventMask) + assertEquals(RuntimeEventMask.ALL_MAP_EVENTS, map.eventMask) - val rest = runtime.drainEvents() - assertEquals(0L, rest.remainingCount) - assertEquals(bounded.remainingCount, rest.events.size.toLong()) - assertFailsWith { runtime.drainEvents(maxEvents = -1) } + // Read, clear one bit, write back: every other bit survives. + map.setEventMask(map.eventMask - RuntimeEventMask.MAP_TILE_ACTION).await() + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_TILE_ACTION, + map.eventMask, + ) + assertTrue(RuntimeEventType.MAP_STYLE_LOADED in map.eventMask) + + runtime.eventMask = runtime.eventMask - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED + assertEquals( + RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED, + runtime.eventMask, + ) + } } - } @Test - fun styleReplacementReleasesADroppedSourceWithNoStyleLoadedEvent() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != SERVED_STYLE_URL) { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + fun maskBitOutsideEveryKnownTypeFailsEverySetterAndBothCreations(): Unit = + org.maplibre.nativeffi.runtime + .runSuspendTest { // The bit sits above the low 32, so a mask this binding narrowed on the + // way + // out would reach native as a value it accepts. + val unknownBit = RuntimeEventMask.ALL + RuntimeEventMask(1L shl 63) + assertFailsWith { + runSuspendTest { RuntimeHandle.create(RuntimeOptions().apply { eventMask = unknownBit }) } + } + withMap { runtime, map -> + assertFailsWith { runtime.eventMask = unknownBit } + assertFailsWith { map.setEventMask(unknownBit).await() } + assertFailsWith { + runSuspendTest { + MapHandle.create(runtime, mapOptions().apply { eventMask = unknownBit }).await() + } } - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = STYLE_JSON.encodeToByteArray() + } + } + + @Test + fun styleReplacementReleasesADroppedSourceWithNoStyleLoadedEvent(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + RuntimeHandle.create(RuntimeOptions()).use { runtime -> + runtime + .setResourceProvider( + ResourceProviderCallback { request, handle -> + if (request.requestedUrl != SERVED_STYLE_URL) { + return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH + } + handle.complete( + ResourceResponse(ResourceResponseStatus.OK).apply { + bytes = STYLE_JSON.encodeToByteArray() + } + ) + ResourceProviderDecision.HANDLE } ) - ResourceProviderDecision.HANDLE - } - ) - val map = - MapHandle.create( - runtime, - mapOptions().apply { - eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED - }, - ) - try { - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - map.addCustomGeometrySource("custom", customGeometrySourceOptions()) - assertEquals(1, map.customGeometrySourceCountForTesting()) - // The binding adds no subscription of its own, so the mask reads back as written. - assertEquals( - RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED, - map.eventMask, - "the host's own mask changed", - ) + .await() + val map = + MapHandle.create( + runtime, + mapOptions().apply { + eventMask = RuntimeEventMask.ALL - RuntimeEventMask.MAP_STYLE_LOADED + }, + ) + .await() + try { + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + map.addCustomGeometrySource("custom", customGeometrySourceOptions()).await() + assertEquals(1, map.customGeometrySourceCountForTesting()) + // The binding adds no subscription of its own, so the mask reads back as written. + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_STYLE_LOADED, + map.eventMask, + "the host's own mask changed", + ) - // A URL load drops the source in the pump that completes it, and native - // reports that through the release callback rather than through an event. - map.setStyleUrl(SERVED_STYLE_URL) - val types = drainUntil(runtime) { map.customGeometrySourceCountForTesting() == 0 } - assertTrue( - RuntimeEventType.MAP_STYLE_LOADED !in types, - "a cleared style-loaded event reached the host", - ) - } finally { - map.close() + // A URL load drops the source when the asynchronous load completes, and native + // reports that through the release callback rather than through an event. + map.setStyleUrl(SERVED_STYLE_URL).await() + val types = drainUntil(runtime) { map.customGeometrySourceCountForTesting() == 0 } + assertTrue( + RuntimeEventType.MAP_STYLE_LOADED !in types, + "a cleared style-loaded event reached the host", + ) + } finally { + map.close() + } } } - } @Test - fun inlineStyleLoadAndRemovalReleaseTheirSourcesBeforeReturning() { - withMap { _, map -> - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - map.addCustomGeometrySource("removed", customGeometrySourceOptions()) - map.addCustomGeometrySource("dropped", customGeometrySourceOptions()) - assertEquals(2, map.customGeometrySourceCountForTesting()) + fun orderedStyleReplacementReleasesDroppedSourcesBeforeCompletion(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + map.addCustomGeometrySource("removed", customGeometrySourceOptions()).await() + map.addCustomGeometrySource("dropped", customGeometrySourceOptions()).await() + runtime.barrier().await() + assertEquals(2, map.customGeometrySourceCountForTesting()) - assertTrue(map.removeStyleSource("removed")) - assertEquals(1, map.customGeometrySourceCountForTesting()) + map.removeStyleSource("removed").await() + runtime.barrier().await() + assertEquals(1, map.customGeometrySourceCountForTesting()) - // An inline load replaces the style before it returns, so the source it - // dropped is released by then too. - map.setStyleJson(STYLE_JSON.encodeToByteArray()) - assertEquals(0, map.customGeometrySourceCountForTesting()) + map.setStyleJson(STYLE_JSON.encodeToByteArray()).await() + runtime.barrier().await() + assertEquals(0, map.customGeometrySourceCountForTesting()) - // Closing the map releases the source it still owns, before the callbacks - // it closes could be called again. - map.addCustomGeometrySource("surviving", customGeometrySourceOptions()) - map.close() - assertEquals(0, map.customGeometrySourceCountForTesting()) + map.addCustomGeometrySource("surviving", customGeometrySourceOptions()).await() + runtime.barrier().await() + map.close() + runtime.barrier().await() + assertEquals(0, map.customGeometrySourceCountForTesting()) + } } - } @Test - fun drainAndBothMaskSettersReportTheWrongThread() { - withMap { runtime, map -> - val failure = AtomicReference(null) - runOnNativeThread(BackgroundEventCalls(runtime, map, failure)) - failure.load()?.let { throw it } + fun drainAndBothMaskSettersAreAnyThread(): Unit = + org.maplibre.nativeffi.runtime.runSuspendTest { + withMap { runtime, map -> + val failure = AtomicReference(null) + val completion = AtomicReference?>(null) + runOnNativeThread(BackgroundEventCalls(runtime, map, failure, completion)) + failure.load()?.let { throw it } + completion.load()?.await() + assertEquals( + RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED, + runtime.eventMask, + ) + assertEquals( + RuntimeEventMask.ALL_MAP_EVENTS - RuntimeEventMask.MAP_TILE_ACTION, + map.eventMask, + ) + } } - } private fun customGeometrySourceOptions(): CustomGeometrySourceOptions = CustomGeometrySourceOptions( @@ -220,9 +234,9 @@ class RuntimeEventsTest : org.maplibre.nativeffi.NativeTestBase() { height = 128 } - private fun withMap(body: (RuntimeHandle, MapHandle) -> Unit) { + private suspend fun withMap(body: suspend (RuntimeHandle, MapHandle) -> Unit) { RuntimeHandle.create(RuntimeOptions()).use { runtime -> - val map = MapHandle.create(runtime, mapOptions()) + val map = MapHandle.create(runtime, mapOptions()).await() try { body(runtime, map) } finally { @@ -231,14 +245,14 @@ class RuntimeEventsTest : org.maplibre.nativeffi.NativeTestBase() { } } - /** Pumps and drains until [done] holds for the event types seen so far. */ - private fun drainUntil( + /** Drains until [done] holds for the event types seen so far. */ + private suspend fun drainUntil( runtime: RuntimeHandle, done: (Set) -> Boolean, ): Set { val types = mutableSetOf() repeat(10_000) { - runtime.pump(0) + runtime.barrier().await() types += runtime.drainEvents().events.map { it.type } if (done(types)) return types usleep(1_000U) @@ -271,12 +285,13 @@ private class BackgroundEventCalls( private val runtime: RuntimeHandle, private val map: MapHandle, private val failure: AtomicReference, + private val completion: AtomicReference?>, ) { fun run() { try { - assertFailsWith { runtime.drainEvents() } - assertFailsWith { runtime.eventMask = RuntimeEventMask.ALL } - assertFailsWith { map.eventMask = RuntimeEventMask.ALL } + runtime.drainEvents() + runtime.eventMask = RuntimeEventMask.ALL - RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED + completion.store(map.setEventMask(RuntimeEventMask.ALL - RuntimeEventMask.MAP_TILE_ACTION)) } catch (throwable: Throwable) { failure.store(throwable) } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleNativeTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleNativeTest.kt index 16b10d617..698921953 100644 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleNativeTest.kt +++ b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/RuntimeHandleNativeTest.kt @@ -1,517 +1,53 @@ package org.maplibre.nativeffi.runtime -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.test.Test -import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertSame import kotlin.test.assertTrue -import kotlinx.cinterop.ByteVar -import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.asStableRef -import kotlinx.cinterop.get -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.set -import kotlinx.cinterop.sizeOf -import kotlinx.cinterop.staticCFunction -import kotlinx.cinterop.toCValues import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.error.AbiVersionMismatchException -import org.maplibre.nativeffi.error.MaplibreException import org.maplibre.nativeffi.error.MaplibreStatus -import org.maplibre.nativeffi.error.NativeErrorException -import org.maplibre.nativeffi.error.WrongThreadException -import org.maplibre.nativeffi.internal.c.mln_runtime_event -import org.maplibre.nativeffi.internal.c.mln_runtime_event_payload -import org.maplibre.nativeffi.internal.callback.ResourceProviderState -import org.maplibre.nativeffi.internal.callback.ResourceTransformState -import org.maplibre.nativeffi.internal.lifecycle.SyntheticHandles -import org.maplibre.nativeffi.internal.lifecycle.rawHandleValue import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapOptions -import org.maplibre.nativeffi.resource.ResourceProviderCallback -import org.maplibre.nativeffi.resource.ResourceProviderDecision -import org.maplibre.nativeffi.resource.ResourceRequestHandle -import org.maplibre.nativeffi.resource.ResourceResponse -import org.maplibre.nativeffi.resource.ResourceResponseStatus -import org.maplibre.nativeffi.resource.ResourceTransformCallback -import platform.posix.pthread_create -import platform.posix.pthread_join -import platform.posix.pthread_tVar -import platform.posix.usleep -@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) +@OptIn(ExperimentalForeignApi::class) class RuntimeHandleNativeTest : org.maplibre.nativeffi.NativeTestBase() { @Test - fun abiMismatchPreventsNativeRuntimeCreation() { + fun abiMismatchPreventsNativeRuntimeCreation(): Unit = runSuspendTest { var creates = 0 - - val error = - assertFailsWith { - RuntimeHandle.createForTesting( - actualAbiVersion = Maplibre.EXPECTED_C_ABI_VERSION + 1L, - creator = { _, _ -> - creates += 1 - MaplibreStatus.OK.nativeCode - }, - ) - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, error.status) - assertIs(error) - assertEquals(Maplibre.EXPECTED_C_ABI_VERSION + 1L, error.actualVersion) - assertEquals(Maplibre.EXPECTED_C_ABI_VERSION, error.expectedVersion) - assertEquals(0, creates) - } - - @Test - fun failedResourceCallbackReplacementPreservesPreviousAndClosesReplacement() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - try { - runtime.setResourceTransform(ResourceTransformCallback { request -> request.url }) - val initialTransform = runtime.resourceTransformStateForTesting() - var failedTransform: ResourceTransformState? = null - - val transformError = - assertFailsWith { - runtime.setResourceTransformForTesting(ResourceTransformCallback { null }) { replacement - -> - failedTransform = replacement - MaplibreStatus.NATIVE_ERROR.nativeCode - } - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, transformError.status) - assertSame(initialTransform, runtime.resourceTransformStateForTesting()) - assertFalse(initialTransform?.isClosedForTesting() ?: true) - assertTrue(failedTransform?.isClosedForTesting() == true) - - runtime.setResourceProvider( - ResourceProviderCallback { _, _ -> ResourceProviderDecision.PASS_THROUGH } + assertFailsWith { + RuntimeHandle.createForTesting( + actualAbiVersion = Maplibre.EXPECTED_C_ABI_VERSION + 1L, + creator = { _, _ -> + creates += 1 + MaplibreStatus.OK.nativeCode + }, ) - val initialProvider = runtime.resourceProviderStateForTesting() - var failedProvider: ResourceProviderState? = null - - val providerError = - assertFailsWith { - runtime.setResourceProviderForTesting( - ResourceProviderCallback { _, _ -> ResourceProviderDecision.PASS_THROUGH } - ) { replacement -> - failedProvider = replacement - MaplibreStatus.NATIVE_ERROR.nativeCode - } - } - - assertEquals(MaplibreStatus.NATIVE_ERROR, providerError.status) - assertSame(initialProvider, runtime.resourceProviderStateForTesting()) - assertFalse(initialProvider?.isClosedForTesting() ?: true) - assertTrue(failedProvider?.isClosedForTesting() == true) - } finally { - runtime.close() } + assertEquals(0, creates) } @Test - fun resourceProviderCompletesHandledRequestFromAnotherNativeThread() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val handledRequest = AtomicReference(null) - val completionError = AtomicReference(null) - try { - runtime.setResourceProvider( - ResourceProviderCallback { request, handle -> - if (request.requestedUrl != "custom://threaded-style.json") { - return@ResourceProviderCallback ResourceProviderDecision.PASS_THROUGH - } - handledRequest.store(handle) - ResourceProviderDecision.HANDLE - } - ) - val map = - MapHandle.create( + fun lifecycleAndOrderedQueryResumeWithoutThreadAffinity(): Unit = runSuspendTest { + val runtime = RuntimeHandle.create(RuntimeOptions()) + assertFalse(runtime.isClosed) + val map = + MapHandle.create( runtime, MapOptions().apply { width = 128 height = 128 }, ) - try { - map.setStyleUrl("custom://threaded-style.json") - val handle = waitForHandledRequest(runtime, handledRequest) - completeOnNativeThread(handle, completionError) - completionError.load()?.let { throw it } - assertTrue(waitForMapEvent(runtime, map, RuntimeEventType.MAP_STYLE_LOADED)) - } finally { - map.close() - } - } finally { - runtime.close() - } - } - - // BND-044, BND-046. - - @Test - fun runtimeOwnerThreadCallFromAnotherNativeThreadReportsCopiedDiagnostic() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val callError = AtomicReference(null) - try { - runRuntimeOnNativeThread(runtime, callError) - val error = callError.load() - if (error !is WrongThreadException) - throw error ?: AssertionError("wrong-thread call succeeded") - val diagnostic = error.diagnostic - assertEquals(MaplibreStatus.WRONG_THREAD, error.status) - assertEquals(MaplibreStatus.WRONG_THREAD.nativeCode, error.nativeStatusCode) - assertTrue(diagnostic.isNotBlank()) - - runtime.pump(0) - - assertEquals(diagnostic, error.diagnostic) - } finally { - runtime.close() - } - } - - @Test - fun runtimeCloseFromAnotherNativeThreadReportsWrongThreadAndLeavesHandleLive() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val closeError = AtomicReference(null) - try { - runRuntimeCloseOnNativeThread(runtime, closeError) - val error = closeError.load() - if (error !is WrongThreadException) - throw error ?: AssertionError("wrong-thread close succeeded") - assertEquals(MaplibreStatus.WRONG_THREAD, error.status) - assertFalse(runtime.isClosed) - - runtime.pump(0) - } finally { - runtime.close() - } - assertTrue(runtime.isClosed) - } - - @Test - fun mapOriginatedEventWithoutLiveWrapperExposesNoMapHandle() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 128 - height = 128 - }, - ) + .await() + val snapshot = map.snapshot() + runtime.barrier().await() + assertTrue(map.queryCamera().await().generation >= snapshot.generation) map.close() - try { - var copiedEvent: RuntimeEvent? = null - val syntheticSource = SyntheticHandles.map().rawHandleValue - memScoped { - val event = alloc() - event.type = RuntimeEventType.MAP_STYLE_LOADED.nativeValue.toUInt() - event.source_type = RuntimeEventSourceType.MAP.nativeValue.toUInt() - event.source = syntheticSource - event.code = 0 - event.payload_type = 0U - event.message_offset = 0U - event.message_size = 0U - - copiedEvent = runtime.copyEventForTesting(event, null) - } - val event = assertNotNull(copiedEvent) - assertEquals(RuntimeEventType.MAP_STYLE_LOADED, event.type) - assertEquals(RuntimeEventSourceType.MAP, event.sourceType) - assertNull(event.mapSource) - assertNull(event.runtimeSource) - // The identity the native record carried survives a source that resolves to - // no live wrapper. - assertEquals(syntheticSource.toLong(), event.sourceId) - assertEquals(RuntimeEventPayload.None, event.payload) - } finally { - runtime.close() - } - } - - @Test - fun unknownRuntimeEventDomainsPreserveRawValuesAndCopiedPayload() { - val runtime = RuntimeHandle.create(org.maplibre.nativeffi.runtime.RuntimeOptions()) - var copied: RuntimeEvent? = null - try { - memScoped { - // The message arena holds unrelated bytes ahead of this event's message, - // so a decode that ignored message_offset would read the wrong text. - val messageBytes = "padfuture event".encodeToByteArray() - val messages = messageBytes.toCValues().getPointer(this) - val event = alloc() - event.type = 900U - event.source_type = 901U - event.source = 0x5Au - event.code = 902 - event.payload_type = 903U - event.message_offset = 3U - event.message_size = (messageBytes.size - 3).toUInt() - val payload = event.payload.ptr.reinterpret() - for (index in 0 until payloadWindowSize) { - payload[index] = 0 - } - payload[0] = 1 - payload[1] = 2 - payload[2] = 3 - - copied = runtime.copyEventForTesting(event, messages) - payload[0] = 9 - } - - val event = assertNotNull(copied) - assertEquals(RuntimeEventType(900), event.type) - assertEquals(900, event.type.nativeValue) - assertEquals(RuntimeEventSourceType(901), event.sourceType) - assertEquals(901, event.sourceType.nativeValue) - assertNull(event.runtimeSource) - assertNull(event.mapSource) - // An unnamed source kind still reports the identity the record carried. - assertEquals(0x5AL, event.sourceId) - assertEquals(902, event.code) - assertEquals("future event", event.message) - val payload = event.payload as RuntimeEventPayload.Unknown - assertEquals(903, payload.rawPayloadType) - // The window is the whole inline union, and the copy survives the mutation - // above. - assertEquals(payloadWindowSize, payload.payloadBytes.size) - assertContentEquals( - byteArrayOf(1, 2, 3) + ByteArray(payloadWindowSize - 3), - payload.payloadBytes, - ) - } finally { - runtime.close() - } - } - - // BND-084. - - private fun waitForMapEvent( - runtime: RuntimeHandle, - map: MapHandle, - eventType: RuntimeEventType, - ): Boolean { - repeat(10_000) { - runtime.pump(0) - for (event in runtime.drainEvents().events) { - if (event.type == eventType && event.mapSource == map) return true - if (event.type == RuntimeEventType.MAP_LOADING_FAILED) { - throw MaplibreException.forStatus( - MaplibreStatus.NATIVE_ERROR, - MaplibreStatus.NATIVE_ERROR.nativeCode, - event.message, - ) - } - } - usleep(1_000U) - } - return false - } - - private fun waitForMapEventRecord( - runtime: RuntimeHandle, - map: MapHandle, - eventType: RuntimeEventType, - ): RuntimeEvent { - repeat(10_000) { - runtime.pump(0) - runtime - .drainEvents() - .events - .find { it.type == eventType && it.mapSource == map } - ?.let { - return it - } - usleep(1_000U) - } - error("runtime event $eventType did not arrive") - } - - /** - * Loads a style URL whose scheme no file source serves; the failure names the scheme and URL, - * proving the request reached the network file source. - */ - private fun waitForHandledRequest( - runtime: RuntimeHandle, - handledRequest: AtomicReference, - ): ResourceRequestHandle { - repeat(10_000) { - handledRequest.load()?.let { - return it - } - runtime.pump(0) - usleep(1_000U) - } - error("resource provider did not receive handled request") - } - - private fun completeOnNativeThread( - handle: ResourceRequestHandle, - completionError: AtomicReference, - ) { - memScoped { - val completion = BackgroundResourceCompletion(handle, completionError) - val selfRef = StableRef.create(completion) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::completeResourceRequestOnNativeThread), - selfRef.asCPointer(), - ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - pthread_join(thread.ptr[0], null) - } - } - - private fun runRuntimeOnNativeThread( - runtime: RuntimeHandle, - callError: AtomicReference, - ) { - memScoped { - val call = BackgroundRuntimeCall(runtime, callError) - val selfRef = StableRef.create(call) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::runRuntimeCallOnNativeThread), - selfRef.asCPointer(), - ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - pthread_join(thread.ptr[0], null) - } - } - - private fun runRuntimeCloseOnNativeThread( - runtime: RuntimeHandle, - closeError: AtomicReference, - ) { - memScoped { - val close = BackgroundRuntimeClose(runtime, closeError) - val selfRef = StableRef.create(close) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::closeRuntimeOnNativeThread), - selfRef.asCPointer(), - ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - pthread_join(thread.ptr[0], null) - } - } -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundResourceCompletion( - private val handle: ResourceRequestHandle, - private val error: AtomicReference, -) { - fun run() { - try { - handle.complete( - ResourceResponse(ResourceResponseStatus.OK).apply { - bytes = RuntimeHandleTestStyle.styleJson.encodeToByteArray() - } - ) - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun completeResourceRequestOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() - } - return null -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundRuntimeCall( - private val runtime: RuntimeHandle, - private val error: AtomicReference, -) { - fun run() { - try { - runtime.pump(0) - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun runRuntimeCallOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() - } - return null -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundRuntimeClose( - private val runtime: RuntimeHandle, - private val error: AtomicReference, -) { - fun run() { - try { - runtime.close() - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun closeRuntimeOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() + assertTrue(map.isClosed) + runtime.close() + assertTrue(runtime.isClosed) } - return null -} - -/** Bytes of the inline payload union that one event record carries. */ -@OptIn(ExperimentalForeignApi::class) -private val payloadWindowSize: Int = sizeOf().toInt() - -private object RuntimeHandleTestStyle { - const val styleJson: String = "{\"version\":8,\"sources\":{},\"layers\":[]}" } diff --git a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt b/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt deleted file mode 100644 index d5a79bc41..000000000 --- a/bindings/kotlin/src/nativeTest/kotlin/org/maplibre/nativeffi/runtime/WakeSourceTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package org.maplibre.nativeffi.runtime - -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.test.Test -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue -import kotlin.time.TimeSource -import kotlinx.cinterop.COpaquePointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.asStableRef -import kotlinx.cinterop.get -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.staticCFunction -import org.maplibre.nativeffi.error.InvalidStateException -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapOptions -import platform.posix.pthread_create -import platform.posix.pthread_join -import platform.posix.pthread_tVar -import platform.posix.usleep - -@OptIn(ExperimentalAtomicApi::class, ExperimentalForeignApi::class) -class WakeSourceTest : org.maplibre.nativeffi.NativeTestBase() { - // BND-088, BND-089. - - // Pumps until the runtime is idle, so a later park is released only by the test's signal. - private fun quiesce(runtime: RuntimeHandle) { - repeat(100) { - runtime.pump(0) - if (runtime.drainEvents().events.isEmpty()) { - return - } - } - error("the runtime kept producing events while idle") - } - - @Test - fun parkedOwnerThreadWakesForNativeWorkAndForAWakeSource() { - val runtime = RuntimeHandle.create(RuntimeOptions()) - val map = - MapHandle.create( - runtime, - MapOptions().apply { - width = 512 - height = 512 - }, - ) - quiesce(runtime) - - // Native reports the load failure from its own threads; it must reach the parked - // owner thread. - map.setStyleUrl("unsupported://style.json") - var loadingFailed = false - val loadStarted = TimeSource.Monotonic.markNow() - repeat(20) { - if (!loadingFailed) { - runtime.pump(10_000) - assertTrue( - loadStarted.elapsedNow().inWholeMilliseconds < 5_000, - "parks sat out their timeouts while the style load was pending", - ) - if (runtime.drainEvents().events.any { it.type == RuntimeEventType.MAP_LOADING_FAILED }) { - loadingFailed = true - } - } - } - assertTrue(loadingFailed) - - // Nothing else can end this park, so only the cross-thread signal releases it. - val source = runtime.acquireWakeSource() - quiesce(runtime) - val signalError = AtomicReference(null) - memScoped { - val signal = BackgroundWakeSignal(source, signalError) - val selfRef = StableRef.create(signal) - val thread = alloc() - val status = - pthread_create( - thread.ptr, - null, - staticCFunction(::signalWakeSourceOnNativeThread), - selfRef.asCPointer(), - ) - if (status != 0) { - selfRef.dispose() - error("pthread_create failed with status $status") - } - val parkStarted = TimeSource.Monotonic.markNow() - runtime.pump(10_000) - assertTrue( - parkStarted.elapsedNow().inWholeMilliseconds < 5_000, - "the parked owner thread timed out instead of taking the signal", - ) - pthread_join(thread.ptr[0], null) - } - signalError.load()?.let { throw AssertionError("wake source signal failed", it) } - - // A wake source stays usable after its runtime closes, in either teardown order. - map.close() - runtime.close() - source.signal() - source.close() - assertTrue(source.isClosed) - assertFailsWith { source.signal() } - } - - @Test - fun pumpClearsTheWakeFlagItReturnsOn() { - RuntimeHandle.create(RuntimeOptions()).use { runtime -> - runtime.acquireWakeSource().use { source -> - quiesce(runtime) - - source.signal() - val signalledStarted = TimeSource.Monotonic.markNow() - runtime.pump(10_000) - assertTrue( - signalledStarted.elapsedNow().inWholeMilliseconds < 5_000, - "a pump waited even though the wake flag was set", - ) - - // The pump above cleared the wake flag, so this one waits its full timeout. - val idleStarted = TimeSource.Monotonic.markNow() - runtime.pump(200) - assertTrue( - idleStarted.elapsedNow().inWholeMilliseconds >= 100, - "the first pump left the wake flag set", - ) - } - } - } -} - -@OptIn(ExperimentalAtomicApi::class) -private class BackgroundWakeSignal( - private val source: WakeSource, - private val error: AtomicReference, -) { - fun run() { - try { - usleep(20_000u) - source.signal() - } catch (throwable: Throwable) { - error.store(throwable) - } - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun signalWakeSourceOnNativeThread(raw: COpaquePointer?): COpaquePointer? { - val selfRef = requireNotNull(raw).asStableRef() - try { - selfRef.get().run() - } finally { - selfRef.dispose() - } - return null -} diff --git a/bindings/python/python/maplibre_native_ffi/__init__.py b/bindings/python/python/maplibre_native_ffi/__init__.py index 0c6b8ef45..3227016f8 100644 --- a/bindings/python/python/maplibre_native_ffi/__init__.py +++ b/bindings/python/python/maplibre_native_ffi/__init__.py @@ -10,27 +10,46 @@ supported_render_backends, ) from .errors import ( + BusyError, + CancelledError, InvalidArgumentError, InvalidStateError, MaplibreError, MaplibreStatus, NativeError, + NotFoundError, + NotReadyError, + TargetLostError, UnknownStatusError, UnsupportedFeatureError, WrongThreadError, ) -from .map import MapHandle, MapMode, MapOptions +from .map import CameraSnapshot, MapHandle, MapMode, MapOptions, MapSnapshot from .render import ( + FrameDemand, + FrameDemandFlag, FrameOpenGLTextureName, + GpuSync, + GpuSyncKind, NativePointer, OpenGLContextProvider, + RenderAbandonDisposition, + RenderAbandonResult, RenderBackend, + RenderDriver, + RenderFrameResult, RenderResult, - RenderUpdate, + RenderSessionAttachOptions, + RenderSessionCapabilities, + RenderSessionCapability, + RenderSessionSnapshot, + RenderSessionState, ) from .runtime import ( CameraChangeMode, CameraTransitionFinishedPayload, + CommandCompletion, + CommandDisposition, NetworkStatus, RenderFramePayload, RenderingStats, @@ -49,31 +68,50 @@ TileId, TileOperation, UnknownRuntimeEventPayload, - WakeSource, ) __all__ = [ "EXPECTED_C_ABI_VERSION", + "BusyError", "CameraChangeMode", + "CameraSnapshot", "CameraTransitionFinishedPayload", + "CancelledError", + "CommandCompletion", + "CommandDisposition", + "FrameDemand", + "FrameDemandFlag", "FrameOpenGLTextureName", + "GpuSync", + "GpuSyncKind", "InvalidArgumentError", "InvalidStateError", "MapHandle", "MapMode", "MapOptions", + "MapSnapshot", "MaplibreError", "MaplibreStatus", "NativeError", "NativePointer", "NetworkStatus", + "NotFoundError", + "NotReadyError", "OpenGLContextProvider", + "RenderAbandonDisposition", + "RenderAbandonResult", "RenderBackend", + "RenderDriver", "RenderFramePayload", + "RenderFrameResult", "RenderMapPayload", "RenderMode", "RenderResult", - "RenderUpdate", + "RenderSessionAttachOptions", + "RenderSessionCapabilities", + "RenderSessionCapability", + "RenderSessionSnapshot", + "RenderSessionState", "RenderingStats", "RuntimeEvent", "RuntimeEventBatch", @@ -84,13 +122,13 @@ "RuntimeEventType", "RuntimeHandle", "RuntimeOptions", + "TargetLostError", "TileActionPayload", "TileId", "TileOperation", "UnknownRuntimeEventPayload", "UnknownStatusError", "UnsupportedFeatureError", - "WakeSource", "WrongThreadError", "c_version", "network_status", diff --git a/bindings/python/python/maplibre_native_ffi/_future.py b/bindings/python/python/maplibre_native_ffi/_future.py new file mode 100644 index 000000000..c485b26ed --- /dev/null +++ b/bindings/python/python/maplibre_native_ffi/_future.py @@ -0,0 +1,41 @@ +"""Helpers for adapting native completion futures.""" + +from __future__ import annotations + +from collections.abc import Callable +from concurrent.futures import Future + + +def map_future[T, U](source: Future[T], transform: Callable[[T], U]) -> Future[U]: + """Return an eager future that transforms a native completion result.""" + result: Future[U] = Future() + + def complete(completed: Future[T]) -> None: + if result.cancelled(): + return + try: + result.set_result(transform(completed.result())) + except Exception as error: # noqa: BLE001 - preserve the source failure. + result.set_exception(error) + + source.add_done_callback(complete) + return result + + +def retain_future[T](source: Future[T], retained: object) -> Future[T]: + """Keep an owner alive until a source future becomes terminal.""" + result: Future[T] = Future() + + def complete(completed: Future[T]) -> None: + try: + if result.cancelled(): + return + result.set_result(completed.result()) + except Exception as error: # noqa: BLE001 - preserve the source failure. + if not result.cancelled(): + result.set_exception(error) + finally: + _ = retained + + source.add_done_callback(complete) + return result diff --git a/bindings/python/python/maplibre_native_ffi/_lifecycle.py b/bindings/python/python/maplibre_native_ffi/_lifecycle.py index 50859a997..e429c4f2e 100644 --- a/bindings/python/python/maplibre_native_ffi/_lifecycle.py +++ b/bindings/python/python/maplibre_native_ffi/_lifecycle.py @@ -27,8 +27,13 @@ def warn_unclosed( class ContextHandleMixin: """Provide context-manager behavior for explicit-close handles.""" - def close(self) -> None: - """Release this handle.""" + def close(self) -> object: + """Release this handle. + + A handle whose release reports asynchronous teardown returns a future + here. Leaving the ``with`` block discards it, so a host that must + observe teardown calls ``close`` itself and waits. + """ raise NotImplementedError def __enter__(self) -> Self: @@ -70,6 +75,6 @@ def closed(self) -> bool: """Return whether the private native handle has been closed.""" return bool(self._native.closed) - def close(self) -> None: + def close(self) -> object: """Release the private native handle exactly once.""" - self._native.close() + return self._native.close() diff --git a/bindings/python/python/maplibre_native_ffi/_native.pyi b/bindings/python/python/maplibre_native_ffi/_native.pyi index 08a753853..9a7c5c731 100644 --- a/bindings/python/python/maplibre_native_ffi/_native.pyi +++ b/bindings/python/python/maplibre_native_ffi/_native.pyi @@ -1,104 +1,98 @@ from collections.abc import Callable, Sequence -from typing import Any, TypeAlias +from concurrent.futures import Future +from typing import Any + +from .runtime import CommandCompletion # Hand-maintained: PyO3's generated stubs only support inline Rust modules, and # this module uses a function-style #[pymodule]. # https://pyo3.rs/main/python-typing-hints -_Point: TypeAlias = tuple[float, float] -_Vec3: TypeAlias = tuple[float, float, float] -_Quat: TypeAlias = tuple[float, float, float, float] -_Insets: TypeAlias = tuple[float, float, float, float] -_Bounds: TypeAlias = tuple[_Point, _Point] -_Animation: TypeAlias = tuple[ +type _Point = tuple[float, float] +type _Vec3 = tuple[float, float, float] +type _Quat = tuple[float, float, float, float] +type _Insets = tuple[float, float, float, float] +type _Bounds = tuple[_Point, _Point] +type _Animation = tuple[ float | None, float | None, float | None, _Insets | None, int | None ] -_WireDict: TypeAlias = dict[str, Any] +type _WireDict = dict[str, Any] class _RuntimeHandle: @property def closed(self) -> bool: ... - def close(self) -> None: ... - def pump(self, timeout_ms: int, budget_ms: int = -1) -> None: ... - def wake_source(self) -> "_WakeSource": ... - def run_ambient_cache_operation_start(self, operation: int) -> int: ... - def set_maximum_ambient_cache_size_start(self, size: int) -> int: ... - def offline_region_create_start( + def close(self) -> Future[None]: ... + def barrier(self) -> Future[None]: ... + def run_ambient_cache_operation(self, operation: int) -> Future[None]: ... + def set_maximum_ambient_cache_size(self, size: int) -> Future[None]: ... + def create_offline_region( self, definition: object, metadata: bytes - ) -> int: ... - def offline_region_get_start(self, region_id: int) -> int: ... - def offline_regions_list_start(self) -> int: ... - def offline_regions_merge_database_start(self, side_database_path: str) -> int: ... - def offline_region_update_metadata_start( + ) -> Future[_WireDict]: ... + def get_offline_region(self, region_id: int) -> Future[_WireDict | None]: ... + def list_offline_regions(self) -> Future[list[_WireDict]]: ... + def merge_offline_regions_database( + self, side_database_path: str + ) -> Future[list[_WireDict]]: ... + def update_offline_region_metadata( self, region_id: int, metadata: bytes - ) -> int: ... - def offline_region_get_status_start(self, region_id: int) -> int: ... - def offline_region_set_observed_start( + ) -> Future[_WireDict]: ... + def get_offline_region_status(self, region_id: int) -> Future[_WireDict]: ... + def set_offline_region_observed( self, region_id: int, observed: bool - ) -> int: ... - def offline_region_set_download_state_start( + ) -> Future[None]: ... + def set_offline_region_download_state( self, region_id: int, state: int - ) -> int: ... - def offline_region_invalidate_start(self, region_id: int) -> int: ... - def offline_region_delete_start(self, region_id: int) -> int: ... - def offline_region_create_take_result(self, operation_id: int) -> _WireDict: ... - def offline_region_get_take_result(self, operation_id: int) -> _WireDict | None: ... - def offline_regions_list_take_result( - self, operation_id: int - ) -> list[_WireDict]: ... - def offline_regions_merge_database_take_result( - self, operation_id: int - ) -> list[_WireDict]: ... - def offline_region_update_metadata_take_result( - self, operation_id: int - ) -> _WireDict: ... - def offline_region_get_status_take_result(self, operation_id: int) -> _WireDict: ... - def offline_operation_discard(self, operation_id: int) -> None: ... + ) -> Future[None]: ... + def invalidate_offline_region(self, region_id: int) -> Future[None]: ... + def delete_offline_region(self, region_id: int) -> Future[None]: ... def set_resource_provider( self, - callback: Callable[[_WireDict, "_ResourceRequestHandle"], int], + callback: Callable[[_WireDict, _ResourceRequestHandle], int], max_pending_callbacks: int, - ) -> None: ... - def clear_resource_provider(self) -> None: ... + ) -> Future[CommandCompletion]: ... + def clear_resource_provider(self) -> Future[CommandCompletion]: ... def set_resource_transform( self, callback: Callable[[_WireDict], str | None], max_pending_callbacks: int - ) -> None: ... - def clear_resource_transform(self) -> None: ... + ) -> Future[CommandCompletion]: ... + def clear_resource_transform(self) -> Future[CommandCompletion]: ... def set_http_header_transform( self, callback: Callable[[_WireDict], Sequence[_WireDict]], max_pending_callbacks: int, - ) -> None: ... - def clear_http_header_transform(self) -> None: ... - def drain_events(self, max_events: int) -> _WireDict: ... + ) -> Future[CommandCompletion]: ... + def clear_http_header_transform(self) -> Future[CommandCompletion]: ... + def drain_events(self) -> _WireDict: ... def set_event_mask(self, mask: int) -> None: ... def get_event_mask(self) -> int: ... + def set_event_wake_callback(self, callback: Callable[[], None]) -> None: ... + def clear_event_wake_callback(self) -> None: ... class _MapHandle: @property def closed(self) -> bool: ... - def close(self) -> None: ... + def close(self) -> Future[None]: ... def id(self) -> int: ... - def request_repaint(self) -> None: ... - def request_still_image(self) -> None: ... - def dump_debug_logs(self) -> None: ... - def set_event_mask(self, mask: int) -> None: ... + def request_repaint(self) -> Future[CommandCompletion]: ... + def request_still_image(self) -> Future[None]: ... + def dump_debug_logs(self) -> Future[CommandCompletion]: ... + def set_event_mask(self, mask: int) -> Future[CommandCompletion]: ... def get_event_mask(self) -> int: ... - def set_debug_options(self, options: int) -> None: ... - def get_debug_options(self) -> int: ... - def set_rendering_stats_view_enabled(self, enabled: bool) -> None: ... - def get_rendering_stats_view_enabled(self) -> bool: ... - def is_fully_loaded(self) -> bool: ... + def set_debug_options(self, options: int) -> Future[CommandCompletion]: ... + def set_rendering_stats_view_enabled( + self, enabled: bool + ) -> Future[CommandCompletion]: ... def get_size(self) -> tuple[int, int, float]: ... - def get_viewport_options(self) -> _WireDict: ... + def resize( + self, width: int, height: int, scale_factor: float + ) -> Future[CommandCompletion]: ... + def snapshot(self) -> _WireDict: ... def set_viewport_options( self, north_orientation: int | None, constrain_mode: int | None, viewport_mode: int | None, frustum_offset: _Insets | None, - ) -> None: ... - def get_tile_options(self) -> _WireDict: ... + ) -> Future[CommandCompletion]: ... def set_tile_options( self, prefetch_zoom_delta: int | None, @@ -107,10 +101,10 @@ class _MapHandle: lod_pitch_threshold: float | None, lod_zoom_shift: float | None, lod_mode: int | None, - ) -> None: ... - def create_projection(self) -> "_MapProjectionHandle": ... - def set_style_url(self, url: str) -> None: ... - def set_style_json(self, json: bytes) -> None: ... + ) -> Future[CommandCompletion]: ... + def create_projection(self) -> Future[_MapProjectionHandle]: ... + def set_style_url(self, url: str) -> Future[CommandCompletion]: ... + def set_style_json(self, json: bytes) -> Future[CommandCompletion]: ... def set_feature_state( self, source_id: str, @@ -118,24 +112,25 @@ class _MapHandle: feature_id: str | None, state_key: str | None, state_value: bytes, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def get_feature_state( self, source_id: str, source_layer_id: str | None, feature_id: str | None, state_key: str | None, - ) -> bytes: ... + ) -> Future[bytes]: ... def remove_feature_state( self, source_id: str, source_layer_id: str | None, feature_id: str | None, state_key: str | None, - ) -> None: ... - def copy_loaded_style_json(self) -> bytes: ... - def copy_style_url(self) -> str: ... + ) -> Future[CommandCompletion]: ... + def copy_loaded_style_json(self) -> Future[bytes]: ... + def copy_style_url(self) -> Future[str]: ... def get_camera(self) -> _WireDict: ... + def get_camera_ordered(self) -> Future[_WireDict]: ... def jump_to( self, center: _Point | None, @@ -147,7 +142,7 @@ class _MapHandle: anchor: _Point | None, roll: float | None, field_of_view: float | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def ease_to( self, center: _Point | None, @@ -160,7 +155,7 @@ class _MapHandle: roll: float | None, field_of_view: float | None, animation: _Animation | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def fly_to( self, center: _Point | None, @@ -173,7 +168,15 @@ class _MapHandle: roll: float | None, field_of_view: float | None, animation: _Animation | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... + def apply_camera_delta( + self, + kind: int, + offset: _Point, + amount: float, + anchor: _Point | None, + animation: _Animation | None, + ) -> Future[CommandCompletion]: ... def camera_for_lat_lng_bounds( self, southwest: _Point, @@ -181,21 +184,21 @@ class _MapHandle: fit_padding: _Insets | None, fit_bearing: float | None, fit_pitch: float | None, - ) -> _WireDict: ... + ) -> Future[_WireDict]: ... def camera_for_lat_lngs( self, coordinates: Sequence[_Point], fit_padding: _Insets | None, fit_bearing: float | None, fit_pitch: float | None, - ) -> _WireDict: ... + ) -> Future[_WireDict]: ... def camera_for_geometry( self, geometry: bytes, fit_padding: _Insets | None, fit_bearing: float | None, fit_pitch: float | None, - ) -> _WireDict: ... + ) -> Future[_WireDict]: ... def lat_lng_bounds_for_camera( self, center: _Point | None, @@ -208,8 +211,7 @@ class _MapHandle: roll: float | None, field_of_view: float | None, unwrapped: bool, - ) -> _WireDict: ... - def get_bounds(self) -> _WireDict: ... + ) -> Future[_WireDict]: ... def set_bounds( self, bounds: _Bounds | None, @@ -218,37 +220,26 @@ class _MapHandle: max_zoom: float | None, min_pitch: float | None, max_pitch: float | None, - ) -> None: ... - def move_by(self, delta_x: float, delta_y: float) -> None: ... - def move_by_animated( - self, delta_x: float, delta_y: float, animation: _Animation | None - ) -> None: ... - def scale_by(self, scale: float, anchor: _Point | None) -> None: ... - def scale_by_animated( - self, scale: float, anchor: _Point | None, animation: _Animation | None - ) -> None: ... - def rotate_by(self, first: _Point, second: _Point) -> None: ... - def rotate_by_animated( - self, first: _Point, second: _Point, animation: _Animation | None - ) -> None: ... - def pitch_by(self, pitch: float) -> None: ... - def pitch_by_animated(self, pitch: float, animation: _Animation | None) -> None: ... - def cancel_transitions(self) -> None: ... - def set_gesture_in_progress(self, in_progress: bool) -> None: ... - def is_gesture_in_progress(self) -> bool: ... - def get_free_camera_options(self) -> _WireDict: ... + ) -> Future[CommandCompletion]: ... def set_free_camera_options( self, position: _Vec3 | None, orientation: _Quat | None - ) -> None: ... - def get_projection_mode(self) -> _WireDict: ... + ) -> Future[CommandCompletion]: ... def set_projection_mode( self, axonometric: bool | None, x_skew: float | None, y_skew: float | None - ) -> None: ... - def pixel_for_lat_lng(self, latitude: float, longitude: float) -> _WireDict: ... - def lat_lng_for_pixel(self, x: float, y: float) -> _WireDict: ... - def pixels_for_lat_lngs(self, coordinates: Sequence[_Point]) -> list[_WireDict]: ... - def lat_lngs_for_pixels(self, points: Sequence[_Point]) -> list[_WireDict]: ... - def add_style_source_json(self, source_id: str, source_json: bytes) -> None: ... + ) -> Future[CommandCompletion]: ... + def pixel_for_lat_lng( + self, latitude: float, longitude: float + ) -> Future[_WireDict]: ... + def lat_lng_for_pixel(self, x: float, y: float) -> Future[_WireDict]: ... + def pixels_for_lat_lngs( + self, coordinates: Sequence[_Point] + ) -> Future[list[_WireDict]]: ... + def lat_lngs_for_pixels( + self, points: Sequence[_Point] + ) -> Future[list[_WireDict]]: ... + def add_style_source_json( + self, source_id: str, source_json: bytes + ) -> Future[CommandCompletion]: ... def add_geojson_source_url( self, source_id: str, @@ -265,17 +256,19 @@ class _MapHandle: line_metrics: bool | None, cluster: bool | None, synchronous_tiling: bool | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_geojson_source_data( - self, source_id: str, data: "_GeoJsonSourceDataHandle" - ) -> None: ... - def set_geojson_source_url(self, source_id: str, url: str) -> None: ... + self, source_id: str, data: _GeoJsonSourceDataHandle + ) -> Future[CommandCompletion]: ... + def set_geojson_source_url( + self, source_id: str, url: str + ) -> Future[CommandCompletion]: ... def set_geojson_source_data( - self, source_id: str, data: "_GeoJsonSourceDataHandle" - ) -> None: ... + self, source_id: str, data: _GeoJsonSourceDataHandle + ) -> Future[CommandCompletion]: ... def set_geojson_source_synchronous_tiling( self, source_id: str, enabled: bool - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_vector_source_url( self, source_id: str, @@ -288,7 +281,7 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_raster_source_url( self, source_id: str, @@ -301,7 +294,7 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_raster_dem_source_url( self, source_id: str, @@ -314,7 +307,7 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_vector_source_tiles( self, source_id: str, @@ -327,7 +320,7 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_raster_source_tiles( self, source_id: str, @@ -340,7 +333,7 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_raster_dem_source_tiles( self, source_id: str, @@ -353,66 +346,80 @@ class _MapHandle: tile_size: int | None, vector_encoding: int | None, raster_dem_encoding: int | None, - ) -> None: ... - def remove_style_source(self, source_id: str) -> bool: ... - def style_source_exists(self, source_id: str) -> bool: ... - def get_style_source_type(self, source_id: str) -> int | None: ... - def get_style_source_info(self, source_id: str) -> _WireDict | None: ... - def list_style_source_ids(self) -> list[str]: ... + ) -> Future[CommandCompletion]: ... + def remove_style_source(self, source_id: str) -> Future[CommandCompletion]: ... + def get_style_source_info(self, source_id: str) -> Future[_WireDict | None]: ... + def list_style_source_ids(self) -> Future[list[str]]: ... def add_hillshade_layer( self, layer_id: str, source_id: str, before_layer_id: str | None - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_color_relief_layer( self, layer_id: str, source_id: str, before_layer_id: str | None - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_location_indicator_layer( self, layer_id: str, before_layer_id: str | None - ) -> None: ... + ) -> Future[CommandCompletion]: ... def set_location_indicator_location( self, layer_id: str, latitude: float, longitude: float, altitude: float - ) -> None: ... - def set_location_indicator_bearing(self, layer_id: str, bearing: float) -> None: ... + ) -> Future[CommandCompletion]: ... + def set_location_indicator_bearing( + self, layer_id: str, bearing: float + ) -> Future[CommandCompletion]: ... def set_location_indicator_accuracy_radius( self, layer_id: str, radius: float - ) -> None: ... + ) -> Future[CommandCompletion]: ... def set_location_indicator_image_name( self, layer_id: str, image_kind: int, image_id: str - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_style_layer_json( self, layer_json: bytes, before_layer_id: str | None - ) -> None: ... - def get_style_layer_json(self, layer_id: str) -> bytes | None: ... - def set_style_light_json(self, light_json: bytes) -> None: ... - def set_style_light_property(self, property_name: str, value: bytes) -> None: ... - def get_style_light_property(self, property_name: str) -> bytes | None: ... + ) -> Future[CommandCompletion]: ... + def get_style_layer_json(self, layer_id: str) -> Future[bytes | None]: ... + def set_style_light_json(self, light_json: bytes) -> Future[CommandCompletion]: ... + def set_style_light_property( + self, property_name: str, value: bytes + ) -> Future[CommandCompletion]: ... + def get_style_light_property(self, property_name: str) -> Future[bytes | None]: ... def set_style_transition_options( self, duration_ms: float | None, delay_ms: float | None, enable_placement_transitions: bool | None, - ) -> None: ... - def get_style_transition_options(self) -> _WireDict: ... + ) -> Future[CommandCompletion]: ... + def get_style_transition_options(self) -> Future[_WireDict]: ... def set_layer_property( self, layer_id: str, property_name: str, value: bytes - ) -> None: ... - def get_layer_property(self, layer_id: str, property_name: str) -> bytes | None: ... - def set_layer_source_layer(self, layer_id: str, source_layer: str) -> None: ... - def copy_layer_source_layer(self, layer_id: str) -> str: ... - def set_layer_source_id(self, layer_id: str, source_id: str) -> None: ... - def copy_layer_source_id(self, layer_id: str) -> str: ... - def set_layer_min_zoom(self, layer_id: str, min_zoom: float) -> None: ... - def get_layer_min_zoom(self, layer_id: str) -> float: ... - def set_layer_max_zoom(self, layer_id: str, max_zoom: float) -> None: ... - def get_layer_max_zoom(self, layer_id: str) -> float: ... - def set_layer_visibility(self, layer_id: str, visibility: int) -> None: ... - def get_layer_visibility(self, layer_id: str) -> int: ... - def set_layer_filter(self, layer_id: str, filter: bytes | None) -> None: ... - def get_layer_filter(self, layer_id: str) -> bytes | None: ... - def remove_style_layer(self, layer_id: str) -> bool: ... - def style_layer_exists(self, layer_id: str) -> bool: ... - def get_style_layer_type(self, layer_id: str) -> str | None: ... - def list_style_layer_ids(self) -> list[str]: ... - def move_style_layer(self, layer_id: str, before_layer_id: str | None) -> None: ... + ) -> Future[CommandCompletion]: ... + def get_layer_property( + self, layer_id: str, property_name: str + ) -> Future[bytes | None]: ... + def set_layer_source_layer( + self, layer_id: str, source_layer: str + ) -> Future[CommandCompletion]: ... + def copy_layer_source_layer(self, layer_id: str) -> Future[str]: ... + def set_layer_source_id( + self, layer_id: str, source_id: str + ) -> Future[CommandCompletion]: ... + def copy_layer_source_id(self, layer_id: str) -> Future[str]: ... + def set_layer_min_zoom( + self, layer_id: str, min_zoom: float + ) -> Future[CommandCompletion]: ... + def set_layer_max_zoom( + self, layer_id: str, max_zoom: float + ) -> Future[CommandCompletion]: ... + def set_layer_visibility( + self, layer_id: str, visibility: int + ) -> Future[CommandCompletion]: ... + def set_layer_filter( + self, layer_id: str, filter: bytes | None + ) -> Future[CommandCompletion]: ... + def get_layer_filter(self, layer_id: str) -> Future[bytes | None]: ... + def remove_style_layer(self, layer_id: str) -> Future[CommandCompletion]: ... + def get_style_layer_info(self, layer_id: str) -> Future[_WireDict | None]: ... + def list_style_layer_ids(self) -> Future[list[str]]: ... + def move_style_layer( + self, layer_id: str, before_layer_id: str | None + ) -> Future[CommandCompletion]: ... def set_style_image( self, image_id: str, @@ -427,19 +434,18 @@ class _MapHandle: content: tuple[float, float, float, float] | None, text_fit_width: int | None, text_fit_height: int | None, - ) -> None: ... + ) -> Future[CommandCompletion]: ... def copy_style_image_stretches( self, image_id: str - ) -> tuple[list[tuple[float, float]], list[tuple[float, float]]] | None: ... - def remove_style_image(self, image_id: str) -> bool: ... - def style_image_exists(self, image_id: str) -> bool: ... - def get_style_image_info(self, image_id: str) -> _WireDict | None: ... + ) -> Future[tuple[list[tuple[float, float]], list[tuple[float, float]]] | None]: ... + def remove_style_image(self, image_id: str) -> Future[CommandCompletion]: ... + def get_style_image_info(self, image_id: str) -> Future[_WireDict | None]: ... def copy_style_image_premultiplied_rgba8( self, image_id: str - ) -> _WireDict | None: ... + ) -> Future[_WireDict | None]: ... def add_image_source_url( self, source_id: str, coordinates: Sequence[_Point], url: str - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_image_source_image( self, source_id: str, @@ -448,17 +454,19 @@ class _MapHandle: height: int, stride: int, pixels: bytes, - ) -> None: ... - def set_image_source_url(self, source_id: str, url: str) -> None: ... + ) -> Future[CommandCompletion]: ... + def set_image_source_url( + self, source_id: str, url: str + ) -> Future[CommandCompletion]: ... def set_image_source_image( self, source_id: str, width: int, height: int, stride: int, pixels: bytes - ) -> None: ... + ) -> Future[CommandCompletion]: ... def set_image_source_coordinates( self, source_id: str, coordinates: Sequence[_Point] - ) -> None: ... + ) -> Future[CommandCompletion]: ... def get_image_source_coordinates( self, source_id: str - ) -> list[_WireDict] | None: ... + ) -> Future[list[_WireDict] | None]: ... def add_custom_geometry_source( self, source_id: str, @@ -471,16 +479,16 @@ class _MapHandle: clip: bool | None, wrap: bool | None, has_cancel_tile: bool, - ) -> "_CustomGeometrySourceHandle": ... + ) -> tuple[_CustomGeometrySourceHandle, Future[CommandCompletion]]: ... def set_custom_geometry_source_tile_data( self, source_id: str, z: int, x: int, y: int, data: bytes - ) -> None: ... + ) -> Future[CommandCompletion]: ... def invalidate_custom_geometry_source_tile( self, source_id: str, z: int, x: int, y: int - ) -> None: ... + ) -> Future[CommandCompletion]: ... def invalidate_custom_geometry_source_region( self, source_id: str, southwest: _Point, northeast: _Point - ) -> None: ... + ) -> Future[CommandCompletion]: ... def add_custom_mvt_vector_source( self, source_id: str, @@ -488,16 +496,16 @@ class _MapHandle: min_zoom: float | None, max_zoom: float | None, has_cancel_tile: bool, - ) -> "_CustomMvtVectorSourceHandle": ... + ) -> tuple[_CustomMvtVectorSourceHandle, Future[CommandCompletion]]: ... def set_custom_mvt_vector_source_tile_data( self, source_id: str, z: int, x: int, y: int, data: bytes - ) -> None: ... + ) -> Future[CommandCompletion]: ... def set_custom_mvt_vector_source_tile_error( self, source_id: str, z: int, x: int, y: int, message: str - ) -> None: ... + ) -> Future[CommandCompletion]: ... def invalidate_custom_mvt_vector_source_tile( self, source_id: str, z: int, x: int, y: int - ) -> None: ... + ) -> Future[CommandCompletion]: ... class _MapProjectionHandle: @property @@ -531,10 +539,25 @@ class _GeoJsonSourceDataHandle: class _RenderSessionHandle: @property def closed(self) -> bool: ... - @property - def detached(self) -> bool: ... def close(self) -> None: ... - def resize(self, width: int, height: int, scale_factor: float) -> None: ... + def capabilities(self) -> _WireDict: ... + def snapshot(self) -> _WireDict: ... + def request_frame( + self, + flags: int, + token: int, + coalescing_boundary: int, + timeout_ns: int, + ) -> None: ... + def drain_frame_results(self) -> list[_WireDict]: ... + def service_driver_work(self, max_work: int) -> int: ... + def resize(self, width: int, height: int, scale_factor: float) -> Future[None]: ... + def barrier(self) -> Future[None]: ... + def detach(self) -> Future[None]: ... + def abandon(self) -> _WireDict: ... + def reduce_memory_use(self) -> Future[None]: ... + def clear_data(self) -> Future[None]: ... + def dump_debug_logs(self) -> Future[None]: ... def set_metal_surface_target( self, width: int, @@ -542,7 +565,7 @@ class _RenderSessionHandle: scale_factor: float, device_address: int, layer_address: int, - ) -> None: ... + ) -> Future[None]: ... def set_vulkan_surface_target( self, width: int, @@ -556,7 +579,18 @@ class _RenderSessionHandle: get_instance_proc_addr: int, get_device_proc_addr: int, surface_address: int, - ) -> None: ... + ) -> Future[None]: ... + def set_webgpu_surface_target( + self, + width: int, + height: int, + scale_factor: float, + instance_address: int, + device_address: int, + queue_address: int, + surface_address: int, + format: int, + ) -> Future[None]: ... def set_opengl_surface_target( self, width: int, @@ -570,7 +604,7 @@ class _RenderSessionHandle: client_api: int, get_proc_address: int, surface_address: int, - ) -> None: ... + ) -> Future[None]: ... def set_metal_borrowed_texture_target( self, width: int, @@ -579,7 +613,7 @@ class _RenderSessionHandle: physical_width: int, physical_height: int, texture_address: int, - ) -> None: ... + ) -> Future[None]: ... def set_vulkan_borrowed_texture_target( self, width: int, @@ -599,7 +633,21 @@ class _RenderSessionHandle: format: int, initial_layout: int, final_layout: int, - ) -> None: ... + ) -> Future[None]: ... + def set_webgpu_borrowed_texture_target( + self, + width: int, + height: int, + scale_factor: float, + physical_width: int, + physical_height: int, + instance_address: int, + device_address: int, + queue_address: int, + texture_address: int, + texture_view_address: int, + format: int, + ) -> Future[None]: ... def set_opengl_borrowed_texture_target( self, width: int, @@ -616,21 +664,16 @@ class _RenderSessionHandle: get_proc_address: int, texture: int, target: int, - ) -> None: ... - def render_update(self) -> tuple[int, bool]: ... - def detach(self) -> "_DetachedRenderSessionHandle": ... - def reduce_memory_use(self) -> None: ... - def clear_data(self) -> None: ... - def dump_debug_logs(self) -> None: ... + ) -> Future[None]: ... def query_rendered_features( self, geometry: object, layer_ids: Sequence[str] | None, filter: bytes | None - ) -> list[_WireDict]: ... + ) -> Future[list[_WireDict]]: ... def query_source_features( self, source_id: str, source_layer_ids: Sequence[str] | None, filter: bytes | None, - ) -> list[_WireDict]: ... + ) -> Future[list[_WireDict]]: ... def query_feature_extensions( self, source_id: str, @@ -638,27 +681,19 @@ class _RenderSessionHandle: extension: str, extension_field: str, arguments: bytes | None, - ) -> bytes: ... - def texture_image_info(self) -> _WireDict: ... - def read_premultiplied_rgba8(self) -> _WireDict: ... - def read_premultiplied_rgba8_into(self, buffer: object) -> _WireDict: ... - def acquire_metal_owned_texture_frame(self) -> "_MetalOwnedTextureFrameHandle": ... - def acquire_vulkan_owned_texture_frame( - self, - ) -> "_VulkanOwnedTextureFrameHandle": ... - def acquire_opengl_owned_texture_frame( - self, - ) -> "_OpenGLOwnedTextureFrameHandle": ... - -class _DetachedRenderSessionHandle: - @property - def closed(self) -> bool: ... - def close(self) -> None: ... + ) -> Future[bytes]: ... + def read_premultiplied_rgba8(self) -> Future[_WireDict]: ... + def acquire_metal_owned_texture_frame(self) -> _MetalOwnedTextureFrameHandle: ... + def acquire_vulkan_owned_texture_frame(self) -> _VulkanOwnedTextureFrameHandle: ... + def acquire_opengl_owned_texture_frame(self) -> _OpenGLOwnedTextureFrameHandle: ... + def acquire_webgpu_owned_texture_frame(self) -> _WebGPUOwnedTextureFrameHandle: ... class _MetalOwnedTextureFrameHandle: @property def closed(self) -> bool: ... - def close(self) -> None: ... + def release(self, kind: int, object_address: int, value: int) -> None: ... + def producer_sync(self) -> _WireDict: ... + def result(self) -> _WireDict: ... def frame(self) -> _WireDict: ... def texture_address(self) -> int: ... def device_address(self) -> int: ... @@ -666,16 +701,31 @@ class _MetalOwnedTextureFrameHandle: class _VulkanOwnedTextureFrameHandle: @property def closed(self) -> bool: ... - def close(self) -> None: ... + def release(self, kind: int, object_address: int, value: int) -> None: ... + def producer_sync(self) -> _WireDict: ... + def result(self) -> _WireDict: ... def frame(self) -> _WireDict: ... def image_address(self) -> int: ... def image_view_address(self) -> int: ... def device_address(self) -> int: ... +class _WebGPUOwnedTextureFrameHandle: + @property + def closed(self) -> bool: ... + def release(self, kind: int, object_address: int, value: int) -> None: ... + def producer_sync(self) -> _WireDict: ... + def result(self) -> _WireDict: ... + def frame(self) -> _WireDict: ... + def texture_address(self) -> int: ... + def texture_view_address(self) -> int: ... + def device_address(self) -> int: ... + class _OpenGLOwnedTextureFrameHandle: @property def closed(self) -> bool: ... - def close(self) -> None: ... + def release(self, kind: int, object_address: int, value: int) -> None: ... + def producer_sync(self) -> _WireDict: ... + def result(self) -> _WireDict: ... def frame(self) -> _WireDict: ... def texture(self) -> int: ... @@ -685,12 +735,6 @@ class _ResourceRequestHandle: def is_cancelled(self) -> bool: ... def close(self) -> None: ... -class _WakeSource: - @property - def closed(self) -> bool: ... - def signal(self) -> None: ... - def close(self) -> None: ... - class _LogReceiver: @property def dropped_record_count(self) -> int: ... @@ -744,7 +788,6 @@ def network_status_raw() -> int: ... def set_network_status_raw(raw_status: int) -> None: ... def set_network_status_raw_unchecked_for_test(raw_status: int) -> None: ... def map_size_by_id_for_test(id: int) -> tuple[int, int, float]: ... -def pump_runtime_with_map_id_for_test(id: int) -> None: ... def status_error_for_test(raw_status: int, diagnostic: str) -> None: ... def status_error_after_support_call_for_test( raw_status: int, diagnostic: str @@ -780,7 +823,9 @@ def attach_metal_surface( scale_factor: float, device_address: int, layer_address: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_vulkan_surface( map: _MapHandle, width: int, @@ -794,10 +839,18 @@ def attach_vulkan_surface( get_instance_proc_addr: int, get_device_proc_addr: int, surface_address: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_metal_owned_texture( - map: _MapHandle, width: int, height: int, scale_factor: float, device_address: int -) -> _RenderSessionHandle: ... + map: _MapHandle, + width: int, + height: int, + scale_factor: float, + device_address: int, + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_metal_borrowed_texture( map: _MapHandle, width: int, @@ -806,7 +859,9 @@ def attach_metal_borrowed_texture( physical_width: int, physical_height: int, texture_address: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_vulkan_owned_texture( map: _MapHandle, width: int, @@ -819,7 +874,9 @@ def attach_vulkan_owned_texture( graphics_queue_family_index: int, get_instance_proc_addr: int, get_device_proc_addr: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_vulkan_borrowed_texture( map: _MapHandle, width: int, @@ -839,7 +896,49 @@ def attach_vulkan_borrowed_texture( format: int, initial_layout: int, final_layout: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... +def attach_webgpu_surface( + map: _MapHandle, + width: int, + height: int, + scale_factor: float, + instance_address: int, + device_address: int, + queue_address: int, + surface_address: int, + format: int, + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... +def attach_webgpu_owned_texture( + map: _MapHandle, + width: int, + height: int, + scale_factor: float, + instance_address: int, + device_address: int, + queue_address: int, + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... +def attach_webgpu_borrowed_texture( + map: _MapHandle, + width: int, + height: int, + scale_factor: float, + physical_width: int, + physical_height: int, + instance_address: int, + device_address: int, + queue_address: int, + texture_address: int, + texture_view_address: int, + format: int, + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_opengl_surface( map: _MapHandle, width: int, @@ -853,7 +952,9 @@ def attach_opengl_surface( client_api: int, get_proc_address: int, surface_address: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_opengl_owned_texture( map: _MapHandle, width: int, @@ -866,7 +967,9 @@ def attach_opengl_owned_texture( share_context_address: int, client_api: int, get_proc_address: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... def attach_opengl_borrowed_texture( map: _MapHandle, width: int, @@ -883,4 +986,6 @@ def attach_opengl_borrowed_texture( get_proc_address: int, texture: int, target: int, -) -> _RenderSessionHandle: ... + driver: int, + texture_ring_depth: int, +) -> tuple[_RenderSessionHandle, int]: ... diff --git a/bindings/python/python/maplibre_native_ffi/camera.py b/bindings/python/python/maplibre_native_ffi/camera.py index 04691d28c..dd3e153dd 100644 --- a/bindings/python/python/maplibre_native_ffi/camera.py +++ b/bindings/python/python/maplibre_native_ffi/camera.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from enum import IntEnum from .geo import LatLng, LatLngBounds @@ -106,6 +107,26 @@ class AnimationOptions: transition_id: int | None = None +class CameraDeltaKind(IntEnum): + """Relative camera operation kind.""" + + MOVE = 0 + SCALE = 1 + BEARING = 2 + PITCH = 3 + + +@dataclass(frozen=True, slots=True) +class CameraDelta: + """One relative camera operation.""" + + kind: CameraDeltaKind = CameraDeltaKind.MOVE + offset: ScreenPoint = ScreenPoint(0.0, 0.0) + amount: float = 0.0 + anchor: ScreenPoint | None = None + animation: AnimationOptions = AnimationOptions() + + @dataclass(frozen=True, slots=True) class CameraFitOptions: """Optional fitting controls for camera-for-viewport queries.""" diff --git a/bindings/python/python/maplibre_native_ffi/errors.py b/bindings/python/python/maplibre_native_ffi/errors.py index 1510f8a64..6324452e9 100644 --- a/bindings/python/python/maplibre_native_ffi/errors.py +++ b/bindings/python/python/maplibre_native_ffi/errors.py @@ -12,6 +12,11 @@ class MaplibreStatus(Enum): WRONG_THREAD = -3 UNSUPPORTED = -4 NATIVE_ERROR = -5 + CANCELLED = -6 + BUSY = -7 + TARGET_LOST = -8 + NOT_READY = -9 + NOT_FOUND = -10 UNKNOWN = None @property @@ -91,6 +96,51 @@ def __init__( super().__init__(MaplibreStatus.NATIVE_ERROR, native_status_code, diagnostic) +class CancelledError(MaplibreError): + """Error for an operation that reached its cancelled disposition.""" + + def __init__( + self, native_status_code: int | None = -6, diagnostic: str = "" + ) -> None: + super().__init__(MaplibreStatus.CANCELLED, native_status_code, diagnostic) + + +class BusyError(MaplibreError): + """Error for a conflicting driver call or lifecycle transition.""" + + def __init__( + self, native_status_code: int | None = -7, diagnostic: str = "" + ) -> None: + super().__init__(MaplibreStatus.BUSY, native_status_code, diagnostic) + + +class TargetLostError(MaplibreError): + """Error for an irreversibly lost render target or graphics receiver.""" + + def __init__( + self, native_status_code: int | None = -8, diagnostic: str = "" + ) -> None: + super().__init__(MaplibreStatus.TARGET_LOST, native_status_code, diagnostic) + + +class NotReadyError(MaplibreError): + """Error for a nonblocking call that has no result yet.""" + + def __init__( + self, native_status_code: int | None = -9, diagnostic: str = "" + ) -> None: + super().__init__(MaplibreStatus.NOT_READY, native_status_code, diagnostic) + + +class NotFoundError(MaplibreError): + """Error for a lookup whose ID names no object.""" + + def __init__( + self, native_status_code: int | None = -10, diagnostic: str = "" + ) -> None: + super().__init__(MaplibreStatus.NOT_FOUND, native_status_code, diagnostic) + + class UnknownStatusError(MaplibreError): """Error for future native status values unknown to this binding.""" @@ -98,12 +148,41 @@ def __init__(self, native_status_code: int, diagnostic: str = "") -> None: super().__init__(MaplibreStatus.UNKNOWN, native_status_code, diagnostic) +class _OperationResultConsumedError(Exception): + """Internal marker for a failed adaptation after native result transfer.""" + + +def _from_native_status(native_status_code: int, diagnostic: str) -> MaplibreError: + error_types: dict[MaplibreStatus, type[MaplibreError]] = { + MaplibreStatus.INVALID_ARGUMENT: InvalidArgumentError, + MaplibreStatus.INVALID_STATE: InvalidStateError, + MaplibreStatus.WRONG_THREAD: WrongThreadError, + MaplibreStatus.UNSUPPORTED: UnsupportedFeatureError, + MaplibreStatus.NATIVE_ERROR: NativeError, + MaplibreStatus.CANCELLED: CancelledError, + MaplibreStatus.BUSY: BusyError, + MaplibreStatus.TARGET_LOST: TargetLostError, + MaplibreStatus.NOT_READY: NotReadyError, + MaplibreStatus.NOT_FOUND: NotFoundError, + } + status = MaplibreStatus._from_native(native_status_code) + error_type = error_types.get(status) + if error_type is None: + return UnknownStatusError(native_status_code, diagnostic) + return error_type(native_status_code, diagnostic) + + __all__ = [ + "BusyError", + "CancelledError", "InvalidArgumentError", "InvalidStateError", "MaplibreError", "MaplibreStatus", "NativeError", + "NotFoundError", + "NotReadyError", + "TargetLostError", "UnknownStatusError", "UnsupportedFeatureError", "WrongThreadError", diff --git a/bindings/python/python/maplibre_native_ffi/map.py b/bindings/python/python/maplibre_native_ffi/map.py index 091ba4989..ca6173004 100644 --- a/bindings/python/python/maplibre_native_ffi/map.py +++ b/bindings/python/python/maplibre_native_ffi/map.py @@ -3,17 +3,20 @@ from __future__ import annotations from collections.abc import Callable +from concurrent.futures import Future from dataclasses import dataclass, field from enum import IntFlag from typing import Any from . import _native from ._enum import NativeIntEnum +from ._future import map_future, retain_future from ._lifecycle import NativeHandleMixin from .camera import ( AnimationOptions, Bounded, BoundOptions, + CameraDelta, CameraFitOptions, CameraOptions, EdgeInsets, @@ -33,10 +36,15 @@ OpenGLOwnedTextureDescriptor, OpenGLSurfaceDescriptor, PremultipliedRgba8Image, + RenderDriver, + RenderSessionAttachOptions, RenderSessionHandle, VulkanBorrowedTextureDescriptor, VulkanOwnedTextureDescriptor, VulkanSurfaceDescriptor, + WebGPUBorrowedTextureDescriptor, + WebGPUOwnedTextureDescriptor, + WebGPUSurfaceDescriptor, _opengl_context_parts, ) from .style import ( @@ -52,14 +60,26 @@ StyleImage, StyleImageInfo, StyleImageOptions, + StyleLayerInfo, StyleLayerVisibility, StyleSourceInfo, - StyleSourceType, StyleTransitionOptions, TileSourceOptions, _geojson_source_parts, ) +_CORE_WORKER_ATTACH_OPTIONS = RenderSessionAttachOptions(RenderDriver.CORE_WORKER) +_CORE_WORKER_OWNED_ATTACH_OPTIONS = RenderSessionAttachOptions( + RenderDriver.CORE_WORKER, 2 +) +_CALLER_GRAPHICS_ATTACH_OPTIONS = RenderSessionAttachOptions( + RenderDriver.CALLER_GRAPHICS_THREAD +) +_CALLER_GRAPHICS_OWNED_ATTACH_OPTIONS = RenderSessionAttachOptions( + RenderDriver.CALLER_GRAPHICS_THREAD, 2 +) + + _MAP_HANDLE_CREATE_KEY = object() _MAP_PROJECTION_HANDLE_CREATE_KEY = object() @@ -196,6 +216,70 @@ def _from_native(cls, raw: dict[str, object]) -> MapTileOptions: ) +@dataclass(frozen=True, slots=True) +class CameraSnapshot: + """Camera copied from a complete map-state generation.""" + + generation: int + camera: CameraOptions + + @classmethod + def _from_native(cls, raw: dict[str, object]) -> CameraSnapshot: + return cls( + generation=raw["generation"], + camera=CameraOptions._from_native(raw["camera"]), + ) + + +@dataclass(frozen=True, slots=True) +class MapSnapshot: + """Latest immutable state published by the map worker. + + Every committed map command publishes a snapshot, so a snapshot whose + ``generation`` is at or past a command's reported generation observes that + commit. + """ + + generation: int + camera: CameraOptions + width: int + height: int + scale_factor: float + projection_mode: ProjectionMode + viewport: MapViewportOptions + debug_options: MapDebugOptions + fully_loaded: bool + """Whether every requested style and tile resource finished loading.""" + rendering_stats_view_enabled: bool + repaint_demand: bool + event_mask: RuntimeEventMask + latest_render_update_generation: int + tile: MapTileOptions + bounds: BoundOptions + free_camera: FreeCameraOptions + + @classmethod + def _from_native(cls, raw: dict[str, object]) -> MapSnapshot: + return cls( + generation=raw["generation"], + camera=CameraOptions._from_native(raw["camera"]), + width=raw["width"], + height=raw["height"], + scale_factor=raw["scale_factor"], + projection_mode=ProjectionMode._from_native(raw["projection_mode"]), + viewport=MapViewportOptions._from_native(raw["viewport"]), + debug_options=MapDebugOptions(raw["debug_options"]), + fully_loaded=raw["fully_loaded"], + rendering_stats_view_enabled=raw["rendering_stats_view_enabled"], + repaint_demand=raw["repaint_demand"], + event_mask=RuntimeEventMask(raw["event_mask"]), + latest_render_update_generation=raw["latest_render_update_generation"], + tile=MapTileOptions._from_native(raw["tile"]), + bounds=BoundOptions._from_native(raw["bounds"]), + free_camera=FreeCameraOptions._from_native(raw["free_camera"]), + ) + + @dataclass(frozen=True, slots=True) class ProjectedMeters: """Spherical Mercator projected-meter coordinate.""" @@ -399,7 +483,14 @@ def lat_lng_for_projected_meters(meters: ProjectedMeters) -> LatLng: class MapProjectionHandle(NativeHandleMixin): - """Any-thread projection helper snapshotted from a map transform.""" + """Any-thread standalone projection helper. + + Every method is synchronous, thread-safe, and internally serialized. A + projection copies the map transform once at creation and never observes + map changes made after that, while its own setters apply before they + return, so a later read or conversion observes them. The projection remains + usable after its source map and runtime close. + """ _handle_name = "MapProjectionHandle" @@ -413,14 +504,8 @@ def __init__(self, native: object, *, _create_key: object | None = None) -> None def _from_native(cls, native: object) -> MapProjectionHandle: return cls(native, _create_key=_MAP_PROJECTION_HANDLE_CREATE_KEY) - def get_camera(self) -> CameraOptions: - """Return the helper's current camera snapshot.""" - from .camera import CameraOptions - - return CameraOptions._from_native(self._native.get_camera()) - def set_camera(self, camera: CameraOptions) -> None: - """Apply camera fields to this projection helper.""" + """Apply camera fields to this projection before returning.""" self._native.set_camera(*_camera_parts(camera)) def set_visible_coordinates( @@ -428,23 +513,38 @@ def set_visible_coordinates( coordinates: list[LatLng] | tuple[LatLng, ...], padding: EdgeInsets, ) -> None: - """Update this helper's camera so coordinates are visible.""" + """Apply a visible-coordinate camera fit before returning.""" self._native.set_visible_coordinates( [(coordinate.latitude, coordinate.longitude) for coordinate in coordinates], (padding.top, padding.left, padding.bottom, padding.right), ) def set_visible_geometry(self, geometry: bytes, padding: EdgeInsets) -> None: - """Update this helper's camera so geometry coordinates are visible.""" + """Apply a visible-geometry camera fit before returning.""" self._native.set_visible_geometry( geometry, (padding.top, padding.left, padding.bottom, padding.right), ) + @property + def closed(self) -> bool: + """Whether this projection handle has been released.""" + return self._native.closed + + def close(self) -> None: + """Release this projection handle exactly once. + + The close is synchronous and waits for projection calls already running + on other threads before it returns. + """ + self._native.close() + + def get_camera(self) -> CameraOptions: + """Return the projection camera, observing every earlier setter.""" + return CameraOptions._from_native(self._native.get_camera()) + def pixel_for_lat_lng(self, coordinate: LatLng) -> ScreenPoint: """Convert a geographic coordinate to a screen-space point.""" - from .camera import ScreenPoint - raw = self._native.pixel_for_lat_lng( coordinate.latitude, coordinate.longitude, @@ -453,27 +553,34 @@ def pixel_for_lat_lng(self, coordinate: LatLng) -> ScreenPoint: def lat_lng_for_pixel(self, point: ScreenPoint) -> LatLng: """Convert a screen-space point to a geographic coordinate.""" - from .geo import LatLng - raw = self._native.lat_lng_for_pixel(point.x, point.y) return LatLng(latitude=raw["latitude"], longitude=raw["longitude"]) class MapHandle(NativeHandleMixin): - """Owner-thread map handle.""" + """Any-thread map handle backed by autonomous native execution.""" _handle_name = "MapHandle" def __init__( self, runtime: RuntimeHandle, - options: MapOptions | None = None, + native: Any, *, _create_key: object | None = None, ) -> None: if _create_key is not _MAP_HANDLE_CREATE_KEY: msg = "MapHandle instances are created by RuntimeHandle.create_map" raise TypeError(msg) + self._runtime = runtime + self._native = native + self._native_id_value = int(self._native.id()) + runtime._register_map(self) + + @classmethod + def _create( + cls, runtime: RuntimeHandle, options: MapOptions | None = None + ) -> Future[MapHandle]: options = options or MapOptions() map_mode = ( None @@ -484,48 +591,42 @@ def __init__( else MapMode(options.mode) ) ) - self._runtime = runtime - self._native = _native.create_map( - runtime._native, - options.width, - options.height, - options.scale_factor, - None if map_mode is None else map_mode.native_code, - options.fast_pfor_enabled, - int(options.event_mask), + return map_future( + _native.create_map( + runtime._native, + options.width, + options.height, + options.scale_factor, + None if map_mode is None else map_mode.native_code, + options.fast_pfor_enabled, + int(options.event_mask), + ), + lambda native: cls(runtime, native, _create_key=_MAP_HANDLE_CREATE_KEY), ) - self._native_id_value = int(self._native.id()) - runtime._register_map(self) - - @classmethod - def _create( - cls, runtime: RuntimeHandle, options: MapOptions | None = None - ) -> MapHandle: - return cls(runtime, options, _create_key=_MAP_HANDLE_CREATE_KEY) def _native_id(self) -> int: return self._native_id_value - def close(self) -> None: + def close(self) -> Future[None]: """Release this map handle exactly once. - Closing discards this map's queued runtime events and its recorded - loading failure. There is no flush and no terminal event, so read any - state you mirror from events before closing. + Closing prevents future runtime events from this map and leaves queued + events unchanged. A queued event keeps the map's copied source ID. """ - self._native.close() + teardown: Future[None] = self._native.close() self._runtime._unregister_map(self) + return teardown - def request_repaint(self) -> None: - """Request a repaint for a continuous map.""" - self._native.request_repaint() + def request_repaint(self) -> Future[CommandCompletion]: + """Request a repaint and return its completion future.""" + return self._native.request_repaint() - def request_still_image(self) -> None: - """Request one still image for a static or tile map.""" - self._native.request_still_image() + def request_still_image(self) -> Future[None]: + """Start one noncoalescing still-image operation.""" + return self._native.request_still_image() - def set_event_mask(self, mask: RuntimeEventMask) -> None: - """Select which map-originated event types this map queues. + def set_event_mask(self, mask: RuntimeEventMask) -> Future[CommandCompletion]: + """Select queued map event types and return its completion future. The call reads the bits in :attr:`RuntimeEventMask.ALL_MAP_EVENTS` and ignores the rest, so :attr:`RuntimeEventMask.ALL` selects every @@ -539,7 +640,7 @@ def set_event_mask(self, mask: RuntimeEventMask) -> None: :meth:`set_style_url` or :meth:`set_style_json` reaches the caller as an exception whatever this mask selects. """ - self._native.set_event_mask(int(mask)) + return self._native.set_event_mask(int(mask)) @property def event_mask(self) -> RuntimeEventMask: @@ -550,44 +651,52 @@ def event_mask(self) -> RuntimeEventMask: """ return RuntimeEventMask(self._native.get_event_mask()) - def set_debug_options(self, options: MapDebugOptions) -> None: - """Apply MapLibre debug overlay mask bits.""" - self._native.set_debug_options(int(options)) - - def get_debug_options(self) -> MapDebugOptions: - """Return the current MapLibre debug overlay mask bits.""" - return MapDebugOptions(self._native.get_debug_options()) + def set_debug_options(self, options: MapDebugOptions) -> Future[CommandCompletion]: + """Submit debug overlay mask bits and return its completion future. - def set_rendering_stats_view_enabled(self, enabled: bool) -> None: - """Enable or disable MapLibre's rendering stats overlay view.""" - self._native.set_rendering_stats_view_enabled(enabled) + Read the committed value back from :attr:`MapSnapshot.debug_options`. + """ + return self._native.set_debug_options(int(options)) - def get_rendering_stats_view_enabled(self) -> bool: - """Return whether MapLibre's rendering stats overlay view is enabled.""" - return self._native.get_rendering_stats_view_enabled() + def set_rendering_stats_view_enabled( + self, enabled: bool + ) -> Future[CommandCompletion]: + """Submit rendering-stats visibility and return its completion future. - def is_fully_loaded(self) -> bool: - """Return whether MapLibre currently considers the map fully loaded.""" - return self._native.is_fully_loaded() + Read the committed value back from + :attr:`MapSnapshot.rendering_stats_view_enabled`. + """ + return self._native.set_rendering_stats_view_enabled(enabled) - def dump_debug_logs(self) -> None: - """Dump map debug logs through MapLibre Native logging.""" - self._native.dump_debug_logs() + def dump_debug_logs(self) -> Future[CommandCompletion]: + """Submit a debug-log command and return its completion future.""" + return self._native.dump_debug_logs() def get_size(self) -> tuple[int, int, float]: """Return the map's logical width, height, and pixel ratio. - The scale factor is fixed for the lifetime of the map and is - independent of any render target's scale factor. + An attached render session updates the map viewport when it applies an + extent, so a session resize moves all three values. """ return self._native.get_size() - def get_viewport_options(self) -> MapViewportOptions: - """Return live map viewport and render-transform controls.""" - return MapViewportOptions._from_native(self._native.get_viewport_options()) + def resize( + self, width: int, height: int, scale_factor: float + ) -> Future[CommandCompletion]: + """Submit a logical extent update and return its completion future.""" + return self._native.resize(width, height, scale_factor) + + def snapshot(self) -> MapSnapshot: + """Copy the latest immutable map-state generation.""" + return MapSnapshot._from_native(self._native.snapshot()) + + def set_viewport_options( + self, options: MapViewportOptions + ) -> Future[CommandCompletion]: + """Submit viewport controls and return its completion future. - def set_viewport_options(self, options: MapViewportOptions) -> None: - """Apply selected live map viewport and render-transform controls.""" + Read the committed values back from :attr:`MapSnapshot.viewport`. + """ frustum_offset = ( ( options.frustum_offset.top, @@ -598,7 +707,7 @@ def set_viewport_options(self, options: MapViewportOptions) -> None: if options.frustum_offset is not None else None ) - self._native.set_viewport_options( + return self._native.set_viewport_options( int(options.north_orientation) if options.north_orientation is not None else None, @@ -607,13 +716,12 @@ def set_viewport_options(self, options: MapViewportOptions) -> None: frustum_offset, ) - def get_tile_options(self) -> MapTileOptions: - """Return tile prefetch and LOD tuning controls.""" - return MapTileOptions._from_native(self._native.get_tile_options()) + def set_tile_options(self, options: MapTileOptions) -> Future[CommandCompletion]: + """Submit tile prefetch and LOD controls and return its completion future. - def set_tile_options(self, options: MapTileOptions) -> None: - """Apply selected tile prefetch and LOD tuning controls.""" - self._native.set_tile_options( + Read the committed values back from :attr:`MapSnapshot.tile`. + """ + return self._native.set_tile_options( options.prefetch_zoom_delta, options.lod_min_radius, options.lod_scale, @@ -622,7 +730,7 @@ def set_tile_options(self, options: MapTileOptions) -> None: int(options.lod_mode) if options.lod_mode is not None else None, ) - def set_style_url(self, url: str) -> None: + def set_style_url(self, url: str) -> Future[CommandCompletion]: """Load a style URL through MapLibre Native style APIs. Loading is asynchronous: a style that fails to fetch or parse returns @@ -630,25 +738,29 @@ def set_style_url(self, url: str) -> None: A well-formed style with semantically invalid contents loads without an error and without an event. """ - self._native.set_style_url(url) + return self._native.set_style_url(url) - def set_style_json(self, json: bytes) -> None: - """Load inline style JSON through MapLibre Native style APIs. + def set_style_json(self, json: bytes) -> Future[CommandCompletion]: + """Submit inline style JSON and return its completion future. - Malformed JSON is reported twice: this call raises the parse error, and - the same message also arrives as a loading-failed runtime event. A - well-formed style with semantically invalid contents loads without an - error and without an event. + Malformed JSON is accepted as a copied command, then reports a failed + command disposition and a loading-failed runtime event. A failed parse + leaves the previously loaded style unchanged. """ - self._native.set_style_json(json) + return self._native.set_style_json(json) def set_feature_state( self, selector: FeatureStateSelector, state: bytes, - ) -> None: - """Set per-feature state on this map.""" - self._native.set_feature_state( + ) -> Future[CommandCompletion]: + """Submit per-feature state and return its completion future. + + Feature state belongs to the map. A render session pushes it into the + renderer on the next render update, including the first presented + frame that contains the source. + """ + return self._native.set_feature_state( selector.source_id, selector.source_layer_id, selector.feature_id, @@ -656,8 +768,13 @@ def set_feature_state( state, ) - def get_feature_state(self, selector: FeatureStateSelector) -> bytes: - """Return copied per-feature state JSON from this map.""" + def get_feature_state(self, selector: FeatureStateSelector) -> Future[bytes]: + """Start an ordered read of copied per-feature state JSON. + + The read observes every map command accepted before it and copies the + map store, not the last rendered frame. Missing feature state resolves + to an empty JSON object. + """ return self._native.get_feature_state( selector.source_id, selector.source_layer_id, @@ -665,16 +782,18 @@ def get_feature_state(self, selector: FeatureStateSelector) -> bytes: selector.state_key, ) - def remove_feature_state(self, selector: FeatureStateSelector) -> None: - """Remove per-feature state from this map.""" - self._native.remove_feature_state( + def remove_feature_state( + self, selector: FeatureStateSelector + ) -> Future[CommandCompletion]: + """Submit per-feature state removal and return its completion future.""" + return self._native.remove_feature_state( selector.source_id, selector.source_layer_id, selector.feature_id, selector.state_key, ) - def get_loaded_style_json(self) -> bytes: + def get_loaded_style_json(self) -> Future[bytes]: """Return the style document this map's style was last parsed from. This is the loaded document, not a serialization of the live style, so @@ -684,7 +803,7 @@ def get_loaded_style_json(self) -> bytes: """ return self._native.copy_loaded_style_json() - def get_style_url(self) -> str: + def get_style_url(self) -> Future[str]: """Return the URL this map's style was last requested from. set_style_url() records the URL before the response arrives, and @@ -694,18 +813,20 @@ def get_style_url(self) -> str: """ return self._native.copy_style_url() - def add_style_source_json(self, source_id: str, source_json: bytes) -> None: + def add_style_source_json( + self, source_id: str, source_json: bytes + ) -> Future[CommandCompletion]: """Add one style source from a style-spec source JSON object.""" - self._native.add_style_source_json(source_id, source_json) + return self._native.add_style_source_json(source_id, source_json) def add_geojson_source_url( self, source_id: str, url: str, options: GeoJsonSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a GeoJSON source that loads data from a URL.""" - self._native.add_geojson_source_url( + return self._native.add_geojson_source_url( source_id, url, *_geojson_source_parts(options) ) @@ -713,64 +834,66 @@ def add_geojson_source_data( self, source_id: str, data: GeoJsonSourceDataHandle, - ) -> None: + ) -> Future[CommandCompletion]: """Add a GeoJSON source backed by prepared data. The call borrows the handle, and the source adopts the options the data was prepared with. """ - self._native.add_geojson_source_data(source_id, data._native) + return self._native.add_geojson_source_data(source_id, data._native) - def set_geojson_source_url(self, source_id: str, url: str) -> None: + def set_geojson_source_url( + self, source_id: str, url: str + ) -> Future[CommandCompletion]: """Update one GeoJSON source to load data from a URL.""" - self._native.set_geojson_source_url(source_id, url) + return self._native.set_geojson_source_url(source_id, url) def set_geojson_source_data( self, source_id: str, data: GeoJsonSourceDataHandle - ) -> None: + ) -> Future[CommandCompletion]: """Update one GeoJSON source with prepared data. The call borrows the handle and rejects data whose baked-in options differ from the source's, cluster properties excepted. """ - self._native.set_geojson_source_data(source_id, data._native) + return self._native.set_geojson_source_data(source_id, data._native) def set_geojson_source_synchronous_tiling( self, source_id: str, enabled: bool - ) -> None: + ) -> Future[CommandCompletion]: """Override one GeoJSON source's synchronous tiling at runtime. Tiling runs inline when either the source's baked-in option or this override enables it. """ - self._native.set_geojson_source_synchronous_tiling(source_id, enabled) + return self._native.set_geojson_source_synchronous_tiling(source_id, enabled) def _add_tile_source_url( self, - add: Callable[..., None], + add: Callable[..., Future[CommandCompletion]], source_id: str, url: str, options: TileSourceOptions | None, - ) -> None: - add(source_id, url, *_tile_source_parts(options)) + ) -> Future[CommandCompletion]: + return add(source_id, url, *_tile_source_parts(options)) def _add_tile_source_tiles( self, - add: Callable[..., None], + add: Callable[..., Future[CommandCompletion]], source_id: str, tiles: list[str] | tuple[str, ...], options: TileSourceOptions | None, - ) -> None: - add(source_id, list(tiles), *_tile_source_parts(options)) + ) -> Future[CommandCompletion]: + return add(source_id, list(tiles), *_tile_source_parts(options)) def add_vector_source_url( self, source_id: str, url: str, options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a vector source with a TileJSON URL.""" - self._add_tile_source_url( + return self._add_tile_source_url( self._native.add_vector_source_url, source_id, url, options ) @@ -779,9 +902,9 @@ def add_raster_source_url( source_id: str, url: str, options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a raster source with a TileJSON URL.""" - self._add_tile_source_url( + return self._add_tile_source_url( self._native.add_raster_source_url, source_id, url, options ) @@ -790,9 +913,9 @@ def add_raster_dem_source_url( source_id: str, url: str, options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a raster DEM source with a TileJSON URL.""" - self._add_tile_source_url( + return self._add_tile_source_url( self._native.add_raster_dem_source_url, source_id, url, options ) @@ -801,9 +924,9 @@ def add_vector_source_tiles( source_id: str, tiles: list[str] | tuple[str, ...], options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a vector source with inline tile URLs.""" - self._add_tile_source_tiles( + return self._add_tile_source_tiles( self._native.add_vector_source_tiles, source_id, tiles, options ) @@ -812,9 +935,9 @@ def add_raster_source_tiles( source_id: str, tiles: list[str] | tuple[str, ...], options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a raster source with inline tile URLs.""" - self._add_tile_source_tiles( + return self._add_tile_source_tiles( self._native.add_raster_source_tiles, source_id, tiles, options ) @@ -823,168 +946,187 @@ def add_raster_dem_source_tiles( source_id: str, tiles: list[str] | tuple[str, ...], options: TileSourceOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a raster DEM source with inline tile URLs.""" - self._add_tile_source_tiles( + return self._add_tile_source_tiles( self._native.add_raster_dem_source_tiles, source_id, tiles, options ) - def remove_style_source(self, source_id: str) -> bool: - """Remove a style source by ID and report whether it existed.""" - return self._native.remove_style_source(source_id) - - def style_source_exists(self, source_id: str) -> bool: - """Return whether a style source ID exists.""" - return self._native.style_source_exists(source_id) + def remove_style_source(self, source_id: str) -> Future[CommandCompletion]: + """Submit a style source removal and return its completion future. - def get_style_source_type(self, source_id: str) -> StyleSourceType | None: - """Return a style source type, or None when the source is missing.""" - from .style import StyleSourceType + The command's terminal event reports ``FAILED`` with + :attr:`MaplibreStatus.NOT_FOUND` when no style source has the ID and + ``FAILED`` with :attr:`MaplibreStatus.INVALID_STATE` while a layer + still uses the source. Re-check existence through + :meth:`get_style_source_info`. + """ + return self._native.remove_style_source(source_id) - raw = self._native.get_style_source_type(source_id) - return StyleSourceType(raw) if raw is not None else None + def get_style_source_info(self, source_id: str) -> Future[StyleSourceInfo | None]: + """Return copied retained metadata for one style source. - def get_style_source_info(self, source_id: str) -> StyleSourceInfo | None: - """Return copied retained metadata for one style source.""" + The result is None when the source is missing, so this is also the + existence check. + """ from .style import StyleSourceInfo - raw = self._native.get_style_source_info(source_id) - return StyleSourceInfo._from_native(raw) if raw is not None else None + return map_future( + self._native.get_style_source_info(source_id), + lambda raw: StyleSourceInfo._from_native(raw) if raw is not None else None, + ) - def list_style_source_ids(self) -> tuple[str, ...]: + def list_style_source_ids(self) -> Future[tuple[str, ...]]: """Return style source IDs in style order.""" - return tuple(self._native.list_style_source_ids()) + return map_future(self._native.list_style_source_ids(), tuple) def add_hillshade_layer( self, layer_id: str, source_id: str, before_layer_id: str | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a hillshade layer for a raster DEM source.""" - self._native.add_hillshade_layer(layer_id, source_id, before_layer_id) + return self._native.add_hillshade_layer(layer_id, source_id, before_layer_id) def add_color_relief_layer( self, layer_id: str, source_id: str, before_layer_id: str | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a color-relief layer for a raster DEM source.""" - self._native.add_color_relief_layer(layer_id, source_id, before_layer_id) + return self._native.add_color_relief_layer(layer_id, source_id, before_layer_id) def add_location_indicator_layer( self, layer_id: str, before_layer_id: str | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add a source-free location indicator layer.""" - self._native.add_location_indicator_layer(layer_id, before_layer_id) + return self._native.add_location_indicator_layer(layer_id, before_layer_id) def set_location_indicator_location( self, layer_id: str, coordinate: LatLng, altitude: float, - ) -> None: + ) -> Future[CommandCompletion]: """Set a location indicator layer location.""" - self._native.set_location_indicator_location( + return self._native.set_location_indicator_location( layer_id, coordinate.latitude, coordinate.longitude, altitude, ) - def set_location_indicator_bearing(self, layer_id: str, bearing: float) -> None: + def set_location_indicator_bearing( + self, layer_id: str, bearing: float + ) -> Future[CommandCompletion]: """Set a location indicator layer bearing in degrees.""" - self._native.set_location_indicator_bearing(layer_id, bearing) + return self._native.set_location_indicator_bearing(layer_id, bearing) def set_location_indicator_accuracy_radius( self, layer_id: str, radius: float, - ) -> None: + ) -> Future[CommandCompletion]: """Set a location indicator layer accuracy radius in meters.""" - self._native.set_location_indicator_accuracy_radius(layer_id, radius) + return self._native.set_location_indicator_accuracy_radius(layer_id, radius) def set_location_indicator_image_name( self, layer_id: str, image_kind: LocationIndicatorImageKind, image_id: str, - ) -> None: + ) -> Future[CommandCompletion]: """Set one location indicator image-name property.""" - self._native.set_location_indicator_image_name( + return self._native.set_location_indicator_image_name( layer_id, image_kind.native_code, image_id, ) - def remove_style_layer(self, layer_id: str) -> bool: - """Remove a style layer by ID and report whether it existed.""" + def remove_style_layer(self, layer_id: str) -> Future[CommandCompletion]: + """Submit a style layer removal and return its completion future. + + The command's terminal event reports ``FAILED`` with + :attr:`MaplibreStatus.NOT_FOUND` when no style layer has the ID. + Re-check existence through :meth:`get_style_layer_info`. + """ return self._native.remove_style_layer(layer_id) - def style_layer_exists(self, layer_id: str) -> bool: - """Return whether a style layer ID exists.""" - return self._native.style_layer_exists(layer_id) + def get_style_layer_info(self, layer_id: str) -> Future[StyleLayerInfo | None]: + """Return copied fixed metadata for one style layer. - def get_style_layer_type(self, layer_id: str) -> str | None: - """Return a style layer type string, or None when the layer is missing.""" - return self._native.get_style_layer_type(layer_id) + The result is None when the layer is missing, so this is also the + existence check. + """ + from .style import StyleLayerInfo - def list_style_layer_ids(self) -> tuple[str, ...]: + return map_future( + self._native.get_style_layer_info(layer_id), + lambda raw: StyleLayerInfo._from_native(raw) if raw is not None else None, + ) + + def list_style_layer_ids(self) -> Future[tuple[str, ...]]: """Return style layer IDs in style order.""" - return tuple(self._native.list_style_layer_ids()) + return map_future(self._native.list_style_layer_ids(), tuple) def move_style_layer( self, layer_id: str, before_layer_id: str | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Move one style layer before another layer or to the top.""" - self._native.move_style_layer(layer_id, before_layer_id) + return self._native.move_style_layer(layer_id, before_layer_id) def add_style_layer_json( self, layer_json: bytes, before_layer_id: str | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add one style layer from a full style-spec layer JSON object.""" - self._native.add_style_layer_json(layer_json, before_layer_id) + return self._native.add_style_layer_json(layer_json, before_layer_id) - def get_style_layer_json(self, layer_id: str) -> bytes | None: + def get_style_layer_json(self, layer_id: str) -> Future[bytes | None]: """Return one style layer as a full style-spec layer JSON object.""" return self._native.get_style_layer_json(layer_id) - def set_style_light_json(self, light_json: bytes) -> None: + def set_style_light_json(self, light_json: bytes) -> Future[CommandCompletion]: """Set the style light from a style-spec light JSON object.""" - self._native.set_style_light_json(light_json) + return self._native.set_style_light_json(light_json) - def set_style_light_property(self, property_name: str, value: bytes) -> None: + def set_style_light_property( + self, property_name: str, value: bytes + ) -> Future[CommandCompletion]: """Set one style light property by style-spec property name.""" - self._native.set_style_light_property(property_name, value) + return self._native.set_style_light_property(property_name, value) - def get_style_light_property(self, property_name: str) -> bytes | None: + def get_style_light_property(self, property_name: str) -> Future[bytes | None]: """Return one style light property as a style-spec JSON value.""" return self._native.get_style_light_property(property_name) - def set_style_transition_options(self, options: StyleTransitionOptions) -> None: + def set_style_transition_options( + self, options: StyleTransitionOptions + ) -> Future[CommandCompletion]: """Set the style's global transition options. This call replaces the whole transition configuration rather than merging into it. Loading a style replaces these options with the ones that style declares, so apply an override after the style loads. """ - self._native.set_style_transition_options( + return self._native.set_style_transition_options( options.duration_ms, options.delay_ms, options.enable_placement_transitions, ) - def get_style_transition_options(self) -> StyleTransitionOptions: + def get_style_transition_options(self) -> Future[StyleTransitionOptions]: """Return the style's global transition options.""" - return StyleTransitionOptions._from_native( - self._native.get_style_transition_options() + return map_future( + self._native.get_style_transition_options(), + StyleTransitionOptions._from_native, ) def set_layer_property( @@ -992,89 +1134,88 @@ def set_layer_property( layer_id: str, property_name: str, value: bytes, - ) -> None: + ) -> Future[CommandCompletion]: """Set one layer property by style-spec property name.""" - self._native.set_layer_property(layer_id, property_name, value) + return self._native.set_layer_property(layer_id, property_name, value) - def get_layer_property(self, layer_id: str, property_name: str) -> bytes | None: + def get_layer_property( + self, layer_id: str, property_name: str + ) -> Future[bytes | None]: """Return one layer property as a style-spec JSON value.""" return self._native.get_layer_property(layer_id, property_name) - def set_layer_filter(self, layer_id: str, filter: bytes | None) -> None: + def set_layer_filter( + self, layer_id: str, filter: bytes | None + ) -> Future[CommandCompletion]: """Set or clear one layer filter.""" - self._native.set_layer_filter(layer_id, filter) + return self._native.set_layer_filter(layer_id, filter) - def get_layer_filter(self, layer_id: str) -> bytes | None: + def get_layer_filter(self, layer_id: str) -> Future[bytes | None]: """Return one layer filter as a style-spec JSON value.""" return self._native.get_layer_filter(layer_id) - def set_layer_source_layer(self, layer_id: str, source_layer: str) -> None: + def set_layer_source_layer( + self, layer_id: str, source_layer: str + ) -> Future[CommandCompletion]: """Set one layer's source-layer ID. Layer types that take no source, such as background, are rejected. """ - self._native.set_layer_source_layer(layer_id, source_layer) + return self._native.set_layer_source_layer(layer_id, source_layer) - def get_layer_source_layer(self, layer_id: str) -> str: + def get_layer_source_layer(self, layer_id: str) -> Future[str]: """Return one layer's source-layer ID, empty when it carries none.""" return self._native.copy_layer_source_layer(layer_id) - def set_layer_source_id(self, layer_id: str, source_id: str) -> None: + def set_layer_source_id( + self, layer_id: str, source_id: str + ) -> Future[CommandCompletion]: """Set one layer's source ID. Layer types that take no source, such as background, are rejected. The named source need not exist yet. """ - self._native.set_layer_source_id(layer_id, source_id) + return self._native.set_layer_source_id(layer_id, source_id) - def get_layer_source_id(self, layer_id: str) -> str: + def get_layer_source_id(self, layer_id: str) -> Future[str]: """Return one layer's source ID, empty when it carries none.""" return self._native.copy_layer_source_id(layer_id) - def set_layer_min_zoom(self, layer_id: str, min_zoom: float) -> None: + def set_layer_min_zoom( + self, layer_id: str, min_zoom: float + ) -> Future[CommandCompletion]: """Set the lowest zoom at which one layer draws. - Pass ``-math.inf`` for no lower bound. - """ - self._native.set_layer_min_zoom(layer_id, min_zoom) - - def get_layer_min_zoom(self, layer_id: str) -> float: - """Return the lowest zoom at which one layer draws. - - A layer with no lower bound reports ``-math.inf``. + Pass ``-math.inf`` for no lower bound. Read the value back from + :attr:`StyleLayerInfo.min_zoom`. """ - return self._native.get_layer_min_zoom(layer_id) + return self._native.set_layer_min_zoom(layer_id, min_zoom) - def set_layer_max_zoom(self, layer_id: str, max_zoom: float) -> None: + def set_layer_max_zoom( + self, layer_id: str, max_zoom: float + ) -> Future[CommandCompletion]: """Set the highest zoom at which one layer draws. - Pass ``math.inf`` for no upper bound. + Pass ``math.inf`` for no upper bound. Read the value back from + :attr:`StyleLayerInfo.max_zoom`. """ - self._native.set_layer_max_zoom(layer_id, max_zoom) - - def get_layer_max_zoom(self, layer_id: str) -> float: - """Return the highest zoom at which one layer draws. - - A layer with no upper bound reports ``math.inf``. - """ - return self._native.get_layer_max_zoom(layer_id) + return self._native.set_layer_max_zoom(layer_id, max_zoom) def set_layer_visibility( self, layer_id: str, visibility: StyleLayerVisibility - ) -> None: - """Set whether one layer draws.""" - self._native.set_layer_visibility(layer_id, int(visibility)) + ) -> Future[CommandCompletion]: + """Set whether one layer draws. - def get_layer_visibility(self, layer_id: str) -> StyleLayerVisibility: - """Return whether one layer draws.""" - return StyleLayerVisibility(self._native.get_layer_visibility(layer_id)) + Read the value back from :attr:`StyleLayerInfo.visibility`. + """ + return self._native.set_layer_visibility(layer_id, int(visibility)) def set_style_image( self, image_id: str, image: PremultipliedRgba8Image, options: StyleImageOptions | None = None, - ) -> None: + ) -> Future[CommandCompletion]: """Add or replace one runtime style image.""" from .render import PremultipliedRgba8Image from .style import StyleImageOptions @@ -1083,7 +1224,7 @@ def set_style_image( if not isinstance(image, PremultipliedRgba8Image): msg = "image must be a PremultipliedRgba8Image" raise TypeError(msg) - self._native.set_style_image( + return self._native.set_style_image( image_id, image.info.width, image.info.height, @@ -1111,49 +1252,65 @@ def set_style_image( def get_style_image_stretches( self, image_id: str - ) -> tuple[tuple[ImageStretch, ...], tuple[ImageStretch, ...]] | None: + ) -> Future[tuple[tuple[ImageStretch, ...], tuple[ImageStretch, ...]] | None]: """Return one style image's stretchable intervals, or None when missing.""" from .style import ImageStretch - copied = self._native.copy_style_image_stretches(image_id) - if copied is None: - return None - stretch_x, stretch_y = copied - return ( - tuple(ImageStretch(from_, to) for from_, to in stretch_x), - tuple(ImageStretch(from_, to) for from_, to in stretch_y), + def adapt(copied: object): + if copied is None: + return None + stretch_x, stretch_y = copied + return ( + tuple(ImageStretch(from_, to) for from_, to in stretch_x), + tuple(ImageStretch(from_, to) for from_, to in stretch_y), + ) + + return map_future( + self._native.copy_style_image_stretches(image_id), + adapt, ) - def remove_style_image(self, image_id: str) -> bool: - """Remove a runtime style image by ID and report whether it existed.""" + def remove_style_image(self, image_id: str) -> Future[CommandCompletion]: + """Submit a runtime style image removal and return its completion future. + + The command's terminal event reports ``FAILED`` with + :attr:`MaplibreStatus.NOT_FOUND` when no runtime style image has the + ID. Re-check existence through :meth:`get_style_image_info`. + """ return self._native.remove_style_image(image_id) - def style_image_exists(self, image_id: str) -> bool: - """Return whether a runtime style image ID exists.""" - return self._native.style_image_exists(image_id) + def get_style_image_info(self, image_id: str) -> Future[StyleImageInfo | None]: + """Return fixed metadata for one runtime style image. - def get_style_image_info(self, image_id: str) -> StyleImageInfo | None: - """Return fixed metadata for one runtime style image.""" + The result is None when the image is missing, so this is also the + existence check. + """ from .style import StyleImageInfo - raw = self._native.get_style_image_info(image_id) - return StyleImageInfo._from_native(raw) if raw is not None else None + return map_future( + self._native.get_style_image_info(image_id), + lambda raw: StyleImageInfo._from_native(raw) if raw is not None else None, + ) - def copy_style_image_premultiplied_rgba8(self, image_id: str) -> StyleImage | None: + def copy_style_image_premultiplied_rgba8( + self, image_id: str + ) -> Future[StyleImage | None]: """Copy one runtime style image as premultiplied RGBA8 pixels.""" from .style import StyleImage - raw = self._native.copy_style_image_premultiplied_rgba8(image_id) - return StyleImage._from_native(raw) if raw is not None else None + return map_future( + self._native.copy_style_image_premultiplied_rgba8(image_id), + lambda raw: StyleImage._from_native(raw) if raw is not None else None, + ) def add_image_source_url( self, source_id: str, coordinates: list[LatLng] | tuple[LatLng, ...], url: str, - ) -> None: + ) -> Future[CommandCompletion]: """Add an image source that loads its image from a URL.""" - self._native.add_image_source_url( + return self._native.add_image_source_url( source_id, _coordinate_parts(coordinates), url ) @@ -1162,48 +1319,58 @@ def add_image_source_image( source_id: str, coordinates: list[LatLng] | tuple[LatLng, ...], image: PremultipliedRgba8Image, - ) -> None: + ) -> Future[CommandCompletion]: """Add an image source with inline image pixels.""" - self._native.add_image_source_image( + return self._native.add_image_source_image( source_id, _coordinate_parts(coordinates), *_image_parts(image), ) - def set_image_source_url(self, source_id: str, url: str) -> None: + def set_image_source_url( + self, source_id: str, url: str + ) -> Future[CommandCompletion]: """Update an image source to load its image from a URL.""" - self._native.set_image_source_url(source_id, url) + return self._native.set_image_source_url(source_id, url) def set_image_source_image( self, source_id: str, image: PremultipliedRgba8Image, - ) -> None: + ) -> Future[CommandCompletion]: """Update an image source with inline image pixels.""" - self._native.set_image_source_image(source_id, *_image_parts(image)) + return self._native.set_image_source_image(source_id, *_image_parts(image)) def set_image_source_coordinates( self, source_id: str, coordinates: list[LatLng] | tuple[LatLng, ...], - ) -> None: + ) -> Future[CommandCompletion]: """Update image source coordinates.""" - self._native.set_image_source_coordinates( + return self._native.set_image_source_coordinates( source_id, _coordinate_parts(coordinates) ) - def get_image_source_coordinates(self, source_id: str) -> tuple[LatLng, ...] | None: + def get_image_source_coordinates( + self, source_id: str + ) -> Future[tuple[LatLng, ...] | None]: """Return copied image source coordinates, or None when missing.""" from .geo import LatLng - raw = self._native.get_image_source_coordinates(source_id) - if raw is None: - return None - return tuple(LatLng(**coordinate) for coordinate in raw) + return map_future( + self._native.get_image_source_coordinates(source_id), + lambda raw: ( + None + if raw is None + else tuple(LatLng(**coordinate) for coordinate in raw) + ), + ) - def create_projection(self) -> MapProjectionHandle: + def create_projection(self) -> Future[MapProjectionHandle]: """Create a standalone projection helper from the current map transform.""" - return MapProjectionHandle._from_native(self._native.create_projection()) + return map_future( + self._native.create_projection(), MapProjectionHandle._from_native + ) def get_camera(self) -> CameraOptions: """Return the current camera snapshot.""" @@ -1211,84 +1378,98 @@ def get_camera(self) -> CameraOptions: return CameraOptions._from_native(self._native.get_camera()) - def jump_to(self, camera: CameraOptions) -> None: - """Apply a camera jump command.""" - self._native.jump_to(*_camera_parts(camera)) + def get_camera_ordered(self) -> Future[CameraSnapshot]: + """Return a future for a camera read ordered after prior commands.""" + return map_future( + self._native.get_camera_ordered(), CameraSnapshot._from_native + ) + + def jump_to(self, camera: CameraOptions) -> Future[CommandCompletion]: + """Submit a camera jump and return its completion future.""" + return self._native.jump_to(*_camera_parts(camera)) def ease_to( self, camera: CameraOptions, animation: AnimationOptions | None = None, - ) -> None: - """Apply a camera ease transition command. - - An absent `animation`, or one with no duration, reaches the target - before this call returns, with no runtime pump in between. - """ - self._native.ease_to(*_camera_parts(camera), _animation_parts(animation)) + ) -> Future[CommandCompletion]: + """Submit a camera ease transition and return its completion future.""" + return self._native.ease_to(*_camera_parts(camera), _animation_parts(animation)) def fly_to( self, camera: CameraOptions, animation: AnimationOptions | None = None, - ) -> None: - """Apply a camera fly transition command. + ) -> Future[CommandCompletion]: + """Submit a camera fly transition and return its completion future.""" + return self._native.fly_to(*_camera_parts(camera), _animation_parts(animation)) - Fly animates by default: an absent `animation`, or one with no - duration, derives a duration from a velocity of 1.2 screenfuls per - second, so the camera advances as the runtime is pumped. - """ - self._native.fly_to(*_camera_parts(camera), _animation_parts(animation)) + def apply_camera_delta(self, delta: CameraDelta) -> Future[CommandCompletion]: + """Submit one relative camera operation.""" + anchor = delta.anchor + return self._native.apply_camera_delta( + int(delta.kind), + (delta.offset.x, delta.offset.y), + delta.amount, + None if anchor is None else (anchor.x, anchor.y), + _animation_parts(delta.animation), + ) def camera_for_lat_lng_bounds( self, bounds: LatLngBounds, fit: CameraFitOptions | None = None, - ) -> CameraOptions: + ) -> Future[CameraOptions]: """Compute a camera that fits geographic bounds in the current viewport.""" from .camera import CameraOptions - raw = self._native.camera_for_lat_lng_bounds( - (bounds.southwest.latitude, bounds.southwest.longitude), - (bounds.northeast.latitude, bounds.northeast.longitude), - *_fit_parts(fit), + return map_future( + self._native.camera_for_lat_lng_bounds( + (bounds.southwest.latitude, bounds.southwest.longitude), + (bounds.northeast.latitude, bounds.northeast.longitude), + *_fit_parts(fit), + ), + CameraOptions._from_native, ) - return CameraOptions._from_native(raw) def camera_for_lat_lngs( self, coordinates: list[LatLng] | tuple[LatLng, ...], fit: CameraFitOptions | None = None, - ) -> CameraOptions: + ) -> Future[CameraOptions]: """Compute a camera that fits geographic coordinates in the current viewport.""" from .camera import CameraOptions - raw = self._native.camera_for_lat_lngs( - _coordinate_parts(coordinates), - *_fit_parts(fit), + return map_future( + self._native.camera_for_lat_lngs( + _coordinate_parts(coordinates), + *_fit_parts(fit), + ), + CameraOptions._from_native, ) - return CameraOptions._from_native(raw) def camera_for_geometry( self, geometry: bytes, fit: CameraFitOptions | None = None, - ) -> CameraOptions: + ) -> Future[CameraOptions]: """Compute a camera that fits a geometry in the current viewport.""" from .camera import CameraOptions - raw = self._native.camera_for_geometry( - geometry, - *_fit_parts(fit), + return map_future( + self._native.camera_for_geometry( + geometry, + *_fit_parts(fit), + ), + CameraOptions._from_native, ) - return CameraOptions._from_native(raw) def lat_lng_bounds_for_camera( self, camera: CameraOptions, *, unwrapped: bool = False, - ) -> LatLngBounds: + ) -> Future[LatLngBounds]: """Compute geographic bounds for a camera from two viewport corners. The box is the hull of the top-left and bottom-right screen corners @@ -1304,124 +1485,28 @@ def lat_lng_bounds_for_camera( """ from .geo import LatLng, LatLngBounds - raw = self._native.lat_lng_bounds_for_camera(*_camera_parts(camera), unwrapped) - return LatLngBounds( - southwest=LatLng(**raw["southwest"]), - northeast=LatLng(**raw["northeast"]), - ) - - def get_bounds(self) -> BoundOptions: - """Return map camera constraint options.""" - from .camera import BoundOptions - - return BoundOptions._from_native(self._native.get_bounds()) - - def set_bounds(self, bounds: BoundOptions) -> None: - """Apply selected map camera constraint options.""" - self._native.set_bounds(*_bounds_parts(bounds)) - - def move_by(self, delta_x: float, delta_y: float) -> None: - """Apply a screen-space pan command.""" - self._native.move_by(delta_x, delta_y) - - def move_by_animated( - self, - delta_x: float, - delta_y: float, - animation: AnimationOptions | None = None, - ) -> None: - """Apply an animated screen-space pan command. - - Native routes this delta through the ease transition, so an absent - `animation`, or one with no duration, applies the pan instantly. - """ - self._native.move_by_animated(delta_x, delta_y, _animation_parts(animation)) - - def scale_by(self, scale: float, anchor: ScreenPoint | None = None) -> None: - """Apply a screen-space zoom command.""" - raw_anchor = (anchor.x, anchor.y) if anchor is not None else None - self._native.scale_by(scale, raw_anchor) - - def scale_by_animated( - self, - scale: float, - anchor: ScreenPoint | None = None, - animation: AnimationOptions | None = None, - ) -> None: - """Apply an animated screen-space zoom command. - - Native routes this delta through the ease transition, so an absent - `animation`, or one with no duration, applies the zoom instantly. - """ - raw_anchor = (anchor.x, anchor.y) if anchor is not None else None - self._native.scale_by_animated(scale, raw_anchor, _animation_parts(animation)) - - def rotate_by(self, first: ScreenPoint, second: ScreenPoint) -> None: - """Apply a screen-space rotate command.""" - self._native.rotate_by((first.x, first.y), (second.x, second.y)) - - def rotate_by_animated( - self, - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions | None = None, - ) -> None: - """Apply an animated screen-space rotate command. - - Native routes this delta through the ease transition, so an absent - `animation`, or one with no duration, applies the rotation instantly. - """ - self._native.rotate_by_animated( - (first.x, first.y), - (second.x, second.y), - _animation_parts(animation), + return map_future( + self._native.lat_lng_bounds_for_camera(*_camera_parts(camera), unwrapped), + lambda raw: LatLngBounds( + southwest=LatLng(**raw["southwest"]), + northeast=LatLng(**raw["northeast"]), + ), ) - def pitch_by(self, pitch: float) -> None: - """Apply a pitch delta command.""" - self._native.pitch_by(pitch) - - def pitch_by_animated( - self, - pitch: float, - animation: AnimationOptions | None = None, - ) -> None: - """Apply an animated pitch delta command. + def set_bounds(self, bounds: BoundOptions) -> Future[CommandCompletion]: + """Submit map camera constraints and return its completion future. - Native routes this delta through the ease transition, so an absent - `animation`, or one with no duration, applies the pitch instantly. + Read the committed values back from :attr:`MapSnapshot.bounds`. """ - self._native.pitch_by_animated(pitch, _animation_parts(animation)) + return self._native.set_bounds(*_bounds_parts(bounds)) - def cancel_transitions(self) -> None: - """Cancel active camera transitions. + def set_free_camera_options( + self, options: FreeCameraOptions + ) -> Future[CommandCompletion]: + """Submit free-camera fields and return its completion future. - A cancelled transition carrying a ``transition_id`` reports its end - through a ``MAP_CAMERA_TRANSITION_FINISHED`` runtime event. + Read the committed values back from :attr:`MapSnapshot.free_camera`. """ - self._native.cancel_transitions() - - def set_gesture_in_progress(self, in_progress: bool) -> None: - """Mark whether a host-driven gesture is in progress. - - Camera commands issued while the flag is set belong to one gesture. The - flag stays set until the host clears it, so pair every ``True`` with a - ``False``. - """ - self._native.set_gesture_in_progress(in_progress) - - def is_gesture_in_progress(self) -> bool: - """Return whether a host-driven gesture is currently in progress.""" - return self._native.is_gesture_in_progress() - - def get_free_camera_options(self) -> FreeCameraOptions: - """Return the current free camera position and orientation.""" - from .camera import FreeCameraOptions - - return FreeCameraOptions._from_native(self._native.get_free_camera_options()) - - def set_free_camera_options(self, options: FreeCameraOptions) -> None: - """Apply selected free camera position and orientation fields.""" position = ( (options.position.x, options.position.y, options.position.z) if options.position is not None @@ -1437,60 +1522,68 @@ def set_free_camera_options(self, options: FreeCameraOptions) -> None: if options.orientation is not None else None ) - self._native.set_free_camera_options(position, orientation) + return self._native.set_free_camera_options(position, orientation) def get_projection_mode(self) -> ProjectionMode: - """Return the current axonometric rendering options.""" - from .camera import ProjectionMode - - return ProjectionMode._from_native(self._native.get_projection_mode()) + """Return the latest published axonometric rendering options.""" + return self.snapshot().projection_mode - def set_projection_mode(self, mode: ProjectionMode) -> None: + def set_projection_mode(self, mode: ProjectionMode) -> Future[CommandCompletion]: """Apply axonometric rendering option fields to the map.""" - self._native.set_projection_mode(mode.axonometric, mode.x_skew, mode.y_skew) + return self._native.set_projection_mode( + mode.axonometric, mode.x_skew, mode.y_skew + ) - def pixel_for_lat_lng(self, coordinate: LatLng) -> ScreenPoint: + def pixel_for_lat_lng(self, coordinate: LatLng) -> Future[ScreenPoint]: """Convert a geographic world coordinate to a screen point for this map.""" from .camera import ScreenPoint - raw = self._native.pixel_for_lat_lng( - coordinate.latitude, - coordinate.longitude, + return map_future( + self._native.pixel_for_lat_lng( + coordinate.latitude, + coordinate.longitude, + ), + lambda raw: ScreenPoint(x=raw["x"], y=raw["y"]), ) - return ScreenPoint(x=raw["x"], y=raw["y"]) - def lat_lng_for_pixel(self, point: ScreenPoint) -> LatLng: + def lat_lng_for_pixel(self, point: ScreenPoint) -> Future[LatLng]: """Convert a screen point to a geographic world coordinate for this map.""" from .geo import LatLng - raw = self._native.lat_lng_for_pixel(point.x, point.y) - return LatLng(latitude=raw["latitude"], longitude=raw["longitude"]) + return map_future( + self._native.lat_lng_for_pixel(point.x, point.y), + lambda raw: LatLng(latitude=raw["latitude"], longitude=raw["longitude"]), + ) def pixels_for_lat_lngs( self, coordinates: list[LatLng] | tuple[LatLng, ...], - ) -> tuple[ScreenPoint, ...]: + ) -> Future[tuple[ScreenPoint, ...]]: """Convert geographic world coordinates to screen points for this map.""" from .camera import ScreenPoint - raw = self._native.pixels_for_lat_lngs(_coordinate_parts(coordinates)) - return tuple(ScreenPoint(x=point["x"], y=point["y"]) for point in raw) + return map_future( + self._native.pixels_for_lat_lngs(_coordinate_parts(coordinates)), + lambda raw: tuple(ScreenPoint(x=point["x"], y=point["y"]) for point in raw), + ) def lat_lngs_for_pixels( self, points: list[ScreenPoint] | tuple[ScreenPoint, ...], - ) -> tuple[LatLng, ...]: + ) -> Future[tuple[LatLng, ...]]: """Convert screen points to geographic world coordinates for this map.""" from .geo import LatLng - raw = self._native.lat_lngs_for_pixels([(point.x, point.y) for point in points]) - return tuple(LatLng(**coordinate) for coordinate in raw) + return map_future( + self._native.lat_lngs_for_pixels([(point.x, point.y) for point in points]), + lambda raw: tuple(LatLng(**coordinate) for coordinate in raw), + ) def add_custom_geometry_source( self, source_id: str, options: CustomGeometrySourceOptions | None = None, - ) -> CustomGeometrySourceHandle: + ) -> tuple[CustomGeometrySourceHandle, Future[CommandCompletion]]: """Add a custom geometry source and return its queued-event handle. The handle closes when the source goes away: an explicit removal, a @@ -1499,7 +1592,7 @@ def add_custom_geometry_source( from .style import CustomGeometrySourceHandle, CustomGeometrySourceOptions options = options or CustomGeometrySourceOptions() - native = self._native.add_custom_geometry_source( + native, completion = self._native.add_custom_geometry_source( source_id, options.max_queued_events, options.min_zoom, @@ -1511,16 +1604,16 @@ def add_custom_geometry_source( options.wrap, options.has_cancel_tile, ) - return CustomGeometrySourceHandle._from_native(native) + return CustomGeometrySourceHandle._from_native(native), completion def set_custom_geometry_source_tile_data( self, source_id: str, tile_id: CanonicalTileId, data: bytes, - ) -> None: + ) -> Future[CommandCompletion]: """Set custom geometry source data for one canonical tile.""" - self._native.set_custom_geometry_source_tile_data( + return self._native.set_custom_geometry_source_tile_data( source_id, tile_id.z, tile_id.x, @@ -1532,9 +1625,9 @@ def invalidate_custom_geometry_source_tile( self, source_id: str, tile_id: CanonicalTileId, - ) -> None: + ) -> Future[CommandCompletion]: """Invalidate custom geometry source data for one canonical tile.""" - self._native.invalidate_custom_geometry_source_tile( + return self._native.invalidate_custom_geometry_source_tile( source_id, tile_id.z, tile_id.x, @@ -1545,9 +1638,9 @@ def invalidate_custom_geometry_source_region( self, source_id: str, bounds: LatLngBounds, - ) -> None: + ) -> Future[CommandCompletion]: """Invalidate custom geometry source data inside one geographic region.""" - self._native.invalidate_custom_geometry_source_region( + return self._native.invalidate_custom_geometry_source_region( source_id, (bounds.southwest.latitude, bounds.southwest.longitude), (bounds.northeast.latitude, bounds.northeast.longitude), @@ -1557,7 +1650,7 @@ def add_custom_mvt_vector_source( self, source_id: str, options: CustomMvtVectorSourceOptions | None = None, - ) -> CustomMvtVectorSourceHandle: + ) -> tuple[CustomMvtVectorSourceHandle, Future[CommandCompletion]]: """Add a custom MVT vector source and return its queued-event handle. The handle closes when the source goes away: an explicit removal, a @@ -1566,23 +1659,23 @@ def add_custom_mvt_vector_source( from .style import CustomMvtVectorSourceHandle, CustomMvtVectorSourceOptions options = options or CustomMvtVectorSourceOptions() - native = self._native.add_custom_mvt_vector_source( + native, completion = self._native.add_custom_mvt_vector_source( source_id, options.max_queued_events, options.min_zoom, options.max_zoom, options.has_cancel_tile, ) - return CustomMvtVectorSourceHandle._from_native(native) + return CustomMvtVectorSourceHandle._from_native(native), completion def set_custom_mvt_vector_source_tile_data( self, source_id: str, tile_id: CanonicalTileId, data: bytes, - ) -> None: + ) -> Future[CommandCompletion]: """Set custom MVT vector source data for one canonical tile.""" - self._native.set_custom_mvt_vector_source_tile_data( + return self._native.set_custom_mvt_vector_source_tile_data( source_id, tile_id.z, tile_id.x, @@ -1595,9 +1688,9 @@ def set_custom_mvt_vector_source_tile_error( source_id: str, tile_id: CanonicalTileId, message: str, - ) -> None: + ) -> Future[CommandCompletion]: """Report a custom MVT vector source error for one canonical tile.""" - self._native.set_custom_mvt_vector_source_tile_error( + return self._native.set_custom_mvt_vector_source_tile_error( source_id, tile_id.z, tile_id.x, @@ -1609,9 +1702,9 @@ def invalidate_custom_mvt_vector_source_tile( self, source_id: str, tile_id: CanonicalTileId, - ) -> None: + ) -> Future[CommandCompletion]: """Invalidate custom MVT vector source data for one canonical tile.""" - self._native.invalidate_custom_mvt_vector_source_tile( + return self._native.invalidate_custom_mvt_vector_source_tile( source_id, tile_id.z, tile_id.x, @@ -1622,36 +1715,46 @@ def _attach_render_session( self, attach: Callable[..., object], descriptor: Any, + options: RenderSessionAttachOptions, *args: object, - ) -> RenderSessionHandle: + ) -> tuple[RenderSessionHandle, Any]: extent = descriptor.extent - native = attach( + native, completion = attach( self._native, extent.width, extent.height, extent.scale_factor, *args, + options.driver.native_code, + options.requested_texture_ring_depth, ) - return RenderSessionHandle._from_native(native, self) + session = RenderSessionHandle._from_native(native, self) + return session, retain_future(completion, session) def attach_metal_surface( - self, descriptor: MetalSurfaceDescriptor - ) -> RenderSessionHandle: - """Attach a Metal native surface render target to this map.""" + self, + descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a Metal native surface render target.""" return self._attach_render_session( _native.attach_metal_surface, descriptor, + options, descriptor.context.device.address, descriptor.layer.address, ) def attach_vulkan_surface( - self, descriptor: VulkanSurfaceDescriptor - ) -> RenderSessionHandle: - """Attach a Vulkan native surface render target to this map.""" + self, + descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a Vulkan native surface render target.""" return self._attach_render_session( _native.attach_vulkan_surface, descriptor, + options, descriptor.context.instance.address, descriptor.context.physical_device.address, descriptor.context.device.address, @@ -1662,35 +1765,62 @@ def attach_vulkan_surface( descriptor.surface.address, ) + def attach_webgpu_surface( + self, + descriptor: WebGPUSurfaceDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a WebGPU native surface render target.""" + context = descriptor.context + return self._attach_render_session( + _native.attach_webgpu_surface, + descriptor, + options, + context.instance.address, + context.device.address, + context.queue.address, + descriptor.surface.address, + descriptor.format, + ) + def attach_metal_owned_texture( - self, descriptor: MetalOwnedTextureDescriptor - ) -> RenderSessionHandle: - """Attach a Metal session-owned texture render target to this map.""" + self, + descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_OWNED_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a Metal session-owned texture ring.""" return self._attach_render_session( _native.attach_metal_owned_texture, descriptor, + options, descriptor.context.device.address, ) def attach_metal_borrowed_texture( - self, descriptor: MetalBorrowedTextureDescriptor - ) -> RenderSessionHandle: - """Attach a Metal caller-owned texture render target to this map.""" + self, + descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a caller-owned Metal texture.""" return self._attach_render_session( _native.attach_metal_borrowed_texture, descriptor, + options, descriptor.physical_width, descriptor.physical_height, descriptor.texture.address, ) def attach_vulkan_owned_texture( - self, descriptor: VulkanOwnedTextureDescriptor - ) -> RenderSessionHandle: - """Attach a Vulkan session-owned texture render target to this map.""" + self, + descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_OWNED_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a Vulkan session-owned texture ring.""" return self._attach_render_session( _native.attach_vulkan_owned_texture, descriptor, + options, descriptor.context.instance.address, descriptor.context.physical_device.address, descriptor.context.device.address, @@ -1700,13 +1830,53 @@ def attach_vulkan_owned_texture( descriptor.context.get_device_proc_addr.address, ) + def attach_webgpu_owned_texture( + self, + descriptor: WebGPUOwnedTextureDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_OWNED_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a WebGPU session-owned texture ring.""" + context = descriptor.context + return self._attach_render_session( + _native.attach_webgpu_owned_texture, + descriptor, + options, + context.instance.address, + context.device.address, + context.queue.address, + ) + + def attach_webgpu_borrowed_texture( + self, + descriptor: WebGPUBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a caller-owned WebGPU texture.""" + context = descriptor.context + return self._attach_render_session( + _native.attach_webgpu_borrowed_texture, + descriptor, + options, + descriptor.physical_width, + descriptor.physical_height, + context.instance.address, + context.device.address, + context.queue.address, + descriptor.texture.address, + descriptor.texture_view.address, + descriptor.format, + ) + def attach_vulkan_borrowed_texture( - self, descriptor: VulkanBorrowedTextureDescriptor - ) -> RenderSessionHandle: - """Attach a Vulkan caller-owned texture render target to this map.""" + self, + descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = _CORE_WORKER_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a caller-owned Vulkan texture.""" return self._attach_render_session( _native.attach_vulkan_borrowed_texture, descriptor, + options, descriptor.physical_width, descriptor.physical_height, descriptor.context.instance.address, @@ -1724,15 +1894,18 @@ def attach_vulkan_borrowed_texture( ) def attach_opengl_surface( - self, descriptor: OpenGLSurfaceDescriptor - ) -> RenderSessionHandle: - """Attach an OpenGL native surface render target to this map.""" + self, + descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching an OpenGL native surface render target.""" platform, ownership, first, second, share, client_api, get_proc = ( _opengl_context_parts(descriptor.context) ) return self._attach_render_session( _native.attach_opengl_surface, descriptor, + options, platform, ownership, first, @@ -1744,15 +1917,18 @@ def attach_opengl_surface( ) def attach_opengl_owned_texture( - self, descriptor: OpenGLOwnedTextureDescriptor - ) -> RenderSessionHandle: - """Attach an OpenGL session-owned texture render target to this map.""" + self, + descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_OWNED_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching an OpenGL session-owned texture ring.""" platform, ownership, first, second, share, client_api, get_proc = ( _opengl_context_parts(descriptor.context) ) return self._attach_render_session( _native.attach_opengl_owned_texture, descriptor, + options, platform, ownership, first, @@ -1763,15 +1939,18 @@ def attach_opengl_owned_texture( ) def attach_opengl_borrowed_texture( - self, descriptor: OpenGLBorrowedTextureDescriptor - ) -> RenderSessionHandle: - """Attach an OpenGL caller-owned texture render target to this map.""" + self, + descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = _CALLER_GRAPHICS_ATTACH_OPTIONS, + ) -> tuple[RenderSessionHandle, Any]: + """Start attaching a caller-owned OpenGL texture.""" platform, ownership, first, second, share, client_api, get_proc = ( _opengl_context_parts(descriptor.context) ) return self._attach_render_session( _native.attach_opengl_borrowed_texture, descriptor, + options, descriptor.physical_width, descriptor.physical_height, platform, @@ -1787,12 +1966,14 @@ def attach_opengl_borrowed_texture( __all__ = [ + "CameraSnapshot", "ConstrainMode", "MapDebugOptions", "MapHandle", "MapMode", "MapOptions", "MapProjectionHandle", + "MapSnapshot", "MapTileOptions", "MapViewportOptions", "NorthOrientation", @@ -1803,4 +1984,9 @@ def attach_opengl_borrowed_texture( "projected_meters_for_lat_lng", ] -from .runtime import RuntimeEventMask, RuntimeHandle, _default_map_event_mask +from .runtime import ( + CommandCompletion, + RuntimeEventMask, + RuntimeHandle, + _default_map_event_mask, +) diff --git a/bindings/python/python/maplibre_native_ffi/offline.py b/bindings/python/python/maplibre_native_ffi/offline.py index 41517c6d4..707f82894 100644 --- a/bindings/python/python/maplibre_native_ffi/offline.py +++ b/bindings/python/python/maplibre_native_ffi/offline.py @@ -2,16 +2,12 @@ from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass from ._enum import NativeIntEnum, UnknownIntEnum -from ._lifecycle import ContextHandleMixin, WarnUnclosedMixin from .geo import LatLngBounds from .resource import ResourceErrorReason -_OPERATION_HANDLE_CREATE_KEY = object() - class AmbientCacheOperation(NativeIntEnum): """Ambient cache maintenance operation kinds.""" @@ -40,135 +36,6 @@ def native_code_for_set(self) -> int: return self.known_native_code("offline region download state") -class OfflineOperationKind(UnknownIntEnum): - """Offline database operation kinds reported by completion events.""" - - AMBIENT_CACHE = 1 - REGION_CREATE = 2 - REGION_GET = 3 - REGIONS_LIST = 4 - REGIONS_MERGE_DATABASE = 5 - REGION_UPDATE_METADATA = 6 - REGION_GET_STATUS = 7 - REGION_SET_OBSERVED = 8 - REGION_SET_DOWNLOAD_STATE = 9 - REGION_INVALIDATE = 10 - REGION_DELETE = 11 - SET_MAXIMUM_AMBIENT_CACHE_SIZE = 12 - - -class OfflineOperationResultKind(UnknownIntEnum): - """Offline database operation result kinds reported by completion events.""" - - NONE = 0 - REGION = 1 - OPTIONAL_REGION = 2 - REGION_LIST = 3 - REGION_STATUS = 4 - - -class OfflineOperationHandle(WarnUnclosedMixin, ContextHandleMixin): - """Runtime-owned offline database operation token.""" - - _handle_name = "OfflineOperationHandle" - - def __init__( - self, - runtime: RuntimeHandle, - operation_id: int, - *, - _create_key: object | None = None, - ) -> None: - if _create_key is not _OPERATION_HANDLE_CREATE_KEY: - msg = "OfflineOperationHandle instances are created by RuntimeHandle" - raise TypeError(msg) - self._runtime = runtime - self._operation_id = operation_id - self._closed = False - runtime._register_offline_operation(self) - - @classmethod - def _from_native( - cls, runtime: RuntimeHandle, operation_id: int - ) -> OfflineOperationHandle: - return cls( - runtime, - operation_id, - _create_key=_OPERATION_HANDLE_CREATE_KEY, - ) - - @property - def closed(self) -> bool: - """Return whether this operation token has been discarded.""" - return self._closed - - def close(self) -> None: - """Discard runtime-owned state for this operation.""" - if self._closed: - return - self._runtime._native.offline_operation_discard(self._operation_id) - self._mark_closed() - - def _mark_closed(self) -> None: - if self._closed: - return - self._closed = True - self._runtime._unregister_offline_operation(self) - - def _mark_runtime_closed(self) -> None: - self._mark_closed() - - def _ensure_open(self) -> None: - if self._closed: - from .errors import InvalidStateError - - raise InvalidStateError(None, "offline operation handle is already closed") - - def _take(self, take: Callable[[int], object]) -> object: - self._ensure_open() - raw = take(self._operation_id) - self._mark_closed() - return raw - - def take_region(self) -> OfflineRegionInfo: - """Take a completed region snapshot result.""" - return OfflineRegionInfo._from_native( - self._take(self._runtime._native.offline_region_create_take_result) - ) - - def take_optional_region(self) -> OfflineRegionInfo | None: - """Take a completed optional region snapshot result.""" - raw = self._take(self._runtime._native.offline_region_get_take_result) - return OfflineRegionInfo._from_native(raw) if raw is not None else None - - def take_region_list( - self, - *, - merge_result: bool = False, - ) -> tuple[OfflineRegionInfo, ...]: - """Take a completed region-list result.""" - take = ( - self._runtime._native.offline_regions_merge_database_take_result - if merge_result - else self._runtime._native.offline_regions_list_take_result - ) - return tuple( - OfflineRegionInfo._from_native(region) for region in self._take(take) - ) - - def take_updated_region(self) -> OfflineRegionInfo: - """Take a completed updated region snapshot result.""" - return OfflineRegionInfo._from_native( - self._take(self._runtime._native.offline_region_update_metadata_take_result) - ) - - def take_status(self) -> OfflineRegionStatus: - """Take a completed offline region status result.""" - return OfflineRegionStatus._from_native( - self._take(self._runtime._native.offline_region_get_status_take_result) - ) - - @dataclass(frozen=True, slots=True) class OfflineRegionStatus: """Offline region status snapshot.""" @@ -309,31 +176,20 @@ def _from_runtime_payload( ) -@dataclass(frozen=True, slots=True) -class OfflineOperationCompleted: - """Offline operation completion event payload.""" +def _adapt_region_result(raw: object) -> OfflineRegionInfo: + return OfflineRegionInfo._from_native(raw) - operation_id: int - operation_kind: OfflineOperationKind - result_kind: OfflineOperationResultKind - result_status: int - found: bool - @classmethod - def _from_runtime_payload( - cls, payload: dict[str, object] - ) -> OfflineOperationCompleted: - return cls( - operation_id=_payload_int(payload, "operation_id"), - operation_kind=OfflineOperationKind( - _payload_int(payload, "operation_kind") - ), - result_kind=OfflineOperationResultKind( - _payload_int(payload, "result_kind") - ), - result_status=_payload_int(payload, "result_status"), - found=_payload_bool(payload, "found"), - ) +def _adapt_optional_region_result(raw: object) -> OfflineRegionInfo | None: + return OfflineRegionInfo._from_native(raw) if raw is not None else None + + +def _adapt_region_list_result(raw: object) -> tuple[OfflineRegionInfo, ...]: + return tuple(OfflineRegionInfo._from_native(region) for region in raw) + + +def _adapt_status_result(raw: object) -> OfflineRegionStatus: + return OfflineRegionStatus._from_native(raw) def _definition_from_native_wire(raw: dict[str, object]) -> OfflineRegionDefinition: @@ -378,21 +234,9 @@ def _payload_int(payload: dict[str, object], key: str) -> int: return value -def _payload_bool(payload: dict[str, object], key: str) -> bool: - value = payload[key] - if not isinstance(value, bool): - msg = f"offline payload {key} must be a bool" - raise TypeError(msg) - return value - - __all__ = [ "AmbientCacheOperation", "OfflineGeometryRegionDefinition", - "OfflineOperationCompleted", - "OfflineOperationHandle", - "OfflineOperationKind", - "OfflineOperationResultKind", "OfflineRegionDefinition", "OfflineRegionDefinitionType", "OfflineRegionDownloadState", @@ -403,5 +247,3 @@ def _payload_bool(payload: dict[str, object], key: str) -> bool: "OfflineRegionTileCountLimitExceeded", "OfflineTilePyramidRegionDefinition", ] - -from .runtime import RuntimeHandle diff --git a/bindings/python/python/maplibre_native_ffi/render.py b/bindings/python/python/maplibre_native_ffi/render.py index b1e321556..113b1a75c 100644 --- a/bindings/python/python/maplibre_native_ffi/render.py +++ b/bindings/python/python/maplibre_native_ffi/render.py @@ -3,12 +3,14 @@ from __future__ import annotations from collections.abc import Callable +from concurrent.futures import Future from dataclasses import dataclass, field from enum import IntFlag -from typing import Any +from typing import Any, cast from . import _native from ._enum import UnknownIntEnum +from ._future import map_future from ._lifecycle import NativeHandleMixin from .query import ( QueriedFeature, @@ -17,11 +19,25 @@ SourceFeatureQueryOptions, ) -_DETACHED_RENDER_SESSION_HANDLE_CREATE_KEY = object() _RENDER_SESSION_HANDLE_CREATE_KEY = object() _METAL_FRAME_HANDLE_CREATE_KEY = object() _VULKAN_FRAME_HANDLE_CREATE_KEY = object() _OPENGL_FRAME_HANDLE_CREATE_KEY = object() +_WEBGPU_FRAME_HANDLE_CREATE_KEY = object() + + +def _identity(value: object) -> object: + return value + + +def _cast_bytes(value: object) -> bytes: + return cast(bytes, value) + + +def _cast_queried_features(value: object) -> list[QueriedFeature]: + return [ + QueriedFeature._from_native(raw) for raw in cast("list[dict[str, Any]]", value) + ] class RenderBackend(IntFlag): @@ -44,39 +60,79 @@ class OpenGLContextProvider(IntFlag): class RenderResult(UnknownIntEnum): - """Outcome reported by :meth:`RenderSessionHandle.render_update`.""" + """Terminal disposition of an accepted frame demand.""" RENDERED = 0 NO_UPDATE = 1 SIZE_PENDING = 2 TARGET_NOT_READY = 3 + SUPERSEDED = 4 + DEADLINE_MISSED = 5 -@dataclass(frozen=True, slots=True) -class RenderUpdate: - """Outcome of one :meth:`RenderSessionHandle.render_update` call. +class RenderDriver(UnknownIntEnum): + """Execution placement for a render session.""" - ``result`` names the wake to wait for before rendering again. - ``needs_repaint`` reports whether the map asked for another frame while it - rendered this one, as during an ongoing camera transition. It is set only - when ``result`` is ``RENDERED``, and reads false for every other outcome. - """ + CORE_WORKER = 1 + CALLER_GRAPHICS_THREAD = 2 - result: RenderResult - needs_repaint: bool + +class RenderSessionState(UnknownIntEnum): + """Lifecycle state reported by a render-session snapshot.""" + + ATTACHING = 1 + ATTACHED = 2 + DETACHING = 3 + DETACHED = 4 + TARGET_LOST = 5 + ABANDONED = 6 + + +class FrameDemandFlag(IntFlag): + """Frame-demand policy bits.""" + + IF_NEEDED = 1 << 0 + PRESENT = 1 << 1 + + +class RenderSessionCapability(IntFlag): + """Optional render-session capabilities.""" + + FRAME_ACQUISITION = 1 << 0 + READBACK = 1 << 1 + CONSUMER_SYNC = 1 << 2 + PRESENTATION = 1 << 3 + + +class GpuSyncKind(UnknownIntEnum): + """Synchronization payload kind for an acquired texture frame.""" + + CPU_COMPLETE = 0 + METAL_SHARED_EVENT = 1 + VULKAN_TIMELINE_SEMAPHORE = 2 + OPENGL_FENCE = 3 + WEBGPU_TOKEN = 4 + + +class RenderAbandonDisposition(UnknownIntEnum): + """Resource disposition after irreversible target abandonment.""" + + CLEAN = 0 + QUARANTINED = 1 class OpenGLContextOwnership(UnknownIntEnum): - """How a session's OpenGL context relates to the thread that attached it. + """How a session's OpenGL context relates to its driver thread and host + graphics state. A shared session leaves the thread as it found it: every render makes the session context current and restores whatever was current before. The session context joins the share group named by the descriptor, so a host may hand the session a texture and sample it from its own context. - A dedicated session owns the thread. It makes its context current once and - keeps it current between renders, and it joins no share group. Use this when - a thread exists to drive one render session and runs no other graphics work. + A dedicated session owns its driver thread's context. It keeps the context + current between renders and joins no share group. The driver may be a native + core worker or a dedicated host thread. """ SHARED = 0 @@ -202,6 +258,146 @@ def physical_size(self) -> tuple[int, int]: ) +@dataclass(frozen=True, slots=True) +class RenderSessionAttachOptions: + """Driver placement and texture-ring policy for attachment.""" + + driver: RenderDriver + requested_texture_ring_depth: int = 0 + + +@dataclass(frozen=True, slots=True) +class FrameDemand: + """One nonblocking frame request.""" + + flags: FrameDemandFlag = FrameDemandFlag.IF_NEEDED + token: int = 0 + coalescing_boundary: int = 0 + timeout_ns: int = 0 + + +@dataclass(frozen=True, slots=True) +class RenderFrameResult: + """Owned terminal result for one accepted frame demand. + + ``needs_repaint`` reports whether the map asked for another frame while it + rendered this one, as during an ongoing camera transition. It is set only + when ``disposition`` is ``RENDERED``, and reads false for every other + outcome. + """ + + disposition: RenderResult + token: int + map_update_generation: int + extent_generation: int + frame_generation: int + needs_repaint: bool + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> RenderFrameResult: + return cls( + disposition=RenderResult(raw["disposition"]), + token=raw["token"], + map_update_generation=raw["map_update_generation"], + extent_generation=raw["extent_generation"], + frame_generation=raw["frame_generation"], + needs_repaint=raw["needs_repaint"], + ) + + +@dataclass(frozen=True, slots=True) +class RenderSessionCapabilities: + """Immutable capabilities negotiated during attachment.""" + + driver: RenderDriver + texture_ring_depth: int + flags: RenderSessionCapability + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> RenderSessionCapabilities: + return cls( + driver=RenderDriver(raw["driver"]), + texture_ring_depth=raw["texture_ring_depth"], + flags=RenderSessionCapability(raw["flags"]), + ) + + +@dataclass(frozen=True, slots=True) +class RenderSessionSnapshot: + """Any-thread snapshot of render-session state and generations.""" + + state: RenderSessionState + driver: RenderDriver + latest_result: RenderResult + extent: RenderTargetExtent + generation: int + map_update_generation: int + rendered_update_generation: int + extent_generation: int + frame_generation: int + latest_demand_token: int + pending_demand_count: int + acquired_frame_count: int + target_ready: bool + pending_changes: bool + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> RenderSessionSnapshot: + width, height, scale_factor = raw["extent"] + return cls( + state=RenderSessionState(raw["state"]), + driver=RenderDriver(raw["driver"]), + latest_result=RenderResult(raw["latest_result"]), + extent=RenderTargetExtent(width, height, scale_factor), + generation=raw["generation"], + map_update_generation=raw["map_update_generation"], + rendered_update_generation=raw["rendered_update_generation"], + extent_generation=raw["extent_generation"], + frame_generation=raw["frame_generation"], + latest_demand_token=raw["latest_demand_token"], + pending_demand_count=raw["pending_demand_count"], + acquired_frame_count=raw["acquired_frame_count"], + target_ready=raw["target_ready"], + pending_changes=raw["pending_changes"], + ) + + +@dataclass(frozen=True, slots=True) +class GpuSync: + """Backend synchronization copied across a frame lease boundary.""" + + kind: GpuSyncKind = GpuSyncKind.CPU_COMPLETE + object: NativePointer = field(default_factory=NativePointer.null) + value: int = 0 + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> GpuSync: + return cls( + kind=GpuSyncKind(raw["kind"]), + object=NativePointer(raw["object_address"]), + value=raw["value"], + ) + + +_DEFAULT_FRAME_DEMAND = FrameDemand() +_DEFAULT_GPU_SYNC = GpuSync() + + +@dataclass(frozen=True, slots=True) +class RenderAbandonResult: + """Result of irreversible CPU-side target abandonment.""" + + disposition: RenderAbandonDisposition + quarantined_resource_count: int + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> RenderAbandonResult: + return cls( + disposition=RenderAbandonDisposition(raw["disposition"]), + quarantined_resource_count=raw["quarantined_resource_count"], + ) + + @dataclass(frozen=True, slots=True) class MetalContextDescriptor: """Borrowed Metal context values shared by Metal render targets.""" @@ -222,6 +418,15 @@ class VulkanContextDescriptor: get_device_proc_addr: NativePointer = field(default_factory=NativePointer.null) +@dataclass(frozen=True, slots=True) +class WebGPUContextDescriptor: + """Borrowed WebGPU context values shared by WebGPU render targets.""" + + instance: NativePointer = field(default_factory=NativePointer.null) + device: NativePointer = field(default_factory=NativePointer.null) + queue: NativePointer = field(default_factory=NativePointer.null) + + @dataclass(frozen=True, slots=True) class WglContextDescriptor: """Borrowed WGL context values shared by OpenGL render targets. @@ -276,6 +481,16 @@ class VulkanSurfaceDescriptor: surface: NativePointer = field(default_factory=NativePointer.null) +@dataclass(frozen=True, slots=True) +class WebGPUSurfaceDescriptor: + """WebGPU native surface attachment descriptor.""" + + extent: RenderTargetExtent = RenderTargetExtent() + context: WebGPUContextDescriptor = WebGPUContextDescriptor() + surface: NativePointer = field(default_factory=NativePointer.null) + format: int = 0 + + @dataclass(frozen=True, slots=True) class OpenGLSurfaceDescriptor: """OpenGL native surface attachment descriptor.""" @@ -328,6 +543,27 @@ class VulkanBorrowedTextureDescriptor: final_layout: int = 0 +@dataclass(frozen=True, slots=True) +class WebGPUOwnedTextureDescriptor: + """WebGPU session-owned texture attachment descriptor.""" + + extent: RenderTargetExtent = RenderTargetExtent() + context: WebGPUContextDescriptor = WebGPUContextDescriptor() + + +@dataclass(frozen=True, slots=True) +class WebGPUBorrowedTextureDescriptor: + """WebGPU caller-owned texture attachment descriptor.""" + + extent: RenderTargetExtent + physical_width: int + physical_height: int + context: WebGPUContextDescriptor = WebGPUContextDescriptor() + texture: NativePointer = field(default_factory=NativePointer.null) + texture_view: NativePointer = field(default_factory=NativePointer.null) + format: int = 0 + + @dataclass(frozen=True, slots=True) class OpenGLOwnedTextureDescriptor: """OpenGL session-owned texture attachment descriptor.""" @@ -428,6 +664,29 @@ def _from_native(cls, raw: dict[str, Any]) -> VulkanOwnedTextureFrame: ) +@dataclass(frozen=True, slots=True) +class WebGPUOwnedTextureFrame: + """Copied metadata for an acquired WebGPU session-owned texture frame.""" + + generation: int + width: int + height: int + scale_factor: float + frame_id: int + format: int + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> WebGPUOwnedTextureFrame: + return cls( + generation=raw["generation"], + width=raw["width"], + height=raw["height"], + scale_factor=raw["scale_factor"], + frame_id=raw["frame_id"], + format=raw["format"], + ) + + @dataclass(frozen=True, slots=True) class OpenGLOwnedTextureFrame: """Copied metadata for an acquired OpenGL session-owned texture frame.""" @@ -457,29 +716,14 @@ def _from_native(cls, raw: dict[str, Any]) -> OpenGLOwnedTextureFrame: ) -class DetachedRenderSessionHandle(NativeHandleMixin): - """Close-only render session handle after backend resources detach. +class RenderSessionHandle(NativeHandleMixin): + """Any-thread render-session control handle. - A detached session holds no reference to its former map, so it stays - destroyable after that map closes. + Caller-graphics-thread sessions additionally require + :meth:`service_driver_work` on the graphics thread. The first successful + service call fixes that thread identity for the session. """ - _handle_name = "DetachedRenderSessionHandle" - - def __init__(self, native: Any, *, _create_key: object | None = None) -> None: - if _create_key is not _DETACHED_RENDER_SESSION_HANDLE_CREATE_KEY: - msg = "DetachedRenderSessionHandle instances are created by RenderSessionHandle" - raise TypeError(msg) - self._native = native - - @classmethod - def _from_native(cls, native: Any) -> DetachedRenderSessionHandle: - return cls(native, _create_key=_DETACHED_RENDER_SESSION_HANDLE_CREATE_KEY) - - -class RenderSessionHandle(NativeHandleMixin): - """Render session handle affine to the thread that attached it.""" - _handle_name = "RenderSessionHandle" def __init__( @@ -494,23 +738,58 @@ def __init__( raise TypeError(msg) self._native = native self._map: MapHandle | None = map_handle + self._runtime = map_handle._runtime @classmethod def _from_native(cls, native: Any, map_handle: MapHandle) -> RenderSessionHandle: return cls(native, map_handle, _create_key=_RENDER_SESSION_HANDLE_CREATE_KEY) + def close(self) -> None: + """Destroy detached or abandoned CPU-side session state.""" + self._native.close() + self._map = None + + def _future( + self, + start: Callable[..., Future[Any]], + adapt_result: Callable[[Any], Any] | None = None, + *args: object, + ) -> Future[Any]: + source = start(*args) + return source if adapt_result is None else map_future(source, adapt_result) + @property - def detached(self) -> bool: - """Return whether backend resources have been detached.""" - return bool(self._native.detached) + def capabilities(self) -> RenderSessionCapabilities: + """Return capabilities fixed during attachment.""" + return RenderSessionCapabilities._from_native(self._native.capabilities()) - def resize(self, width: int, height: int, scale_factor: float) -> None: - """Resize this attached render session. + @property + def snapshot(self) -> RenderSessionSnapshot: + """Return the latest state and generation snapshot.""" + return RenderSessionSnapshot._from_native(self._native.snapshot()) + + def request_frame(self, demand: FrameDemand = _DEFAULT_FRAME_DEMAND) -> None: + """Submit one nonblocking frame demand.""" + self._native.request_frame( + int(demand.flags), + demand.token, + demand.coalescing_boundary, + demand.timeout_ns, + ) - Surface and session-owned texture targets resize in place. A - caller-owned texture target is sized by its owner and reports - :class:`UnsupportedFeatureError`; hand over a new texture with the - backend's ``set_*_borrowed_texture_target`` method instead. + def drain_frame_results(self) -> list[RenderFrameResult]: + """Drain an owned batch of terminal frame-demand results.""" + return [ + RenderFrameResult._from_native(raw) + for raw in self._native.drain_frame_results() + ] + + def service_driver_work(self, max_work: int = 64) -> int: + """Service caller-driver work on this session's graphics thread.""" + return self._native.service_driver_work(max_work) + + def resize(self, extent: RenderTargetExtent) -> Future[None]: + """Start an ordered logical resize. The session keeps its renderer, along with the tile pyramid, glyph and image atlases, and symbol placement. A new scale factor retires the @@ -518,94 +797,122 @@ def resize(self, width: int, height: int, scale_factor: float) -> None: Map-owned feature state survives either way. The same scale-factor rule applies to every ``set_*_target`` method. """ - self._native.resize(width, height, scale_factor) + return self._future( + self._native.resize, + None, + extent.width, + extent.height, + extent.scale_factor, + ) + + def barrier(self) -> Future[None]: + """Start a barrier for previously accepted render work.""" + return self._native.barrier() + + def reduce_memory_use(self) -> Future[None]: + """Start best-effort renderer cache release.""" + return self._native.reduce_memory_use() + + def clear_data(self) -> Future[None]: + """Start clearing renderer data.""" + return self._native.clear_data() + + def dump_debug_logs(self) -> Future[None]: + """Start renderer diagnostic-log emission.""" + return self._native.dump_debug_logs() + + def detach(self) -> Future[None]: + """Start normal graphics-owner teardown and map detachment.""" + return self._native.detach() + + def abandon(self) -> RenderAbandonResult: + """Irreversibly abandon a lost target without graphics calls.""" + result = RenderAbandonResult._from_native(self._native.abandon()) + self._map = None + return result def _set_target( self, - set_target: Callable[..., None], + start: Callable[..., Future[None]], descriptor: Any, *args: object, - ) -> None: + ) -> Future[None]: extent = descriptor.extent - set_target(extent.width, extent.height, extent.scale_factor, *args) + return self._future( + start, + None, + extent.width, + extent.height, + extent.scale_factor, + *args, + ) - def set_metal_surface_target(self, descriptor: MetalSurfaceDescriptor) -> None: - """Present this attached surface session through a new surface. + def set_metal_surface_target( + self, descriptor: MetalSurfaceDescriptor + ) -> Future[None]: + """Start an ordered Metal surface replacement. The session keeps its renderer, along with the tile pyramid, atlases, and symbol placement. Map-owned feature state is unchanged. The - descriptor's extent applies as a resize does. A ``context.device`` that - is neither null nor this - session's device raises :class:`InvalidArgumentError` and leaves this - session rendering into the surface it has. The session assigns the layer - its own device and pixel format. + descriptor's extent applies as a resize does. The session assigns the + layer its own device and pixel format. """ - self._set_target( + return self._set_target( self._native.set_metal_surface_target, descriptor, descriptor.context.device.address, descriptor.layer.address, ) - def set_vulkan_surface_target(self, descriptor: VulkanSurfaceDescriptor) -> None: - """Present this attached surface session through a new surface. - - See :meth:`set_metal_surface_target` for what replacing a surface - preserves. The outgoing ``VkSurfaceKHR`` must still be valid, since this - session holds a swapchain built from it. - """ - self._set_target( + def set_vulkan_surface_target( + self, descriptor: VulkanSurfaceDescriptor + ) -> Future[None]: + """Start an ordered Vulkan surface replacement.""" + context = descriptor.context + return self._set_target( self._native.set_vulkan_surface_target, descriptor, - descriptor.context.instance.address, - descriptor.context.physical_device.address, - descriptor.context.device.address, - descriptor.context.graphics_queue.address, - descriptor.context.graphics_queue_family_index, - descriptor.context.get_instance_proc_addr.address, - descriptor.context.get_device_proc_addr.address, + context.instance.address, + context.physical_device.address, + context.device.address, + context.graphics_queue.address, + context.graphics_queue_family_index, + context.get_instance_proc_addr.address, + context.get_device_proc_addr.address, descriptor.surface.address, ) - def set_opengl_surface_target(self, descriptor: OpenGLSurfaceDescriptor) -> None: - """Present this attached surface session through a new surface. - - See :meth:`set_metal_surface_target` for what replacing a surface - preserves. The new surface is made current on the next render, so a host - may hand over a replacement for one it has already destroyed, and an - unusable surface is reported by the next :meth:`render_update`. - """ - platform, ownership, first, second, share, client_api, get_proc = ( - _opengl_context_parts(descriptor.context) + def set_webgpu_surface_target( + self, descriptor: WebGPUSurfaceDescriptor + ) -> Future[None]: + """Start an ordered WebGPU surface replacement.""" + context = descriptor.context + return self._set_target( + self._native.set_webgpu_surface_target, + descriptor, + context.instance.address, + context.device.address, + context.queue.address, + descriptor.surface.address, + descriptor.format, ) - self._set_target( + + def set_opengl_surface_target( + self, descriptor: OpenGLSurfaceDescriptor + ) -> Future[None]: + """Start an ordered OpenGL surface replacement.""" + return self._set_target( self._native.set_opengl_surface_target, descriptor, - platform, - ownership, - first, - second, - share, - client_api, - get_proc, + *_opengl_context_parts(descriptor.context), descriptor.surface.address, ) def set_metal_borrowed_texture_target( self, descriptor: MetalBorrowedTextureDescriptor - ) -> None: - """Render this attached texture session into a new caller-owned texture. - - This is how a caller-owned texture target resizes, and the session keeps - its renderer. The replacement must belong to the device this session - attached with, which raises :class:`InvalidArgumentError` otherwise, and - carry the pixel format it attached with, which raises - :class:`UnsupportedFeatureError` otherwise; both leave this session - rendering into the texture it has. The caller owns the replacement and - keeps it valid until the next replacement, detach, or close. The session - never retains or reads the outgoing texture. - """ - self._set_target( + ) -> Future[None]: + """Start an ordered caller-owned Metal texture replacement.""" + return self._set_target( self._native.set_metal_borrowed_texture_target, descriptor, descriptor.physical_width, @@ -615,25 +922,21 @@ def set_metal_borrowed_texture_target( def set_vulkan_borrowed_texture_target( self, descriptor: VulkanBorrowedTextureDescriptor - ) -> None: - """Render this attached texture session into a new caller-owned image. - - See :meth:`set_metal_borrowed_texture_target` for what replacing a - target preserves. The replacement must carry the format and both layouts - this session attached with, since its render pass was built around them. - """ - self._set_target( + ) -> Future[None]: + """Start an ordered caller-owned Vulkan texture replacement.""" + context = descriptor.context + return self._set_target( self._native.set_vulkan_borrowed_texture_target, descriptor, descriptor.physical_width, descriptor.physical_height, - descriptor.context.instance.address, - descriptor.context.physical_device.address, - descriptor.context.device.address, - descriptor.context.graphics_queue.address, - descriptor.context.graphics_queue_family_index, - descriptor.context.get_instance_proc_addr.address, - descriptor.context.get_device_proc_addr.address, + context.instance.address, + context.physical_device.address, + context.device.address, + context.graphics_queue.address, + context.graphics_queue_family_index, + context.get_instance_proc_addr.address, + context.get_device_proc_addr.address, descriptor.image.address, descriptor.image_view.address, descriptor.format, @@ -641,149 +944,76 @@ def set_vulkan_borrowed_texture_target( descriptor.final_layout, ) + def set_webgpu_borrowed_texture_target( + self, descriptor: WebGPUBorrowedTextureDescriptor + ) -> Future[None]: + """Start an ordered caller-owned WebGPU texture replacement.""" + context = descriptor.context + return self._set_target( + self._native.set_webgpu_borrowed_texture_target, + descriptor, + descriptor.physical_width, + descriptor.physical_height, + context.instance.address, + context.device.address, + context.queue.address, + descriptor.texture.address, + descriptor.texture_view.address, + descriptor.format, + ) + def set_opengl_borrowed_texture_target( self, descriptor: OpenGLBorrowedTextureDescriptor - ) -> None: - """Render this attached texture session into a new caller-owned texture. - - See :meth:`set_metal_borrowed_texture_target` for what replacing a - target preserves. The replacement must belong to the context this - session attached with or one in its share group, and that context must - be current on this thread. - """ - platform, ownership, first, second, share, client_api, get_proc = ( - _opengl_context_parts(descriptor.context) - ) - self._set_target( + ) -> Future[None]: + """Start an ordered caller-owned OpenGL texture replacement.""" + return self._set_target( self._native.set_opengl_borrowed_texture_target, descriptor, descriptor.physical_width, descriptor.physical_height, - platform, - ownership, - first, - second, - share, - client_api, - get_proc, + *_opengl_context_parts(descriptor.context), descriptor.texture, descriptor.target, ) - def render_update(self) -> RenderUpdate: - """Render the latest map update into this session's render target. - - The returned :class:`RenderUpdate` carries a :class:`RenderResult` that - names the wake to wait for before calling again: - - - ``RENDERED``: the target holds a new frame. The map retains its latest - update, so redraw on demand after a resize or a surface expose, and - gate a frame loop on - ``RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE``. - - ``NO_UPDATE``: the call produced no frame. The map either has no - update yet, or the Metal backend has not created an owned texture - because content is not ready. Wait for - ``RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE``. - - ``SIZE_PENDING``: this session resized and the map, which applies its - size on its own thread, is still behind. The map publishes an update - for the new size on its own, so wait for the next - ``RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE``. - - ``TARGET_NOT_READY``: the render target had no frame available, such - as a Metal surface whose next drawable is nil. No map update resolves - this, so wait for a host event that changes the target, or back off - and retry. - - ``needs_repaint`` reports whether the map asked for another frame - while it rendered this one, as during an ongoing camera transition. - It is set only when ``result`` is ``RENDERED``. This is the same signal - the ``RuntimeEventType.MAP_RENDER_FRAME_FINISHED`` event carries in its - ``needs_repaint`` field, delivered here without the event round trip, - so a host can re-arm its frame loop before it drains events. - """ - result, needs_repaint = self._native.render_update() - return RenderUpdate(result=RenderResult(result), needs_repaint=needs_repaint) - - def detach(self) -> DetachedRenderSessionHandle: - """Detach backend resources and return a close-only handle. - - Detach ends this session's hold on the map, leaving the map free to - close. The returned handle stays destroyable after the map closes. - """ - native = self._native.detach() - self._map = None - return DetachedRenderSessionHandle._from_native(native) - - def reduce_memory_use(self) -> None: - """Ask the session renderer to release cached resources where possible.""" - self._native.reduce_memory_use() - - def clear_data(self) -> None: - """Clear renderer data for the session.""" - self._native.clear_data() - - def dump_debug_logs(self) -> None: - """Dump renderer debug logs through MapLibre Native logging.""" - self._native.dump_debug_logs() - - def texture_image_info(self) -> TextureImageInfo: - """Return readback metadata for the latest texture frame.""" - return TextureImageInfo._from_native(self._native.texture_image_info()) - - def read_premultiplied_rgba8_into(self, buffer: object) -> TextureImageInfo: - """Read the latest texture frame into caller-owned writable storage.""" - return TextureImageInfo._from_native( - self._native.read_premultiplied_rgba8_into(buffer) - ) - - def acquire_metal_owned_texture_frame(self) -> MetalOwnedTextureFrameHandle: - """Acquire a borrowed Metal frame from a session-owned texture target.""" - return MetalOwnedTextureFrameHandle._from_native( - self._native.acquire_metal_owned_texture_frame() - ) - - def acquire_vulkan_owned_texture_frame(self) -> VulkanOwnedTextureFrameHandle: - """Acquire a borrowed Vulkan frame from a session-owned texture target.""" - return VulkanOwnedTextureFrameHandle._from_native( - self._native.acquire_vulkan_owned_texture_frame() - ) - - def acquire_opengl_owned_texture_frame(self) -> OpenGLOwnedTextureFrameHandle: - """Acquire a borrowed OpenGL frame from a session-owned texture target.""" - return OpenGLOwnedTextureFrameHandle._from_native( - self._native.acquire_opengl_owned_texture_frame() + def read_premultiplied_rgba8( + self, + ) -> Future[PremultipliedRgba8Image]: + """Start readback of the latest rendered texture frame.""" + return self._future( + self._native.read_premultiplied_rgba8, + PremultipliedRgba8Image._from_native, ) def query_rendered_features( self, geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions | None = None, - ) -> list[QueriedFeature]: - """Query rendered features as copied queried-feature values.""" + ) -> Future[list[QueriedFeature]]: + """Start a rendered-feature query.""" from .query import _geometry_to_native_wire - return [ - QueriedFeature._from_native(raw) - for raw in self._native.query_rendered_features( - _geometry_to_native_wire(geometry), - options.layer_ids if options is not None else None, - options.filter if options is not None else None, - ) - ] + return self._future( + self._native.query_rendered_features, + _cast_queried_features, + _geometry_to_native_wire(geometry), + options.layer_ids if options is not None else None, + options.filter if options is not None else None, + ) def query_source_features( self, source_id: str, options: SourceFeatureQueryOptions | None = None, - ) -> list[QueriedFeature]: - """Query source features as copied queried-feature values.""" - return [ - QueriedFeature._from_native(raw) - for raw in self._native.query_source_features( - source_id, - options.source_layer_ids if options is not None else None, - options.filter if options is not None else None, - ) - ] + ) -> Future[list[QueriedFeature]]: + """Start a source-feature query.""" + return self._future( + self._native.query_source_features, + _cast_queried_features, + source_id, + options.source_layer_ids if options is not None else None, + options.filter if options is not None else None, + ) def query_feature_extensions( self, @@ -792,9 +1022,11 @@ def query_feature_extensions( extension: str, extension_field: str, arguments: bytes | None = None, - ) -> bytes: - """Query a feature extension as UTF-8 JSON or GeoJSON bytes.""" - return self._native.query_feature_extensions( + ) -> Future[bytes]: + """Start a feature-extension query.""" + return self._future( + self._native.query_feature_extensions, + _cast_bytes, source_id, feature, extension, @@ -802,18 +1034,78 @@ def query_feature_extensions( arguments, ) + def acquire_metal_owned_texture_frame(self) -> MetalOwnedTextureFrameHandle: + """Acquire a Metal texture-slot lease without waiting.""" + return MetalOwnedTextureFrameHandle._from_native( + self._native.acquire_metal_owned_texture_frame() + ) + + def acquire_vulkan_owned_texture_frame(self) -> VulkanOwnedTextureFrameHandle: + """Acquire a Vulkan texture-slot lease without waiting.""" + return VulkanOwnedTextureFrameHandle._from_native( + self._native.acquire_vulkan_owned_texture_frame() + ) + + def acquire_webgpu_owned_texture_frame(self) -> WebGPUOwnedTextureFrameHandle: + """Acquire a WebGPU texture-slot lease without waiting.""" + return WebGPUOwnedTextureFrameHandle._from_native( + self._native.acquire_webgpu_owned_texture_frame() + ) -class MetalOwnedTextureFrameHandle(NativeHandleMixin): - """Scoped handle for an acquired Metal session-owned texture frame.""" + def acquire_opengl_owned_texture_frame(self) -> OpenGLOwnedTextureFrameHandle: + """Acquire an OpenGL texture-slot lease without waiting.""" + return OpenGLOwnedTextureFrameHandle._from_native( + self._native.acquire_opengl_owned_texture_frame() + ) - _handle_name = "MetalOwnedTextureFrameHandle" - def __init__(self, native: Any, *, _create_key: object | None = None) -> None: - if _create_key is not _METAL_FRAME_HANDLE_CREATE_KEY: - msg = "MetalOwnedTextureFrameHandle instances are created by RenderSessionHandle" +class _AcquiredFrameHandle: + def __init__( + self, + native: Any, + create_key: object, + expected_key: object, + ) -> None: + if create_key is not expected_key: + msg = "acquired frame handles are created by RenderSessionHandle" raise TypeError(msg) self._native = native + @property + def closed(self) -> bool: + """Return whether this lease was released.""" + return bool(self._native.closed) + + @property + def producer_sync(self) -> GpuSync: + """Return synchronization that guards producer completion.""" + return GpuSync._from_native(self._native.producer_sync()) + + @property + def result(self) -> RenderFrameResult: + """Return common frame-demand metadata for this lease.""" + return RenderFrameResult._from_native(self._native.result()) + + def release(self, consumer_completion: GpuSync = _DEFAULT_GPU_SYNC) -> None: + """Release this lease after optional consumer GPU work.""" + self._native.release( + consumer_completion.kind.native_code, + consumer_completion.object.address, + consumer_completion.value, + ) + + +class MetalOwnedTextureFrameHandle(_AcquiredFrameHandle): + """Acquired Metal texture-slot lease.""" + + def __init__( + self, + native: Any, + *, + _create_key: object | None = None, + ) -> None: + super().__init__(native, _create_key, _METAL_FRAME_HANDLE_CREATE_KEY) + @classmethod def _from_native(cls, native: Any) -> MetalOwnedTextureFrameHandle: return cls(native, _create_key=_METAL_FRAME_HANDLE_CREATE_KEY) @@ -825,7 +1117,7 @@ def frame(self) -> MetalOwnedTextureFrame: @property def texture(self) -> NativePointer: - """Return the borrowed Metal texture pointer while the frame is open.""" + """Return the borrowed Metal texture while this lease is live.""" return NativePointer( self._native.texture_address(), _is_live=lambda: not self.closed, @@ -834,7 +1126,7 @@ def texture(self) -> NativePointer: @property def device(self) -> NativePointer: - """Return the borrowed Metal device pointer while the frame is open.""" + """Return the borrowed Metal device while this lease is live.""" return NativePointer( self._native.device_address(), _is_live=lambda: not self.closed, @@ -842,16 +1134,16 @@ def device(self) -> NativePointer: ) -class VulkanOwnedTextureFrameHandle(NativeHandleMixin): - """Scoped handle for an acquired Vulkan session-owned texture frame.""" +class VulkanOwnedTextureFrameHandle(_AcquiredFrameHandle): + """Acquired Vulkan texture-slot lease.""" - _handle_name = "VulkanOwnedTextureFrameHandle" - - def __init__(self, native: Any, *, _create_key: object | None = None) -> None: - if _create_key is not _VULKAN_FRAME_HANDLE_CREATE_KEY: - msg = "VulkanOwnedTextureFrameHandle instances are created by RenderSessionHandle" - raise TypeError(msg) - self._native = native + def __init__( + self, + native: Any, + *, + _create_key: object | None = None, + ) -> None: + super().__init__(native, _create_key, _VULKAN_FRAME_HANDLE_CREATE_KEY) @classmethod def _from_native(cls, native: Any) -> VulkanOwnedTextureFrameHandle: @@ -864,7 +1156,7 @@ def frame(self) -> VulkanOwnedTextureFrame: @property def image(self) -> NativePointer: - """Return the borrowed Vulkan image pointer while the frame is open.""" + """Return the borrowed Vulkan image while this lease is live.""" return NativePointer( self._native.image_address(), _is_live=lambda: not self.closed, @@ -873,7 +1165,7 @@ def image(self) -> NativePointer: @property def image_view(self) -> NativePointer: - """Return the borrowed Vulkan image-view pointer while the frame is open.""" + """Return the borrowed Vulkan image view while this lease is live.""" return NativePointer( self._native.image_view_address(), _is_live=lambda: not self.closed, @@ -882,7 +1174,7 @@ def image_view(self) -> NativePointer: @property def device(self) -> NativePointer: - """Return the borrowed Vulkan device pointer while the frame is open.""" + """Return the borrowed Vulkan device while this lease is live.""" return NativePointer( self._native.device_address(), _is_live=lambda: not self.closed, @@ -890,16 +1182,64 @@ def device(self) -> NativePointer: ) -class OpenGLOwnedTextureFrameHandle(NativeHandleMixin): - """Scoped handle for an acquired OpenGL session-owned texture frame.""" +class WebGPUOwnedTextureFrameHandle(_AcquiredFrameHandle): + """Acquired WebGPU texture-slot lease.""" + + def __init__( + self, + native: Any, + *, + _create_key: object | None = None, + ) -> None: + super().__init__(native, _create_key, _WEBGPU_FRAME_HANDLE_CREATE_KEY) + + @classmethod + def _from_native(cls, native: Any) -> WebGPUOwnedTextureFrameHandle: + return cls(native, _create_key=_WEBGPU_FRAME_HANDLE_CREATE_KEY) + + @property + def frame(self) -> WebGPUOwnedTextureFrame: + """Return copied frame metadata.""" + return WebGPUOwnedTextureFrame._from_native(self._native.frame()) + + @property + def texture(self) -> NativePointer: + """Return the borrowed WebGPU texture while this lease is live.""" + return NativePointer( + self._native.texture_address(), + _is_live=lambda: not self.closed, + _diagnostic_name="WebGPU texture", + ) + + @property + def texture_view(self) -> NativePointer: + """Return the borrowed WebGPU texture view while this lease is live.""" + return NativePointer( + self._native.texture_view_address(), + _is_live=lambda: not self.closed, + _diagnostic_name="WebGPU texture view", + ) + + @property + def device(self) -> NativePointer: + """Return the borrowed WebGPU device while this lease is live.""" + return NativePointer( + self._native.device_address(), + _is_live=lambda: not self.closed, + _diagnostic_name="WebGPU device", + ) - _handle_name = "OpenGLOwnedTextureFrameHandle" - def __init__(self, native: Any, *, _create_key: object | None = None) -> None: - if _create_key is not _OPENGL_FRAME_HANDLE_CREATE_KEY: - msg = "OpenGLOwnedTextureFrameHandle instances are created by RenderSessionHandle" - raise TypeError(msg) - self._native = native +class OpenGLOwnedTextureFrameHandle(_AcquiredFrameHandle): + """Acquired OpenGL texture-slot lease.""" + + def __init__( + self, + native: Any, + *, + _create_key: object | None = None, + ) -> None: + super().__init__(native, _create_key, _OPENGL_FRAME_HANDLE_CREATE_KEY) @classmethod def _from_native(cls, native: Any) -> OpenGLOwnedTextureFrameHandle: @@ -912,7 +1252,7 @@ def frame(self) -> OpenGLOwnedTextureFrame: @property def texture(self) -> FrameOpenGLTextureName: - """Return the borrowed OpenGL texture object name while the frame is open.""" + """Return the borrowed OpenGL texture while this lease is live.""" return FrameOpenGLTextureName( int(self._native.texture()), _is_live=lambda: not self.closed, @@ -947,9 +1287,12 @@ def _opengl_context_parts( __all__ = [ - "DetachedRenderSessionHandle", "EglContextDescriptor", + "FrameDemand", + "FrameDemandFlag", "FrameOpenGLTextureName", + "GpuSync", + "GpuSyncKind", "MetalBorrowedTextureDescriptor", "MetalContextDescriptor", "MetalOwnedTextureDescriptor", @@ -967,11 +1310,19 @@ def _opengl_context_parts( "OpenGLOwnedTextureFrameHandle", "OpenGLSurfaceDescriptor", "PremultipliedRgba8Image", + "RenderAbandonDisposition", + "RenderAbandonResult", "RenderBackend", + "RenderDriver", + "RenderFrameResult", "RenderResult", + "RenderSessionAttachOptions", + "RenderSessionCapabilities", + "RenderSessionCapability", "RenderSessionHandle", + "RenderSessionSnapshot", + "RenderSessionState", "RenderTargetExtent", - "RenderUpdate", "TextureImageInfo", "VulkanBorrowedTextureDescriptor", "VulkanContextDescriptor", @@ -979,6 +1330,12 @@ def _opengl_context_parts( "VulkanOwnedTextureFrame", "VulkanOwnedTextureFrameHandle", "VulkanSurfaceDescriptor", + "WebGPUBorrowedTextureDescriptor", + "WebGPUContextDescriptor", + "WebGPUOwnedTextureDescriptor", + "WebGPUOwnedTextureFrame", + "WebGPUOwnedTextureFrameHandle", + "WebGPUSurfaceDescriptor", "WglContextDescriptor", ] diff --git a/bindings/python/python/maplibre_native_ffi/runtime.py b/bindings/python/python/maplibre_native_ffi/runtime.py index 0e42c33e2..6dbcbf675 100644 --- a/bindings/python/python/maplibre_native_ffi/runtime.py +++ b/bindings/python/python/maplibre_native_ffi/runtime.py @@ -4,12 +4,14 @@ import weakref from collections.abc import Callable +from concurrent.futures import Future from dataclasses import dataclass, field from enum import IntFlag from typing import Any from . import _native from ._enum import UnknownIntEnum +from ._future import map_future from ._lifecycle import NativeHandleMixin from .errors import InvalidArgumentError from .resource import ( @@ -54,8 +56,7 @@ class RuntimeEventType(UnknownIntEnum): OFFLINE_REGION_STATUS_CHANGED = 19 OFFLINE_REGION_RESPONSE_ERROR = 20 OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = 21 - OFFLINE_OPERATION_COMPLETED = 22 - MAP_CAMERA_TRANSITION_FINISHED = 23 + MAP_CAMERA_TRANSITION_FINISHED = 22 class RuntimeEventMask(IntFlag): @@ -63,8 +64,7 @@ class RuntimeEventMask(IntFlag): Each bit is ``1 << RuntimeEventType``, so a host derives a bit from a type it read from an event. A map or a runtime builds and queues only the types - its mask selects, so narrowing a mask also removes those events' wake - signals. + that its mask selects. :attr:`ALL_MAP_EVENTS` covers every map-originated type and :attr:`ALL_RUNTIME_EVENTS` every runtime-originated one. @@ -100,7 +100,6 @@ class RuntimeEventMask(IntFlag): OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = ( 1 << RuntimeEventType.OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED ) - OFFLINE_OPERATION_COMPLETED = 1 << RuntimeEventType.OFFLINE_OPERATION_COMPLETED ALL_MAP_EVENTS = ( MAP_CAMERA_WILL_CHANGE | MAP_CAMERA_IS_CHANGING @@ -126,7 +125,6 @@ class RuntimeEventMask(IntFlag): OFFLINE_REGION_STATUS_CHANGED | OFFLINE_REGION_RESPONSE_ERROR | OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED - | OFFLINE_OPERATION_COMPLETED ) ALL = ALL_MAP_EVENTS | ALL_RUNTIME_EVENTS @@ -150,6 +148,28 @@ class CameraChangeMode(UnknownIntEnum): ANIMATED = 1 +class CommandDisposition(UnknownIntEnum): + """Terminal disposition of an accepted command.""" + + COMMITTED = 0 + SUPERSEDED = 1 + FAILED = 2 + CANCELLED = 3 + + +@dataclass(frozen=True, slots=True) +class CommandCompletion: + """Terminal result of an accepted map command.""" + + disposition: CommandDisposition + generation: int + native_status_code: int + diagnostic: str + + def __post_init__(self) -> None: + object.__setattr__(self, "disposition", CommandDisposition(self.disposition)) + + class RuntimeEventSourceType(UnknownIntEnum): """Runtime event source kind values reported by the C API.""" @@ -270,11 +290,7 @@ def _from_runtime_payload(cls, payload: dict[str, object]) -> TileActionPayload: @dataclass(frozen=True, slots=True) class CameraTransitionFinishedPayload: - """Runtime camera transition-finished event payload. - - A transition carrying a ``transition_id`` reports its end once for every - terminal outcome, without naming which outcome occurred. - """ + """Runtime camera transition-finished event payload.""" transition_id: int @@ -383,14 +399,6 @@ class RuntimeEventBatch: events: list[RuntimeEvent] """Drained events in queue order.""" - remaining_count: int - """Events still queued after this batch. - - An unbounded drain leaves this at zero. A nonzero value means the - ``max_events`` bound ended the batch early, so another drain reports more - events. - """ - @classmethod def _from_native( cls, @@ -401,7 +409,6 @@ def _from_native( events=[ RuntimeEvent._from_native(event, runtime) for event in raw["events"] ], - remaining_count=raw["remaining_count"], ) @@ -422,39 +429,8 @@ class RuntimeOptions: """ -class WakeSource(NativeHandleMixin): - """Releases a runtime owner thread parked in :meth:`RuntimeHandle.pump`. - - Usable from any thread. It stays usable after its runtime closes, and - signalling it then does nothing. - """ - - _handle_name = "WakeSource" - - def __init__(self, native: Any, *, _create_key: object | None = None) -> None: - if _create_key is not _WAKE_SOURCE_CREATE_KEY: - msg = "WakeSource instances are created by RuntimeHandle.wake_source()" - raise TypeError(msg) - self._native = native - - @classmethod - def _from_native(cls, native: Any) -> WakeSource: - return cls(native, _create_key=_WAKE_SOURCE_CREATE_KEY) - - def signal(self) -> None: - """Set the runtime's wake flag and release the parked owner thread. - - A signal raised while the owner thread runs makes the next - :meth:`RuntimeHandle.pump` return without parking. - """ - self._native.signal() - - -_WAKE_SOURCE_CREATE_KEY = object() - - class RuntimeHandle(NativeHandleMixin): - """Owner-thread runtime handle.""" + """Any-thread runtime handle with autonomous native execution.""" _handle_name = "RuntimeHandle" @@ -465,24 +441,35 @@ def __init__(self, options: RuntimeOptions | None = None) -> None: options.cache_path, int(options.event_mask), ) - self._offline_operations: weakref.WeakSet[OfflineOperationHandle] = ( - weakref.WeakSet() - ) self._maps: dict[int, weakref.ReferenceType[MapHandle]] = {} - def close(self) -> None: - """Release this runtime handle exactly once.""" - if self._offline_operations: - from .errors import InvalidStateError + def close(self) -> Future[None]: + """Release this runtime handle exactly once and report native teardown. + + The returned future resolves after every earlier accepted submission, + including released maps' teardown, has finished and the runtime's + threads and resources are gone. No library thread touches library state + afterward, so a host that waits for it may exit the process without + racing native teardown. Discarding the future starts the same teardown + and drops its completion. + """ + return self._native.close() + + def barrier(self) -> Future[None]: + """Return a future for all previously accepted runtime commands.""" + return self._native.barrier() - raise InvalidStateError(None, "runtime has live offline operation handles") - self._native.close() + def set_event_wake_callback(self, callback: Callable[[], None]) -> None: + """Install the callback that schedules a later :meth:`drain_events`. - def _register_offline_operation(self, operation: OfflineOperationHandle) -> None: - self._offline_operations.add(operation) + Native code may invoke the callback from any thread. The callback must + only arrange receiver work and must not call this binding directly. + """ + self._native.set_event_wake_callback(callback) - def _unregister_offline_operation(self, operation: OfflineOperationHandle) -> None: - self._offline_operations.discard(operation) + def clear_event_wake_callback(self) -> None: + """Clear the scheduling callback after in-flight entries return.""" + self._native.clear_event_wake_callback() def _register_map(self, map_handle: MapHandle) -> None: self._maps[map_handle._native_id()] = weakref.ref(map_handle) @@ -500,62 +487,17 @@ def _map_for_source_id(self, source_id: int) -> MapHandle | None: return None return map_handle - def pump(self, timeout: float | None = 0.0, budget: float | None = None) -> None: - """Advance this runtime. - - The call parks the owner thread when ``timeout`` allows it, then drains - the owner-thread task queues. Take the queued runtime events with - :meth:`drain_events` afterwards. - - ``timeout`` is in seconds and bounds the park: zero drains and returns, - a positive value parks for up to that long, and ``None`` parks until a - wake arrives. Timers and ready file descriptors set the wake flag only - when they queue owner-thread work, so pass a bounded timeout to cap how - long a call waits. - - ``budget`` is in seconds and bounds the drain: ``None`` drains without - a bound, and a value stops the drain at the first task boundary after - that long, measured from the start of the drain. The first queued task - always runs, so a bounded pump always makes progress, and tasks left - behind set the wake flag so the next pump returns without parking and - continues them. The budget bounds the task queues alone: expired - timers and ready file descriptors are serviced regardless, and a - single task runs to completion once started, so one long task can - overrun the budget. - - A non-zero timeout releases the GIL while it parks. Call it outside any - lock that a signalling thread takes. - """ - # A negative timeout collapses to no wait; ``None`` spells an unbounded - # park. - timeout_ms = -1 if timeout is None else max(0, int(timeout * 1000)) - # ``None`` spells an unbounded drain. - budget_ms = -1 if budget is None else max(0, int(budget * 1000)) - self._native.pump(timeout_ms, budget_ms) - - def wake_source(self) -> WakeSource: - """Acquire a wake source for this runtime, usable from any thread.""" - return WakeSource._from_native(self._native.wake_source()) - - def _offline_operation( - self, start: Callable[..., int], *args: object - ) -> OfflineOperationHandle: - from .offline import OfflineOperationHandle - - return OfflineOperationHandle._from_native(self, start(*args)) - def run_ambient_cache_operation( self, operation: AmbientCacheOperation - ) -> OfflineOperationHandle: + ) -> Future[None]: """Start an ambient cache maintenance operation.""" from .offline import AmbientCacheOperation - return self._offline_operation( - self._native.run_ambient_cache_operation_start, - AmbientCacheOperation(operation).native_code, + return self._native.run_ambient_cache_operation( + AmbientCacheOperation(operation).native_code ) - def set_maximum_ambient_cache_size(self, size: int) -> OfflineOperationHandle: + def set_maximum_ambient_cache_size(self, size: int) -> Future[None]: """Start a change to this runtime's maximum ambient cache size. MapLibre evicts ambient resources to fit the new budget, so lowering it @@ -567,134 +509,122 @@ def set_maximum_ambient_cache_size(self, size: int) -> OfflineOperationHandle: raise InvalidArgumentError( f"maximum ambient cache size must fit in 64 unsigned bits, not {size}" ) - return self._offline_operation( - self._native.set_maximum_ambient_cache_size_start, - size, - ) + return self._native.set_maximum_ambient_cache_size(size) def create_offline_region( self, definition: OfflineRegionDefinition, metadata: bytes = b"" - ) -> OfflineOperationHandle: + ) -> Future[OfflineRegionInfo]: """Start creating an offline region.""" - return self._offline_operation( - self._native.offline_region_create_start, - definition, - metadata, + return map_future( + self._native.create_offline_region(definition, metadata), + _adapt_region_result, ) - def get_offline_region(self, region_id: int) -> OfflineOperationHandle: + def get_offline_region(self, region_id: int) -> Future[OfflineRegionInfo | None]: """Start getting an offline region snapshot by ID.""" - return self._offline_operation(self._native.offline_region_get_start, region_id) + return map_future( + self._native.get_offline_region(region_id), _adapt_optional_region_result + ) - def list_offline_regions(self) -> OfflineOperationHandle: + def list_offline_regions(self) -> Future[tuple[OfflineRegionInfo, ...]]: """Start listing offline region snapshots.""" - return self._offline_operation(self._native.offline_regions_list_start) + return map_future( + self._native.list_offline_regions(), _adapt_region_list_result + ) def merge_offline_regions_database( self, side_database_path: str - ) -> OfflineOperationHandle: + ) -> Future[tuple[OfflineRegionInfo, ...]]: """Start merging offline regions from another database path.""" - return self._offline_operation( - self._native.offline_regions_merge_database_start, - side_database_path, + return map_future( + self._native.merge_offline_regions_database(side_database_path), + _adapt_region_list_result, ) def update_offline_region_metadata( self, region_id: int, metadata: bytes, - ) -> OfflineOperationHandle: + ) -> Future[OfflineRegionInfo]: """Start updating opaque binary metadata for an offline region.""" - return self._offline_operation( - self._native.offline_region_update_metadata_start, - region_id, - metadata, + return map_future( + self._native.update_offline_region_metadata(region_id, metadata), + _adapt_region_result, ) - def get_offline_region_status(self, region_id: int) -> OfflineOperationHandle: + def get_offline_region_status(self, region_id: int) -> Future[OfflineRegionStatus]: """Start getting completed/download status for an offline region.""" - return self._offline_operation( - self._native.offline_region_get_status_start, region_id + return map_future( + self._native.get_offline_region_status(region_id), _adapt_status_result ) def set_offline_region_observed( self, region_id: int, observed: bool - ) -> OfflineOperationHandle: + ) -> Future[None]: """Start enabling or disabling runtime events for an offline region.""" - return self._offline_operation( - self._native.offline_region_set_observed_start, - region_id, - observed, - ) + return self._native.set_offline_region_observed(region_id, observed) def set_offline_region_download_state( self, region_id: int, state: OfflineRegionDownloadState, - ) -> OfflineOperationHandle: + ) -> Future[None]: """Start setting an offline region's native download state.""" from .offline import OfflineRegionDownloadState - return self._offline_operation( - self._native.offline_region_set_download_state_start, - region_id, - OfflineRegionDownloadState(state).native_code_for_set(), - ) + native_state = OfflineRegionDownloadState(state).native_code_for_set() + return self._native.set_offline_region_download_state(region_id, native_state) - def invalidate_offline_region(self, region_id: int) -> OfflineOperationHandle: + def invalidate_offline_region(self, region_id: int) -> Future[None]: """Start invalidating cached resources for an offline region.""" - return self._offline_operation( - self._native.offline_region_invalidate_start, region_id - ) + return self._native.invalidate_offline_region(region_id) - def delete_offline_region(self, region_id: int) -> OfflineOperationHandle: + def delete_offline_region(self, region_id: int) -> Future[None]: """Start deleting an offline region.""" - return self._offline_operation( - self._native.offline_region_delete_start, region_id - ) + return self._native.delete_offline_region(region_id) def set_resource_transform( self, callback: ResourceTransformCallback, *, max_pending_callbacks: int = 64, - ) -> None: + ) -> Future[CommandCompletion]: """Install or replace the runtime-scoped network URL transform.""" from .resource import _adapt_resource_transform_callback - self._native.set_resource_transform( + return self._native.set_resource_transform( _adapt_resource_transform_callback(callback), max_pending_callbacks, ) - def clear_resource_transform(self) -> None: + def clear_resource_transform(self) -> Future[CommandCompletion]: """Clear the runtime-scoped network URL transform.""" - self._native.clear_resource_transform() + return self._native.clear_resource_transform() def set_http_header_transform( self, callback: HttpHeaderTransformCallback, *, max_pending_callbacks: int = 64, - ) -> None: + ) -> Future[CommandCompletion]: """Install or replace the outgoing HTTP header transform.""" from .resource import _adapt_http_header_transform_callback - self._native.set_http_header_transform( + return self._native.set_http_header_transform( _adapt_http_header_transform_callback(callback), max_pending_callbacks, ) - def clear_http_header_transform(self) -> None: + def clear_http_header_transform(self) -> Future[CommandCompletion]: """Clear the outgoing HTTP header transform.""" - self._native.clear_http_header_transform() + return self._native.clear_http_header_transform() def set_resource_provider( self, callback: ResourceProviderCallback, *, max_pending_callbacks: int = 64, - ) -> None: + ) -> Future[CommandCompletion]: """Install or replace the runtime-scoped network resource provider. Replacement is allowed while maps are live. When this call returns, the @@ -703,43 +633,29 @@ def set_resource_provider( """ from .resource import _adapt_resource_provider_callback - self._native.set_resource_provider( + return self._native.set_resource_provider( _adapt_resource_provider_callback(callback), max_pending_callbacks, ) - def clear_resource_provider(self) -> None: + def clear_resource_provider(self) -> Future[CommandCompletion]: """Clear the runtime-scoped network resource provider. Later requests go to MapLibre's online file source. Requests the previous callback already took a handle for keep that handle and are completed and released as usual. """ - self._native.clear_resource_provider() + return self._native.clear_resource_provider() - def drain_events(self, max_events: int = 0) -> RuntimeEventBatch: + def drain_events(self) -> RuntimeEventBatch: """Drain and copy this runtime's queued runtime events into one batch. - ``max_events`` bounds the drain: zero drains every queued event, and a - positive value takes at most that many and reports the rest as - :attr:`RuntimeEventBatch.remaining_count`. - - A drain is a queue operation: it never parks and runs no owner-thread - work. Call :meth:`pump` to advance the runtime, then drain what the pump - produced. + Draining never waits for worker progress; native execution is autonomous. `RuntimeEvent.source.map_handle` is None once the caller drops its last reference to the source map. """ - if not 0 <= max_events < 2**64: - # PyO3 raises a bare OverflowError extracting `usize` before the - # binding's error conversion runs. - raise InvalidArgumentError( - f"max_events must fit in 64 unsigned bits, not {max_events}" - ) - return RuntimeEventBatch._from_native( - self._native.drain_events(max_events), runtime=self - ) + return RuntimeEventBatch._from_native(self._native.drain_events(), runtime=self) def set_event_mask(self, mask: RuntimeEventMask) -> None: """Select which runtime-originated event types this runtime queues. @@ -749,11 +665,8 @@ def set_event_mask(self, mask: RuntimeEventMask) -> None: runtime-originated type. Narrowing gates later events and keeps queued ones, so a host drains what it already caused. - Region status, response error, and tile count limit events also need an - observed region, so this mask narrows that subscription rather than - replacing it. An offline operation records its result before the mask is - read, so an :class:`offline.OfflineOperationHandle` still reports its - result with the completion type unselected. + Region status, response error, and tile count limit events also require + an observed region. This mask narrows that subscription. """ self._native.set_event_mask(int(mask)) @@ -766,8 +679,8 @@ def event_mask(self) -> RuntimeEventMask: """ return RuntimeEventMask(self._native.get_event_mask()) - def create_map(self, options: MapOptions | None = None) -> MapHandle: - """Create a map owned by this runtime.""" + def create_map(self, options: MapOptions | None = None) -> Future[MapHandle]: + """Return an eager future for a map owned by this runtime.""" from .map import MapHandle return MapHandle._create(self, options) @@ -789,8 +702,6 @@ def _runtime_payload_from_native(payload: dict[str, object]) -> RuntimeEventPayl return OfflineRegionResponseError._from_runtime_payload(payload) if kind == "offline_region_tile_count_limit": return OfflineRegionTileCountLimitExceeded._from_runtime_payload(payload) - if kind == "offline_operation_completed": - return OfflineOperationCompleted._from_runtime_payload(payload) if kind == "camera_transition_finished": return CameraTransitionFinishedPayload._from_runtime_payload(payload) return UnknownRuntimeEventPayload._from_runtime_payload(payload) @@ -799,6 +710,8 @@ def _runtime_payload_from_native(payload: dict[str, object]) -> RuntimeEventPayl __all__ = [ "CameraChangeMode", "CameraTransitionFinishedPayload", + "CommandCompletion", + "CommandDisposition", "NetworkStatus", "RenderFramePayload", "RenderMapPayload", @@ -822,13 +735,17 @@ def _runtime_payload_from_native(payload: dict[str, object]) -> RuntimeEventPayl from .map import MapHandle, MapOptions from .offline import ( AmbientCacheOperation, - OfflineOperationCompleted, - OfflineOperationHandle, OfflineRegionDefinition, OfflineRegionDownloadState, + OfflineRegionInfo, OfflineRegionResponseError, + OfflineRegionStatus, OfflineRegionStatusChanged, OfflineRegionTileCountLimitExceeded, + _adapt_optional_region_result, + _adapt_region_list_result, + _adapt_region_result, + _adapt_status_result, ) RuntimeEventPayload = ( @@ -839,7 +756,6 @@ def _runtime_payload_from_native(payload: dict[str, object]) -> RuntimeEventPayl | OfflineRegionStatusChanged | OfflineRegionResponseError | OfflineRegionTileCountLimitExceeded - | OfflineOperationCompleted | CameraTransitionFinishedPayload | UnknownRuntimeEventPayload ) diff --git a/bindings/python/python/maplibre_native_ffi/style.py b/bindings/python/python/maplibre_native_ffi/style.py index e52c2886a..a4d70a187 100644 --- a/bindings/python/python/maplibre_native_ffi/style.py +++ b/bindings/python/python/maplibre_native_ffi/style.py @@ -242,6 +242,36 @@ def _from_native(cls, raw: dict[str, Any]) -> StyleSourceInfo: ) +@dataclass(frozen=True, slots=True) +class StyleLayerInfo: + """Copied fixed metadata for one style layer.""" + + layer_type: str + """Style-spec layer type string, such as ``"background"``.""" + min_zoom: float + """Lowest zoom at which the layer draws; ``-math.inf`` with no lower + bound.""" + max_zoom: float + """Highest zoom at which the layer draws; ``math.inf`` with no upper + bound.""" + visibility: StyleLayerVisibility + source_id: str | None = None + """Source ID, or None for layer types that carry no source.""" + source_layer: str | None = None + """Source-layer ID, or None for layer types that carry none.""" + + @classmethod + def _from_native(cls, raw: dict[str, Any]) -> StyleLayerInfo: + return cls( + layer_type=raw["layer_type"], + min_zoom=raw["min_zoom"], + max_zoom=raw["max_zoom"], + visibility=StyleLayerVisibility(raw["visibility"]), + source_id=raw["source_id"], + source_layer=raw["source_layer"], + ) + + @dataclass(frozen=True, slots=True) class ImageStretch: """One stretchable interval along an image axis, in image pixels.""" @@ -365,7 +395,7 @@ class StyleImage: @classmethod def _from_native(cls, raw: dict[str, Any]) -> StyleImage: - info = StyleImageInfo._from_native(raw["info"]) + info = StyleImageInfo._from_native(raw) return cls( image=PremultipliedRgba8Image( TextureImageInfo( @@ -374,7 +404,7 @@ def _from_native(cls, raw: dict[str, Any]) -> StyleImage: stride=info.stride, byte_length=info.byte_length, ), - raw["data"], + raw["pixels"], ), pixel_ratio=info.pixel_ratio, sdf=info.sdf, @@ -468,7 +498,7 @@ class CustomMvtVectorSourceOptions: class CustomGeometrySourceHandle(NativeHandleMixin): - """Owner-thread handle for queued custom geometry source callback events.""" + """Any-thread handle for queued custom geometry source callback events.""" _handle_name = "CustomGeometrySourceHandle" @@ -501,7 +531,7 @@ def poll_event(self) -> CustomGeometrySourceEvent | None: class CustomMvtVectorSourceHandle(NativeHandleMixin): - """Owner-thread handle for queued custom MVT vector source callback events.""" + """Any-thread handle for queued custom MVT vector source callback events.""" _handle_name = "CustomMvtVectorSourceHandle" @@ -550,6 +580,7 @@ def poll_event(self) -> CustomMvtVectorSourceEvent | None: "StyleImage", "StyleImageInfo", "StyleImageOptions", + "StyleLayerInfo", "StyleSourceInfo", "StyleSourceType", "StyleTransitionOptions", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index f54cafcca..e82be4ed6 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -6,7 +6,6 @@ use maplibre_native_ffi_core::{ TileOperation, }; use maplibre_native_ffi_sys as sys; -use pyo3::buffer::PyBuffer; use pyo3::prelude::*; use pyo3::types::{PyAny, PyBytes, PyDict, PyList}; use std::collections::VecDeque; @@ -29,9 +28,15 @@ type AnimationParts = ( ); mod py_errors { + pyo3::import_exception!(maplibre_native_ffi.errors, BusyError); + pyo3::import_exception!(maplibre_native_ffi.errors, CancelledError); pyo3::import_exception!(maplibre_native_ffi.errors, InvalidArgumentError); + pyo3::import_exception!(maplibre_native_ffi.errors, _OperationResultConsumedError); pyo3::import_exception!(maplibre_native_ffi.errors, InvalidStateError); pyo3::import_exception!(maplibre_native_ffi.errors, NativeError); + pyo3::import_exception!(maplibre_native_ffi.errors, NotFoundError); + pyo3::import_exception!(maplibre_native_ffi.errors, NotReadyError); + pyo3::import_exception!(maplibre_native_ffi.errors, TargetLostError); pyo3::import_exception!(maplibre_native_ffi.errors, UnknownStatusError); pyo3::import_exception!(maplibre_native_ffi.errors, UnsupportedFeatureError); pyo3::import_exception!(maplibre_native_ffi.errors, WrongThreadError); @@ -40,26 +45,7 @@ mod py_errors { #[pyclass(name = "_RuntimeHandle")] struct RuntimeHandle { state: Mutex>, - operation_gate: RuntimeOperationGate, - resource_provider: Mutex>>, - resource_transform: Mutex>>, - http_header_transform: Mutex>>, -} - -#[derive(Debug)] -struct RuntimeOperationGate { - state: Mutex, -} - -#[derive(Debug, Default)] -struct RuntimeOperationGateState { - active_detached_operation: bool, - closing: bool, - closed: bool, -} - -struct RuntimeDetachedOperationGuard<'a> { - gate: &'a RuntimeOperationGate, + event_wake: Arc>>>, } struct PyResourceProviderState { @@ -85,14 +71,6 @@ struct ResourceRequestHandle { state: Arc, } -// The C API accepts signals and destruction from any thread, so this pyclass is -// `Send + Sync` without an unsafe assertion. The mutex makes close once-only -// against a concurrent signal. -#[pyclass(name = "_WakeSource")] -struct WakeSource { - handle: Mutex>, -} - #[derive(Debug, Clone)] struct CopiedLogRecordRaw { severity: u32, @@ -109,17 +87,6 @@ struct PyLogCallbackState { consume: bool, } -#[derive(Debug)] -struct GlobalPyLogCallbackState { - current: Option>, - retired: Vec>, -} - -static LOG_CALLBACK_STATE: Mutex = Mutex::new(GlobalPyLogCallbackState { - current: None, - retired: Vec::new(), -}); - #[pyclass(name = "_LogReceiver")] struct LogReceiver { state: Arc, @@ -198,8 +165,6 @@ struct CustomMvtVectorSourceHandle { struct RenderSessionState { handle: maplibre_core::handle::NativeHandleState, - detached: bool, - frame_acquired: bool, } #[pyclass(name = "_RenderSessionHandle")] @@ -207,81 +172,23 @@ struct RenderSessionHandle { state: Arc>, } -#[pyclass(name = "_DetachedRenderSessionHandle")] -struct DetachedRenderSessionHandle { - state: Arc>, -} - -#[derive(Debug, Clone, Copy)] -struct MetalOwnedTextureFrameRaw { - generation: u64, - width: u32, - height: u32, - scale_factor: f64, - frame_id: u64, - texture_address: usize, - device_address: usize, - pixel_format: u64, -} - #[pyclass(name = "_MetalOwnedTextureFrameHandle")] struct MetalOwnedTextureFrameHandle { - session: Arc>, - raw: MetalOwnedTextureFrameRaw, - closed: Mutex, -} - -#[derive(Debug, Clone, Copy)] -struct VulkanOwnedTextureFrameRaw { - generation: u64, - width: u32, - height: u32, - scale_factor: f64, - frame_id: u64, - image_address: usize, - image_view_address: usize, - device_address: usize, - format: u32, - layout: u32, + frame: Mutex, } #[pyclass(name = "_VulkanOwnedTextureFrameHandle")] struct VulkanOwnedTextureFrameHandle { - session: Arc>, - raw: VulkanOwnedTextureFrameRaw, - closed: Mutex, -} - -#[derive(Debug, Clone, Copy)] -struct OpenGLOwnedTextureFrameRaw { - generation: u64, - width: u32, - height: u32, - scale_factor: f64, - frame_id: u64, - texture: u32, - target: u32, - internal_format: u32, - format: u32, - type_: u32, -} - -enum OwnedTextureFrameRelease { - Metal(MetalOwnedTextureFrameRaw), - Vulkan(VulkanOwnedTextureFrameRaw), - OpenGL(OpenGLOwnedTextureFrameRaw), + frame: Mutex, } -struct OwnedTextureFrameAcquisitionGuard { - session: Arc>, - frame: Option, +#[pyclass(name = "_WebGPUOwnedTextureFrameHandle")] +struct WebGPUOwnedTextureFrameHandle { + frame: Mutex, } - #[pyclass(name = "_OpenGLOwnedTextureFrameHandle")] struct OpenGLOwnedTextureFrameHandle { - session: Arc>, - raw: OpenGLOwnedTextureFrameRaw, - closed: Mutex, + frame: Mutex, } impl RuntimeHandle { @@ -291,147 +198,501 @@ impl RuntimeHandle { .unwrap_or_else(|poisoned| poisoned.into_inner()) } - fn state_for_operation( - &self, - ) -> PyResult>> { - self.operation_gate.ensure_open()?; - let state = self.state(); - self.operation_gate.ensure_open()?; - Ok(state) + fn native(&self) -> PyResult { + self.state() + .live_handle() + .ok_or_else(|| invalid_state_error("runtime handle is closed")) + } + + fn ensure_open(&self) -> PyResult<()> { + self.native().map(drop) } } -impl RuntimeOperationGate { - fn new() -> Self { - Self { - state: Mutex::new(RuntimeOperationGateState::default()), - } +unsafe extern "C" fn event_wake_trampoline(user_data: *mut c_void) { + if user_data.is_null() { + return; } + let _ = catch_unwind(AssertUnwindSafe(|| { + // SAFETY: user_data points to an Arc retained by the runtime wake + // descriptor until native invokes its release callback. + let state = unsafe { &*user_data.cast::>>>>() }; + Python::attach(|py| { + let callback = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_ref() + .map(|callback| callback.clone_ref(py)); + if let Some(callback) = callback { + if let Err(error) = callback.bind(py).call0() { + error.write_unraisable(py, None); + } + } + }); + })); +} - fn begin_detached_operation(&self) -> PyResult> { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.closed { - return Err(invalid_state_error("runtime handle is closed")); - } - if state.closing { - return Err(invalid_state_error("runtime is closing")); - } - if state.active_detached_operation { - return Err(invalid_state_error( - "runtime has an active native operation", - )); - } - state.active_detached_operation = true; - Ok(RuntimeDetachedOperationGuard { gate: self }) +unsafe extern "C" fn release_event_wake(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: native releases the one Arc box accepted at runtime creation. + drop(unsafe { Box::from_raw(user_data.cast::>>>>()) }); } +} - fn ensure_open(&self) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.closed { - return Err(invalid_state_error("runtime handle is closed")); - } - if state.closing { - return Err(invalid_state_error("runtime is closing")); - } - Ok(()) +type PyCompletionConverter = + Box, &sys::mln_completion_result) -> PyResult> + Send>; + +struct PyCompletionBridge { + future: Py, + convert: Mutex>, + accept_error_status: bool, +} + +fn new_python_future(py: Python<'_>) -> PyResult> { + let future = py + .import("concurrent.futures")? + .getattr("Future")? + .call0()?; + // Native work is already running once its C submission is accepted. Mark + // the future accordingly so cancel() cannot claim that it stopped the work. + future.call_method0("set_running_or_notify_cancel")?; + Ok(future.unbind()) +} + +struct OwnedRenderFrameBatch(sys::mln_render_frame_batch); + +impl Drop for OwnedRenderFrameBatch { + fn drop(&mut self) { + unsafe { sys::mln_render_frame_batch_release(self.0) }; } +} +unsafe extern "C" fn complete_python_future( + user_data: *mut c_void, + result: *const sys::mln_completion_result, +) { + if user_data.is_null() || result.is_null() { + return; + } + let _ = catch_unwind(AssertUnwindSafe(|| { + // SAFETY: native owns the bridge from accepted submission through its + // release callback. The result is borrowed for this callback. + let bridge = unsafe { &*user_data.cast::() }; + let result = unsafe { &*result }; + Python::attach(|py| { + let converted = if result.status == sys::MLN_STATUS_OK || bridge.accept_error_status { + match bridge + .convert + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take() + { + Some(convert) => convert(py, result), + None => Err(invalid_state_error("native completion ran more than once")), + } + } else { + let diagnostic = if result.diagnostic.data.is_null() || result.diagnostic.size == 0 + { + String::new() + } else { + // SAFETY: the diagnostic is borrowed for this callback. + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts( + result.diagnostic.data.cast::(), + result.diagnostic.size, + ) + }) + .into_owned() + }; + Err(map_error(Error::from_status_and_diagnostic( + result.status, + diagnostic, + ))) + }; + let call = match converted { + Ok(value) => bridge.future.bind(py).call_method1("set_result", (value,)), + Err(error) => bridge + .future + .bind(py) + .call_method1("set_exception", (error.value(py),)), + }; + if let Err(error) = call { + error.write_unraisable(py, None); + } + }); + })); +} - fn begin_close(&self) -> PyResult { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.closed { - return Ok(false); - } - if state.active_detached_operation { - return Err(invalid_state_error( - "runtime has an active native operation", - )); - } - if state.closing { - return Err(invalid_state_error("runtime is closing")); - } - state.closing = true; - Ok(true) +unsafe extern "C" fn release_python_future(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: native releases accepted completion state exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); } +} - fn finish_successful_close(&self) { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.closing = false; - state.closed = true; +fn submit_python_future(py: Python<'_>, submit: S, convert: C) -> PyResult> +where + S: FnOnce(*const sys::mln_completion) -> sys::mln_status, + C: FnOnce(Python<'_>, &sys::mln_completion_result) -> PyResult> + Send + 'static, +{ + let future = new_python_future(py)?; + let bridge = Box::new(PyCompletionBridge { + future: future.clone_ref(py), + convert: Mutex::new(Some(Box::new(convert))), + accept_error_status: false, + }); + let bridge = Box::into_raw(bridge); + let completion = sys::mln_completion { + size: std::mem::size_of::() as u32, + callback: Some(complete_python_future), + user_data: bridge.cast(), + release_user_data: Some(release_python_future), + }; + let status = submit(&completion); + if status != sys::MLN_STATUS_OK { + // SAFETY: rejected submissions retain no callback state. + drop(unsafe { Box::from_raw(bridge) }); + return maplibre_core::check(status) + .map(|()| unreachable!()) + .map_err(map_error); } + Ok(future) +} - fn finish_failed_close(&self) { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .closing = false; +fn submit_python_command_future(py: Python<'_>, submit: S) -> PyResult> +where + S: FnOnce(*const sys::mln_completion) -> sys::mln_status, +{ + let future = new_python_future(py)?; + let bridge = Box::new(PyCompletionBridge { + future: future.clone_ref(py), + convert: Mutex::new(Some(Box::new(py_command))), + accept_error_status: true, + }); + let bridge = Box::into_raw(bridge); + let completion = sys::mln_completion { + size: std::mem::size_of::() as u32, + callback: Some(complete_python_future), + user_data: bridge.cast(), + release_user_data: Some(release_python_future), + }; + let status = submit(&completion); + if status != sys::MLN_STATUS_OK { + // SAFETY: rejected submissions retain no callback state. + drop(unsafe { Box::from_raw(bridge) }); + return maplibre_core::check(status) + .map(|()| unreachable!()) + .map_err(map_error); + } + Ok(future) +} + +/// An already-resolved future, for a call that finished without submitting. +fn completed_python_future(py: Python<'_>) -> PyResult> { + let future = py + .import("concurrent.futures")? + .getattr("Future")? + .call0()?; + future.call_method1("set_result", (py.None(),))?; + Ok(future.unbind()) +} + +fn py_none(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if !result.value.is_null() || result.value_count != 0 { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "unit completion returned a value", + ))); + } + Ok(py.None()) +} + +fn py_command(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if !result.value.is_null() || result.value_count != 0 { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "command completion returned a value", + ))); + } + Ok(py + .import("maplibre_native_ffi.runtime")? + .getattr("CommandCompletion")? + .call1(( + result.disposition, + result.generation, + result.status, + copy_completion_diagnostic(result), + ))? + .unbind()) +} + +fn copy_completion_diagnostic(result: &sys::mln_completion_result) -> String { + if result.diagnostic.data.is_null() || result.diagnostic.size == 0 { + return String::new(); + } + // SAFETY: completion diagnostics are borrowed for this callback. + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts(result.diagnostic.data.cast::(), result.diagnostic.size) + }) + .into_owned() +} + +fn completion_value(result: &sys::mln_completion_result) -> PyResult { + if result.value.is_null() || result.value_count != 1 { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "native completion returned no value", + ))); + } + // SAFETY: each caller supplies the result type documented by the + // submitting C function, and the value is borrowed for this callback. + Ok(unsafe { result.value.cast::().read_unaligned() }) +} + +fn completion_slice(result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + return Ok(Vec::new()); } + if result.value.is_null() { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "native completion returned a null slice", + ))); + } + // SAFETY: each caller supplies the element type documented by the + // submitting C function, and the slice is borrowed for this callback. + Ok( + unsafe { + std::slice::from_raw_parts(result.value.cast::(), result.value_count).to_vec() + }, + ) +} - fn is_closed(&self) -> bool { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .closed +fn py_offline_region(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let raw = completion_value::(result)?; + let info = maplibre_core::runtime::copy_offline_region_info(&raw).map_err(map_error)?; + offline_region_info_to_py(py, &info) +} + +fn py_optional_offline_region( + py: Python<'_>, + result: &sys::mln_completion_result, +) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); } + py_offline_region(py, result) } -impl Drop for RuntimeDetachedOperationGuard<'_> { - fn drop(&mut self) { - self.gate - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .active_detached_operation = false; +fn py_offline_regions(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let regions = completion_slice::(result)? + .iter() + .map(maplibre_core::runtime::copy_offline_region_info) + .collect::>>() + .map_err(map_error)?; + offline_region_list_to_py(py, ®ions) +} + +fn py_offline_region_status( + py: Python<'_>, + result: &sys::mln_completion_result, +) -> PyResult> { + let raw = completion_value::(result)?; + offline_region_status_to_py(py, &raw) +} + +fn copied_string_view(view: sys::mln_buffer_view) -> PyResult { + unsafe { maplibre_core::string::copy_string_view(view) }.map_err(map_error) +} + +fn py_buffer(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let view = completion_value::(result)?; + let bytes = + unsafe { maplibre_core::string::copy_string_view_bytes(view) }.map_err(map_error)?; + Ok(PyBytes::new(py, &bytes).into_any().unbind()) +} + +fn py_optional_buffer(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + Ok(py.None()) + } else { + py_buffer(py, result) } } -fn start_offline_operation(runtime: &RuntimeHandle, start: F) -> PyResult -where - F: FnOnce(sys::mln_runtime, *mut u64) -> i32, -{ - let state = runtime.state_for_operation()?; - let mut operation_id = 0; - maplibre_core::check(start(state.handle(), &mut operation_id)).map_err(map_error)?; - Ok(operation_id) +fn py_string(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + Ok(copied_string_view(completion_value(result)?)? + .into_pyobject(py)? + .into_any() + .unbind()) } -fn leak_optional_box(slot: &Mutex>>) { - let Some(value) = slot - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .take() - else { - return; - }; - Box::leak(value); +fn py_string_list(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let values = completion_slice::(result)?; + let list = PyList::empty(py); + for value in values { + list.append(copied_string_view(value)?)?; + } + Ok(list.into_any().unbind()) } -impl Drop for RuntimeHandle { - fn drop(&mut self) { - let native_live = self - .state - .lock() - .map(|state| !state.is_closed()) - .unwrap_or(true); - if native_live { - leak_optional_box(&self.resource_provider); - leak_optional_box(&self.resource_transform); - leak_optional_box(&self.http_header_transform); +fn py_source_info(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); + } + let raw = completion_value::(result)?; + let attribution = raw + .info + .has_attribution + .then(|| copied_string_view(raw.attribution)) + .transpose()?; + let url = (raw.info.fields & sys::MLN_STYLE_SOURCE_INFO_URL != 0) + .then(|| copied_string_view(raw.url)) + .transpose()?; + let mut tiles = Vec::with_capacity(raw.tile_url_count); + if raw.tile_url_count != 0 { + if raw.tile_urls.is_null() { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "source completion returned a null tile URL array", + ))); } + for view in unsafe { std::slice::from_raw_parts(raw.tile_urls, raw.tile_url_count) } { + tiles.push(copied_string_view(*view)?); + } + } + source_info_to_py( + py, + maplibre_core::style::style_source_info_from_native(&raw.info, attribution, url, tiles), + ) +} + +fn py_layer_info(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); + } + let raw = completion_value::(result)?; + let source_id = (raw.info.fields & sys::MLN_STYLE_LAYER_INFO_SOURCE_ID != 0) + .then(|| copied_string_view(raw.source_id)) + .transpose()?; + let source_layer = (raw.info.fields & sys::MLN_STYLE_LAYER_INFO_SOURCE_LAYER != 0) + .then(|| copied_string_view(raw.source_layer)) + .transpose()?; + let info = unsafe { + maplibre_core::style::style_layer_info_from_native(&raw.info, source_id, source_layer) + } + .map_err(map_error)?; + let dict = PyDict::new(py); + dict.set_item("layer_type", info.layer_type)?; + dict.set_item("min_zoom", info.min_zoom)?; + dict.set_item("max_zoom", info.max_zoom)?; + dict.set_item("visibility", info.visibility.raw_value())?; + dict.set_item("source_id", info.source_id)?; + dict.set_item("source_layer", info.source_layer)?; + Ok(dict.into_any().unbind()) +} + +fn py_style_image_info(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); + } + let raw = completion_value::(result)?; + style_image_info_to_py(py, &raw.info) +} + +fn copy_stretches( + values: *const sys::mln_image_stretch, + count: usize, +) -> PyResult> { + if count == 0 { + return Ok(Vec::new()); + } + if values.is_null() { + return Err(py_errors::NativeError::new_err(( + Option::::None, + "native style image returned a null stretch array", + ))); + } + Ok(unsafe { std::slice::from_raw_parts(values, count) } + .iter() + .map(|value| (value.from, value.to)) + .collect()) +} + +fn py_style_image(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); + } + let raw = completion_value::(result)?; + let value = style_image_info_to_py(py, &raw.info)?; + let dict = value.bind(py).cast::()?; + let pixels = + unsafe { maplibre_core::string::copy_string_view_bytes(raw.pixels) }.map_err(map_error)?; + dict.set_item("pixels", PyBytes::new(py, &pixels))?; + dict.set_item( + "stretch_x", + copy_stretches(raw.stretch_x, raw.stretch_x_count)?, + )?; + dict.set_item( + "stretch_y", + copy_stretches(raw.stretch_y, raw.stretch_y_count)?, + )?; + Ok(value) +} + +fn py_style_image_stretches( + py: Python<'_>, + result: &sys::mln_completion_result, +) -> PyResult> { + if result.value_count == 0 { + return Ok(py.None()); + } + let raw = completion_value::(result)?; + ( + copy_stretches(raw.stretch_x, raw.stretch_x_count)?, + copy_stretches(raw.stretch_y, raw.stretch_y_count)?, + ) + .into_pyobject(py) + .map(|value| value.into_any().unbind()) +} + +fn py_optional_lat_lngs( + py: Python<'_>, + result: &sys::mln_completion_result, +) -> PyResult> { + let coordinates = completion_slice::(result)?; + if coordinates.is_empty() { + Ok(py.None()) + } else { + lat_lng_list_to_py(py, &coordinates) } } +fn py_queried_features(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let raw = completion_slice::(result)?; + // SAFETY: the completion keeps every nested feature view alive while this + // callback copies it into Python-owned values. + let features = + unsafe { maplibre_core::query::copy_queried_features(&raw) }.map_err(map_error)?; + Ok(queried_features_to_py(py, features)?.into_any()) +} + +fn py_texture_readback(py: Python<'_>, result: &sys::mln_completion_result) -> PyResult> { + let raw = completion_value::(result)?; + let dict = PyDict::new(py); + dict.set_item( + "info", + texture_image_info_to_py( + py, + maplibre_core::values::texture_image_info_from_native(&raw.info), + )? + .bind(py), + )?; + let bytes = + unsafe { maplibre_core::string::copy_string_view_bytes(raw.data) }.map_err(map_error)?; + dict.set_item("data", PyBytes::new(py, &bytes))?; + Ok(dict.into_any().unbind()) +} + impl PyLogCallbackState { fn new(max_queued_records: usize, consume: bool) -> Arc { Arc::new(Self { @@ -481,9 +742,9 @@ impl MapHandle { sys::mln_buffer_view, sys::mln_buffer_view, *const sys::mln_style_tile_source_options, + *const sys::mln_completion, ) -> sys::mln_status, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let url = maplibre_core::string::string_view(&url); let options = tile_source_options_from_parts( @@ -497,11 +758,15 @@ impl MapHandle { raster_dem_encoding, )?; let options = maplibre_core::style::tile_source_options_to_native(&options); - // SAFETY: The C API validates the map pointer, string views, and options. - maplibre_core::check(unsafe { - add(state.handle(), source_id.raw(), url.raw(), options.as_ptr()) + self.run_style_command(|map, completion| unsafe { + add( + map, + source_id.raw(), + url.raw(), + options.as_ptr(), + completion, + ) }) - .map_err(map_error) } #[allow(clippy::too_many_arguments)] @@ -523,9 +788,9 @@ impl MapHandle { *const sys::mln_buffer_view, usize, *const sys::mln_style_tile_source_options, + *const sys::mln_completion, ) -> sys::mln_status, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let tiles = maplibre_core::style::NativeTileUrls::new(&tiles); let options = tile_source_options_from_parts( @@ -539,35 +804,38 @@ impl MapHandle { raster_dem_encoding, )?; let options = maplibre_core::style::tile_source_options_to_native(&options); - // SAFETY: The C API validates the map pointer, source ID, tile URL views, and options. - maplibre_core::check(unsafe { + self.run_style_command(|map, completion| unsafe { add( - state.handle(), + map, source_id.raw(), tiles.as_ptr(), tiles.len(), options.as_ptr(), + completion, ) }) - .map_err(map_error) } - fn string_bool_call_with( + fn run_style_operation( &self, - value: String, - call: unsafe extern "C" fn( - sys::mln_map, - sys::mln_buffer_view, - *mut bool, - ) -> sys::mln_status, - ) -> PyResult { + py: Python<'_>, + start: impl FnOnce(sys::mln_map, *const sys::mln_completion) -> sys::mln_status, + convert: impl FnOnce(Python<'_>, &sys::mln_completion_result) -> PyResult> + + Send + + 'static, + ) -> PyResult> { let state = self.state(); - let value = maplibre_core::string::string_view(&value); - let mut out = false; - // SAFETY: The C API validates the map pointer, borrowed string view, and out pointer. - maplibre_core::check(unsafe { call(state.handle(), value.raw(), &mut out) }) - .map_err(map_error)?; - Ok(out) + submit_python_future(py, |completion| start(state.handle(), completion), convert) + } + + fn run_style_command( + &self, + accept: impl FnOnce(sys::mln_map, *const sys::mln_completion) -> sys::mln_status, + ) -> PyResult> { + let state = self.state(); + Python::attach(|py| { + submit_python_command_future(py, |completion| accept(state.handle(), completion)) + }) } } @@ -788,9 +1056,8 @@ impl Drop for PyCustomMvtVectorSourceState { /// Takes back the callback state the C API stopped referencing. /// -/// The C API calls this once on the map owner thread, whether the source was -/// removed explicitly, dropped by a style load, or retired with the map, so the -/// binding never watches style loads to find its own detached sources. +/// The C API calls this once after the source is removed, dropped by a style +/// load, or retired with the map. unsafe extern "C" fn custom_geometry_release_trampoline(user_data: *mut c_void) { let _ = catch_unwind(AssertUnwindSafe(|| { let Some(state) = ptr::NonNull::new(user_data.cast::()) else { @@ -896,11 +1163,7 @@ impl RenderSessionState { maplibre_core::handle::NativeHandleState::from_handle(native, "mln_render_session") } .map_err(map_error)?; - Ok(Self { - handle, - detached: false, - frame_acquired: false, - }) + Ok(Self { handle }) } fn native(&self) -> sys::mln_render_session { @@ -910,372 +1173,237 @@ impl RenderSessionState { fn is_closed(&self) -> bool { self.handle.is_closed() } - - fn ensure_no_frame_acquired(&self) -> PyResult<()> { - if self.frame_acquired { - Err(invalid_state_error( - "render session has an acquired texture frame", - )) - } else { - Ok(()) - } - } } - #[pymethods] impl RuntimeHandle { - fn close(&self, py: Python<'_>) -> PyResult<()> { - if !self.operation_gate.begin_close()? { - return Ok(()); - } - let runtime_handle = { + fn close(&self, py: Python<'_>) -> PyResult> { + let teardown = { let state = self.state(); let Some(runtime_handle) = state.live_handle() else { - self.operation_gate.finish_successful_close(); - return Ok(()); + return completed_python_future(py); }; - state.mark_closed(); - runtime_handle + // Release returns after synchronous child preflight, so this does + // not block; the future reports the end of native teardown. + let teardown = submit_python_future( + py, + |completion| unsafe { sys::mln_runtime_release(runtime_handle, completion) }, + py_none, + )?; + state.mark_closed(); + teardown }; - // SAFETY: state owns an mln_runtime handle created by mln_runtime_create - // and pairs it with the matching destroy function. Destroy can wait for - // in-flight callbacks, so it runs without the GIL or the state mutex. - let status = py.detach(move || unsafe { sys::mln_runtime_destroy(runtime_handle) }); - if let Err(error) = maplibre_core::check(status) { - self.state().restore_handle_for_retry(runtime_handle); - self.operation_gate.finish_failed_close(); - return Err(map_error(error)); - } - self.resource_provider + + self.event_wake .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .take(); - self.resource_transform + Ok(teardown) + } + + fn barrier(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { sys::mln_runtime_barrier(runtime, completion) }, + py_none, + ) + } + + fn set_event_wake_callback(&self, py: Python<'_>, callback: Py) -> PyResult<()> { + self.ensure_open()?; + self.event_wake .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) - .take(); - self.http_header_transform + .replace(callback.clone_ref(py)); + Ok(()) + } + + fn clear_event_wake_callback(&self) -> PyResult<()> { + self.ensure_open()?; + self.event_wake .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .take(); - self.operation_gate.finish_successful_close(); Ok(()) } - #[pyo3(signature = (timeout_ms, budget_ms = -1))] - fn pump(&self, py: Python<'_>, timeout_ms: i64, budget_ms: i64) -> PyResult<()> { - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = { - let state = self.state_for_operation()?; - let Some(runtime_handle) = state.live_handle() else { - return Err(invalid_state_error("runtime handle is closed")); - }; - runtime_handle - }; - // SAFETY: runtime_handle passed the binding lifecycle gate, and the C - // API validates that it is live and on the owner thread. The call parks, - // so it runs without the GIL or the state mutex: another Python thread - // signalling a wake source is what ends the park. - let status = - py.detach(|| unsafe { sys::mln_runtime_pump(runtime_handle, timeout_ms, budget_ms) }); - maplibre_core::check(status).map_err(map_error) - } - - fn wake_source(&self) -> PyResult { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates that the pointer is a live runtime handle - // and that the call occurs on the runtime owner thread, and out is a - // null-initialized out-pointer owned by this call. - maplibre_core::check(unsafe { - sys::mln_runtime_wake_source_acquire(state.handle(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - let source = out.into_live("mln_wake_source").map_err(map_error)?; - Ok(WakeSource { - handle: Mutex::new(Some(source)), - }) - } - - fn run_ambient_cache_operation_start(&self, operation: u32) -> PyResult { - let state = self.state_for_operation()?; - let mut operation_id = 0; - // SAFETY: The C API validates the runtime handle, operation enum value, - // owner-thread affinity, and writable operation_id pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_run_ambient_cache_operation_start( - state.handle(), - operation, - &mut operation_id, - ) - }) - .map_err(map_error)?; - Ok(operation_id) + fn run_ambient_cache_operation( + &self, + py: Python<'_>, + ambient_operation: u32, + ) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_run_ambient_cache_operation(runtime, ambient_operation, completion) + }, + py_none, + ) } - fn set_maximum_ambient_cache_size_start(&self, size: u64) -> PyResult { - let state = self.state_for_operation()?; - let mut operation_id = 0; - // SAFETY: The C API validates the runtime handle, owner-thread - // affinity, and writable operation_id pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_set_maximum_ambient_cache_size_start( - state.handle(), - size, - &mut operation_id, - ) - }) - .map_err(map_error)?; - Ok(operation_id) + fn set_maximum_ambient_cache_size(&self, py: Python<'_>, size: u64) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_set_maximum_ambient_cache_size(runtime, size, completion) + }, + py_none, + ) } - fn offline_region_create_start( + fn create_offline_region( &self, + py: Python<'_>, definition: &Bound<'_, PyAny>, metadata: Vec, - ) -> PyResult { - let state = self.state_for_operation()?; + ) -> PyResult> { + let runtime = self.native()?; let definition = offline_region_definition_from_wire(definition)?; let definition = maplibre_core::runtime::offline_region_definition_to_native(&definition) .map_err(map_error)?; let raw = definition.to_raw(); - let mut operation_id = 0; - // SAFETY: The C API validates the runtime, definition, metadata pointer/length, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_create_start( - state.handle(), - &raw, - maplibre_core::runtime::metadata_ptr(&metadata), - metadata.len(), - &mut operation_id, - ) - }) - .map_err(map_error)?; - Ok(operation_id) - } - - fn offline_region_get_start(&self, region_id: i64) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_get_start(runtime, region_id, out) - }) - } - - fn offline_regions_list_start(&self) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_regions_list_start(runtime, out) - }) - } - - fn offline_regions_merge_database_start(&self, side_database_path: String) -> PyResult { - let path = maplibre_core::string::c_string(&side_database_path).map_err(map_error)?; - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_regions_merge_database_start(runtime, path.as_ptr(), out) - }) - } - - fn offline_region_update_metadata_start( - &self, - region_id: i64, - metadata: Vec, - ) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_update_metadata_start( - runtime, - region_id, - maplibre_core::runtime::metadata_ptr(&metadata), - metadata.len(), - out, - ) - }) - } - - fn offline_region_get_status_start(&self, region_id: i64) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_get_status_start(runtime, region_id, out) - }) - } - - fn offline_region_set_observed_start(&self, region_id: i64, observed: bool) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_set_observed_start(runtime, region_id, observed, out) - }) - } - - fn offline_region_set_download_state_start(&self, region_id: i64, state: u32) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_set_download_state_start(runtime, region_id, state, out) - }) + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_create( + runtime, + &raw, + maplibre_core::runtime::metadata_ptr(&metadata), + metadata.len(), + completion, + ) + }, + py_offline_region, + ) } - fn offline_region_invalidate_start(&self, region_id: i64) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_invalidate_start(runtime, region_id, out) - }) + fn get_offline_region(&self, py: Python<'_>, region_id: i64) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_get(runtime, region_id, completion) + }, + py_optional_offline_region, + ) } - fn offline_region_delete_start(&self, region_id: i64) -> PyResult { - start_offline_operation(self, |runtime, out| unsafe { - sys::mln_runtime_offline_region_delete_start(runtime, region_id, out) - }) + fn list_offline_regions(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { sys::mln_runtime_offline_regions_list(runtime, completion) }, + py_offline_regions, + ) } - fn offline_region_create_take_result( + fn merge_offline_regions_database( &self, py: Python<'_>, - operation_id: u64, + side_database_path: String, ) -> PyResult> { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the runtime handle, operation ID, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_create_take_result( - state.handle(), - operation_id, - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_offline_region_snapshot") - .map_err(map_error)?; - // SAFETY: native is an owned offline-region snapshot returned by the C API. - let info = unsafe { maplibre_core::runtime::copy_offline_region_snapshot(native) } - .map_err(map_error)?; - offline_region_info_to_py(py, &info) + let runtime = self.native()?; + let path = maplibre_core::string::c_string(&side_database_path).map_err(map_error)?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_regions_merge_database(runtime, path.as_ptr(), completion) + }, + py_offline_regions, + ) } - fn offline_region_get_take_result( + fn update_offline_region_metadata( &self, py: Python<'_>, - operation_id: u64, - ) -> PyResult>> { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut found = false; - // SAFETY: The C API validates the runtime handle, operation ID, output pointer, and found pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_get_take_result( - state.handle(), - operation_id, - out.as_mut_ptr(), - &mut found, - ) - }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - let native = out - .into_live("mln_offline_region_snapshot") - .map_err(map_error)?; - // SAFETY: native is an owned offline-region snapshot returned by the C API. - let info = unsafe { maplibre_core::runtime::copy_offline_region_snapshot(native) } - .map_err(map_error)?; - offline_region_info_to_py(py, &info).map(Some) + region_id: i64, + metadata: Vec, + ) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_update_metadata( + runtime, + region_id, + maplibre_core::runtime::metadata_ptr(&metadata), + metadata.len(), + completion, + ) + }, + py_offline_region, + ) } - fn offline_regions_list_take_result( - &self, - py: Python<'_>, - operation_id: u64, - ) -> PyResult> { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the runtime handle, operation ID, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_regions_list_take_result( - state.handle(), - operation_id, - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_offline_region_list") - .map_err(map_error)?; - // SAFETY: native is an owned offline-region list returned by the C API. - let regions = unsafe { maplibre_core::runtime::copy_offline_region_list(native) } - .map_err(map_error)?; - offline_region_list_to_py(py, ®ions) + fn get_offline_region_status(&self, py: Python<'_>, region_id: i64) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_get_status(runtime, region_id, completion) + }, + py_offline_region_status, + ) } - fn offline_regions_merge_database_take_result( + fn set_offline_region_observed( &self, py: Python<'_>, - operation_id: u64, + region_id: i64, + observed: bool, ) -> PyResult> { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the runtime handle, operation ID, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_regions_merge_database_take_result( - state.handle(), - operation_id, - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_offline_region_list") - .map_err(map_error)?; - // SAFETY: native is an owned offline-region list returned by the C API. - let regions = unsafe { maplibre_core::runtime::copy_offline_region_list(native) } - .map_err(map_error)?; - offline_region_list_to_py(py, ®ions) + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_set_observed( + runtime, region_id, observed, completion, + ) + }, + py_none, + ) } - fn offline_region_update_metadata_take_result( + fn set_offline_region_download_state( &self, py: Python<'_>, - operation_id: u64, + region_id: i64, + state: u32, ) -> PyResult> { - let state = self.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the runtime handle, operation ID, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_update_metadata_take_result( - state.handle(), - operation_id, - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_offline_region_snapshot") - .map_err(map_error)?; - // SAFETY: native is an owned offline-region snapshot returned by the C API. - let info = unsafe { maplibre_core::runtime::copy_offline_region_snapshot(native) } - .map_err(map_error)?; - offline_region_info_to_py(py, &info) + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_set_download_state( + runtime, region_id, state, completion, + ) + }, + py_none, + ) } - fn offline_region_get_status_take_result( - &self, - py: Python<'_>, - operation_id: u64, - ) -> PyResult> { - let state = self.state_for_operation()?; - let mut status = empty_offline_region_status(); - // SAFETY: The C API validates the runtime handle, operation ID, and output pointer. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_get_status_take_result( - state.handle(), - operation_id, - &mut status, - ) - }) - .map_err(map_error)?; - offline_region_status_to_py(py, &status) + fn invalidate_offline_region(&self, py: Python<'_>, region_id: i64) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_invalidate(runtime, region_id, completion) + }, + py_none, + ) } - fn offline_operation_discard(&self, operation_id: u64) -> PyResult<()> { - let state = self.state_for_operation()?; - // SAFETY: The C API validates the runtime handle, owner-thread affinity, - // and operation ID. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_operation_discard(state.handle(), operation_id) - }) - .map_err(map_error) + fn delete_offline_region(&self, py: Python<'_>, region_id: i64) -> PyResult> { + let runtime = self.native()?; + submit_python_future( + py, + |completion| unsafe { + sys::mln_runtime_offline_region_delete(runtime, region_id, completion) + }, + py_none, + ) } fn set_resource_provider( @@ -1283,7 +1411,7 @@ impl RuntimeHandle { py: Python<'_>, callback: Py, max_pending_callbacks: usize, - ) -> PyResult<()> { + ) -> PyResult> { if max_pending_callbacks == 0 { return Err(invalid_argument_error( "max_pending_callbacks must be greater than zero", @@ -1294,61 +1422,26 @@ impl RuntimeHandle { max_pending_callbacks, )); let descriptor = replacement.descriptor(); - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = { - let state = self.state_for_operation()?; - let Some(runtime_handle) = state.live_handle() else { - return Err(invalid_state_error("runtime handle is closed")); - }; - runtime_handle - }; - let callback = descriptor.callback; - let user_data_address = descriptor.user_data as usize; - let size = descriptor.size; - // SAFETY: runtime_handle passed the binding lifecycle gate and the C API - // validates that it is live. descriptor points to replacement state, - // retained after a successful registration. Replacement can wait for - // in-flight callbacks that need the GIL, so it runs without the GIL or - // the state mutex. - let status = py.detach(move || { - let descriptor = sys::mln_resource_provider { - size, - callback, - user_data: user_data_address as *mut c_void, - }; - unsafe { sys::mln_runtime_set_resource_provider(runtime_handle, &descriptor) } + let runtime = self.native()?; + let replacement = Box::into_raw(replacement); + let result = submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_set_resource_provider(runtime, &descriptor, completion) }); - maplibre_core::check(status).map_err(map_error)?; - // Native retired the previous provider before returning, so dropping - // the state it replaces here can no longer be reached from native. - self.resource_provider - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .replace(replacement); - Ok(()) + match result { + Ok(future) => Ok(future), + Err(error) => { + // SAFETY: native never invokes release_user_data for a rejected registration. + drop(unsafe { Box::from_raw(replacement) }); + Err(error) + } + } } - fn clear_resource_provider(&self, py: Python<'_>) -> PyResult<()> { - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = { - let state = self.state_for_operation()?; - let Some(runtime_handle) = state.live_handle() else { - return Err(invalid_state_error("runtime handle is closed")); - }; - runtime_handle - }; - // SAFETY: runtime_handle passed the binding lifecycle gate and the C API - // validates that it is live. The call waits for in-flight callbacks that - // need the GIL, so it runs without the GIL or the state mutex. - let status = - py.detach(move || unsafe { sys::mln_runtime_clear_resource_provider(runtime_handle) }); - maplibre_core::check(status).map_err(map_error)?; - // Native can no longer reach the cleared provider state, so drop it. - self.resource_provider - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .take(); - Ok(()) + fn clear_resource_provider(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_clear_resource_provider(runtime, completion) + }) } fn set_resource_transform( @@ -1356,7 +1449,7 @@ impl RuntimeHandle { py: Python<'_>, callback: Py, max_pending_callbacks: usize, - ) -> PyResult<()> { + ) -> PyResult> { if max_pending_callbacks == 0 { return Err(invalid_argument_error( "max_pending_callbacks must be greater than zero", @@ -1367,57 +1460,26 @@ impl RuntimeHandle { max_pending_callbacks, )); let descriptor = replacement.descriptor(); - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = { - let state = self.state_for_operation()?; - let Some(runtime_handle) = state.live_handle() else { - return Err(invalid_state_error("runtime handle is closed")); - }; - runtime_handle - }; - let callback = descriptor.callback; - let user_data_address = descriptor.user_data as usize; - let size = descriptor.size; - // SAFETY: runtime_handle passed the binding lifecycle gate and the C API - // validates that it is live. descriptor points to replacement state, - // retained after a successful registration. Replacement can wait for - // in-flight callbacks, so it runs without the GIL or the state mutex. - let status = py.detach(move || { - let descriptor = sys::mln_resource_transform { - size, - callback, - user_data: user_data_address as *mut c_void, - }; - unsafe { sys::mln_runtime_set_resource_transform(runtime_handle, &descriptor) } + let runtime = self.native()?; + let replacement = Box::into_raw(replacement); + let result = submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_set_resource_transform(runtime, &descriptor, completion) }); - maplibre_core::check(status).map_err(map_error)?; - self.resource_transform - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .replace(replacement); - Ok(()) + match result { + Ok(future) => Ok(future), + Err(error) => { + // SAFETY: native never invokes release_user_data for a rejected registration. + drop(unsafe { Box::from_raw(replacement) }); + Err(error) + } + } } - fn clear_resource_transform(&self, py: Python<'_>) -> PyResult<()> { - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = { - let state = self.state_for_operation()?; - let Some(runtime_handle) = state.live_handle() else { - return Err(invalid_state_error("runtime handle is closed")); - }; - runtime_handle - }; - // SAFETY: runtime_handle passed the binding lifecycle gate and the C API - // validates that it is live. The call waits for in-flight callbacks, so - // it runs without the GIL or the state mutex. - let status = - py.detach(move || unsafe { sys::mln_runtime_clear_resource_transform(runtime_handle) }); - maplibre_core::check(status).map_err(map_error)?; - self.resource_transform - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .take(); - Ok(()) + fn clear_resource_transform(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_clear_resource_transform(runtime, completion) + }) } fn set_http_header_transform( @@ -1425,7 +1487,7 @@ impl RuntimeHandle { py: Python<'_>, callback: Py, max_pending_callbacks: usize, - ) -> PyResult<()> { + ) -> PyResult> { if max_pending_callbacks == 0 { return Err(invalid_argument_error( "max_pending_callbacks must be greater than zero", @@ -1436,104 +1498,86 @@ impl RuntimeHandle { max_pending_callbacks, )); let descriptor = replacement.descriptor(); - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = self - .state_for_operation()? - .live_handle() - .ok_or_else(|| invalid_state_error("runtime handle is closed"))?; - let callback = descriptor.callback; - let user_data_address = descriptor.user_data as usize; - let size = descriptor.size; - let status = py.detach(move || { - let descriptor = sys::mln_http_header_transform { - size, - callback, - user_data: user_data_address as *mut c_void, - }; - // SAFETY: runtime is live and replacement remains retained on success. - unsafe { sys::mln_runtime_set_http_header_transform(runtime_handle, &descriptor) } - }); - maplibre_core::check(status).map_err(map_error)?; - self.http_header_transform - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .replace(replacement); - Ok(()) - } - - fn clear_http_header_transform(&self, py: Python<'_>) -> PyResult<()> { - let _operation = self.operation_gate.begin_detached_operation()?; - let runtime_handle = self - .state_for_operation()? - .live_handle() - .ok_or_else(|| invalid_state_error("runtime handle is closed"))?; - let status = py.detach(move || { - // SAFETY: runtime is live; native waits for callback retirement. - unsafe { sys::mln_runtime_clear_http_header_transform(runtime_handle) } + let runtime = self.native()?; + let replacement = Box::into_raw(replacement); + let result = submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_set_http_header_transform(runtime, &descriptor, completion) }); - maplibre_core::check(status).map_err(map_error)?; - self.http_header_transform - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .take(); - Ok(()) + match result { + Ok(future) => Ok(future), + Err(error) => { + // SAFETY: native never invokes release_user_data for a rejected registration. + drop(unsafe { Box::from_raw(replacement) }); + Err(error) + } + } } - fn drain_events(&self, py: Python<'_>, max_events: usize) -> PyResult> { - let state = self.state_for_operation()?; - // SAFETY: The default constructor takes no arguments and fills in the - // size field for this C ABI version. - let mut batch = unsafe { sys::mln_runtime_event_batch_default() }; - // SAFETY: The C API validates that the pointer is a live runtime handle. - // batch carries this header's size field and is writable for one batch. - maplibre_core::check(unsafe { - sys::mln_runtime_drain_events(state.handle(), max_events, &mut batch) + fn clear_http_header_transform(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + submit_python_command_future(py, |completion| unsafe { + sys::mln_runtime_clear_http_header_transform(runtime, completion) }) - .map_err(map_error)?; + } - // SAFETY: The batch borrows runtime-owned storage that stays readable - // until the next drain for this runtime, which the still-held state - // mutex prevents. The decoder walks it by the reported event_size and - // every event is copied before the borrow ends here. - let mut copied = Vec::with_capacity(batch.event_count); - for event in unsafe { maplibre_core::events::drain_batch(&batch) } { - copied.push(event.map_err(map_error)?); - } - event_batch_to_py(py, copied, batch.remaining_count) + fn drain_events(&self, py: Python<'_>) -> PyResult> { + let runtime = self.native()?; + let mut batch = sys::mln_event_batch(0); + maplibre_core::check(unsafe { sys::mln_runtime_drain_events(runtime, &mut batch) }) + .map_err(map_error)?; + let result = (|| { + let mut view: sys::mln_runtime_event_batch_view = unsafe { std::mem::zeroed() }; + view.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_event_batch_get(batch, &mut view) }) + .map_err(map_error)?; + let mut copied = Vec::with_capacity(view.event_count); + for event in unsafe { maplibre_core::events::drain_batch(&view) } { + let event = event.map_err(map_error)?; + copied.push(event); + } + event_batch_to_py(py, copied) + })(); + unsafe { sys::mln_event_batch_release(batch) }; + result } fn set_event_mask(&self, mask: u64) -> PyResult<()> { - let state = self.state_for_operation()?; - // SAFETY: The C API validates the runtime handle, owner-thread - // affinity, and the mask bits. - maplibre_core::check(unsafe { sys::mln_runtime_set_event_mask(state.handle(), mask) }) + let runtime = self.native()?; + // SAFETY: The C API validates the runtime handle and mask bits. + maplibre_core::check(unsafe { sys::mln_runtime_set_event_mask(runtime, mask) }) .map_err(map_error) } fn get_event_mask(&self) -> PyResult { - let state = self.state_for_operation()?; + let runtime = self.native()?; let mut mask = 0u64; - // SAFETY: The C API validates the runtime handle and owner-thread - // affinity, and mask points to writable storage for one u64. - maplibre_core::check(unsafe { sys::mln_runtime_get_event_mask(state.handle(), &mut mask) }) + // SAFETY: The C API validates the runtime handle, and mask points to + // writable storage for one u64. + maplibre_core::check(unsafe { sys::mln_runtime_get_event_mask(runtime, &mut mask) }) .map_err(map_error)?; Ok(mask) } #[getter] fn closed(&self) -> bool { - self.operation_gate.is_closed() + self.state().is_closed() } } #[pymethods] impl MapHandle { - fn close(&self) -> PyResult<()> { + fn close(&self, py: Python<'_>) -> PyResult> { let state = self.state(); - // SAFETY: state owns an mln_map handle created by mln_map_create and - // pairs it with the matching status-returning destroy function. - unsafe { state.close_status(sys::mln_map_destroy) }.map_err(map_error)?; - Ok(()) + let Some(handle) = state.live_handle() else { + return completed_python_future(py); + }; + let teardown = submit_python_future( + py, + |completion| unsafe { sys::mln_map_release(handle, completion) }, + py_none, + )?; + state.mark_closed(); + Ok(teardown) } /// This map's native handle, which names one map for the life of the @@ -1543,119 +1587,109 @@ impl MapHandle { Ok(state.handle().0) } - fn request_repaint(&self) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates that the handle is live and - // that the call occurs on the map owner thread. - maplibre_core::check(unsafe { sys::mln_map_request_repaint(state.handle()) }) - .map_err(map_error) + fn request_repaint(&self) -> PyResult> { + self.run_style_command(|map, completion| unsafe { + sys::mln_map_request_repaint(map, completion) + }) } - fn request_still_image(&self) -> PyResult<()> { + fn request_still_image(&self, py: Python<'_>) -> PyResult> { let state = self.state(); - // SAFETY: The C API validates that the handle is live and - // that the call occurs on the map owner thread. - maplibre_core::check(unsafe { sys::mln_map_request_still_image(state.handle()) }) - .map_err(map_error) + submit_python_future( + py, + |completion| unsafe { sys::mln_map_request_still_image(state.handle(), completion) }, + py_none, + ) } - fn dump_debug_logs(&self) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates that the handle is live and - // that the call occurs on the map owner thread. - maplibre_core::check(unsafe { sys::mln_map_dump_debug_logs(state.handle()) }) - .map_err(map_error) + fn dump_debug_logs(&self) -> PyResult> { + self.run_style_command(|map, out| unsafe { sys::mln_map_dump_debug_logs(map, out) }) } - fn set_event_mask(&self, mask: u64) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer, owner-thread affinity, - // and the mask bits. - maplibre_core::check(unsafe { sys::mln_map_set_event_mask(state.handle(), mask) }) - .map_err(map_error) + fn set_event_mask(&self, mask: u64) -> PyResult> { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_event_mask(map, mask, out) }) } fn get_event_mask(&self) -> PyResult { let state = self.state(); - let mut mask = 0u64; - // SAFETY: The C API validates the map pointer and owner-thread affinity, - // and mask points to writable storage for one u64. - maplibre_core::check(unsafe { sys::mln_map_get_event_mask(state.handle(), &mut mask) }) + let mut snapshot: sys::mln_map_snapshot = unsafe { std::mem::zeroed() }; + snapshot.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_map_snapshot_get(state.handle(), &mut snapshot) }) .map_err(map_error)?; - Ok(mask) + Ok(snapshot.event_mask) } - fn set_debug_options(&self, options: u32) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer, owner-thread affinity, - // and debug option mask bits. - maplibre_core::check(unsafe { sys::mln_map_set_debug_options(state.handle(), options) }) - .map_err(map_error) - } - - fn get_debug_options(&self) -> PyResult { - let state = self.state(); - let mut options = 0; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_debug_options(state.handle(), &mut options) + fn set_debug_options(&self, options: u32) -> PyResult> { + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_debug_options(map, options, out) }) - .map_err(map_error)?; - Ok(options) } - fn set_rendering_stats_view_enabled(&self, enabled: bool) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer and owner-thread affinity. - maplibre_core::check(unsafe { - sys::mln_map_set_rendering_stats_view_enabled(state.handle(), enabled) + fn set_rendering_stats_view_enabled(&self, enabled: bool) -> PyResult> { + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_rendering_stats_view_enabled(map, enabled, out) }) - .map_err(map_error) - } - - fn get_rendering_stats_view_enabled(&self) -> PyResult { - let state = self.state(); - let mut enabled = false; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_rendering_stats_view_enabled(state.handle(), &mut enabled) - }) - .map_err(map_error)?; - Ok(enabled) } - fn is_fully_loaded(&self) -> PyResult { + fn get_size(&self) -> PyResult<(u32, u32, f64)> { let state = self.state(); - let mut loaded = false; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { sys::mln_map_is_fully_loaded(state.handle(), &mut loaded) }) + let mut snapshot: sys::mln_map_snapshot = unsafe { std::mem::zeroed() }; + snapshot.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_map_snapshot_get(state.handle(), &mut snapshot) }) .map_err(map_error)?; - Ok(loaded) + Ok(( + snapshot.logical_extent.width, + snapshot.logical_extent.height, + snapshot.logical_extent.scale_factor, + )) } - fn get_size(&self) -> PyResult<(u32, u32, f64)> { - let state = self.state(); - let mut width = 0u32; - let mut height = 0u32; - let mut scale_factor = 0f64; - // SAFETY: The C API validates the map pointer and out pointers. - maplibre_core::check(unsafe { - sys::mln_map_get_size(state.handle(), &mut width, &mut height, &mut scale_factor) + fn resize(&self, width: u32, height: u32, scale_factor: f64) -> PyResult> { + let extent = sys::mln_logical_extent { + width, + height, + scale_factor, + }; + self.run_style_command(|map, completion| unsafe { + sys::mln_map_resize(map, extent, completion) }) - .map_err(map_error)?; - Ok((width, height, scale_factor)) } - - fn get_viewport_options(&self, py: Python<'_>) -> PyResult> { + fn snapshot(&self, py: Python<'_>) -> PyResult> { let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut options = unsafe { sys::mln_map_viewport_options_default() }; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_viewport_options(state.handle(), &mut options) - }) - .map_err(map_error)?; - viewport_options_to_py(py, &options) + let mut snapshot: sys::mln_map_snapshot = unsafe { std::mem::zeroed() }; + snapshot.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_map_snapshot_get(state.handle(), &mut snapshot) }) + .map_err(map_error)?; + let result = PyDict::new(py); + result.set_item("generation", snapshot.generation)?; + result.set_item("camera", camera_options_to_py(py, &snapshot.camera)?)?; + result.set_item("width", snapshot.logical_extent.width)?; + result.set_item("height", snapshot.logical_extent.height)?; + result.set_item("scale_factor", snapshot.logical_extent.scale_factor)?; + result.set_item( + "projection_mode", + projection_mode_to_py(py, &snapshot.projection_mode)?, + )?; + result.set_item("viewport", viewport_options_to_py(py, &snapshot.viewport)?)?; + result.set_item("debug_options", snapshot.debug_options)?; + result.set_item("fully_loaded", snapshot.fully_loaded)?; + result.set_item( + "rendering_stats_view_enabled", + snapshot.rendering_stats_view_enabled, + )?; + result.set_item("repaint_demand", snapshot.repaint_demand)?; + result.set_item("event_mask", snapshot.event_mask)?; + result.set_item( + "latest_render_update_generation", + snapshot.latest_render_update_generation, + )?; + result.set_item("tile", tile_options_to_py(py, &snapshot.tile)?)?; + result.set_item("bounds", bound_options_to_py(py, &snapshot.bounds)?)?; + result.set_item( + "free_camera", + free_camera_options_to_py(py, &snapshot.free_camera)?, + )?; + Ok(result.into_any().unbind()) } fn set_viewport_options( @@ -1664,29 +1698,16 @@ impl MapHandle { constrain_mode: Option, viewport_mode: Option, frustum_offset: Option<(f64, f64, f64, f64)>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let options = viewport_options_from_parts( north_orientation, constrain_mode, viewport_mode, frustum_offset, - ); - // SAFETY: The C API validates the map pointer and viewport options. - maplibre_core::check(unsafe { sys::mln_map_set_viewport_options(state.handle(), &options) }) - .map_err(map_error) - } - - fn get_tile_options(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut options = unsafe { sys::mln_map_tile_options_default() }; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_tile_options(state.handle(), &mut options) + ); + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_viewport_options(map, &options, out) }) - .map_err(map_error)?; - tile_options_to_py(py, &options) } fn set_tile_options( @@ -1697,8 +1718,7 @@ impl MapHandle { lod_pitch_threshold: Option, lod_zoom_shift: Option, lod_mode: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let options = tile_options_from_parts( prefetch_zoom_delta, lod_min_radius, @@ -1707,50 +1727,46 @@ impl MapHandle { lod_zoom_shift, lod_mode, ); - // SAFETY: The C API validates the map pointer and tile options. - maplibre_core::check(unsafe { sys::mln_map_set_tile_options(state.handle(), &options) }) - .map_err(map_error) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_tile_options(map, &options, out) + }) } - fn create_projection(&self) -> PyResult { + fn create_projection(&self, py: Python<'_>) -> PyResult> { let state = self.state(); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map handle, owner-thread affinity, and - // output pointer. out starts null and is consumed immediately on success. - maplibre_core::check(unsafe { - sys::mln_map_projection_create(state.handle(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - let native = out.into_live("mln_map_projection").map_err(map_error)?; - // SAFETY: ptr came from mln_map_projection_create and is paired with - // mln_map_projection_destroy in close. - let handle = unsafe { - maplibre_core::handle::NativeHandleState::from_handle(native, "mln_map_projection") - } - .map_err(map_error)?; - Ok(MapProjectionHandle { - state: Mutex::new(handle), - }) + submit_python_future( + py, + |completion| unsafe { sys::mln_map_projection_create(state.handle(), completion) }, + |py, result| { + let native = completion_value::(result)?; + let handle = unsafe { + maplibre_core::handle::NativeHandleState::from_handle( + native, + "mln_map_projection", + ) + } + .map_err(map_error)?; + Py::new( + py, + MapProjectionHandle { + state: Mutex::new(handle), + }, + ) + .map(|value| value.into_any()) + }, + ) } - fn set_style_url(&self, url: String) -> PyResult<()> { - let state = self.state(); + fn set_style_url(&self, url: String) -> PyResult> { let url = maplibre_core::string::c_string(&url).map_err(map_error)?; - // SAFETY: The C API validates that the pointer is a live map handle. - // url is a null-terminated C string whose storage lives for this call. - maplibre_core::check(unsafe { sys::mln_map_set_style_url(state.handle(), url.as_ptr()) }) - .map_err(map_error)?; - Ok(()) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_style_url(map, url.as_ptr(), out) + }) } - fn set_style_json(&self, json: &Bound<'_, PyBytes>) -> PyResult<()> { - let state = self.state(); + fn set_style_json(&self, json: &Bound<'_, PyBytes>) -> PyResult> { let json = maplibre_core::string::buffer_view(json.as_bytes()); - // SAFETY: The C API validates that the pointer is a live map handle. - // json is a null-terminated C string whose storage lives for this call. - maplibre_core::check(unsafe { sys::mln_map_set_style_json(state.handle(), json) }) - .map_err(map_error)?; - Ok(()) + self.run_style_command(|map, out| unsafe { sys::mln_map_set_style_json(map, json, out) }) } fn set_feature_state( @@ -1760,17 +1776,16 @@ impl MapHandle { feature_id: Option, state_key: Option, state_value: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let selector = feature_state_selector_from_parts(source_id, source_layer_id, feature_id, state_key)?; let selector = maplibre_core::query::feature_state_selector_to_native(&selector); let state_value = maplibre_core::string::buffer_view(state_value.as_bytes()); - // SAFETY: The C API validates the map pointer, selector, and JSON state. - maplibre_core::check(unsafe { - sys::mln_map_set_feature_state(state.handle(), selector.as_ptr(), state_value) + // SAFETY: The C API validates the map pointer, selector, and JSON + // state, and copies each borrowed buffer before returning. + self.run_style_command(|map, completion| unsafe { + sys::mln_map_set_feature_state(map, selector.as_ptr(), state_value, completion) }) - .map_err(map_error) } fn get_feature_state( @@ -1780,18 +1795,19 @@ impl MapHandle { source_layer_id: Option, feature_id: Option, state_key: Option, - ) -> PyResult> { - let state = self.state(); + ) -> PyResult> { let selector = feature_state_selector_from_parts(source_id, source_layer_id, feature_id, state_key)?; let selector = maplibre_core::query::feature_state_selector_to_native(&selector); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer, selector, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_feature_state(state.handle(), selector.as_ptr(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - owned_buffer_to_py(py, out.get()) + // SAFETY: The C API validates the map pointer and selector, and copies + // the borrowed selector strings before returning. + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_feature_state(map, selector.as_ptr(), completion) + }, + py_buffer, + ) } fn remove_feature_state( @@ -1800,51 +1816,72 @@ impl MapHandle { source_layer_id: Option, feature_id: Option, state_key: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let selector = feature_state_selector_from_parts(source_id, source_layer_id, feature_id, state_key)?; let selector = maplibre_core::query::feature_state_selector_to_native(&selector); - // SAFETY: The C API validates the map pointer and selector. - maplibre_core::check(unsafe { - sys::mln_map_remove_feature_state(state.handle(), selector.as_ptr()) + // SAFETY: The C API validates the map pointer and selector, and copies + // the borrowed selector strings before returning. + self.run_style_command(|map, completion| unsafe { + sys::mln_map_remove_feature_state(map, selector.as_ptr(), completion) }) - .map_err(map_error) } - fn copy_loaded_style_json(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: The C API validates the map pointer and each buffer and out - // pointer it is given. - unsafe { - copy_bytes(py, |text, capacity, out_size| { - sys::mln_map_copy_loaded_style_json(state.handle(), text, capacity, out_size) - }) - } + fn copy_loaded_style_json(&self, py: Python<'_>) -> PyResult> { + self.run_style_operation( + py, + |map, completion| unsafe { sys::mln_map_loaded_style_json(map, completion) }, + |py, result| { + let view = completion_value::(result)?; + let bytes = unsafe { maplibre_core::string::copy_string_view_bytes(view) } + .map_err(map_error)?; + Ok(PyBytes::new(py, &bytes).into_any().unbind()) + }, + ) } - fn copy_style_url(&self) -> PyResult { - let state = self.state(); - // SAFETY: The C API validates the map pointer and each buffer and out - // pointer it is given. - unsafe { - copy_text(|text, capacity, out_size| { - sys::mln_map_copy_style_url(state.handle(), text, capacity, out_size) - }) - } + fn copy_style_url(&self, py: Python<'_>) -> PyResult> { + self.run_style_operation( + py, + |map, completion| unsafe { sys::mln_map_style_url(map, completion) }, + |py, result| { + let view = completion_value::(result)?; + let bytes = unsafe { maplibre_core::string::copy_string_view_bytes(view) } + .map_err(map_error)?; + let text = String::from_utf8(bytes).map_err(|error| { + invalid_argument_error(format!("native text is not UTF-8: {error}")) + })?; + Ok(text.into_pyobject(py)?.into_any().unbind()) + }, + ) } fn get_camera(&self, py: Python<'_>) -> PyResult> { let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. let mut camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: The C API validates that the handle is live and - // camera points to initialized writable storage. - maplibre_core::check(unsafe { sys::mln_map_get_camera(state.handle(), &mut camera) }) - .map_err(map_error)?; + let mut generation = 0; + maplibre_core::check(unsafe { + sys::mln_map_camera_snapshot_get(state.handle(), &mut camera, &mut generation) + }) + .map_err(map_error)?; camera_options_to_py(py, &camera) } + fn get_camera_ordered(&self, py: Python<'_>) -> PyResult> { + let state = self.state(); + submit_python_future( + py, + |completion| unsafe { sys::mln_map_camera_query(state.handle(), completion) }, + |py, completion| { + let result = completion_value::(completion)?; + let raw = PyDict::new(py); + raw.set_item("generation", result.generation)?; + raw.set_item("camera", camera_options_to_py(py, &result.camera)?)?; + Ok(raw.into_any().unbind()) + }, + ) + } + #[allow(clippy::too_many_arguments)] fn jump_to( &self, @@ -1857,9 +1894,10 @@ impl MapHandle { anchor: Option<(f64, f64)>, roll: Option, field_of_view: Option, - ) -> PyResult<()> { - let state = self.state(); - let camera = camera_options_from_parts( + ) -> PyResult> { + let mut update = unsafe { sys::mln_camera_update_default() }; + update.mode = sys::MLN_CAMERA_UPDATE_MODE_JUMP; + update.camera = camera_options_from_parts( center, zoom, bearing, @@ -1870,9 +1908,9 @@ impl MapHandle { roll, field_of_view, ); - // SAFETY: The C API validates the map pointer and camera fields. - maplibre_core::check(unsafe { sys::mln_map_jump_to(state.handle(), &camera) }) - .map_err(map_error) + self.run_style_command(|map, completion| unsafe { + sys::mln_map_update_camera(map, &update, completion) + }) } #[allow(clippy::too_many_arguments)] @@ -1888,9 +1926,10 @@ impl MapHandle { roll: Option, field_of_view: Option, animation: Option, - ) -> PyResult<()> { - let state = self.state(); - let camera = camera_options_from_parts( + ) -> PyResult> { + let mut update = unsafe { sys::mln_camera_update_default() }; + update.mode = sys::MLN_CAMERA_UPDATE_MODE_EASE; + update.camera = camera_options_from_parts( center, zoom, bearing, @@ -1901,17 +1940,12 @@ impl MapHandle { roll, field_of_view, ); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, camera fields, and - // optional animation fields. - maplibre_core::check(unsafe { - sys::mln_map_ease_to( - state.handle(), - &camera, - optional_ref_ptr(animation.as_ref()), - ) + if let Some(animation) = animation { + update.animation = animation_options_from_parts(animation); + } + self.run_style_command(|map, completion| unsafe { + sys::mln_map_update_camera(map, &update, completion) }) - .map_err(map_error) } #[allow(clippy::too_many_arguments)] @@ -1927,9 +1961,10 @@ impl MapHandle { roll: Option, field_of_view: Option, animation: Option, - ) -> PyResult<()> { - let state = self.state(); - let camera = camera_options_from_parts( + ) -> PyResult> { + let mut update = unsafe { sys::mln_camera_update_default() }; + update.mode = sys::MLN_CAMERA_UPDATE_MODE_FLY; + update.camera = camera_options_from_parts( center, zoom, bearing, @@ -1940,17 +1975,36 @@ impl MapHandle { roll, field_of_view, ); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, camera fields, and - // optional animation fields. - maplibre_core::check(unsafe { - sys::mln_map_fly_to( - state.handle(), - &camera, - optional_ref_ptr(animation.as_ref()), - ) + if let Some(animation) = animation { + update.animation = animation_options_from_parts(animation); + } + self.run_style_command(|map, completion| unsafe { + sys::mln_map_update_camera(map, &update, completion) + }) + } + + fn apply_camera_delta( + &self, + kind: u32, + offset: (f64, f64), + amount: f64, + anchor: Option<(f64, f64)>, + animation: Option, + ) -> PyResult> { + let mut delta = unsafe { sys::mln_camera_delta_default() }; + delta.kind = kind; + delta.offset = screen_point_from_tuple(offset); + delta.amount = amount; + if let Some(anchor) = anchor { + delta.has_anchor = true; + delta.anchor = screen_point_from_tuple(anchor); + } + if let Some(animation) = animation { + delta.animation = animation_options_from_parts(animation); + } + self.run_style_command(|map, completion| unsafe { + sys::mln_map_apply_camera_delta(map, &delta, completion) }) - .map_err(map_error) } fn camera_for_lat_lng_bounds( @@ -1965,315 +2019,140 @@ impl MapHandle { let state = self.state(); let bounds = lat_lng_bounds_from_tuple((southwest, northeast)); let fit = camera_fit_options_from_parts(fit_padding, fit_bearing, fit_pitch); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: The C API validates the map pointer, bounds, fit options, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_lat_lng_bounds(state.handle(), bounds, &fit, &mut camera) - }) - .map_err(map_error)?; - camera_options_to_py(py, &camera) + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_camera_for_lat_lng_bounds(state.handle(), bounds, &fit, completion) + }, + |py, result| { + camera_options_to_py(py, &completion_value::(result)?) + }, + ) } fn camera_for_lat_lngs( &self, py: Python<'_>, coordinates: Vec<(f64, f64)>, - fit_padding: Option<(f64, f64, f64, f64)>, - fit_bearing: Option, - fit_pitch: Option, - ) -> PyResult> { - let state = self.state(); - let coordinates = lat_lngs_from_tuples(coordinates); - let fit = camera_fit_options_from_parts(fit_padding, fit_bearing, fit_pitch); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: The C API validates the map pointer, coordinate slice, fit options, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_lat_lngs( - state.handle(), - coordinates.as_ptr(), - coordinates.len(), - &fit, - &mut camera, - ) - }) - .map_err(map_error)?; - camera_options_to_py(py, &camera) - } - - fn camera_for_geometry( - &self, - py: Python<'_>, - geometry: &Bound<'_, PyBytes>, - fit_padding: Option<(f64, f64, f64, f64)>, - fit_bearing: Option, - fit_pitch: Option, - ) -> PyResult> { - let state = self.state(); - let geometry = maplibre_core::string::buffer_view(geometry.as_bytes()); - let fit = camera_fit_options_from_parts(fit_padding, fit_bearing, fit_pitch); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: The C API validates the map pointer, geometry, fit options, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_geometry(state.handle(), geometry, &fit, &mut camera) - }) - .map_err(map_error)?; - camera_options_to_py(py, &camera) - } - - #[allow(clippy::too_many_arguments)] - fn lat_lng_bounds_for_camera( - &self, - py: Python<'_>, - center: Option<(f64, f64)>, - zoom: Option, - bearing: Option, - pitch: Option, - center_altitude: Option, - padding: Option<(f64, f64, f64, f64)>, - anchor: Option<(f64, f64)>, - roll: Option, - field_of_view: Option, - unwrapped: bool, - ) -> PyResult> { - let state = self.state(); - let camera = camera_options_from_parts( - center, - zoom, - bearing, - pitch, - center_altitude, - padding, - anchor, - roll, - field_of_view, - ); - let mut bounds = empty_lat_lng_bounds(); - let status = if unwrapped { - // SAFETY: The C API validates the map pointer, camera options, and output pointer. - unsafe { - sys::mln_map_lat_lng_bounds_for_camera_unwrapped( - state.handle(), - &camera, - &mut bounds, - ) - } - } else { - // SAFETY: The C API validates the map pointer, camera options, and output pointer. - unsafe { sys::mln_map_lat_lng_bounds_for_camera(state.handle(), &camera, &mut bounds) } - }; - maplibre_core::check(status).map_err(map_error)?; - lat_lng_bounds_to_py(py, bounds) - } - - fn get_bounds(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut bounds = unsafe { sys::mln_bound_options_default() }; - // SAFETY: The C API validates the map pointer and output pointer. - maplibre_core::check(unsafe { sys::mln_map_get_bounds(state.handle(), &mut bounds) }) - .map_err(map_error)?; - bound_options_to_py(py, &bounds) - } - - fn set_bounds( - &self, - bounds: Option<((f64, f64), (f64, f64))>, - unbounded: bool, - min_zoom: Option, - max_zoom: Option, - min_pitch: Option, - max_pitch: Option, - ) -> PyResult<()> { - let state = self.state(); - let bounds = - bound_options_from_parts(bounds, unbounded, min_zoom, max_zoom, min_pitch, max_pitch); - // SAFETY: The C API validates the map pointer and bounds fields. - maplibre_core::check(unsafe { sys::mln_map_set_bounds(state.handle(), &bounds) }) - .map_err(map_error) - } - - fn move_by(&self, delta_x: f64, delta_y: f64) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer and delta values. - maplibre_core::check(unsafe { sys::mln_map_move_by(state.handle(), delta_x, delta_y) }) - .map_err(map_error) - } - - fn move_by_animated( - &self, - delta_x: f64, - delta_y: f64, - animation: Option, - ) -> PyResult<()> { - let state = self.state(); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, delta values, and - // optional animation fields. - maplibre_core::check(unsafe { - sys::mln_map_move_by_animated( - state.handle(), - delta_x, - delta_y, - optional_ref_ptr(animation.as_ref()), - ) - }) - .map_err(map_error) - } - - fn scale_by(&self, scale: f64, anchor: Option<(f64, f64)>) -> PyResult<()> { - let state = self.state(); - let anchor = anchor.map(screen_point_from_tuple); - // SAFETY: The C API validates the map pointer, scale, and optional anchor. - maplibre_core::check(unsafe { - sys::mln_map_scale_by(state.handle(), scale, optional_ref_ptr(anchor.as_ref())) - }) - .map_err(map_error) - } - - fn scale_by_animated( - &self, - scale: f64, - anchor: Option<(f64, f64)>, - animation: Option, - ) -> PyResult<()> { - let state = self.state(); - let anchor = anchor.map(screen_point_from_tuple); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, scale, optional anchor, - // and optional animation fields. - maplibre_core::check(unsafe { - sys::mln_map_scale_by_animated( - state.handle(), - scale, - optional_ref_ptr(anchor.as_ref()), - optional_ref_ptr(animation.as_ref()), - ) - }) - .map_err(map_error) - } - - fn rotate_by(&self, first: (f64, f64), second: (f64, f64)) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer and points. - maplibre_core::check(unsafe { - sys::mln_map_rotate_by( - state.handle(), - screen_point_from_tuple(first), - screen_point_from_tuple(second), - ) - }) - .map_err(map_error) - } - - fn rotate_by_animated( - &self, - first: (f64, f64), - second: (f64, f64), - animation: Option, - ) -> PyResult<()> { - let state = self.state(); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, points, and optional - // animation fields. - maplibre_core::check(unsafe { - sys::mln_map_rotate_by_animated( - state.handle(), - screen_point_from_tuple(first), - screen_point_from_tuple(second), - optional_ref_ptr(animation.as_ref()), - ) - }) - .map_err(map_error) - } - - fn pitch_by(&self, pitch: f64) -> PyResult<()> { - let state = self.state(); - // SAFETY: The C API validates the map pointer and pitch value. - maplibre_core::check(unsafe { sys::mln_map_pitch_by(state.handle(), pitch) }) - .map_err(map_error) - } - - fn pitch_by_animated(&self, pitch: f64, animation: Option) -> PyResult<()> { - let state = self.state(); - let animation = animation.map(animation_options_from_parts); - // SAFETY: The C API validates the map pointer, pitch value, and optional - // animation fields. - maplibre_core::check(unsafe { - sys::mln_map_pitch_by_animated( - state.handle(), - pitch, - optional_ref_ptr(animation.as_ref()), - ) - }) - .map_err(map_error) - } - - fn cancel_transitions(&self) -> PyResult<()> { + fit_padding: Option<(f64, f64, f64, f64)>, + fit_bearing: Option, + fit_pitch: Option, + ) -> PyResult> { let state = self.state(); - // SAFETY: The C API validates the map pointer. - maplibre_core::check(unsafe { sys::mln_map_cancel_transitions(state.handle()) }) - .map_err(map_error) + let coordinates = lat_lngs_from_tuples(coordinates); + let fit = camera_fit_options_from_parts(fit_padding, fit_bearing, fit_pitch); + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_camera_for_lat_lngs( + state.handle(), + coordinates.as_ptr(), + coordinates.len(), + &fit, + completion, + ) + }, + |py, result| { + camera_options_to_py(py, &completion_value::(result)?) + }, + ) } - fn set_gesture_in_progress(&self, in_progress: bool) -> PyResult<()> { + fn camera_for_geometry( + &self, + py: Python<'_>, + geometry: &Bound<'_, PyBytes>, + fit_padding: Option<(f64, f64, f64, f64)>, + fit_bearing: Option, + fit_pitch: Option, + ) -> PyResult> { let state = self.state(); - // SAFETY: The C API validates the map pointer and owner-thread affinity. - maplibre_core::check(unsafe { - sys::mln_map_set_gesture_in_progress(state.handle(), in_progress) - }) - .map_err(map_error) + let geometry = maplibre_core::string::buffer_view(geometry.as_bytes()); + let fit = camera_fit_options_from_parts(fit_padding, fit_bearing, fit_pitch); + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_camera_for_geometry(state.handle(), geometry, &fit, completion) + }, + |py, result| { + camera_options_to_py(py, &completion_value::(result)?) + }, + ) } - fn is_gesture_in_progress(&self) -> PyResult { + #[allow(clippy::too_many_arguments)] + fn lat_lng_bounds_for_camera( + &self, + py: Python<'_>, + center: Option<(f64, f64)>, + zoom: Option, + bearing: Option, + pitch: Option, + center_altitude: Option, + padding: Option<(f64, f64, f64, f64)>, + anchor: Option<(f64, f64)>, + roll: Option, + field_of_view: Option, + unwrapped: bool, + ) -> PyResult> { let state = self.state(); - let mut in_progress = false; - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_is_gesture_in_progress(state.handle(), &mut in_progress) - }) - .map_err(map_error)?; - Ok(in_progress) + let camera = camera_options_from_parts( + center, + zoom, + bearing, + pitch, + center_altitude, + padding, + anchor, + roll, + field_of_view, + ); + submit_python_future( + py, + |completion| { + if unwrapped { + unsafe { + sys::mln_map_lat_lng_bounds_for_camera_unwrapped( + state.handle(), + &camera, + completion, + ) + } + } else { + unsafe { + sys::mln_map_lat_lng_bounds_for_camera(state.handle(), &camera, completion) + } + } + }, + |py, result| { + lat_lng_bounds_to_py(py, completion_value::(result)?) + }, + ) } - fn get_free_camera_options(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut options = unsafe { sys::mln_free_camera_options_default() }; - // SAFETY: The C API validates the map pointer and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_free_camera_options(state.handle(), &mut options) - }) - .map_err(map_error)?; - free_camera_options_to_py(py, &options) + fn set_bounds( + &self, + bounds: Option<((f64, f64), (f64, f64))>, + unbounded: bool, + min_zoom: Option, + max_zoom: Option, + min_pitch: Option, + max_pitch: Option, + ) -> PyResult> { + let bounds = + bound_options_from_parts(bounds, unbounded, min_zoom, max_zoom, min_pitch, max_pitch); + self.run_style_command(|map, out| unsafe { sys::mln_map_set_bounds(map, &bounds, out) }) } fn set_free_camera_options( &self, position: Option<(f64, f64, f64)>, orientation: Option<(f64, f64, f64, f64)>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let options = free_camera_options_from_parts(position, orientation); - // SAFETY: The C API validates the map pointer and free camera fields. - maplibre_core::check(unsafe { - sys::mln_map_set_free_camera_options(state.handle(), &options) - }) - .map_err(map_error) - } - - fn get_projection_mode(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut mode = unsafe { sys::mln_projection_mode_default() }; - // SAFETY: The C API validates the map pointer and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_projection_mode(state.handle(), &mut mode) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_free_camera_options(map, &options, out) }) - .map_err(map_error)?; - projection_mode_to_py(py, &mode) } fn set_projection_mode( @@ -2281,12 +2160,11 @@ impl MapHandle { axonometric: Option, x_skew: Option, y_skew: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let mode = projection_mode_from_parts(axonometric, x_skew, y_skew); - // SAFETY: The C API validates the map pointer and projection mode fields. - maplibre_core::check(unsafe { sys::mln_map_set_projection_mode(state.handle(), &mode) }) - .map_err(map_error) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_projection_mode(map, &mode, out) + }) } fn pixel_for_lat_lng( @@ -2296,38 +2174,35 @@ impl MapHandle { longitude: f64, ) -> PyResult> { let state = self.state(); - let mut point = sys::mln_screen_point { x: 0.0, y: 0.0 }; - // SAFETY: The C API validates the map pointer, coordinate, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_pixel_for_lat_lng( - state.handle(), - sys::mln_lat_lng { - latitude, - longitude, - }, - &mut point, - ) - }) - .map_err(map_error)?; - screen_point_to_py(py, point) + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_pixel_for_lat_lng( + state.handle(), + sys::mln_lat_lng { + latitude, + longitude, + }, + completion, + ) + }, + |py, result| screen_point_to_py(py, completion_value::(result)?), + ) } fn lat_lng_for_pixel(&self, py: Python<'_>, x: f64, y: f64) -> PyResult> { let state = self.state(); - let mut coordinate = sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0, - }; - // SAFETY: The C API validates the map pointer, point, and output pointer. - maplibre_core::check(unsafe { - sys::mln_map_lat_lng_for_pixel( - state.handle(), - sys::mln_screen_point { x, y }, - &mut coordinate, - ) - }) - .map_err(map_error)?; - lat_lng_to_py(py, coordinate) + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_lat_lng_for_pixel( + state.handle(), + sys::mln_screen_point { x, y }, + completion, + ) + }, + |py, result| lat_lng_to_py(py, completion_value::(result)?), + ) } fn pixels_for_lat_lngs( @@ -2337,57 +2212,47 @@ impl MapHandle { ) -> PyResult> { let state = self.state(); let coordinates = lat_lngs_from_tuples(coordinates); - let mut points = vec![sys::mln_screen_point { x: 0.0, y: 0.0 }; coordinates.len()]; - // SAFETY: The C API validates the map pointer and coordinate/output slices. - maplibre_core::check(unsafe { - sys::mln_map_pixels_for_lat_lngs( - state.handle(), - coordinates.as_ptr(), - coordinates.len(), - points.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - screen_point_list_to_py(py, &points) + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_pixels_for_lat_lngs( + state.handle(), + coordinates.as_ptr(), + coordinates.len(), + completion, + ) + }, + |py, result| screen_point_list_to_py(py, &completion_slice(result)?), + ) } fn lat_lngs_for_pixels(&self, py: Python<'_>, points: Vec<(f64, f64)>) -> PyResult> { let state = self.state(); let points: Vec<_> = points.into_iter().map(screen_point_from_tuple).collect(); - let mut coordinates = vec![ - sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0 - }; - points.len() - ]; - // SAFETY: The C API validates the map pointer and point/output slices. - maplibre_core::check(unsafe { - sys::mln_map_lat_lngs_for_pixels( - state.handle(), - points.as_ptr(), - points.len(), - coordinates.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - lat_lng_list_to_py(py, &coordinates) + submit_python_future( + py, + |completion| unsafe { + sys::mln_map_lat_lngs_for_pixels( + state.handle(), + points.as_ptr(), + points.len(), + completion, + ) + }, + |py, result| lat_lng_list_to_py(py, &completion_slice(result)?), + ) } - #[allow(clippy::too_many_arguments)] fn add_style_source_json( &self, source_id: String, source_json: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let source_json = maplibre_core::string::buffer_view(source_json.as_bytes()); - // SAFETY: The C API validates the map pointer, source ID, and JSON buffer view. - maplibre_core::check(unsafe { - sys::mln_map_add_style_source_json(state.handle(), source_id.raw(), source_json) + self.run_style_command(|map, out| unsafe { + sys::mln_map_add_style_source_json(map, source_id.raw(), source_json, out) }) - .map_err(map_error) } #[allow(clippy::too_many_arguments)] @@ -2407,8 +2272,7 @@ impl MapHandle { line_metrics: Option, cluster: Option, synchronous_tiling: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let url = maplibre_core::string::string_view(&url); let options = geojson_source_options_from_parts( @@ -2428,23 +2292,22 @@ impl MapHandle { let options = maplibre_core::style::geojson_source_options_to_native(&options).map_err(map_error)?; // SAFETY: The C API validates the map pointer, borrowed string views, and options. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_geojson_source_url( - state.handle(), + map, source_id.raw(), url.raw(), options.as_ptr(), + out, ) }) - .map_err(map_error) } fn add_geojson_source_data( &self, source_id: String, data: &GeoJsonSourceDataHandle, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let data_state = data.state(); let Some(data_handle) = data_state.live_handle() else { return Err(invalid_state_error("GeoJSON source data handle is closed")); @@ -2453,29 +2316,25 @@ impl MapHandle { // SAFETY: The C API validates the map pointer, source ID, and data // handle. The call borrows the data handle, which the held state mutex // keeps live for the duration of the call. - maplibre_core::check(unsafe { - sys::mln_map_add_geojson_source_data(state.handle(), source_id.raw(), data_handle) + self.run_style_command(|map, out| unsafe { + sys::mln_map_add_geojson_source_data(map, source_id.raw(), data_handle, out) }) - .map_err(map_error) } - fn set_geojson_source_url(&self, source_id: String, url: String) -> PyResult<()> { - let state = self.state(); + fn set_geojson_source_url(&self, source_id: String, url: String) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let url = maplibre_core::string::string_view(&url); // SAFETY: The C API validates the map pointer and borrowed string views. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_url(state.handle(), source_id.raw(), url.raw()) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_geojson_source_url(map, source_id.raw(), url.raw(), out) }) - .map_err(map_error) } fn set_geojson_source_data( &self, source_id: String, data: &GeoJsonSourceDataHandle, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let data_state = data.state(); let Some(data_handle) = data_state.live_handle() else { return Err(invalid_state_error("GeoJSON source data handle is closed")); @@ -2484,28 +2343,21 @@ impl MapHandle { // SAFETY: The C API validates the map pointer, source ID, and data // handle. The call borrows the data handle, which the held state mutex // keeps live for the duration of the call. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_data(state.handle(), source_id.raw(), data_handle) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_geojson_source_data(map, source_id.raw(), data_handle, out) }) - .map_err(map_error) } fn set_geojson_source_synchronous_tiling( &self, source_id: String, enabled: bool, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); // SAFETY: The C API validates the map pointer and source ID. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_synchronous_tiling( - state.handle(), - source_id.raw(), - enabled, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_geojson_source_synchronous_tiling(map, source_id.raw(), enabled, out) }) - .map_err(map_error) } #[allow(clippy::too_many_arguments)] @@ -2521,7 +2373,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_url_with( source_id, url, @@ -2550,7 +2402,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_url_with( source_id, url, @@ -2579,7 +2431,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_url_with( source_id, url, @@ -2608,7 +2460,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_tiles_with( source_id, tiles, @@ -2637,7 +2489,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_tiles_with( source_id, tiles, @@ -2666,7 +2518,7 @@ impl MapHandle { tile_size: Option, vector_encoding: Option, raster_dem_encoding: Option, - ) -> PyResult<()> { + ) -> PyResult> { self.add_tile_source_tiles_with( source_id, tiles, @@ -2682,154 +2534,30 @@ impl MapHandle { ) } - fn remove_style_source(&self, source_id: String) -> PyResult { - let state = self.state(); - let source_id = maplibre_core::string::string_view(&source_id); - let mut removed = false; - // SAFETY: The C API validates the map pointer, source ID view, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_remove_style_source(state.handle(), source_id.raw(), &mut removed) - }) - .map_err(map_error)?; - Ok(removed) - } - - fn style_source_exists(&self, source_id: String) -> PyResult { - self.string_bool_call_with(source_id, sys::mln_map_style_source_exists) - } - - fn get_style_source_type(&self, source_id: String) -> PyResult> { - let state = self.state(); - let source_id = maplibre_core::string::string_view(&source_id); - let mut source_type = 0; - let mut found = false; - // SAFETY: The C API validates the map pointer, source ID view, and out pointers. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_type( - state.handle(), - source_id.raw(), - &mut source_type, - &mut found, - ) - }) - .map_err(map_error)?; - Ok(found.then_some(source_type)) - } - - fn get_style_source_info( - &self, - py: Python<'_>, - source_id: String, - ) -> PyResult>> { - let state = self.state(); + fn remove_style_source(&self, source_id: String) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); - let mut info = maplibre_core::style::empty_style_source_info(); - let mut found = false; - // SAFETY: The C API validates the map pointer, source ID view, info, and found pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_info( - state.handle(), - source_id.raw(), - &mut info, - &mut found, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_remove_style_source(map, source_id.raw(), out) }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - let attribution = if info.has_attribution { - let mut bytes = vec![0u8; info.attribution_size]; - let mut attribution_size = 0; - let mut attribution_found = false; - // SAFETY: bytes is writable for attribution_size bytes and retained - // for this call. The C API validates all pointers. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_source_attribution( - state.handle(), - source_id.raw(), - bytes.as_mut_ptr().cast::(), - bytes.len(), - &mut attribution_size, - &mut attribution_found, - ) - }) - .map_err(map_error)?; - if attribution_found { - bytes.truncate(attribution_size); - Some(String::from_utf8(bytes).map_err(|error| { - invalid_argument_error(format!("native attribution is not UTF-8: {error}")) - })?) - } else { - None - } - } else { - None - }; - let url = if info.fields & sys::MLN_STYLE_SOURCE_INFO_URL != 0 { - let mut bytes = vec![0u8; info.url_size]; - let mut url_size = 0; - let mut url_found = false; - // SAFETY: bytes and both output values remain writable for this call. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_source_url( - state.handle(), - source_id.raw(), - bytes.as_mut_ptr().cast::(), - bytes.len(), - &mut url_size, - &mut url_found, - ) - }) - .map_err(map_error)?; - if !url_found { - return Ok(None); - } - bytes.truncate(url_size); - Some(String::from_utf8(bytes).map_err(|error| { - invalid_argument_error(format!("native source URL is not UTF-8: {error}")) - })?) - } else { - None - }; - let tiles = if info.fields & sys::MLN_STYLE_SOURCE_INFO_TILEJSON != 0 { - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut tiles_found = false; - // SAFETY: out starts null and both output values remain writable for this call. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_tile_urls( - state.handle(), - source_id.raw(), - out.as_mut_ptr(), - &mut tiles_found, - ) - }) - .map_err(map_error)?; - if !tiles_found { - return Ok(None); - } - let native = out.into_live("mln_style_string_list").map_err(map_error)?; - // SAFETY: native is an owned style string list returned by C. - unsafe { maplibre_core::style::copy_style_string_list(native) }.map_err(map_error)? - } else { - Vec::new() - }; - let copied = - maplibre_core::style::style_source_info_from_native(&info, attribution, url, tiles); - source_info_to_py(py, copied).map(Some) } - fn list_style_source_ids(&self) -> PyResult> { - let state = self.state(); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_list_style_source_ids(state.handle(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - let native = out.into_live("mln_style_id_list").map_err(map_error)?; - // SAFETY: native is an owned style ID list returned by native. - unsafe { maplibre_core::style::copy_style_id_list(native) }.map_err(map_error) + fn get_style_source_info(&self, py: Python<'_>, source_id: String) -> PyResult> { + let source_id_view = maplibre_core::string::string_view(&source_id); + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_source_info(map, source_id_view.raw(), completion) + }, + py_source_info, + ) + } + + fn list_style_source_ids(&self, py: Python<'_>) -> PyResult> { + self.run_style_operation( + py, + |map, completion| unsafe { sys::mln_map_list_style_source_ids(map, completion) }, + py_string_list, + ) } fn add_hillshade_layer( @@ -2837,22 +2565,21 @@ impl MapHandle { layer_id: String, source_id: String, before_layer_id: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let source_id = maplibre_core::string::string_view(&source_id); let before_layer_id = before_layer_id.unwrap_or_default(); let before_layer_id = maplibre_core::string::string_view(&before_layer_id); // SAFETY: The C API validates the map pointer and borrowed layer/source ID views. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_hillshade_layer( - state.handle(), + map, layer_id.raw(), source_id.raw(), before_layer_id.raw(), + out, ) }) - .map_err(map_error) } fn add_color_relief_layer( @@ -2860,42 +2587,40 @@ impl MapHandle { layer_id: String, source_id: String, before_layer_id: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let source_id = maplibre_core::string::string_view(&source_id); let before_layer_id = before_layer_id.unwrap_or_default(); let before_layer_id = maplibre_core::string::string_view(&before_layer_id); // SAFETY: The C API validates the map pointer and borrowed layer/source ID views. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_color_relief_layer( - state.handle(), + map, layer_id.raw(), source_id.raw(), before_layer_id.raw(), + out, ) }) - .map_err(map_error) } fn add_location_indicator_layer( &self, layer_id: String, before_layer_id: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let before_layer_id = before_layer_id.unwrap_or_default(); let before_layer_id = maplibre_core::string::string_view(&before_layer_id); // SAFETY: The C API validates the map pointer and borrowed layer ID views. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_location_indicator_layer( - state.handle(), + map, layer_id.raw(), before_layer_id.raw(), + out, ) }) - .map_err(map_error) } fn set_location_indicator_location( @@ -2904,50 +2629,45 @@ impl MapHandle { latitude: f64, longitude: f64, altitude: f64, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); // SAFETY: The C API validates the map pointer, layer ID, coordinate, and altitude. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_location_indicator_location( - state.handle(), + map, layer_id.raw(), sys::mln_lat_lng { latitude, longitude, }, altitude, + out, ) }) - .map_err(map_error) } - fn set_location_indicator_bearing(&self, layer_id: String, bearing: f64) -> PyResult<()> { - let state = self.state(); + fn set_location_indicator_bearing( + &self, + layer_id: String, + bearing: f64, + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); // SAFETY: The C API validates the map pointer, layer ID, and bearing. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_bearing(state.handle(), layer_id.raw(), bearing) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_location_indicator_bearing(map, layer_id.raw(), bearing, out) }) - .map_err(map_error) } fn set_location_indicator_accuracy_radius( &self, layer_id: String, radius: f64, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); // SAFETY: The C API validates the map pointer, layer ID, and radius. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_accuracy_radius( - state.handle(), - layer_id.raw(), - radius, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_location_indicator_accuracy_radius(map, layer_id.raw(), radius, out) }) - .map_err(map_error) } fn set_location_indicator_image_name( @@ -2955,108 +2675,77 @@ impl MapHandle { layer_id: String, image_kind: u32, image_id: String, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let image_id = maplibre_core::string::string_view(&image_id); // SAFETY: The C API validates the map pointer, layer ID, image kind, and image ID. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_location_indicator_image_name( - state.handle(), + map, layer_id.raw(), image_kind, image_id.raw(), + out, ) }) - .map_err(map_error) } fn add_style_layer_json( &self, layer_json: &Bound<'_, PyBytes>, before_layer_id: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_json = maplibre_core::string::buffer_view(layer_json.as_bytes()); let before_layer_id = before_layer_id.unwrap_or_default(); let before_layer_id = maplibre_core::string::string_view(&before_layer_id); - // SAFETY: The C API validates the map pointer, JSON buffer view, and before-layer ID. - maplibre_core::check(unsafe { - sys::mln_map_add_style_layer_json(state.handle(), layer_json, before_layer_id.raw()) + self.run_style_command(|map, out| unsafe { + sys::mln_map_add_style_layer_json(map, layer_json, before_layer_id.raw(), out) }) - .map_err(map_error) } - fn get_style_layer_json( - &self, - py: Python<'_>, - layer_id: String, - ) -> PyResult>> { - let state = self.state(); + fn get_style_layer_json(&self, py: Python<'_>, layer_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut found = false; - // SAFETY: The C API validates the map pointer, layer ID, out pointer, and found pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_layer_json( - state.handle(), - layer_id.raw(), - out.as_mut_ptr(), - &mut found, - ) - }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - owned_buffer_to_py(py, out.get()).map(Some) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_layer_json(map, layer_id.raw(), completion) + }, + py_optional_buffer, + ) } - fn set_style_light_json(&self, light_json: &Bound<'_, PyBytes>) -> PyResult<()> { - let state = self.state(); + fn set_style_light_json(&self, light_json: &Bound<'_, PyBytes>) -> PyResult> { let light_json = maplibre_core::string::buffer_view(light_json.as_bytes()); - // SAFETY: The C API validates the map pointer and JSON buffer view. - maplibre_core::check(unsafe { - sys::mln_map_set_style_light_json(state.handle(), light_json) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_style_light_json(map, light_json, out) }) - .map_err(map_error) } fn set_style_light_property( &self, property_name: String, value: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let property_name = maplibre_core::string::string_view(&property_name); let value = maplibre_core::string::buffer_view(value.as_bytes()); - // SAFETY: The C API validates the map pointer, property name, and JSON buffer view. - maplibre_core::check(unsafe { - sys::mln_map_set_style_light_property(state.handle(), property_name.raw(), value) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_style_light_property(map, property_name.raw(), value, out) }) - .map_err(map_error) } fn get_style_light_property( &self, py: Python<'_>, property_name: String, - ) -> PyResult>> { - let state = self.state(); + ) -> PyResult> { let property_name = maplibre_core::string::string_view(&property_name); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer, property name, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_light_property( - state.handle(), - property_name.raw(), - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - out.into_option() - .map(|buffer| owned_buffer_to_py(py, buffer)) - .transpose() + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_light_property(map, property_name.raw(), completion) + }, + py_optional_buffer, + ) } fn set_style_transition_options( @@ -3064,37 +2753,34 @@ impl MapHandle { duration_ms: Option, delay_ms: Option, enable_placement_transitions: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let mut options = maplibre_core::StyleTransitionOptions::default(); options.duration_ms = duration_ms; options.delay_ms = delay_ms; options.enable_placement_transitions = enable_placement_transitions; let options = maplibre_core::style::style_transition_options_to_native(&options); - // SAFETY: The C API validates the map pointer and options struct. - maplibre_core::check(unsafe { - sys::mln_map_set_style_transition_options(state.handle(), &options) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_style_transition_options(map, &options, out) }) - .map_err(map_error) } fn get_style_transition_options(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - let mut options = maplibre_core::style::empty_style_transition_options(); - // SAFETY: The C API validates the map pointer and out-options pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_transition_options(state.handle(), &mut options) - }) - .map_err(map_error)?; - let options = maplibre_core::style::style_transition_options_from_native(&options); - let dict = PyDict::new(py); - dict.set_item("duration_ms", options.duration_ms)?; - dict.set_item("delay_ms", options.delay_ms)?; - dict.set_item( - "enable_placement_transitions", - options.enable_placement_transitions, - )?; - Ok(dict.into_any().unbind()) + self.run_style_operation( + py, + |map, completion| unsafe { sys::mln_map_get_style_transition_options(map, completion) }, + |py, result| { + let raw = completion_value::(result)?; + let options = maplibre_core::style::style_transition_options_from_native(&raw); + let dict = PyDict::new(py); + dict.set_item("duration_ms", options.duration_ms)?; + dict.set_item("delay_ms", options.delay_ms)?; + dict.set_item( + "enable_placement_transitions", + options.enable_placement_transitions, + )?; + Ok(dict.into_any().unbind()) + }, + ) } fn set_layer_property( @@ -3102,21 +2788,13 @@ impl MapHandle { layer_id: String, property_name: String, value: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let property_name = maplibre_core::string::string_view(&property_name); let value = maplibre_core::string::buffer_view(value.as_bytes()); - // SAFETY: The C API validates the map pointer, layer/property names, and JSON buffer view. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_property( - state.handle(), - layer_id.raw(), - property_name.raw(), - value, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_property(map, layer_id.raw(), property_name.raw(), value, out) }) - .map_err(map_error) } fn get_layer_property( @@ -3124,241 +2802,151 @@ impl MapHandle { py: Python<'_>, layer_id: String, property_name: String, - ) -> PyResult>> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let property_name = maplibre_core::string::string_view(&property_name); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer, layer/property names, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_property( - state.handle(), - layer_id.raw(), - property_name.raw(), - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - out.into_option() - .map(|buffer| owned_buffer_to_py(py, buffer)) - .transpose() - } - - fn set_layer_source_layer(&self, layer_id: String, source_layer: String) -> PyResult<()> { - let state = self.state(); - let layer_id = maplibre_core::string::string_view(&layer_id); - let source_layer = maplibre_core::string::string_view(&source_layer); - // SAFETY: The C API validates the map pointer and both string views. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_source_layer(state.handle(), layer_id.raw(), source_layer.raw()) - }) - .map_err(map_error) - } - - fn copy_layer_source_layer(&self, layer_id: String) -> PyResult { - let state = self.state(); - let layer_id = maplibre_core::string::string_view(&layer_id); - // SAFETY: The C API validates the map pointer, layer ID, and each buffer - // and out pointer it is given. - unsafe { - copy_text(|text, capacity, out_size| { - sys::mln_map_copy_layer_source_layer( - state.handle(), + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_layer_property( + map, layer_id.raw(), - text, - capacity, - out_size, + property_name.raw(), + completion, ) - }) - } + }, + py_optional_buffer, + ) } - fn set_layer_source_id(&self, layer_id: String, source_id: String) -> PyResult<()> { - let state = self.state(); + fn set_layer_source_layer( + &self, + layer_id: String, + source_layer: String, + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let source_id = maplibre_core::string::string_view(&source_id); - // SAFETY: The C API validates the map pointer and both string views. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_source_id(state.handle(), layer_id.raw(), source_id.raw()) + let source_layer = maplibre_core::string::string_view(&source_layer); + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_source_layer(map, layer_id.raw(), source_layer.raw(), out) }) - .map_err(map_error) - } - - fn copy_layer_source_id(&self, layer_id: String) -> PyResult { - let state = self.state(); - let layer_id = maplibre_core::string::string_view(&layer_id); - // SAFETY: The C API validates the map pointer, layer ID, and each buffer - // and out pointer it is given. - unsafe { - copy_text(|text, capacity, out_size| { - sys::mln_map_copy_layer_source_id( - state.handle(), - layer_id.raw(), - text, - capacity, - out_size, - ) - }) - } } - fn set_layer_min_zoom(&self, layer_id: String, min_zoom: f64) -> PyResult<()> { - let state = self.state(); + fn copy_layer_source_layer(&self, py: Python<'_>, layer_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - // SAFETY: The C API validates the map pointer and layer ID. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_min_zoom(state.handle(), layer_id.raw(), min_zoom) - }) - .map_err(map_error) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_copy_layer_source_layer(map, layer_id.raw(), completion) + }, + py_string, + ) } - fn get_layer_min_zoom(&self, layer_id: String) -> PyResult { - let state = self.state(); + fn set_layer_source_id(&self, layer_id: String, source_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut min_zoom = 0.0; - // SAFETY: The C API validates the map pointer, layer ID, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_min_zoom(state.handle(), layer_id.raw(), &mut min_zoom) + let source_id = maplibre_core::string::string_view(&source_id); + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_source_id(map, layer_id.raw(), source_id.raw(), out) }) - .map_err(map_error)?; - Ok(min_zoom) } - fn set_layer_max_zoom(&self, layer_id: String, max_zoom: f64) -> PyResult<()> { - let state = self.state(); + fn copy_layer_source_id(&self, py: Python<'_>, layer_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - // SAFETY: The C API validates the map pointer and layer ID. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_max_zoom(state.handle(), layer_id.raw(), max_zoom) - }) - .map_err(map_error) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_copy_layer_source_id(map, layer_id.raw(), completion) + }, + py_string, + ) } - fn get_layer_max_zoom(&self, layer_id: String) -> PyResult { - let state = self.state(); + fn set_layer_min_zoom(&self, layer_id: String, min_zoom: f64) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut max_zoom = 0.0; - // SAFETY: The C API validates the map pointer, layer ID, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_max_zoom(state.handle(), layer_id.raw(), &mut max_zoom) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_min_zoom(map, layer_id.raw(), min_zoom, out) }) - .map_err(map_error)?; - Ok(max_zoom) } - fn set_layer_visibility(&self, layer_id: String, visibility: u32) -> PyResult<()> { - let state = self.state(); + fn set_layer_max_zoom(&self, layer_id: String, max_zoom: f64) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - // SAFETY: The C API validates the map pointer, layer ID, and enum value. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_visibility(state.handle(), layer_id.raw(), visibility) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_max_zoom(map, layer_id.raw(), max_zoom, out) }) - .map_err(map_error) } - fn get_layer_visibility(&self, layer_id: String) -> PyResult { - let state = self.state(); + fn set_layer_visibility(&self, layer_id: String, visibility: u32) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut visibility = 0; - // SAFETY: The C API validates the map pointer, layer ID, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_visibility(state.handle(), layer_id.raw(), &mut visibility) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_layer_visibility(map, layer_id.raw(), visibility, out) }) - .map_err(map_error)?; - Ok(visibility) } fn set_layer_filter( &self, layer_id: String, filter: Option<&Bound<'_, PyBytes>>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let filter = filter.map(|value| maplibre_core::string::buffer_view(value.as_bytes())); - // SAFETY: The C API validates the map pointer, layer ID, and optional JSON buffer view. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_layer_filter( - state.handle(), + map, layer_id.raw(), optional_ref_ptr(filter.as_ref()), + out, ) }) - .map_err(map_error) } - fn get_layer_filter(&self, py: Python<'_>, layer_id: String) -> PyResult>> { - let state = self.state(); + fn get_layer_filter(&self, py: Python<'_>, layer_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer, layer ID, and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_filter(state.handle(), layer_id.raw(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - out.into_option() - .map(|buffer| owned_buffer_to_py(py, buffer)) - .transpose() - } - - fn remove_style_layer(&self, layer_id: String) -> PyResult { - self.string_bool_call_with(layer_id, sys::mln_map_remove_style_layer) - } - - fn style_layer_exists(&self, layer_id: String) -> PyResult { - self.string_bool_call_with(layer_id, sys::mln_map_style_layer_exists) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_layer_filter(map, layer_id.raw(), completion) + }, + py_optional_buffer, + ) } - fn get_style_layer_type(&self, layer_id: String) -> PyResult> { - let state = self.state(); + fn remove_style_layer(&self, layer_id: String) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); - let mut layer_type = sys::mln_buffer_view { - data: ptr::null(), - size: 0, - }; - let mut found = false; - // SAFETY: The C API validates the map pointer, layer ID view, and out pointers. - maplibre_core::check(unsafe { - sys::mln_map_get_style_layer_type( - state.handle(), - layer_id.raw(), - &mut layer_type, - &mut found, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_remove_style_layer(map, layer_id.raw(), out) }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - // SAFETY: The returned layer type view is static/live for this call and copied immediately. - unsafe { maplibre_core::string::copy_string_view(layer_type) } - .map(Some) - .map_err(map_error) } - fn list_style_layer_ids(&self) -> PyResult> { - let state = self.state(); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the map pointer and out pointer. - maplibre_core::check(unsafe { - sys::mln_map_list_style_layer_ids(state.handle(), out.as_mut_ptr()) - }) - .map_err(map_error)?; - let native = out.into_live("mln_style_id_list").map_err(map_error)?; - // SAFETY: native is an owned style ID list returned by native. - unsafe { maplibre_core::style::copy_style_id_list(native) }.map_err(map_error) + fn get_style_layer_info(&self, py: Python<'_>, layer_id: String) -> PyResult> { + let layer_id_view = maplibre_core::string::string_view(&layer_id); + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_layer_info(map, layer_id_view.raw(), completion) + }, + py_layer_info, + ) } - fn move_style_layer(&self, layer_id: String, before_layer_id: Option) -> PyResult<()> { - let state = self.state(); + fn list_style_layer_ids(&self, py: Python<'_>) -> PyResult> { + self.run_style_operation( + py, + |map, completion| unsafe { sys::mln_map_list_style_layer_ids(map, completion) }, + py_string_list, + ) + } + + fn move_style_layer( + &self, + layer_id: String, + before_layer_id: Option, + ) -> PyResult> { let layer_id = maplibre_core::string::string_view(&layer_id); let before_layer_id = before_layer_id.unwrap_or_default(); let before_layer_id = maplibre_core::string::string_view(&before_layer_id); - // SAFETY: The C API validates the map pointer and borrowed layer ID views. - maplibre_core::check(unsafe { - sys::mln_map_move_style_layer(state.handle(), layer_id.raw(), before_layer_id.raw()) + self.run_style_command(|map, out| unsafe { + sys::mln_map_move_style_layer(map, layer_id.raw(), before_layer_id.raw(), out) }) - .map_err(map_error) } #[allow(clippy::too_many_arguments)] @@ -3376,8 +2964,7 @@ impl MapHandle { content: Option<(f32, f32, f32, f32)>, text_fit_width: Option, text_fit_height: Option, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let image_id = maplibre_core::string::string_view(&image_id); let mut image = unsafe { sys::mln_premultiplied_rgba8_image_default() }; image.width = width; @@ -3398,133 +2985,53 @@ impl MapHandle { options.stretch_y = native_stretch_y.as_ptr(); // SAFETY: The C API validates the map pointer, image ID, image descriptor, // options, and pixel storage. pixels is retained for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_style_image(state.handle(), image_id.raw(), &image, &options) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_style_image(map, image_id.raw(), &image, &options, out) }) - .map_err(map_error) - } - - fn remove_style_image(&self, image_id: String) -> PyResult { - self.string_bool_call_with(image_id, sys::mln_map_remove_style_image) - } - - fn style_image_exists(&self, image_id: String) -> PyResult { - self.string_bool_call_with(image_id, sys::mln_map_style_image_exists) } - fn copy_style_image_stretches( - &self, - image_id: String, - ) -> PyResult, Vec<(f32, f32)>)>> { - let state = self.state(); + fn remove_style_image(&self, image_id: String) -> PyResult> { let image_id = maplibre_core::string::string_view(&image_id); - let mut x_count = 0; - let mut y_count = 0; - let mut found = false; - // SAFETY: The C API validates the map pointer and image ID. Null arrays - // with zero capacity make this a size probe. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_stretches( - state.handle(), - image_id.raw(), - std::ptr::null_mut(), - 0, - &mut x_count, - std::ptr::null_mut(), - 0, - &mut y_count, - &mut found, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_remove_style_image(map, image_id.raw(), out) }) - .map_err(map_error)?; - if !found { - return Ok(None); - } + } - let mut raw_x = vec![sys::mln_image_stretch { from: 0.0, to: 0.0 }; x_count]; - let mut raw_y = vec![sys::mln_image_stretch { from: 0.0, to: 0.0 }; y_count]; - // SAFETY: Each buffer is writable for its reported count. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_stretches( - state.handle(), - image_id.raw(), - raw_x.as_mut_ptr(), - raw_x.len(), - &mut x_count, - raw_y.as_mut_ptr(), - raw_y.len(), - &mut y_count, - &mut found, - ) - }) - .map_err(map_error)?; - let to_public = |raw: &[sys::mln_image_stretch]| -> Vec<(f32, f32)> { - raw.iter() - .map(|stretch| (stretch.from, stretch.to)) - .collect() - }; - Ok(Some((to_public(&raw_x), to_public(&raw_y)))) + fn copy_style_image_stretches(&self, py: Python<'_>, image_id: String) -> PyResult> { + let image_id = maplibre_core::string::string_view(&image_id); + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_copy_style_image_stretches(map, image_id.raw(), completion) + }, + py_style_image_stretches, + ) } - fn get_style_image_info( - &self, - py: Python<'_>, - image_id: String, - ) -> PyResult>> { - let state = self.state(); + fn get_style_image_info(&self, py: Python<'_>, image_id: String) -> PyResult> { let image_id = maplibre_core::string::string_view(&image_id); - let mut info = maplibre_core::style::empty_style_image_info(); - let mut found = false; - // SAFETY: The C API validates the map pointer, image ID view, info, and found pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_image_info(state.handle(), image_id.raw(), &mut info, &mut found) - }) - .map_err(map_error)?; - if found { - style_image_info_to_py(py, &info).map(Some) - } else { - Ok(None) - } + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_image_info(map, image_id.raw(), completion) + }, + py_style_image_info, + ) } fn copy_style_image_premultiplied_rgba8( &self, py: Python<'_>, image_id: String, - ) -> PyResult>> { - let state = self.state(); + ) -> PyResult> { let image_id = maplibre_core::string::string_view(&image_id); - let mut info = maplibre_core::style::empty_style_image_info(); - let mut found = false; - // SAFETY: The C API validates the map pointer, image ID view, info, and found pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_image_info(state.handle(), image_id.raw(), &mut info, &mut found) - }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - let mut pixels = vec![0u8; info.byte_length]; - let mut byte_length = 0; - let mut copy_found = false; - // SAFETY: pixels is writable for its length and retained for this call. - // The C API validates all pointers and capacity. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_premultiplied_rgba8( - state.handle(), - image_id.raw(), - pixels.as_mut_ptr(), - pixels.len(), - &mut byte_length, - &mut copy_found, - ) - }) - .map_err(map_error)?; - if !copy_found { - return Ok(None); - } - pixels.truncate(byte_length); - style_image_to_py(py, &info, &pixels).map(Some) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_style_image_info(map, image_id.raw(), completion) + }, + py_style_image, + ) } fn add_image_source_url( @@ -3532,22 +3039,21 @@ impl MapHandle { source_id: String, coordinates: Vec<(f64, f64)>, url: String, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let url = maplibre_core::string::string_view(&url); let coordinates = lat_lngs_from_tuples(coordinates); // SAFETY: The C API validates the map pointer, source ID, coordinate slice, and URL. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_image_source_url( - state.handle(), + map, source_id.raw(), coordinates.as_ptr(), coordinates.len(), url.raw(), + out, ) }) - .map_err(map_error) } fn add_image_source_image( @@ -3558,34 +3064,31 @@ impl MapHandle { height: u32, stride: u32, pixels: Vec, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let coordinates = lat_lngs_from_tuples(coordinates); let image = premultiplied_rgba8_image_from_parts(width, height, stride, &pixels); // SAFETY: The C API validates the map pointer, source ID, coordinates, // image descriptor, and pixel storage. pixels is retained for this call. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_add_image_source_image( - state.handle(), + map, source_id.raw(), coordinates.as_ptr(), coordinates.len(), &image, + out, ) }) - .map_err(map_error) } - fn set_image_source_url(&self, source_id: String, url: String) -> PyResult<()> { - let state = self.state(); + fn set_image_source_url(&self, source_id: String, url: String) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let url = maplibre_core::string::string_view(&url); // SAFETY: The C API validates the map pointer and borrowed string views. - maplibre_core::check(unsafe { - sys::mln_map_set_image_source_url(state.handle(), source_id.raw(), url.raw()) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_image_source_url(map, source_id.raw(), url.raw(), out) }) - .map_err(map_error) } fn set_image_source_image( @@ -3595,77 +3098,54 @@ impl MapHandle { height: u32, stride: u32, pixels: Vec, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let image = premultiplied_rgba8_image_from_parts(width, height, stride, &pixels); // SAFETY: The C API validates the map pointer, source ID, image descriptor, // and pixel storage. pixels is retained for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_image_source_image(state.handle(), source_id.raw(), &image) + self.run_style_command(|map, out| unsafe { + sys::mln_map_set_image_source_image(map, source_id.raw(), &image, out) }) - .map_err(map_error) } fn set_image_source_coordinates( &self, source_id: String, coordinates: Vec<(f64, f64)>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let coordinates = lat_lngs_from_tuples(coordinates); // SAFETY: The C API validates the map pointer, source ID, and coordinate slice. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_image_source_coordinates( - state.handle(), + map, source_id.raw(), coordinates.as_ptr(), coordinates.len(), + out, ) }) - .map_err(map_error) } fn get_image_source_coordinates( &self, py: Python<'_>, source_id: String, - ) -> PyResult>> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); - let mut coordinates = vec![ - sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0 - }; - 4 - ]; - let mut coordinate_count = 0; - let mut found = false; - // SAFETY: coordinates is writable for four entries. The C API validates - // all pointers and returns a copied coordinate count. - maplibre_core::check(unsafe { - sys::mln_map_get_image_source_coordinates( - state.handle(), - source_id.raw(), - coordinates.as_mut_ptr(), - coordinates.len(), - &mut coordinate_count, - &mut found, - ) - }) - .map_err(map_error)?; - if !found { - return Ok(None); - } - coordinates.truncate(coordinate_count); - lat_lng_list_to_py(py, &coordinates).map(Some) + self.run_style_operation( + py, + |map, completion| unsafe { + sys::mln_map_get_image_source_coordinates(map, source_id.raw(), completion) + }, + py_optional_lat_lngs, + ) } #[allow(clippy::too_many_arguments)] fn add_custom_geometry_source( &self, + py: Python<'_>, source_id: String, max_queued_events: usize, min_zoom: Option, @@ -3676,7 +3156,7 @@ impl MapHandle { clip: Option, wrap: Option, has_cancel_tile: bool, - ) -> PyResult { + ) -> PyResult<(CustomGeometrySourceHandle, Py)> { if max_queued_events == 0 { return Err(invalid_argument_error( "max_queued_events must be greater than zero", @@ -3699,22 +3179,26 @@ impl MapHandle { }; let source_id_view = maplibre_core::string::string_view(&source_id); let map_state = self.state(); + let state = Box::into_raw(state); // SAFETY: map_state owns or has released the map pointer. The C API // validates that it is live. source_id_view and descriptor are valid for // this call, and descriptor's release callback takes the leaked state // back once the C API stops referencing it. - maplibre_core::check(unsafe { + match submit_python_command_future(py, |completion| unsafe { sys::mln_map_add_custom_geometry_source( map_state.handle(), source_id_view.raw(), &descriptor, + completion, ) - }) - .map_err(map_error)?; - // A rejected add owes no release, so the state only leaves this box - // after the C API accepted it. - Box::leak(state); - Ok(handle) + }) { + Ok(completion) => Ok((handle, completion)), + Err(error) => { + // SAFETY: rejected submissions retain no source callback state. + drop(unsafe { Box::from_raw(state) }); + Err(error) + } + } } fn set_custom_geometry_source_tile_data( @@ -3724,20 +3208,19 @@ impl MapHandle { x: u32, y: u32, data: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let data = maplibre_core::string::buffer_view(data.as_bytes()); // SAFETY: The C API validates the map pointer, source ID, tile ID, and GeoJSON buffer view. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_custom_geometry_source_tile_data( - state.handle(), + map, source_id.raw(), sys::mln_canonical_tile_id { z, x, y }, data, + out, ) }) - .map_err(map_error) } fn invalidate_custom_geometry_source_tile( @@ -3746,18 +3229,17 @@ impl MapHandle { z: u32, x: u32, y: u32, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); // SAFETY: The C API validates the map pointer, source ID, and tile ID. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_invalidate_custom_geometry_source_tile( - state.handle(), + map, source_id.raw(), sys::mln_canonical_tile_id { z, x, y }, + out, ) }) - .map_err(map_error) } fn invalidate_custom_geometry_source_region( @@ -3765,29 +3247,24 @@ impl MapHandle { source_id: String, southwest: (f64, f64), northeast: (f64, f64), - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let bounds = lat_lng_bounds_from_tuple((southwest, northeast)); // SAFETY: The C API validates the map pointer, source ID, and bounds. - maplibre_core::check(unsafe { - sys::mln_map_invalidate_custom_geometry_source_region( - state.handle(), - source_id.raw(), - bounds, - ) + self.run_style_command(|map, out| unsafe { + sys::mln_map_invalidate_custom_geometry_source_region(map, source_id.raw(), bounds, out) }) - .map_err(map_error) } fn add_custom_mvt_vector_source( &self, + py: Python<'_>, source_id: String, max_queued_events: usize, min_zoom: Option, max_zoom: Option, has_cancel_tile: bool, - ) -> PyResult { + ) -> PyResult<(CustomMvtVectorSourceHandle, Py)> { if max_queued_events == 0 { return Err(invalid_argument_error( "max_queued_events must be greater than zero", @@ -3805,20 +3282,26 @@ impl MapHandle { }; let source_id_view = maplibre_core::string::string_view(&source_id); let map_state = self.state(); + let state = Box::into_raw(state); // SAFETY: map_state owns or has released the map pointer. The C API // validates that it is live. source_id_view and descriptor are valid for // this call, and descriptor's release callback takes the leaked state // back once the C API stops referencing it. - maplibre_core::check(unsafe { + match submit_python_command_future(py, |completion| unsafe { sys::mln_map_add_custom_mvt_vector_source( map_state.handle(), source_id_view.raw(), &descriptor, + completion, ) - }) - .map_err(map_error)?; - Box::leak(state); - Ok(handle) + }) { + Ok(completion) => Ok((handle, completion)), + Err(error) => { + // SAFETY: rejected submissions retain no source callback state. + drop(unsafe { Box::from_raw(state) }); + Err(error) + } + } } fn set_custom_mvt_vector_source_tile_data( @@ -3828,21 +3311,20 @@ impl MapHandle { x: u32, y: u32, data: &Bound<'_, PyBytes>, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let data = maplibre_core::string::buffer_view(data.as_bytes()); // SAFETY: The C API validates the map pointer, source ID, tile ID, and // MVT buffer view. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_custom_mvt_vector_source_tile_data( - state.handle(), + map, source_id.raw(), sys::mln_canonical_tile_id { z, x, y }, data, + out, ) }) - .map_err(map_error) } fn set_custom_mvt_vector_source_tile_error( @@ -3852,21 +3334,20 @@ impl MapHandle { x: u32, y: u32, message: String, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let message = maplibre_core::string::string_view(&message); // SAFETY: The C API validates the map pointer, source ID, tile ID, and // diagnostic message view. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_set_custom_mvt_vector_source_tile_error( - state.handle(), + map, source_id.raw(), sys::mln_canonical_tile_id { z, x, y }, message.raw(), + out, ) }) - .map_err(map_error) } fn invalidate_custom_mvt_vector_source_tile( @@ -3875,18 +3356,17 @@ impl MapHandle { z: u32, x: u32, y: u32, - ) -> PyResult<()> { - let state = self.state(); + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); // SAFETY: The C API validates the map pointer, source ID, and tile ID. - maplibre_core::check(unsafe { + self.run_style_command(|map, out| unsafe { sys::mln_map_invalidate_custom_mvt_vector_source_tile( - state.handle(), + map, source_id.raw(), sys::mln_canonical_tile_id { z, x, y }, + out, ) }) - .map_err(map_error) } #[getter] @@ -3895,31 +3375,46 @@ impl MapHandle { } } +// Every post-creation projection call is a synchronous immediate that takes +// the projection's internal serialization mutex, so each call copies the raw +// handle out of the binding's state lock and releases the GIL around the +// native call. A stalled call on another thread therefore cannot deadlock the +// interpreter, and a concurrent close is safe because retired handles report +// MLN_STATUS_INVALID_ARGUMENT rather than dangling. +impl MapProjectionHandle { + fn native_handle(&self) -> sys::mln_map_projection { + self.state().handle() + } +} + #[pymethods] impl MapProjectionHandle { - fn close(&self) -> PyResult<()> { - let state = self.state(); - // SAFETY: state owns an mln_map_projection handle created by - // mln_map_projection_create and pairs it with the matching destroy. - unsafe { state.close_status(sys::mln_map_projection_destroy) }.map_err(map_error) + fn close(&self, py: Python<'_>) -> PyResult<()> { + let handle = { + let state = self.state(); + let Some(handle) = state.live_handle() else { + return Ok(()); + }; + state.mark_closed(); + handle + }; + let status = py.detach(|| unsafe { sys::mln_map_projection_close(handle) }); + maplibre_core::check(status).map_err(map_error) } fn get_camera(&self, py: Python<'_>) -> PyResult> { - let state = self.state(); - // SAFETY: Default constructor takes no arguments and initializes size. + let handle = self.native_handle(); let mut camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: The C API validates that the projection is live and camera - // points to initialized writable storage. - maplibre_core::check(unsafe { - sys::mln_map_projection_get_camera(state.handle(), &mut camera) - }) - .map_err(map_error)?; + let status = + py.detach(|| unsafe { sys::mln_map_projection_get_camera(handle, &mut camera) }); + maplibre_core::check(status).map_err(map_error)?; camera_options_to_py(py, &camera) } #[allow(clippy::too_many_arguments)] fn set_camera( &self, + py: Python<'_>, center: Option<(f64, f64)>, zoom: Option, bearing: Option, @@ -3930,7 +3425,7 @@ impl MapProjectionHandle { roll: Option, field_of_view: Option, ) -> PyResult<()> { - let state = self.state(); + let handle = self.native_handle(); let camera = camera_options_from_parts( center, zoom, @@ -3942,17 +3437,17 @@ impl MapProjectionHandle { roll, field_of_view, ); - // SAFETY: The C API validates the projection pointer and camera fields. - maplibre_core::check(unsafe { sys::mln_map_projection_set_camera(state.handle(), &camera) }) - .map_err(map_error) + let status = py.detach(|| unsafe { sys::mln_map_projection_set_camera(handle, &camera) }); + maplibre_core::check(status).map_err(map_error) } fn set_visible_coordinates( &self, + py: Python<'_>, coordinates: Vec<(f64, f64)>, padding: (f64, f64, f64, f64), ) -> PyResult<()> { - let state = self.state(); + let handle = self.native_handle(); let coordinates: Vec = coordinates .into_iter() .map(|(latitude, longitude)| sys::mln_lat_lng { @@ -3961,32 +3456,36 @@ impl MapProjectionHandle { }) .collect(); let padding = edge_insets_from_tuple(padding); - // SAFETY: The C API validates the projection pointer, coordinates, and - // padding. coordinates is retained for the duration of this call. - maplibre_core::check(unsafe { + let status = py.detach(|| unsafe { sys::mln_map_projection_set_visible_coordinates( - state.handle(), + handle, coordinates.as_ptr(), coordinates.len(), padding, ) - }) - .map_err(map_error) + }); + maplibre_core::check(status).map_err(map_error) } fn set_visible_geometry( &self, + py: Python<'_>, geometry: &Bound<'_, PyBytes>, padding: (f64, f64, f64, f64), ) -> PyResult<()> { - let state = self.state(); - let geometry = maplibre_core::string::buffer_view(geometry.as_bytes()); + let handle = self.native_handle(); + // Copy the bytes out so the borrowed buffer cannot move while the GIL + // is released. + let geometry = geometry.as_bytes().to_vec(); let padding = edge_insets_from_tuple(padding); - // SAFETY: The C API validates the projection pointer, geometry buffer view, and padding. - maplibre_core::check(unsafe { - sys::mln_map_projection_set_visible_geometry(state.handle(), geometry, padding) - }) - .map_err(map_error) + let status = py.detach(|| unsafe { + sys::mln_map_projection_set_visible_geometry( + handle, + maplibre_core::string::buffer_view(&geometry), + padding, + ) + }); + maplibre_core::check(status).map_err(map_error) } fn pixel_for_lat_lng( @@ -3995,40 +3494,33 @@ impl MapProjectionHandle { latitude: f64, longitude: f64, ) -> PyResult> { - let state = self.state(); + let handle = self.native_handle(); + let coordinate = sys::mln_lat_lng { + latitude, + longitude, + }; let mut point = sys::mln_screen_point { x: 0.0, y: 0.0 }; - // SAFETY: The C API validates the projection pointer, coordinate, and - // output pointer. - maplibre_core::check(unsafe { - sys::mln_map_projection_pixel_for_lat_lng( - state.handle(), - sys::mln_lat_lng { - latitude, - longitude, - }, - &mut point, - ) - }) - .map_err(map_error)?; + let status = py.detach(|| unsafe { + sys::mln_map_projection_pixel_for_lat_lng(handle, coordinate, &mut point) + }); + maplibre_core::check(status).map_err(map_error)?; screen_point_to_py(py, point) } fn lat_lng_for_pixel(&self, py: Python<'_>, x: f64, y: f64) -> PyResult> { - let state = self.state(); + let handle = self.native_handle(); let mut coordinate = sys::mln_lat_lng { latitude: 0.0, longitude: 0.0, }; - // SAFETY: The C API validates the projection pointer, point, and output - // pointer. - maplibre_core::check(unsafe { + let status = py.detach(|| unsafe { sys::mln_map_projection_lat_lng_for_pixel( - state.handle(), + handle, sys::mln_screen_point { x, y }, &mut coordinate, ) - }) - .map_err(map_error)?; + }); + maplibre_core::check(status).map_err(map_error)?; lat_lng_to_py(py, coordinate) } @@ -4036,63 +3528,204 @@ impl MapProjectionHandle { fn closed(&self) -> bool { self.state().is_closed() } -} +} + +impl RenderSessionHandle { + fn native(&self) -> sys::mln_render_session { + self.state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .native() + } + + fn start_target( + &self, + py: Python<'_>, + raw: D, + start: impl FnOnce(sys::mln_render_session, &D, *const sys::mln_completion) -> sys::mln_status, + ) -> PyResult> { + submit_python_future( + py, + |completion| start(self.native(), &raw, completion), + py_none, + ) + } + + fn start_unit( + &self, + py: Python<'_>, + start: impl FnOnce(sys::mln_render_session, *const sys::mln_completion) -> sys::mln_status, + ) -> PyResult> { + submit_python_future(py, |completion| start(self.native(), completion), py_none) + } +} + +#[pymethods] +impl RenderSessionHandle { + fn close(&self) -> PyResult<()> { + let state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if state.is_closed() { + return Ok(()); + } + unsafe { state.handle.close_status(sys::mln_render_session_destroy) }.map_err(map_error) + } + + fn resize( + &self, + py: Python<'_>, + width: u32, + height: u32, + scale_factor: f64, + ) -> PyResult> { + let extent = sys::mln_render_target_extent { + size: std::mem::size_of::() as u32, + width, + height, + scale_factor, + }; + submit_python_future( + py, + |completion| unsafe { + sys::mln_render_session_resize(self.native(), &extent, completion) + }, + py_none, + ) + } + + fn barrier(&self, py: Python<'_>) -> PyResult> { + self.start_unit(py, |session, completion| unsafe { + sys::mln_render_session_barrier(session, completion) + }) + } + + fn reduce_memory_use(&self, py: Python<'_>) -> PyResult> { + self.start_unit(py, |session, completion| unsafe { + sys::mln_render_session_reduce_memory_use(session, completion) + }) + } + + fn clear_data(&self, py: Python<'_>) -> PyResult> { + self.start_unit(py, |session, completion| unsafe { + sys::mln_render_session_clear_data(session, completion) + }) + } + + fn dump_debug_logs(&self, py: Python<'_>) -> PyResult> { + self.start_unit(py, |session, completion| unsafe { + sys::mln_render_session_dump_debug_logs(session, completion) + }) + } + + fn detach(&self, py: Python<'_>) -> PyResult> { + self.start_unit(py, |session, completion| unsafe { + sys::mln_render_session_detach(session, completion) + }) + } + + fn abandon(&self, py: Python<'_>) -> PyResult> { + let mut result: sys::mln_render_abandon_result = unsafe { std::mem::zeroed() }; + result.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_session_abandon(self.native(), &mut result) + }) + .map_err(map_error)?; + let dict = PyDict::new(py); + dict.set_item("disposition", result.disposition)?; + dict.set_item( + "quarantined_resource_count", + result.quarantined_resource_count, + )?; + Ok(dict.into_any().unbind()) + } + + fn service_driver_work(&self, max_work: usize) -> PyResult { + let mut serviced = 0; + maplibre_core::check(unsafe { + sys::mln_render_session_service_driver_work(self.native(), max_work, &mut serviced) + }) + .map_err(map_error)?; + Ok(serviced) + } -impl RenderSessionHandle { - /// Shared body for the `set_*_target` methods. The caller materializes the - /// native descriptor, which the C API borrows only for this call. - fn set_target( + fn request_frame( &self, - raw: D, - set_target: impl FnOnce(sys::mln_render_session, &D) -> sys::mln_status, + flags: u32, + token: u64, + coalescing_boundary: u64, + timeout_ns: u64, ) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - maplibre_core::check(set_target(state.native(), &raw)).map_err(map_error) + let demand = sys::mln_frame_demand { + size: std::mem::size_of::() as u32, + flags, + token, + coalescing_boundary, + timeout_ns, + }; + maplibre_core::check(unsafe { + sys::mln_render_session_request_frame(self.native(), &demand) + }) + .map_err(map_error) } -} -#[pymethods] -impl RenderSessionHandle { - fn close(&self) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.is_closed() { - return Ok(()); - } - state.ensure_no_frame_acquired()?; - // SAFETY: state owns an mln_render_session handle created by an attach - // function and pairs it with the matching status-returning destroy. - unsafe { state.handle.close_status(sys::mln_render_session_destroy) }.map_err(map_error) + fn capabilities(&self, py: Python<'_>) -> PyResult> { + let mut value: sys::mln_render_session_capabilities = unsafe { std::mem::zeroed() }; + value.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_session_get_capabilities(self.native(), &mut value) + }) + .map_err(map_error)?; + let dict = PyDict::new(py); + dict.set_item("driver", value.driver)?; + dict.set_item("texture_ring_depth", value.texture_ring_depth)?; + dict.set_item("flags", value.flags)?; + Ok(dict.into_any().unbind()) } - fn resize(&self, width: u32, height: u32, scale_factor: f64) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: The C API validates that the pointer is live, attached, and - // called on the owner thread. + fn snapshot(&self, py: Python<'_>) -> PyResult> { + let mut value: sys::mln_render_session_snapshot = unsafe { std::mem::zeroed() }; + value.size = std::mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_render_session_resize(state.native(), width, height, scale_factor) + sys::mln_render_session_get_snapshot(self.native(), &mut value) }) - .map_err(map_error) + .map_err(map_error)?; + render_session_snapshot_to_py(py, &value) + } + + fn drain_frame_results(&self, py: Python<'_>) -> PyResult> { + let mut batch = sys::mln_render_frame_batch(0); + maplibre_core::check(unsafe { + sys::mln_render_session_drain_frame_results(self.native(), &mut batch) + }) + .map_err(map_error)?; + let batch = OwnedRenderFrameBatch(batch); + let mut count = 0; + maplibre_core::check(unsafe { sys::mln_render_frame_batch_count(batch.0, &mut count) }) + .map_err(map_error)?; + let results = PyList::empty(py); + for index in 0..count { + let mut value: sys::mln_render_frame_result = unsafe { std::mem::zeroed() }; + value.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_frame_batch_get(batch.0, index, &mut value) + }) + .map_err(map_error)?; + results.append(render_frame_result_to_py(py, &value)?)?; + } + Ok(results.unbind()) } fn set_metal_surface_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, device_address: usize, layer_address: usize, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::metal_surface_descriptor_to_native( maplibre_core::render::MetalSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4106,16 +3739,16 @@ impl RenderSessionHandle { layer: layer_address as *mut c_void, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_metal_surface_set_target(session, raw) } + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_metal_surface_set_target(session, raw, completion) } }) } #[allow(clippy::too_many_arguments)] fn set_vulkan_surface_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, @@ -4127,7 +3760,7 @@ impl RenderSessionHandle { get_instance_proc_addr: usize, get_device_proc_addr: usize, surface_address: usize, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::vulkan_surface_descriptor_to_native( maplibre_core::render::VulkanSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4147,16 +3780,46 @@ impl RenderSessionHandle { surface: surface_address as *mut c_void, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_vulkan_surface_set_target(session, raw) } + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_vulkan_surface_set_target(session, raw, completion) } + }) + } + + #[allow(clippy::too_many_arguments)] + fn set_webgpu_surface_target( + &self, + py: Python<'_>, + width: u32, + height: u32, + scale_factor: f64, + instance_address: usize, + device_address: usize, + queue_address: usize, + surface_address: usize, + format: u32, + ) -> PyResult> { + let descriptor = maplibre_core::render::webgpu_surface_descriptor_to_native( + maplibre_core::render::WebGpuSurfaceDescriptorFields { + extent: maplibre_core::render::RenderTargetExtentFields { + width, + height, + scale_factor, + }, + context: webgpu_context_fields(instance_address, device_address, queue_address), + surface: surface_address as *mut c_void, + format, + }, + ); + self.start_target(py, descriptor, |session, raw, completion| unsafe { + sys::mln_webgpu_surface_set_target(session, raw, completion) }) } #[allow(clippy::too_many_arguments)] fn set_opengl_surface_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, @@ -4168,7 +3831,7 @@ impl RenderSessionHandle { client_api: u32, get_proc_address: usize, surface_address: usize, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::opengl_surface_descriptor_to_native( maplibre_core::render::OpenGLSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4188,22 +3851,22 @@ impl RenderSessionHandle { surface: surface_address as *mut c_void, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_opengl_surface_set_target(session, raw) } + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_opengl_surface_set_target(session, raw, completion) } }) } fn set_metal_borrowed_texture_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, physical_width: u32, physical_height: u32, texture_address: usize, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::metal_borrowed_texture_descriptor_to_native( maplibre_core::render::MetalBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4216,16 +3879,16 @@ impl RenderSessionHandle { texture: texture_address as *mut c_void, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_metal_borrowed_texture_set_target(session, raw) } + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_metal_borrowed_texture_set_target(session, raw, completion) } }) } #[allow(clippy::too_many_arguments)] fn set_vulkan_borrowed_texture_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, @@ -4243,7 +3906,7 @@ impl RenderSessionHandle { format: u32, initial_layout: u32, final_layout: u32, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::vulkan_borrowed_texture_descriptor_to_native( maplibre_core::render::VulkanBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4269,16 +3932,52 @@ impl RenderSessionHandle { final_layout, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_vulkan_borrowed_texture_set_target(session, raw) } + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_vulkan_borrowed_texture_set_target(session, raw, completion) } + }) + } + + #[allow(clippy::too_many_arguments)] + fn set_webgpu_borrowed_texture_target( + &self, + py: Python<'_>, + width: u32, + height: u32, + scale_factor: f64, + physical_width: u32, + physical_height: u32, + instance_address: usize, + device_address: usize, + queue_address: usize, + texture_address: usize, + texture_view_address: usize, + format: u32, + ) -> PyResult> { + let descriptor = maplibre_core::render::webgpu_borrowed_texture_descriptor_to_native( + maplibre_core::render::WebGpuBorrowedTextureDescriptorFields { + extent: maplibre_core::render::RenderTargetExtentFields { + width, + height, + scale_factor, + }, + physical_width, + physical_height, + context: webgpu_context_fields(instance_address, device_address, queue_address), + texture: texture_address as *mut c_void, + texture_view: texture_view_address as *mut c_void, + format, + }, + ); + self.start_target(py, descriptor, |session, raw, completion| unsafe { + sys::mln_webgpu_borrowed_texture_set_target(session, raw, completion) }) } #[allow(clippy::too_many_arguments)] fn set_opengl_borrowed_texture_target( &self, + py: Python<'_>, width: u32, height: u32, scale_factor: f64, @@ -4293,7 +3992,7 @@ impl RenderSessionHandle { get_proc_address: usize, texture: u32, target: u32, - ) -> PyResult<()> { + ) -> PyResult> { let descriptor = maplibre_core::render::opengl_borrowed_texture_descriptor_to_native( maplibre_core::render::OpenGLBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -4316,124 +4015,38 @@ impl RenderSessionHandle { target, }, ); - self.set_target(descriptor, |session, raw| { - // SAFETY: raw is fully initialized and lives for this call. The C - // API validates the session pointer, state, and descriptor fields. - unsafe { sys::mln_opengl_borrowed_texture_set_target(session, raw) } - }) - } - - fn render_update(&self) -> PyResult<(u32, bool)> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let mut result = sys::MLN_RENDER_RESULT_NO_UPDATE; - let mut needs_repaint = false; - // SAFETY: The C API validates the render-session pointer and state, and - // result and needs_repaint point to caller-owned output storage. - maplibre_core::check(unsafe { - sys::mln_render_session_render_update( - state.native(), - &raw mut result, - &raw mut needs_repaint, - ) - }) - .map_err(map_error)?; - Ok((result, needs_repaint)) - } - - fn detach(&self) -> PyResult { - let mut state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: The C API validates the render-session pointer and state. - maplibre_core::check(unsafe { sys::mln_render_session_detach(state.native()) }) - .map_err(map_error)?; - state.detached = true; - drop(state); - Ok(DetachedRenderSessionHandle { - state: Arc::clone(&self.state), + self.start_target(py, descriptor, |session, raw, completion| { + // SAFETY: raw and operation are valid for this copied submission. + unsafe { sys::mln_opengl_borrowed_texture_set_target(session, raw, completion) } }) } - fn reduce_memory_use(&self) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: The C API validates the render-session pointer and state. - maplibre_core::check(unsafe { sys::mln_render_session_reduce_memory_use(state.native()) }) - .map_err(map_error) - } - - fn clear_data(&self) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: The C API validates the render-session pointer and state. - maplibre_core::check(unsafe { sys::mln_render_session_clear_data(state.native()) }) - .map_err(map_error) - } - - fn dump_debug_logs(&self) -> PyResult<()> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: The C API validates the render-session pointer and state. - maplibre_core::check(unsafe { sys::mln_render_session_dump_debug_logs(state.native()) }) - .map_err(map_error) - } - fn query_rendered_features( &self, py: Python<'_>, geometry: &Bound<'_, PyAny>, layer_ids: Option>, filter: Option<&Bound<'_, PyBytes>>, - ) -> PyResult> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; + ) -> PyResult> { let geometry = rendered_query_geometry_from_wire(geometry)?; let geometry = maplibre_core::query::rendered_query_geometry_to_native(&geometry); let mut options = maplibre_core::RenderedFeatureQueryOptions::default(); - if let Some(layer_ids) = layer_ids { - options.layer_ids = Some(layer_ids); - } - if let Some(filter) = filter { - options.filter = Some(filter.as_bytes().to_vec()); - } + options.layer_ids = layer_ids; + options.filter = filter.map(|value| value.as_bytes().to_vec()); let options = maplibre_core::query::rendered_feature_query_options_to_native(&options) .map_err(map_error)?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the render-session pointer, query geometry/options, and output pointer. - maplibre_core::check(unsafe { - sys::mln_render_session_query_rendered_features( - state.native(), - geometry.as_ptr(), - options.as_ptr(), - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_queried_feature_list") - .map_err(map_error)?; - // SAFETY: native is an owned queried-feature list returned by native. - let features = unsafe { maplibre_core::query::copy_queried_feature_list(native) } - .map_err(map_error)?; - queried_features_to_py(py, features) + submit_python_future( + py, + |completion| unsafe { + sys::mln_render_session_query_rendered_features( + self.native(), + geometry.as_ptr(), + options.as_ptr(), + completion, + ) + }, + py_queried_features, + ) } fn query_source_features( @@ -4442,40 +4055,25 @@ impl RenderSessionHandle { source_id: String, source_layer_ids: Option>, filter: Option<&Bound<'_, PyBytes>>, - ) -> PyResult> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let mut options = maplibre_core::SourceFeatureQueryOptions::default(); - if let Some(source_layer_ids) = source_layer_ids { - options.source_layer_ids = Some(source_layer_ids); - } - if let Some(filter) = filter { - options.filter = Some(filter.as_bytes().to_vec()); - } + options.source_layer_ids = source_layer_ids; + options.filter = filter.map(|value| value.as_bytes().to_vec()); let options = maplibre_core::query::source_feature_query_options_to_native(&options) .map_err(map_error)?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the render-session pointer, source ID, query options, and output pointer. - maplibre_core::check(unsafe { - sys::mln_render_session_query_source_features( - state.native(), - source_id.raw(), - options.as_ptr(), - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - let native = out - .into_live("mln_queried_feature_list") - .map_err(map_error)?; - // SAFETY: native is an owned queried-feature list returned by native. - let features = unsafe { maplibre_core::query::copy_queried_feature_list(native) } - .map_err(map_error)?; - queried_features_to_py(py, features) + submit_python_future( + py, + |completion| unsafe { + sys::mln_render_session_query_source_features( + self.native(), + source_id.raw(), + options.as_ptr(), + completion, + ) + }, + py_queried_features, + ) } fn query_feature_extensions( @@ -4486,192 +4084,89 @@ impl RenderSessionHandle { extension: String, extension_field: String, arguments: Option<&Bound<'_, PyBytes>>, - ) -> PyResult> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; + ) -> PyResult> { let source_id = maplibre_core::string::string_view(&source_id); let feature = maplibre_core::string::buffer_view(feature.as_bytes()); let extension = maplibre_core::string::string_view(&extension); let extension_field = maplibre_core::string::string_view(&extension_field); let arguments = arguments.map(|value| maplibre_core::string::buffer_view(value.as_bytes())); - let arguments_ptr = optional_ref_ptr(arguments.as_ref()); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: The C API validates the render-session pointer, feature, strings, arguments, and output pointer. - maplibre_core::check(unsafe { - sys::mln_render_session_query_feature_extensions( - state.native(), - source_id.raw(), - feature, - extension.raw(), - extension_field.raw(), - arguments_ptr, - out.as_mut_ptr(), - ) - }) - .map_err(map_error)?; - owned_buffer_to_py(py, out.get()) - } - - fn texture_image_info(&self, py: Python<'_>) -> PyResult> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let info = probe_texture_image_info(state.native())?; - texture_image_info_to_py(py, info) + submit_python_future( + py, + |completion| unsafe { + sys::mln_render_session_query_feature_extensions( + self.native(), + source_id.raw(), + feature, + extension.raw(), + extension_field.raw(), + optional_ref_ptr(arguments.as_ref()), + completion, + ) + }, + py_buffer, + ) } fn read_premultiplied_rgba8(&self, py: Python<'_>) -> PyResult> { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let info = probe_texture_image_info(state.native())?; - let mut data = vec![0; info.byte_length]; - let info = read_texture_image_into(state.native(), &mut data)?; - image_to_py(py, info, &data) - } - - fn read_premultiplied_rgba8_into( - &self, - py: Python<'_>, - buffer: &Bound<'_, PyAny>, - ) -> PyResult> { - let py_buffer = PyBuffer::::get(buffer).map_err(|error| { - invalid_argument_error(format!("expected writable contiguous u8 buffer: {error}")) - })?; - let Some(cells) = py_buffer.as_mut_slice(py) else { - return Err(invalid_argument_error( - "expected writable contiguous u8 buffer", - )); - }; - let data = if cells.is_empty() { - std::ptr::null_mut() - } else { - cells.as_ptr().cast::().cast_mut() - }; - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - // SAFETY: data points to the writable contiguous Python buffer borrowed - // above for cells.len() u8 elements, or is null when the buffer is empty. - let info = read_texture_image_raw(state.native(), data, cells.len())?; - // An empty destination reaches native as the null pointer and zero - // capacity of a size probe, which succeeds without copying, so report - // the buffer as too small unless the frame carries no bytes. - if cells.is_empty() && info.byte_length > 0 { - return Err(invalid_argument_error(format!( - "buffer length 0 is smaller than the required {} bytes", - info.byte_length - ))); - } - texture_image_info_to_py(py, info) + submit_python_future( + py, + |completion| unsafe { + sys::mln_texture_read_premultiplied_rgba8(self.native(), completion) + }, + py_texture_readback, + ) } fn acquire_metal_owned_texture_frame( &self, py: Python<'_>, ) -> PyResult> { - let session = Arc::clone(&self.state); - let guard_session = Arc::clone(&session); - let mut state = session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let mut raw = empty_metal_owned_texture_frame(); - // SAFETY: raw points to initialized writable frame storage, and the C - // API validates the session pointer and texture-session state. - maplibre_core::check(unsafe { - sys::mln_metal_owned_texture_acquire_frame(state.native(), &mut raw) - }) - .map_err(map_error)?; - state.frame_acquired = true; - let raw = MetalOwnedTextureFrameRaw::from_native(&raw); - let mut guard = OwnedTextureFrameAcquisitionGuard::metal(guard_session, raw); - drop(state); - let frame = Py::new( + let frame = acquire_frame(self.native())?; + Py::new( py, MetalOwnedTextureFrameHandle { - session, - raw, - closed: Mutex::new(false), + frame: Mutex::new(frame), }, - )?; - guard.disarm(); - Ok(frame) + ) } fn acquire_vulkan_owned_texture_frame( &self, py: Python<'_>, ) -> PyResult> { - let session = Arc::clone(&self.state); - let guard_session = Arc::clone(&session); - let mut state = session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let mut raw = empty_vulkan_owned_texture_frame(); - // SAFETY: raw points to initialized writable frame storage, and the C - // API validates the session pointer and texture-session state. - maplibre_core::check(unsafe { - sys::mln_vulkan_owned_texture_acquire_frame(state.native(), &mut raw) - }) - .map_err(map_error)?; - state.frame_acquired = true; - let raw = VulkanOwnedTextureFrameRaw::from_native(&raw); - let mut guard = OwnedTextureFrameAcquisitionGuard::vulkan(guard_session, raw); - drop(state); - let frame = Py::new( + let frame = acquire_frame(self.native())?; + Py::new( py, VulkanOwnedTextureFrameHandle { - session, - raw, - closed: Mutex::new(false), + frame: Mutex::new(frame), + }, + ) + } + + fn acquire_opengl_owned_texture_frame( + &self, + py: Python<'_>, + ) -> PyResult> { + let frame = acquire_frame(self.native())?; + Py::new( + py, + OpenGLOwnedTextureFrameHandle { + frame: Mutex::new(frame), }, - )?; - guard.disarm(); - Ok(frame) + ) } - fn acquire_opengl_owned_texture_frame( + fn acquire_webgpu_owned_texture_frame( &self, py: Python<'_>, - ) -> PyResult> { - let session = Arc::clone(&self.state); - let guard_session = Arc::clone(&session); - let mut state = session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.ensure_no_frame_acquired()?; - let mut raw = empty_opengl_owned_texture_frame(); - // SAFETY: raw points to initialized writable frame storage, and the C - // API validates the session pointer and texture-session state. - maplibre_core::check(unsafe { - sys::mln_opengl_owned_texture_acquire_frame(state.native(), &mut raw) - }) - .map_err(map_error)?; - state.frame_acquired = true; - let raw = OpenGLOwnedTextureFrameRaw::from_native(&raw); - let mut guard = OwnedTextureFrameAcquisitionGuard::opengl(guard_session, raw); - drop(state); - let frame = Py::new( + ) -> PyResult> { + let frame = acquire_frame(self.native())?; + Py::new( py, - OpenGLOwnedTextureFrameHandle { - session, - raw, - closed: Mutex::new(false), + WebGPUOwnedTextureFrameHandle { + frame: Mutex::new(frame), }, - )?; - guard.disarm(); - Ok(frame) + ) } #[getter] @@ -4681,38 +4176,23 @@ impl RenderSessionHandle { .unwrap_or_else(|poisoned| poisoned.into_inner()) .is_closed() } - - #[getter] - fn detached(&self) -> bool { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .detached - } } -#[pymethods] -impl DetachedRenderSessionHandle { - fn close(&self) -> PyResult<()> { +impl Drop for RenderSessionHandle { + fn drop(&mut self) { let state = self .state .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); - if state.is_closed() { - return Ok(()); + let Some(native) = state.handle.live_handle() else { + return; + }; + let mut result: sys::mln_render_abandon_result = unsafe { std::mem::zeroed() }; + result.size = std::mem::size_of::() as u32; + let status = unsafe { sys::mln_render_session_abandon(native, &mut result) }; + if status == sys::MLN_STATUS_OK || status == sys::MLN_STATUS_TARGET_LOST { + let _ = unsafe { state.handle.close_status(sys::mln_render_session_destroy) }; } - state.ensure_no_frame_acquired()?; - // SAFETY: state owns an mln_render_session handle created by an attach - // function and pairs it with the matching status-returning destroy. - unsafe { state.handle.close_status(sys::mln_render_session_destroy) }.map_err(map_error) - } - - #[getter] - fn closed(&self) -> bool { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .is_closed() } } @@ -4833,43 +4313,6 @@ impl CustomMvtVectorSourceHandle { } } -#[pymethods] -impl WakeSource { - fn signal(&self) -> PyResult<()> { - let handle = self - .handle - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let Some(handle) = *handle else { - return Err(invalid_state_error("wake source is closed")); - }; - // SAFETY: handle came from a successful acquire and is still owned by - // this wrapper, and the C API accepts signals from any thread. - maplibre_core::check(unsafe { sys::mln_wake_source_signal(handle) }).map_err(map_error) - } - - fn close(&self) { - let mut handle = self - .handle - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let Some(handle) = handle.take() else { - return; - }; - // SAFETY: handle came from a successful acquire, the mutex makes this - // the only close, and the C API accepts destruction from any thread. - unsafe { sys::mln_wake_source_destroy(handle) }; - } - - #[getter] - fn closed(&self) -> bool { - self.handle - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .is_none() - } -} - #[pymethods] impl ResourceRequestHandle { fn validate_completion_response(&self, response: &Bound<'_, PyAny>) -> PyResult<()> { @@ -4934,10 +4377,12 @@ impl PyResourceProviderState { } fn descriptor(&self) -> sys::mln_resource_provider { - maplibre_core::resource::resource_provider_descriptor( + let mut descriptor = maplibre_core::resource::resource_provider_descriptor( Some(resource_provider_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_py_resource_provider_state); + descriptor } fn invoke( @@ -4995,6 +4440,13 @@ impl PyResourceProviderState { } } +unsafe extern "C" fn release_py_resource_provider_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn resource_provider_trampoline( user_data: *mut c_void, request: *const sys::mln_resource_request, @@ -5021,10 +4473,12 @@ impl PyResourceTransformState { } fn descriptor(&self) -> sys::mln_resource_transform { - maplibre_core::resource::resource_transform_descriptor( + let mut descriptor = maplibre_core::resource::resource_transform_descriptor( Some(resource_transform_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_py_resource_transform_state); + descriptor } fn invoke( @@ -5078,6 +4532,13 @@ impl PyResourceTransformState { } } +unsafe extern "C" fn release_py_resource_transform_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn resource_transform_trampoline( user_data: *mut c_void, kind: u32, @@ -5105,10 +4566,12 @@ impl PyHttpHeaderTransformState { } fn descriptor(&self) -> sys::mln_http_header_transform { - maplibre_core::resource::http_header_transform_descriptor( + let mut descriptor = maplibre_core::resource::http_header_transform_descriptor( Some(http_header_transform_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_py_http_header_transform_state); + descriptor } fn invoke( @@ -5179,6 +4642,13 @@ impl PyHttpHeaderTransformState { } } +unsafe extern "C" fn release_py_http_header_transform_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn http_header_transform_trampoline( user_data: *mut c_void, kind: u32, @@ -5198,7 +4668,6 @@ unsafe extern "C" fn http_header_transform_trampoline( fn event_batch_to_py( py: Python<'_>, events: Vec, - remaining_count: usize, ) -> PyResult> { let list = PyList::empty(py); for event in events { @@ -5206,7 +4675,6 @@ fn event_batch_to_py( } let dict = PyDict::new(py); dict.set_item("events", list)?; - dict.set_item("remaining_count", remaining_count)?; Ok(dict.into_any().unbind()) } @@ -5261,14 +4729,6 @@ fn payload_to_py(py: Python<'_>, payload: RuntimeEventPayload) -> PyResult { - dict.set_item("kind", "offline_operation_completed")?; - dict.set_item("operation_id", payload.operation_id)?; - dict.set_item("operation_kind", payload.raw_operation_kind)?; - dict.set_item("result_kind", payload.raw_result_kind)?; - dict.set_item("result_status", payload.result_status)?; - dict.set_item("found", payload.found)?; - } RuntimeEventPayload::CameraTransitionFinished(payload) => { dict.set_item("kind", "camera_transition_finished")?; dict.set_item("transition_id", payload.transition_id)?; @@ -5405,9 +4865,6 @@ fn event_type_raw(event_type: RuntimeEventType) -> u32 { RuntimeEventType::OfflineRegionTileCountLimitExceeded => { sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED } - RuntimeEventType::OfflineOperationCompleted => { - sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED - } RuntimeEventType::Unknown(raw) => raw, _ => 0, } @@ -5433,17 +4890,6 @@ fn texture_image_info_to_py( Ok(dict.into_any().unbind()) } -fn image_to_py( - py: Python<'_>, - info: maplibre_core::TextureImageInfo, - data: &[u8], -) -> PyResult> { - let dict = PyDict::new(py); - dict.set_item("info", texture_image_info_to_py(py, info)?)?; - dict.set_item("data", PyBytes::new(py, data))?; - Ok(dict.into_any().unbind()) -} - fn log_record_to_py(py: Python<'_>, record: CopiedLogRecordRaw) -> PyResult> { let dict = PyDict::new(py); dict.set_item("severity", record.severity)?; @@ -5546,19 +4992,6 @@ fn lat_lng_bounds_core_from_tuple( ) } -fn empty_lat_lng_bounds() -> sys::mln_lat_lng_bounds { - sys::mln_lat_lng_bounds { - southwest: sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0, - }, - northeast: sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0, - }, - } -} - fn lat_lng_to_py(py: Python<'_>, coordinate: sys::mln_lat_lng) -> PyResult> { let dict = PyDict::new(py); dict.set_item("latitude", coordinate.latitude)?; @@ -6514,17 +5947,6 @@ fn style_image_info_to_py(py: Python<'_>, info: &sys::mln_style_image_info) -> P Ok(dict.into_any().unbind()) } -fn style_image_to_py( - py: Python<'_>, - info: &sys::mln_style_image_info, - pixels: &[u8], -) -> PyResult> { - let dict = PyDict::new(py); - dict.set_item("info", style_image_info_to_py(py, info)?)?; - dict.set_item("data", PyBytes::new(py, pixels))?; - Ok(dict.into_any().unbind()) -} - fn resource_request_to_py( py: Python<'_>, request: maplibre_core::ResourceRequest, @@ -6598,538 +6020,397 @@ fn resource_response_status_from_raw(raw: u32) -> PyResult PyResult { - // SAFETY: Default constructor takes no arguments and initializes size. - let mut info = unsafe { sys::mln_texture_image_info_default() }; - // SAFETY: The C API validates session. Null data and zero capacity are the - // documented metadata probe path, with info pointing to initialized storage. - let status = unsafe { - sys::mln_texture_read_premultiplied_rgba8(session, std::ptr::null_mut(), 0, &mut info) - }; - if status == sys::MLN_STATUS_OK - || (status == sys::MLN_STATUS_INVALID_ARGUMENT && info.byte_length > 0) - { - Ok(maplibre_core::values::texture_image_info_from_native(&info)) +fn acquire_frame(session: sys::mln_render_session) -> PyResult { + let mut frame = sys::mln_acquired_frame(0); + maplibre_core::check(unsafe { sys::mln_render_session_acquire_frame(session, &mut frame) }) + .map_err(map_error)?; + Ok(frame) +} + +fn live_acquired_frame( + frame: &Mutex, +) -> PyResult { + let frame = *frame + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if frame.0 == 0 { + Err(invalid_state_error("acquired frame handle is closed")) } else { - Err(map_error(Error::from_status(status))) + Ok(frame) } } -fn read_texture_image_raw( - session: sys::mln_render_session, - data: *mut u8, - capacity: usize, -) -> PyResult { - // SAFETY: Default constructor takes no arguments and initializes size. - let mut info = unsafe { sys::mln_texture_image_info_default() }; - // SAFETY: The caller guarantees data points to capacity writable bytes or - // is null for an empty buffer. The C API validates session and capacity. - maplibre_core::check(unsafe { - sys::mln_texture_read_premultiplied_rgba8(session, data, capacity, &mut info) - }) - .map_err(map_error)?; - Ok(maplibre_core::values::texture_image_info_from_native(&info)) +fn release_acquired_frame( + frame: &Mutex, + kind: u32, + object: usize, + value: u64, +) -> PyResult<()> { + let mut frame = frame + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if frame.0 == 0 { + return Err(invalid_state_error("acquired frame handle is closed")); + } + let mut sync = unsafe { sys::mln_gpu_sync_default() }; + sync.kind = kind; + sync.object = object as *mut c_void; + sync.value = value; + maplibre_core::check(unsafe { sys::mln_acquired_frame_release(&mut *frame, &sync) }) + .map_err(map_error) } -fn read_texture_image_into( - session: sys::mln_render_session, - data: &mut [u8], -) -> PyResult { - let data_ptr = if data.is_empty() { - std::ptr::null_mut() - } else { - data.as_mut_ptr() - }; - read_texture_image_raw(session, data_ptr, data.len()) +fn release_acquired_frame_on_drop(frame: &Mutex) { + let _ = release_acquired_frame(frame, sys::MLN_GPU_SYNC_CPU_COMPLETE, 0, 0); } -impl MetalOwnedTextureFrameRaw { - fn from_native(raw: &sys::mln_metal_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - texture_address: raw.texture as usize, - device_address: raw.device as usize, - pixel_format: raw.pixel_format, - } - } - - fn to_native(self) -> sys::mln_metal_owned_texture_frame { - sys::mln_metal_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: self.generation, - width: self.width, - height: self.height, - scale_factor: self.scale_factor, - frame_id: self.frame_id, - texture: self.texture_address as *mut c_void, - device: self.device_address as *mut c_void, - pixel_format: self.pixel_format, - } - } +fn producer_sync_to_py(py: Python<'_>, frame: sys::mln_acquired_frame) -> PyResult> { + let mut sync: sys::mln_gpu_sync = unsafe { std::mem::zeroed() }; + sync.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_producer_sync(frame, &mut sync) }) + .map_err(map_error)?; + let dict = PyDict::new(py); + dict.set_item("kind", sync.kind)?; + dict.set_item("object_address", sync.object as usize)?; + dict.set_item("value", sync.value)?; + Ok(dict.into_any().unbind()) } -impl VulkanOwnedTextureFrameRaw { - fn from_native(raw: &sys::mln_vulkan_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - image_address: raw.image as usize, - image_view_address: raw.image_view as usize, - device_address: raw.device as usize, - format: raw.format, - layout: raw.layout, - } - } - - fn to_native(self) -> sys::mln_vulkan_owned_texture_frame { - sys::mln_vulkan_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: self.generation, - width: self.width, - height: self.height, - scale_factor: self.scale_factor, - frame_id: self.frame_id, - image: self.image_address as *mut c_void, - image_view: self.image_view_address as *mut c_void, - device: self.device_address as *mut c_void, - format: self.format, - layout: self.layout, - } - } +fn acquired_frame_result_to_py( + py: Python<'_>, + frame: sys::mln_acquired_frame, +) -> PyResult> { + let mut result: sys::mln_render_frame_result = unsafe { std::mem::zeroed() }; + result.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_result(frame, &mut result) }) + .map_err(map_error)?; + render_frame_result_to_py(py, &result) } -impl OpenGLOwnedTextureFrameRaw { - fn from_native(raw: &sys::mln_opengl_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - texture: raw.texture, - target: raw.target, - internal_format: raw.internal_format, - format: raw.format, - type_: raw.type_, - } +#[pymethods] +impl MetalOwnedTextureFrameHandle { + fn release(&self, kind: u32, object_address: usize, value: u64) -> PyResult<()> { + release_acquired_frame(&self.frame, kind, object_address, value) } - fn to_native(self) -> sys::mln_opengl_owned_texture_frame { - sys::mln_opengl_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: self.generation, - width: self.width, - height: self.height, - scale_factor: self.scale_factor, - frame_id: self.frame_id, - texture: self.texture, - target: self.target, - internal_format: self.internal_format, - format: self.format, - type_: self.type_, - } + fn producer_sync(&self, py: Python<'_>) -> PyResult> { + producer_sync_to_py(py, live_acquired_frame(&self.frame)?) } -} -fn empty_metal_owned_texture_frame() -> sys::mln_metal_owned_texture_frame { - sys::mln_metal_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - texture: std::ptr::null_mut(), - device: std::ptr::null_mut(), - pixel_format: 0, - } -} - -fn empty_vulkan_owned_texture_frame() -> sys::mln_vulkan_owned_texture_frame { - sys::mln_vulkan_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - image: std::ptr::null_mut(), - image_view: std::ptr::null_mut(), - device: std::ptr::null_mut(), - format: 0, - layout: 0, - } -} - -fn empty_opengl_owned_texture_frame() -> sys::mln_opengl_owned_texture_frame { - sys::mln_opengl_owned_texture_frame { - size: std::mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - texture: 0, - target: 0, - internal_format: 0, - format: 0, - type_: 0, + fn result(&self, py: Python<'_>) -> PyResult> { + acquired_frame_result_to_py(py, live_acquired_frame(&self.frame)?) } -} -impl OwnedTextureFrameAcquisitionGuard { - fn metal(session: Arc>, raw: MetalOwnedTextureFrameRaw) -> Self { - Self { - session, - frame: Some(OwnedTextureFrameRelease::Metal(raw)), - } + fn frame(&self, py: Python<'_>) -> PyResult> { + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_metal_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_metal_texture(frame, &mut raw) }) + .map_err(map_error)?; + let dict = PyDict::new(py); + dict.set_item("generation", raw.generation)?; + dict.set_item("width", raw.width)?; + dict.set_item("height", raw.height)?; + dict.set_item("scale_factor", raw.scale_factor)?; + dict.set_item("frame_id", raw.frame_id)?; + dict.set_item("pixel_format", raw.pixel_format)?; + Ok(dict.into_any().unbind()) } - fn vulkan(session: Arc>, raw: VulkanOwnedTextureFrameRaw) -> Self { - Self { - session, - frame: Some(OwnedTextureFrameRelease::Vulkan(raw)), - } + fn texture_address(&self) -> PyResult { + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_metal_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_metal_texture(frame, &mut raw) }) + .map_err(map_error)?; + Ok(raw.texture as usize) } - fn opengl(session: Arc>, raw: OpenGLOwnedTextureFrameRaw) -> Self { - Self { - session, - frame: Some(OwnedTextureFrameRelease::OpenGL(raw)), - } + fn device_address(&self) -> PyResult { + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_metal_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_metal_texture(frame, &mut raw) }) + .map_err(map_error)?; + Ok(raw.device as usize) } - fn disarm(&mut self) { - self.frame = None; + #[getter] + fn closed(&self) -> bool { + self.frame + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .0 + == 0 } } -impl Drop for OwnedTextureFrameAcquisitionGuard { +impl Drop for MetalOwnedTextureFrameHandle { fn drop(&mut self) { - let Some(frame) = self.frame.take() else { - return; - }; - let mut session = self - .session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - match frame { - OwnedTextureFrameRelease::Metal(raw) => { - let raw = raw.to_native(); - // SAFETY: raw reconstructs the frame returned by a successful - // native acquire call whose Python object construction failed. - let _ = maplibre_core::check(unsafe { - sys::mln_metal_owned_texture_release_frame(session.native(), &raw) - }); - } - OwnedTextureFrameRelease::Vulkan(raw) => { - let raw = raw.to_native(); - // SAFETY: raw reconstructs the frame returned by a successful - // native acquire call whose Python object construction failed. - let _ = maplibre_core::check(unsafe { - sys::mln_vulkan_owned_texture_release_frame(session.native(), &raw) - }); - } - OwnedTextureFrameRelease::OpenGL(raw) => { - let raw = raw.to_native(); - // SAFETY: raw reconstructs the frame returned by a successful - // native acquire call whose Python object construction failed. - let _ = maplibre_core::check(unsafe { - sys::mln_opengl_owned_texture_release_frame(session.native(), &raw) - }); - } - } - session.frame_acquired = false; + release_acquired_frame_on_drop(&self.frame); } } #[pymethods] -impl MetalOwnedTextureFrameHandle { - fn close(&self) -> PyResult<()> { - let mut closed = self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if *closed { - return Ok(()); - } - let mut session = self - .session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let raw = self.raw.to_native(); - // SAFETY: raw reconstructs the frame returned by the successful native - // acquire call for this session and has not been released yet. - maplibre_core::check(unsafe { - sys::mln_metal_owned_texture_release_frame(session.native(), &raw) - }) - .map_err(map_error)?; - session.frame_acquired = false; - *closed = true; - Ok(()) +impl VulkanOwnedTextureFrameHandle { + fn release(&self, kind: u32, object_address: usize, value: u64) -> PyResult<()> { + release_acquired_frame(&self.frame, kind, object_address, value) + } + + fn producer_sync(&self, py: Python<'_>) -> PyResult> { + producer_sync_to_py(py, live_acquired_frame(&self.frame)?) + } + + fn result(&self, py: Python<'_>) -> PyResult> { + acquired_frame_result_to_py(py, live_acquired_frame(&self.frame)?) } fn frame(&self, py: Python<'_>) -> PyResult> { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - return Err(invalid_state_error( - "MetalOwnedTextureFrameHandle is closed", - )); - } + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_vulkan_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_vulkan_texture(frame, &mut raw) + }) + .map_err(map_error)?; let dict = PyDict::new(py); - dict.set_item("generation", self.raw.generation)?; - dict.set_item("width", self.raw.width)?; - dict.set_item("height", self.raw.height)?; - dict.set_item("scale_factor", self.raw.scale_factor)?; - dict.set_item("frame_id", self.raw.frame_id)?; - dict.set_item("pixel_format", self.raw.pixel_format)?; + dict.set_item("generation", raw.generation)?; + dict.set_item("width", raw.width)?; + dict.set_item("height", raw.height)?; + dict.set_item("scale_factor", raw.scale_factor)?; + dict.set_item("frame_id", raw.frame_id)?; + dict.set_item("format", raw.format)?; + dict.set_item("layout", raw.layout)?; Ok(dict.into_any().unbind()) } - fn texture_address(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "MetalOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.texture_address) - } + fn image_address(&self) -> PyResult { + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_vulkan_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_vulkan_texture(frame, &mut raw) + }) + .map_err(map_error)?; + Ok(raw.image as usize) + } + + fn image_view_address(&self) -> PyResult { + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_vulkan_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_vulkan_texture(frame, &mut raw) + }) + .map_err(map_error)?; + Ok(raw.image_view as usize) } fn device_address(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "MetalOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.device_address) - } + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_vulkan_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_vulkan_texture(frame, &mut raw) + }) + .map_err(map_error)?; + Ok(raw.device as usize) } #[getter] fn closed(&self) -> bool { - *self - .closed + self.frame .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) + .0 + == 0 } } -impl Drop for MetalOwnedTextureFrameHandle { +impl Drop for VulkanOwnedTextureFrameHandle { fn drop(&mut self) { - // Python finalization may run off the owner thread, so frame release is - // explicit through close(). + release_acquired_frame_on_drop(&self.frame); } } #[pymethods] -impl VulkanOwnedTextureFrameHandle { - fn close(&self) -> PyResult<()> { - let mut closed = self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if *closed { - return Ok(()); - } - let mut session = self - .session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let raw = self.raw.to_native(); - // SAFETY: raw reconstructs the frame returned by the successful native - // acquire call for this session and has not been released yet. - maplibre_core::check(unsafe { - sys::mln_vulkan_owned_texture_release_frame(session.native(), &raw) - }) - .map_err(map_error)?; - session.frame_acquired = false; - *closed = true; - Ok(()) +impl WebGPUOwnedTextureFrameHandle { + fn release(&self, kind: u32, object_address: usize, value: u64) -> PyResult<()> { + release_acquired_frame(&self.frame, kind, object_address, value) + } + + fn producer_sync(&self, py: Python<'_>) -> PyResult> { + producer_sync_to_py(py, live_acquired_frame(&self.frame)?) + } + + fn result(&self, py: Python<'_>) -> PyResult> { + acquired_frame_result_to_py(py, live_acquired_frame(&self.frame)?) } fn frame(&self, py: Python<'_>) -> PyResult> { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - return Err(invalid_state_error( - "VulkanOwnedTextureFrameHandle is closed", - )); - } + let raw = webgpu_owned_texture_frame(&self.frame)?; let dict = PyDict::new(py); - dict.set_item("generation", self.raw.generation)?; - dict.set_item("width", self.raw.width)?; - dict.set_item("height", self.raw.height)?; - dict.set_item("scale_factor", self.raw.scale_factor)?; - dict.set_item("frame_id", self.raw.frame_id)?; - dict.set_item("format", self.raw.format)?; - dict.set_item("layout", self.raw.layout)?; + dict.set_item("generation", raw.generation)?; + dict.set_item("width", raw.width)?; + dict.set_item("height", raw.height)?; + dict.set_item("scale_factor", raw.scale_factor)?; + dict.set_item("frame_id", raw.frame_id)?; + dict.set_item("format", raw.format)?; Ok(dict.into_any().unbind()) } - - fn image_address(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "VulkanOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.image_address) - } + + fn texture_address(&self) -> PyResult { + Ok(webgpu_owned_texture_frame(&self.frame)?.texture as usize) } - fn image_view_address(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "VulkanOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.image_view_address) - } + fn texture_view_address(&self) -> PyResult { + Ok(webgpu_owned_texture_frame(&self.frame)?.texture_view as usize) } fn device_address(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "VulkanOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.device_address) - } + Ok(webgpu_owned_texture_frame(&self.frame)?.device as usize) } #[getter] fn closed(&self) -> bool { - *self - .closed + self.frame .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) + .0 + == 0 } } -impl Drop for VulkanOwnedTextureFrameHandle { +impl Drop for WebGPUOwnedTextureFrameHandle { fn drop(&mut self) { - // Python finalization may run off the owner thread, so frame release is - // explicit through close(). + release_acquired_frame_on_drop(&self.frame); } } +fn webgpu_owned_texture_frame( + frame: &Mutex, +) -> PyResult { + let frame = live_acquired_frame(frame)?; + let mut raw: sys::mln_webgpu_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_acquired_frame_get_webgpu_texture(frame, &mut raw) }) + .map_err(map_error)?; + Ok(raw) +} + #[pymethods] impl OpenGLOwnedTextureFrameHandle { - fn close(&self) -> PyResult<()> { - let mut closed = self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if *closed { - return Ok(()); - } - let mut session = self - .session - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let raw = self.raw.to_native(); - // SAFETY: raw reconstructs the frame returned by the successful native - // acquire call for this session and has not been released yet. - maplibre_core::check(unsafe { - sys::mln_opengl_owned_texture_release_frame(session.native(), &raw) - }) - .map_err(map_error)?; - session.frame_acquired = false; - *closed = true; - Ok(()) + fn release(&self, kind: u32, object_address: usize, value: u64) -> PyResult<()> { + release_acquired_frame(&self.frame, kind, object_address, value) + } + + fn producer_sync(&self, py: Python<'_>) -> PyResult> { + producer_sync_to_py(py, live_acquired_frame(&self.frame)?) + } + + fn result(&self, py: Python<'_>) -> PyResult> { + acquired_frame_result_to_py(py, live_acquired_frame(&self.frame)?) } fn frame(&self, py: Python<'_>) -> PyResult> { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - return Err(invalid_state_error( - "OpenGLOwnedTextureFrameHandle is closed", - )); - } + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_opengl_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_opengl_texture(frame, &mut raw) + }) + .map_err(map_error)?; let dict = PyDict::new(py); - dict.set_item("generation", self.raw.generation)?; - dict.set_item("width", self.raw.width)?; - dict.set_item("height", self.raw.height)?; - dict.set_item("scale_factor", self.raw.scale_factor)?; - dict.set_item("frame_id", self.raw.frame_id)?; - dict.set_item("target", self.raw.target)?; - dict.set_item("internal_format", self.raw.internal_format)?; - dict.set_item("format", self.raw.format)?; - dict.set_item("type", self.raw.type_)?; + dict.set_item("generation", raw.generation)?; + dict.set_item("width", raw.width)?; + dict.set_item("height", raw.height)?; + dict.set_item("scale_factor", raw.scale_factor)?; + dict.set_item("frame_id", raw.frame_id)?; + dict.set_item("target", raw.target)?; + dict.set_item("internal_format", raw.internal_format)?; + dict.set_item("format", raw.format)?; + dict.set_item("type", raw.type_)?; Ok(dict.into_any().unbind()) } fn texture(&self) -> PyResult { - if *self - .closed - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - { - Err(invalid_state_error( - "OpenGLOwnedTextureFrameHandle is closed", - )) - } else { - Ok(self.raw.texture) - } + let frame = live_acquired_frame(&self.frame)?; + let mut raw: sys::mln_opengl_owned_texture_frame = unsafe { std::mem::zeroed() }; + raw.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_opengl_texture(frame, &mut raw) + }) + .map_err(map_error)?; + Ok(raw.texture) } #[getter] fn closed(&self) -> bool { - *self - .closed + self.frame .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) + .0 + == 0 } } impl Drop for OpenGLOwnedTextureFrameHandle { fn drop(&mut self) { - // Python finalization may run off the owner thread, so frame release is - // explicit through close(). + release_acquired_frame_on_drop(&self.frame); } } +fn render_frame_result_to_py( + py: Python<'_>, + value: &sys::mln_render_frame_result, +) -> PyResult> { + let dict = PyDict::new(py); + dict.set_item("disposition", value.disposition)?; + dict.set_item("token", value.token)?; + dict.set_item("map_update_generation", value.map_update_generation)?; + dict.set_item("extent_generation", value.extent_generation)?; + dict.set_item("frame_generation", value.frame_generation)?; + dict.set_item("needs_repaint", value.needs_repaint)?; + Ok(dict.into_any().unbind()) +} + +fn render_session_snapshot_to_py( + py: Python<'_>, + value: &sys::mln_render_session_snapshot, +) -> PyResult> { + let dict = PyDict::new(py); + dict.set_item("state", value.state)?; + dict.set_item("driver", value.driver)?; + dict.set_item("latest_result", value.latest_result)?; + dict.set_item( + "extent", + ( + value.extent.width, + value.extent.height, + value.extent.scale_factor, + ), + )?; + dict.set_item("generation", value.generation)?; + dict.set_item("map_update_generation", value.map_update_generation)?; + dict.set_item( + "rendered_update_generation", + value.rendered_update_generation, + )?; + dict.set_item("extent_generation", value.extent_generation)?; + dict.set_item("frame_generation", value.frame_generation)?; + dict.set_item("latest_demand_token", value.latest_demand_token)?; + dict.set_item("pending_demand_count", value.pending_demand_count)?; + dict.set_item("acquired_frame_count", value.acquired_frame_count)?; + dict.set_item("target_ready", value.target_ready)?; + dict.set_item("pending_changes", value.pending_changes)?; + Ok(dict.into_any().unbind()) +} + fn map_error(error: Error) -> PyErr { let raw_status = error.raw_status(); let diagnostic = error.diagnostic().to_owned(); match error.kind() { + ErrorKind::NotFound => py_errors::NotFoundError::new_err((raw_status, diagnostic)), ErrorKind::InvalidArgument => { py_errors::InvalidArgumentError::new_err((raw_status, diagnostic)) } @@ -7139,6 +6420,10 @@ fn map_error(error: Error) -> PyErr { py_errors::UnsupportedFeatureError::new_err((raw_status, diagnostic)) } ErrorKind::NativeError => py_errors::NativeError::new_err((raw_status, diagnostic)), + ErrorKind::Cancelled => py_errors::CancelledError::new_err((raw_status, diagnostic)), + ErrorKind::Busy => py_errors::BusyError::new_err((raw_status, diagnostic)), + ErrorKind::TargetLost => py_errors::TargetLostError::new_err((raw_status, diagnostic)), + ErrorKind::NotReady => py_errors::NotReadyError::new_err((raw_status, diagnostic)), ErrorKind::UnknownStatus => { py_errors::UnknownStatusError::new_err((raw_status.unwrap_or_default(), diagnostic)) } @@ -7220,20 +6505,24 @@ fn set_log_callback(max_queued_records: usize, consume: bool) -> PyResult(); - // SAFETY: log_callback_trampoline has the C callback ABI. user_data points - // to replacement, which is retained by LOG_CALLBACK_STATE after success. - maplibre_core::check(unsafe { - sys::mln_log_set_callback(Some(log_callback_trampoline), user_data) + let user_data = Arc::into_raw(Arc::clone(&replacement)) + .cast_mut() + .cast::(); + // SAFETY: native owns one Arc strong reference after success and releases + // it after the final callback returns. + let result = maplibre_core::check(unsafe { + sys::mln_log_set_callback( + Some(log_callback_trampoline), + user_data, + Some(release_log_callback_state), + ) }) - .map_err(map_error)?; - let mut state = LOG_CALLBACK_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if let Some(previous) = state.current.take() { - state.retired.push(previous); + .map_err(map_error); + if let Err(error) = result { + // SAFETY: a failed registration does not take ownership. + drop(unsafe { Arc::from_raw(user_data.cast::()) }); + return Err(error); } - state.current = Some(Arc::clone(&replacement)); Ok(LogReceiver { state: replacement }) } @@ -7241,14 +6530,7 @@ fn set_log_callback(max_queued_records: usize, consume: bool) -> PyResult PyResult<()> { // SAFETY: mln_log_clear_callback takes no arguments and clears native's // process-global callback slot. - maplibre_core::check(unsafe { sys::mln_log_clear_callback() }).map_err(map_error)?; - let mut state = LOG_CALLBACK_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if let Some(previous) = state.current.take() { - state.retired.push(previous); - } - Ok(()) + maplibre_core::check(unsafe { sys::mln_log_clear_callback() }).map_err(map_error) } #[pyfunction] @@ -7287,6 +6569,15 @@ unsafe extern "C" fn log_callback_trampoline( .unwrap_or(0) } +unsafe extern "C" fn release_log_callback_state(user_data: *mut c_void) { + let _ = catch_unwind(AssertUnwindSafe(|| { + if let Some(state) = ptr::NonNull::new(user_data.cast::()) { + // SAFETY: native calls this once for the Arc transferred on install. + drop(unsafe { Arc::from_raw(state.as_ptr()) }); + } + })); +} + #[pyfunction] fn projected_meters_for_lat_lng( py: Python<'_>, @@ -7341,47 +6632,18 @@ fn set_network_status_raw(raw_status: u32) -> PyResult<()> { maplibre_core::set_network_status(NetworkStatus::from_raw(raw_status)).map_err(map_error) } -/// Test helper that calls the C size accessor with a raw map id, which the safe -/// API cannot express: a test can replay a released id or one from another -/// thread. -#[pyfunction] -fn map_size_by_id_for_test(py: Python<'_>, id: u64) -> PyResult<(u32, u32, f64)> { - py.detach(|| { - let mut width = 0u32; - let mut height = 0u32; - let mut scale_factor = 0.0f64; - // SAFETY: the id is well-formed and the out-pointers are live locals; - // the C API resolves the id rather than dereferencing it. - let status = unsafe { - sys::mln_map_get_size(sys::mln_map(id), &mut width, &mut height, &mut scale_factor) - }; - if status == sys::MLN_STATUS_OK { - Ok((width, height, scale_factor)) - } else { - Err(map_error(Error::from_status_and_diagnostic( - status, - maplibre_native_ffi_core::error::capture_thread_diagnostic(), - ))) - } - }) -} - -/// Test helper that passes a map id where a runtime id belongs. The two are -/// distinct newtypes, so this call has no expression in the safe API. +/// Test helper that snapshots a raw map id, which the safe API cannot express. #[pyfunction] -fn pump_runtime_with_map_id_for_test(py: Python<'_>, id: u64) -> PyResult<()> { - py.detach(|| { - // SAFETY: the value is well-formed; the C API rejects it on its kind tag. - let status = unsafe { sys::mln_runtime_pump(sys::mln_runtime(id), 0, -1) }; - if status == sys::MLN_STATUS_OK { - Ok(()) - } else { - Err(map_error(Error::from_status_and_diagnostic( - status, - maplibre_native_ffi_core::error::capture_thread_diagnostic(), - ))) - } - }) +fn map_size_by_id_for_test(id: u64) -> PyResult<(u32, u32, f64)> { + let mut snapshot: sys::mln_map_snapshot = unsafe { std::mem::zeroed() }; + snapshot.size = std::mem::size_of::() as u32; + maplibre_core::check(unsafe { sys::mln_map_snapshot_get(sys::mln_map(id), &mut snapshot) }) + .map_err(map_error)?; + Ok(( + snapshot.logical_extent.width, + snapshot.logical_extent.height, + snapshot.logical_extent.scale_factor, + )) } /// Test helper that lets native status conversion see C validation failures. @@ -7418,12 +6680,14 @@ fn status_error_after_support_call_for_test(raw_status: i32, diagnostic: String) /// Test helper that validates an injected ABI version before creating a runtime. #[pyfunction] fn create_runtime_with_abi_version_for_test( + py: Python<'_>, actual_abi_version: u32, asset_path: Option, cache_path: Option, ) -> PyResult { maplibre_core::validate_abi_version_value(actual_abi_version).map_err(map_error)?; create_runtime( + py, asset_path, cache_path, maplibre_core::RuntimeEventMask::ALL.bits(), @@ -7472,7 +6736,7 @@ fn synthetic_event_storage(specs: &[SyntheticEvent]) -> (Vec) -> PyResult> ]; let (mut events, mut messages) = synthetic_event_storage(&specs); - let batch = sys::mln_runtime_event_batch { - size: std::mem::size_of::() as u32, + let batch = sys::mln_runtime_event_batch_view { + size: std::mem::size_of::() as u32, event_size: std::mem::size_of::() as u32, events: events.as_ptr(), event_count: events.len(), messages: messages.as_ptr().cast(), messages_size: messages.len(), - // A bounded drain would report a rest, so carry one through the wire. - remaining_count: 3, }; // SAFETY: batch names the array and arena built above, which outlive this @@ -7634,7 +6896,7 @@ fn synthetic_runtime_event_batch_for_test(py: Python<'_>) -> PyResult> payload: opaque_event_payload(&[0xFF; 8]), }); messages.fill(b'x'); - event_batch_to_py(py, copied, batch.remaining_count) + event_batch_to_py(py, copied) } /// Reports the event stride a drain of `runtime` names and the stride this @@ -7642,76 +6904,21 @@ fn synthetic_runtime_event_batch_for_test(py: Python<'_>) -> PyResult> /// by the batch's own value. #[pyfunction] fn runtime_event_stride_for_test(runtime: &RuntimeHandle) -> PyResult<(u32, u32)> { - let state = runtime.state_for_operation()?; - // SAFETY: The default constructor takes no arguments and fills in size. - let mut batch = unsafe { sys::mln_runtime_event_batch_default() }; - // SAFETY: The C API validates the runtime handle and owner-thread affinity, - // and batch carries this header's size field. - maplibre_core::check(unsafe { sys::mln_runtime_drain_events(state.handle(), 0, &mut batch) }) + let runtime = runtime.native()?; + let mut batch = sys::mln_event_batch(0); + maplibre_core::check(unsafe { sys::mln_runtime_drain_events(runtime, &mut batch) }) .map_err(map_error)?; + let mut view: sys::mln_runtime_event_batch_view = unsafe { std::mem::zeroed() }; + view.size = std::mem::size_of::() as u32; + let status = unsafe { sys::mln_event_batch_get(batch, &mut view) }; + unsafe { sys::mln_event_batch_release(batch) }; + maplibre_core::check(status).map_err(map_error)?; Ok(( - batch.event_size, + view.event_size, std::mem::size_of::() as u32, )) } -/// Probes the required byte length, then copies the layer text into a `String`. -/// -/// # Safety -/// -/// `copy` must forward its arguments to a C entry point that writes at most -/// `capacity` bytes through the text pointer and the required length through the -/// size pointer. -unsafe fn copy_text( - copy: impl Fn(*mut c_char, usize, *mut usize) -> sys::mln_status, -) -> PyResult { - let mut required = 0; - maplibre_core::check(copy(std::ptr::null_mut(), 0, &mut required)).map_err(map_error)?; - if required == 0 { - return Ok(String::new()); - } - - let mut bytes = vec![0u8; required]; - let mut copied = 0; - maplibre_core::check(copy( - bytes.as_mut_ptr().cast::(), - bytes.len(), - &mut copied, - )) - .map_err(map_error)?; - bytes.truncate(copied.min(bytes.len())); - String::from_utf8(bytes) - .map_err(|error| invalid_argument_error(format!("native text is not UTF-8: {error}"))) -} - -unsafe fn copy_bytes( - py: Python<'_>, - copy: impl Fn(*mut u8, usize, *mut usize) -> sys::mln_status, -) -> PyResult> { - let mut required = 0; - maplibre_core::check(copy(ptr::null_mut(), 0, &mut required)).map_err(map_error)?; - let mut bytes = vec![0u8; required]; - let mut copied = 0; - maplibre_core::check(copy( - if bytes.is_empty() { - ptr::null_mut() - } else { - bytes.as_mut_ptr() - }, - bytes.len(), - &mut copied, - )) - .map_err(map_error)?; - bytes.truncate(copied.min(bytes.len())); - Ok(PyBytes::new(py, &bytes).unbind()) -} - -fn owned_buffer_to_py(py: Python<'_>, buffer: sys::mln_buffer) -> PyResult> { - // SAFETY: The buffer is an owned handle returned by the C API. - let bytes = unsafe { maplibre_core::string::copy_owned_buffer(buffer) }.map_err(map_error)?; - Ok(PyBytes::new(py, &bytes).unbind()) -} - /// Returns the raw event mask a runtime selects by default. /// /// The value carries every bit the linked library's creation default selects, @@ -7732,51 +6939,59 @@ fn map_options_default_event_mask() -> u64 { maplibre_core::MapOptions::default().event_mask.bits() } -/// Creates a runtime handle on the current thread. +/// Creates a runtime and blocks without holding the GIL until native startup completes. #[pyfunction] fn create_runtime( + py: Python<'_>, asset_path: Option, cache_path: Option, event_mask: u64, ) -> PyResult { maplibre_core::validate_abi_version().map_err(map_error)?; let mut options = maplibre_core::RuntimeOptions::default(); - if let Some(asset_path) = asset_path { - options.asset_path = Some(asset_path); - } - if let Some(cache_path) = cache_path { - options.cache_path = Some(cache_path); - } + options.asset_path = asset_path; + options.cache_path = cache_path; let native_options = maplibre_core::runtime::runtime_options_to_native(&options).map_err(map_error)?; let mut raw_options = native_options.to_raw(); - // The Python layer always selects a mask, defaulting to every type. The C - // API validates the bits. raw_options.event_mask = event_mask; + let event_wake = Arc::new(Mutex::new(None)); + let event_wake_native = Box::into_raw(Box::new(Arc::clone(&event_wake))); + raw_options.event_wake = sys::mln_wake { + size: std::mem::size_of::() as u32, + callback: Some(event_wake_trampoline), + user_data: event_wake_native.cast(), + release_user_data: Some(release_event_wake), + }; + let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: raw_options points to a materialized mln_runtime_options whose - // backing strings live for this call, and out is a null-initialized - // out-pointer owned by this call. - maplibre_core::check(unsafe { sys::mln_runtime_create(&raw_options, out.as_mut_ptr()) }) - .map_err(map_error)?; + let options_address = std::ptr::addr_of!(raw_options) as usize; + let out_address = out.as_mut_ptr() as usize; + let create_status = py.detach(move || unsafe { + sys::mln_runtime_create( + options_address as *const sys::mln_runtime_options, + out_address as *mut sys::mln_runtime, + ) + }); + if let Err(error) = maplibre_core::check(create_status) { + // SAFETY: rejected creation retains no wake state. + drop(unsafe { Box::from_raw(event_wake_native) }); + return Err(map_error(error)); + } let native = out.into_live("mln_runtime").map_err(map_error)?; - // SAFETY: native came from successful mln_runtime_create and is paired with - // the matching status-returning destroy function in RuntimeHandle.close. let state = unsafe { maplibre_core::handle::NativeHandleState::from_handle(native, "mln_runtime") } .map_err(map_error)?; Ok(RuntimeHandle { state: Mutex::new(state), - operation_gate: RuntimeOperationGate::new(), - resource_provider: Mutex::new(None), - resource_transform: Mutex::new(None), - http_header_transform: Mutex::new(None), + event_wake, }) } /// Creates a map handle owned by a runtime. #[pyfunction] fn create_map( + py: Python<'_>, runtime: &RuntimeHandle, width: Option, height: Option, @@ -7784,9 +6999,7 @@ fn create_map( map_mode: Option, fast_pfor_enabled: Option, event_mask: u64, -) -> PyResult { - // Default() reads mln_map_options_default(), so an unset argument keeps the - // C creation default. +) -> PyResult> { let mut options = maplibre_core::MapOptions::default(); if let Some(width) = width { options.width = width; @@ -7805,50 +7018,73 @@ fn create_map( } let mut raw_options = maplibre_core::options::map_options_to_native(&options).map_err(map_error)?; - // The Python layer always selects a mask, defaulting to every type. The C - // API validates the bits. raw_options.event_mask = event_mask; - let runtime_state = runtime.state_for_operation()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: runtime_state owns a runtime pointer that passed the binding - // lifecycle gate and the C API validates that it is live. raw_options is - // fully initialized, and out is a null-initialized out-pointer. - maplibre_core::check(unsafe { - sys::mln_map_create(runtime_state.handle(), &raw_options, out.as_mut_ptr()) - }) - .map_err(map_error)?; - let native = out.into_live("mln_map").map_err(map_error)?; - // SAFETY: native came from successful mln_map_create and is paired with the - // matching status-returning destroy function in MapHandle.close. - let state = unsafe { maplibre_core::handle::NativeHandleState::from_handle(native, "mln_map") } - .map_err(map_error)?; - Ok(MapHandle { - state: Mutex::new(state), - }) + let runtime = runtime.native()?; + submit_python_future( + py, + |completion| unsafe { sys::mln_map_create(runtime, &raw_options, completion) }, + |py, result| { + let native = completion_value::(result)?; + let state = + unsafe { maplibre_core::handle::NativeHandleState::from_handle(native, "mln_map") } + .map_err(map_error)?; + Ok(Py::new( + py, + MapHandle { + state: Mutex::new(state), + }, + )? + .into_any()) + }, + ) } -fn attach_render_session(map: &MapHandle, attach: F) -> PyResult +fn attach_render_session( + py: Python<'_>, + map: &MapHandle, + driver: u32, + texture_ring_depth: u32, + attach: F, +) -> PyResult<(RenderSessionHandle, Py)> where - F: FnOnce(sys::mln_map, *mut sys::mln_render_session) -> sys::mln_status, + F: FnOnce( + sys::mln_map, + *const sys::mln_render_session_attach_options, + *mut sys::mln_render_session, + *const sys::mln_completion, + ) -> sys::mln_status, { let map_state = map.state(); + let mut options = unsafe { sys::mln_render_session_attach_options_default() }; + options.driver = driver; + options.requested_texture_ring_depth = texture_ring_depth; let mut out = maplibre_core::ptr::OutHandle::::new(); - maplibre_core::check(attach(map_state.handle(), out.as_mut_ptr())).map_err(map_error)?; + let completion = submit_python_future( + py, + |completion| attach(map_state.handle(), &options, out.as_mut_ptr(), completion), + py_none, + )?; let native = out.into_live("mln_render_session").map_err(map_error)?; - Ok(RenderSessionHandle { - state: Arc::new(Mutex::new(RenderSessionState::new(native)?)), - }) + Ok(( + RenderSessionHandle { + state: Arc::new(Mutex::new(RenderSessionState::new(native)?)), + }, + completion, + )) } #[pyfunction] fn attach_metal_surface( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, scale_factor: f64, device_address: usize, layer_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::metal_surface_descriptor_to_native( maplibre_core::render::MetalSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -7862,15 +7098,20 @@ fn attach_metal_surface( layer: layer_address as *mut c_void, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_metal_surface_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_metal_surface_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] fn attach_vulkan_surface( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -7883,7 +7124,9 @@ fn attach_vulkan_surface( get_instance_proc_addr: usize, get_device_proc_addr: usize, surface_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::vulkan_surface_descriptor_to_native( maplibre_core::render::VulkanSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -7903,21 +7146,28 @@ fn attach_vulkan_surface( surface: surface_address as *mut c_void, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_vulkan_surface_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_vulkan_surface_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] fn attach_metal_owned_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, scale_factor: f64, device_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::metal_owned_texture_descriptor_to_native( maplibre_core::render::MetalOwnedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -7930,15 +7180,20 @@ fn attach_metal_owned_texture( }, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_metal_owned_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_metal_owned_texture_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] fn attach_metal_borrowed_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -7946,7 +7201,9 @@ fn attach_metal_borrowed_texture( physical_width: u32, physical_height: u32, texture_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::metal_borrowed_texture_descriptor_to_native( maplibre_core::render::MetalBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -7959,15 +7216,20 @@ fn attach_metal_borrowed_texture( texture: texture_address as *mut c_void, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_metal_borrowed_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_metal_borrowed_texture_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] fn attach_vulkan_owned_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -7979,7 +7241,9 @@ fn attach_vulkan_owned_texture( graphics_queue_family_index: u32, get_instance_proc_addr: usize, get_device_proc_addr: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::vulkan_owned_texture_descriptor_to_native( maplibre_core::render::VulkanOwnedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -7998,16 +7262,21 @@ fn attach_vulkan_owned_texture( ), }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_vulkan_owned_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_vulkan_owned_texture_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] #[allow(clippy::too_many_arguments)] fn attach_vulkan_borrowed_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -8026,7 +7295,9 @@ fn attach_vulkan_borrowed_texture( format: u32, initial_layout: u32, final_layout: u32, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::vulkan_borrowed_texture_descriptor_to_native( maplibre_core::render::VulkanBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -8052,16 +7323,139 @@ fn attach_vulkan_borrowed_texture( final_layout, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_vulkan_borrowed_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_vulkan_borrowed_texture_attach(map, &descriptor, options, session, operation) + }, + ) +} + +#[pyfunction] +#[allow(clippy::too_many_arguments)] +fn attach_webgpu_surface( + py: Python<'_>, + map: &MapHandle, + width: u32, + height: u32, + scale_factor: f64, + instance_address: usize, + device_address: usize, + queue_address: usize, + surface_address: usize, + format: u32, + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { + let descriptor = maplibre_core::render::webgpu_surface_descriptor_to_native( + maplibre_core::render::WebGpuSurfaceDescriptorFields { + extent: maplibre_core::render::RenderTargetExtentFields { + width, + height, + scale_factor, + }, + context: webgpu_context_fields(instance_address, device_address, queue_address), + surface: surface_address as *mut c_void, + format, + }, + ); + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_webgpu_surface_attach(map, &descriptor, options, session, operation) + }, + ) +} + +#[pyfunction] +fn attach_webgpu_owned_texture( + py: Python<'_>, + map: &MapHandle, + width: u32, + height: u32, + scale_factor: f64, + instance_address: usize, + device_address: usize, + queue_address: usize, + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { + let descriptor = maplibre_core::render::webgpu_owned_texture_descriptor_to_native( + maplibre_core::render::WebGpuOwnedTextureDescriptorFields { + extent: maplibre_core::render::RenderTargetExtentFields { + width, + height, + scale_factor, + }, + context: webgpu_context_fields(instance_address, device_address, queue_address), + }, + ); + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_webgpu_owned_texture_attach(map, &descriptor, options, session, operation) + }, + ) +} + +#[pyfunction] +#[allow(clippy::too_many_arguments)] +fn attach_webgpu_borrowed_texture( + py: Python<'_>, + map: &MapHandle, + width: u32, + height: u32, + scale_factor: f64, + physical_width: u32, + physical_height: u32, + instance_address: usize, + device_address: usize, + queue_address: usize, + texture_address: usize, + texture_view_address: usize, + format: u32, + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { + let descriptor = maplibre_core::render::webgpu_borrowed_texture_descriptor_to_native( + maplibre_core::render::WebGpuBorrowedTextureDescriptorFields { + extent: maplibre_core::render::RenderTargetExtentFields { + width, + height, + scale_factor, + }, + physical_width, + physical_height, + context: webgpu_context_fields(instance_address, device_address, queue_address), + texture: texture_address as *mut c_void, + texture_view: texture_view_address as *mut c_void, + format, + }, + ); + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_webgpu_borrowed_texture_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] #[allow(clippy::too_many_arguments)] fn attach_opengl_surface( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -8074,7 +7468,9 @@ fn attach_opengl_surface( client_api: u32, get_proc_address: usize, surface_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::opengl_surface_descriptor_to_native( maplibre_core::render::OpenGLSurfaceDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -8094,16 +7490,21 @@ fn attach_opengl_surface( surface: surface_address as *mut c_void, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_opengl_surface_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_opengl_surface_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] #[allow(clippy::too_many_arguments)] fn attach_opengl_owned_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -8115,7 +7516,9 @@ fn attach_opengl_owned_texture( share_context_address: usize, client_api: u32, get_proc_address: usize, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::opengl_owned_texture_descriptor_to_native( maplibre_core::render::OpenGLOwnedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -8134,16 +7537,21 @@ fn attach_opengl_owned_texture( )?, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_opengl_owned_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_opengl_owned_texture_attach(map, &descriptor, options, session, operation) + }, + ) } #[pyfunction] #[allow(clippy::too_many_arguments)] fn attach_opengl_borrowed_texture( + py: Python<'_>, map: &MapHandle, width: u32, height: u32, @@ -8159,7 +7567,9 @@ fn attach_opengl_borrowed_texture( get_proc_address: usize, texture: u32, target: u32, -) -> PyResult { + driver: u32, + texture_ring_depth: u32, +) -> PyResult<(RenderSessionHandle, Py)> { let descriptor = maplibre_core::render::opengl_borrowed_texture_descriptor_to_native( maplibre_core::render::OpenGLBorrowedTextureDescriptorFields { extent: maplibre_core::render::RenderTargetExtentFields { @@ -8182,11 +7592,27 @@ fn attach_opengl_borrowed_texture( target, }, ); - attach_render_session(map, |map_ptr, out| { - // SAFETY: descriptor is fully initialized and lives for this call. The C - // API validates the map pointer, descriptor fields, and out pointer. - unsafe { sys::mln_opengl_borrowed_texture_attach(map_ptr, &descriptor, out) } - }) + attach_render_session( + py, + map, + driver, + texture_ring_depth, + |map, options, session, operation| unsafe { + sys::mln_opengl_borrowed_texture_attach(map, &descriptor, options, session, operation) + }, + ) +} + +fn webgpu_context_fields( + instance_address: usize, + device_address: usize, + queue_address: usize, +) -> maplibre_core::render::WebGpuContextDescriptorFields { + maplibre_core::render::WebGpuContextDescriptorFields { + instance: instance_address as *mut c_void, + device: device_address as *mut c_void, + queue: queue_address as *mut c_void, + } } fn vulkan_context_fields( @@ -8255,15 +7681,14 @@ fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; - module.add_class::()?; module.add_class::()?; module.add_class::()?; module.add_class::()?; module.add_class::()?; - module.add_class::()?; module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; module.add_function(wrap_pyfunction!(expected_c_abi_version, module)?)?; module.add_function(wrap_pyfunction!(c_version, module)?)?; module.add_function(wrap_pyfunction!(supported_render_backends_raw, module)?)?; @@ -8288,7 +7713,6 @@ fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { module )?)?; module.add_function(wrap_pyfunction!(map_size_by_id_for_test, module)?)?; - module.add_function(wrap_pyfunction!(pump_runtime_with_map_id_for_test, module)?)?; module.add_function(wrap_pyfunction!(status_error_for_test, module)?)?; module.add_function(wrap_pyfunction!( status_error_after_support_call_for_test, @@ -8319,5 +7743,8 @@ fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(attach_opengl_surface, module)?)?; module.add_function(wrap_pyfunction!(attach_opengl_owned_texture, module)?)?; module.add_function(wrap_pyfunction!(attach_opengl_borrowed_texture, module)?)?; + module.add_function(wrap_pyfunction!(attach_webgpu_surface, module)?)?; + module.add_function(wrap_pyfunction!(attach_webgpu_owned_texture, module)?)?; + module.add_function(wrap_pyfunction!(attach_webgpu_borrowed_texture, module)?)?; Ok(()) } diff --git a/bindings/python/tests/render_backend_helpers/runtime.py b/bindings/python/tests/render_backend_helpers/runtime.py index 46b7c21c3..25f64653f 100644 --- a/bindings/python/tests/render_backend_helpers/runtime.py +++ b/bindings/python/tests/render_backend_helpers/runtime.py @@ -3,11 +3,15 @@ import json import time from collections.abc import Callable +from concurrent.futures import Future import maplibre_native_ffi as mln import pytest from maplibre_native_ffi import camera, geo, query, render, style +_DEFAULT_GPU_SYNC = render.GpuSync() + + EMPTY_STYLE_JSON = '{"version":8,"sources":{},"layers":[]}' RED_BACKGROUND_STYLE_JSON = ( @@ -96,7 +100,7 @@ def wait_for_runtime_event( iterations: int = 5000, ) -> mln.RuntimeEvent: for _ in range(iterations): - runtime.pump() + time.sleep(0.001) for event in runtime.drain_events().events: if event.event_type == event_type: return event @@ -104,6 +108,85 @@ def wait_for_runtime_event( raise AssertionError(f"runtime event {event_type!r} was not observed") +def finish_attach( + session: render.RenderSessionHandle, + operation: Future[None], + *, + iterations: int = 5000, +) -> None: + """Complete attachment through its selected native driver.""" + if session.snapshot.driver == render.RenderDriver.CALLER_GRAPHICS_THREAD: + for _ in range(iterations): + session.service_driver_work(16) + if operation.done(): + break + time.sleep(0.001) + else: + operation.result(timeout=5) + return + operation.result(timeout=0) + + +def request_and_finish_frame( + session: render.RenderSessionHandle, + *, + token: int = 1, + flags: render.FrameDemandFlag = render.FrameDemandFlag.IF_NEEDED, + iterations: int = 5000, +) -> render.RenderFrameResult: + """Request one frame and return its owned terminal result. + + Clearing ``IF_NEEDED`` renders even when nothing newer is pending, which + is how a test gets a fresh frame out of a settled style. + """ + session.request_frame(render.FrameDemand(flags=flags, token=token)) + for _ in range(iterations): + if session.snapshot.driver == render.RenderDriver.CALLER_GRAPHICS_THREAD: + session.service_driver_work(16) + try: + results = session.drain_frame_results() + except mln.NotReadyError: + results = [] + if results: + return results[-1] + time.sleep(0.001) + raise AssertionError("frame demand did not produce a terminal result") + + +def finish_render_operation( + session: render.RenderSessionHandle, + operation: Future[object], + *, + return_result: bool = False, + iterations: int = 5000, +) -> object: + """Complete renderer-affine work through either driver.""" + for _ in range(iterations): + if session.snapshot.driver == render.RenderDriver.CALLER_GRAPHICS_THREAD: + session.service_driver_work(16) + if operation.done(): + result = operation.result(timeout=0) + return result if return_result else None + time.sleep(0.001) + raise AssertionError("render operation did not complete") + + +def close_session(session: render.RenderSessionHandle) -> None: + """Detach graphics resources, then destroy CPU-side session state.""" + if session.closed: + return + finish_render_operation(session, session.detach()) + session.close() + + +def release_frame( + frame: object, + sync: render.GpuSync = _DEFAULT_GPU_SYNC, +) -> None: + """Release an acquired texture slot.""" + frame.release(sync) + + def render_until_update( runtime: mln.RuntimeHandle, session: render.RenderSessionHandle, @@ -116,7 +199,7 @@ def render_until_update( iterations=iterations, ) assert event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE - assert session.render_update().result == render.RenderResult.RENDERED + assert request_and_finish_frame(session).disposition == render.RenderResult.RENDERED def render_until( @@ -127,26 +210,17 @@ def render_until( *, iterations: int = 5000, ) -> None: - """Pump and render until `condition` holds, failing with `description`.""" + """Render until `condition` holds, failing with `description`.""" for _ in range(iterations): - runtime.pump() - for event in runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - session.render_update() + time.sleep(0.001) + runtime.drain_events() + request_and_finish_frame(session) if condition(): return time.sleep(0.001) raise AssertionError(description) -def request_still_image_if_needed(map_handle: mln.MapHandle) -> None: - try: - map_handle.request_still_image() - except mln.InvalidStateError as error: - if "pending still-image request" not in error.diagnostic: - raise - - def wait_for_rendered_layer_feature( runtime: mln.RuntimeHandle, map_handle: mln.MapHandle, @@ -156,7 +230,7 @@ def wait_for_rendered_layer_feature( iterations: int = 5000, ) -> query.QueriedFeature: """Render until one feature of `layer_id` covers the map center.""" - query_point = map_handle.pixel_for_lat_lng(geo.LatLng(0.0, 0.0)) + query_point = map_handle.pixel_for_lat_lng(geo.LatLng(0.0, 0.0)).result(timeout=5) geometry = query.RenderedQueryGeometry.box_geometry( query.ScreenBox( camera.ScreenPoint(query_point.x - 30.0, query_point.y - 30.0), @@ -165,20 +239,25 @@ def wait_for_rendered_layer_feature( ) options = query.RenderedFeatureQueryOptions(layer_ids=(layer_id,)) for _ in range(iterations): - request_still_image_if_needed(map_handle) - runtime.pump() - for event in runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - session.render_update() - except mln.InvalidStateError: - pass + # Native execution advances independently; rendering remains host-driven. + time.sleep(0.001) + runtime.drain_events() try: - features = session.query_rendered_features(geometry, options) + result = request_and_finish_frame(session) + if result.disposition != render.RenderResult.RENDERED: + continue + features = finish_render_operation( + session, + session.query_rendered_features(geometry, options), + return_result=True, + ) except mln.InvalidStateError: - features = () + features = [] + assert isinstance(features, list) if features: - return features[0] + first = features[0] + assert isinstance(first, query.QueriedFeature) + return first time.sleep(0.001) raise AssertionError(f"rendered feature query for {layer_id} returned no features") @@ -191,8 +270,10 @@ def wait_for_rendered_cluster( iterations: int = 5000, ) -> query.QueriedFeature: """Load the cluster style and return the first rendered cluster feature.""" - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=0.0)) - map_handle.set_style_json(CLUSTER_STYLE_JSON.encode()) + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=0.0) + ).result(timeout=5) + map_handle.set_style_json(CLUSTER_STYLE_JSON.encode()).result(timeout=5) return wait_for_rendered_layer_feature( runtime, map_handle, @@ -213,13 +294,18 @@ def single_cluster_leaf( offset: int, ) -> dict[str, object]: """Return the one leaf at `offset` through a bounded supercluster query.""" - leaves = session.query_feature_extensions( - "cluster-source", - feature, - "supercluster", - "leaves", - json.dumps({"limit": 1, "offset": offset}, separators=(",", ":")).encode(), + leaves = finish_render_operation( + session, + session.query_feature_extensions( + "cluster-source", + feature, + "supercluster", + "leaves", + json.dumps({"limit": 1, "offset": offset}, separators=(",", ":")).encode(), + ), + return_result=True, ) + assert isinstance(leaves, bytes) collection = json.loads(leaves) assert len(collection["features"]) == 1 return collection["features"][0] @@ -231,8 +317,10 @@ def assert_geojson_cluster_source( session: render.RenderSessionHandle, ) -> None: """Cluster nearby points added through the GeoJSON source data API.""" - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=0.0)) - map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=0.0) + ).result(timeout=5) + map_handle.set_style_json(EMPTY_STYLE_JSON.encode()).result(timeout=5) with style.GeoJsonSourceDataHandle( CLUSTER_POINTS, style.GeoJsonSourceOptions( @@ -244,12 +332,14 @@ def assert_geojson_cluster_source( cluster_properties=b'{"weight_sum":["+",["get","weight"]]}', ), ) as cluster_data: - map_handle.add_geojson_source_data("typed-cluster-source", cluster_data) + map_handle.add_geojson_source_data("typed-cluster-source", cluster_data).result( + timeout=5 + ) map_handle.add_style_layer_json( b'{"id":"typed-cluster-circle","type":"circle",' b'"source":"typed-cluster-source","filter":["has","point_count"],' b'"paint":{"circle-color":"#2563eb","circle-radius":20}}' - ) + ).result(timeout=5) queried = wait_for_rendered_layer_feature( runtime, map_handle, session, "typed-cluster-circle" @@ -277,14 +367,24 @@ def assert_cluster_feature_extensions( assert isinstance(feature, dict) assert isinstance(feature_member(feature, "cluster_id"), int) - children = session.query_feature_extensions( - "cluster-source", cluster.feature, "supercluster", "children", None + children = finish_render_operation( + session, + session.query_feature_extensions( + "cluster-source", cluster.feature, "supercluster", "children", None + ), + return_result=True, ) + assert isinstance(children, bytes) assert json.loads(children)["features"] - expansion_zoom = session.query_feature_extensions( - "cluster-source", cluster.feature, "supercluster", "expansion-zoom", None + expansion_zoom = finish_render_operation( + session, + session.query_feature_extensions( + "cluster-source", cluster.feature, "supercluster", "expansion-zoom", None + ), + return_result=True, ) + assert isinstance(expansion_zoom, bytes) assert isinstance(json.loads(expansion_zoom), int) # Native ignores limit and offset arguments of another type and falls back diff --git a/bindings/python/tests/test_future.py b/bindings/python/tests/test_future.py new file mode 100644 index 000000000..8d0163440 --- /dev/null +++ b/bindings/python/tests/test_future.py @@ -0,0 +1,13 @@ +from concurrent.futures import Future + +from maplibre_native_ffi._future import retain_future + + +def test_cancelled_retained_future_ignores_later_native_completion() -> None: + source: Future[int] = Future() + result = retain_future(source, object()) + + assert result.cancel() + source.set_result(42) + + assert result.cancelled() diff --git a/bindings/python/tests/test_package.py b/bindings/python/tests/test_package.py index a9fc02e27..1c670971c 100644 --- a/bindings/python/tests/test_package.py +++ b/bindings/python/tests/test_package.py @@ -1,4 +1,3 @@ -import concurrent.futures import contextlib import http.server import json @@ -10,7 +9,7 @@ import time import typing import warnings -from collections.abc import Callable +from concurrent.futures import Future from pathlib import Path import maplibre_native_ffi as mln @@ -87,7 +86,6 @@ def _wait_for_runtime_event( iterations: int = 5000, ) -> mln.RuntimeEvent: for _ in range(iterations): - runtime.pump() for event in runtime.drain_events().events: if event.event_type == event_type: return event @@ -102,31 +100,24 @@ def _wait_for_provider_handle( iterations: int = 5000, ) -> resource.ResourceRequestHandle: for _ in range(iterations): - runtime.pump() if handles: return handles.pop(0) time.sleep(0.001) raise AssertionError("resource provider did not expose a handled request") -def _wait_for_offline_operation( - runtime: mln.RuntimeHandle, - operation: offline.OfflineOperationHandle, - *, - iterations: int = 5000, -) -> mln.RuntimeEvent: - operation_id = operation._operation_id - for _ in range(iterations): - runtime.pump() - for event in runtime.drain_events().events: - if ( - event.event_type == mln.RuntimeEventType.OFFLINE_OPERATION_COMPLETED - and isinstance(event.payload, offline.OfflineOperationCompleted) - and event.payload.operation_id == operation_id - ): - return event - time.sleep(0.001) - raise AssertionError(f"offline operation {operation_id!r} did not complete") +def _await(future): + return future.result(timeout=5) + + +def _assert_command_failed( + future: Future[mln.CommandCompletion], status: mln.MaplibreStatus +) -> mln.CommandCompletion: + completion = _await(future) + assert completion.disposition == mln.CommandDisposition.FAILED + assert completion.native_status_code == status.native_code + assert completion.diagnostic + return completion def test_c_version_matches_expected_abi_version() -> None: @@ -151,50 +142,6 @@ def test_native_pointer_is_opaque_value() -> None: assert pointer == mln.NativePointer.null() -def test_frame_backend_values_are_scoped_to_open_frame_handle() -> None: - class FakeMetalFrame: - closed = False - - def texture_address(self) -> int: - return 0x1234 - - def device_address(self) -> int: - return 0x5678 - - def close(self) -> None: - self.closed = True - - with pytest.raises(TypeError, match="created by RenderSessionHandle"): - render.MetalOwnedTextureFrameHandle(FakeMetalFrame()) - - metal = render.MetalOwnedTextureFrameHandle._from_native(FakeMetalFrame()) - texture = metal.texture - device = metal.device - assert texture.address == 0x1234 - assert device.address == 0x5678 - metal.close() - with pytest.raises(mln.InvalidStateError): - _ = texture.address - with pytest.raises(mln.InvalidStateError): - _ = device.address - - class FakeOpenGLFrame: - closed = False - - def texture(self) -> int: - return 42 - - def close(self) -> None: - self.closed = True - - opengl = render.OpenGLOwnedTextureFrameHandle._from_native(FakeOpenGLFrame()) - opengl_texture = opengl.texture - assert int(opengl_texture) == 42 - opengl.close() - with pytest.raises(mln.InvalidStateError): - _ = opengl_texture.value - - def test_network_status_round_trips_through_public_api() -> None: original = mln.network_status() try: @@ -250,6 +197,11 @@ def test_native_status_conversion_preserves_status_and_diagnostic() -> None: (mln.MaplibreStatus.WRONG_THREAD, mln.WrongThreadError), (mln.MaplibreStatus.UNSUPPORTED, mln.UnsupportedFeatureError), (mln.MaplibreStatus.NATIVE_ERROR, mln.NativeError), + (mln.MaplibreStatus.CANCELLED, mln.CancelledError), + (mln.MaplibreStatus.BUSY, mln.BusyError), + (mln.MaplibreStatus.TARGET_LOST, mln.TargetLostError), + (mln.MaplibreStatus.NOT_READY, mln.NotReadyError), + (mln.MaplibreStatus.NOT_FOUND, mln.NotFoundError), ), ) def test_native_status_categories_map_to_public_errors( @@ -346,7 +298,6 @@ def test_public_type_hints_are_resolvable() -> None: mln.RuntimeHandle.create_offline_region, mln.RuntimeHandle.set_resource_transform, mln.RuntimeHandle.set_resource_provider, - offline.OfflineOperationHandle.__init__, offline.OfflineRegionResponseError.__init__, ) @@ -363,7 +314,8 @@ def test_public_type_hints_are_resolvable() -> None: style_hints = typing.get_type_hints(map_module.MapHandle.get_style_layer_json) assert style_hints["return"] != typing.Any - assert set(typing.get_args(style_hints["return"])) == {bytes, type(None)} + assert typing.get_origin(style_hints["return"]) is Future + assert typing.get_args(style_hints["return"]) == (bytes | None,) map_init_hints = typing.get_type_hints(map_module.MapHandle.__init__) assert map_init_hints["runtime"] is mln.RuntimeHandle @@ -376,12 +328,8 @@ def test_public_type_hints_are_resolvable() -> None: create_map_hints = typing.get_type_hints(mln.RuntimeHandle.create_map) assert create_map_hints["options"] == map_module.MapOptions | None - assert create_map_hints["return"] is map_module.MapHandle - - offline_handle_hints = typing.get_type_hints( - offline.OfflineOperationHandle.__init__ - ) - assert offline_handle_hints["runtime"] is mln.RuntimeHandle + assert typing.get_origin(create_map_hints["return"]) is Future + assert typing.get_args(create_map_hints["return"]) == (map_module.MapHandle,) provider_hints = typing.get_type_hints(mln.RuntimeHandle.set_resource_provider) assert provider_hints["callback"] != typing.Any @@ -395,7 +343,8 @@ def test_public_type_hints_are_resolvable() -> None: render.RenderSessionHandle.query_feature_extensions ) assert extension_hints["feature"] is bytes - assert extension_hints["return"] is bytes + assert typing.get_origin(extension_hints["return"]) is Future + assert typing.get_args(extension_hints["return"]) == (bytes,) assert extension_hints["arguments"] == bytes | None rendered_hints = typing.get_type_hints( @@ -403,13 +352,15 @@ def test_public_type_hints_are_resolvable() -> None: ) assert rendered_hints["geometry"] is query.RenderedQueryGeometry assert rendered_hints["options"] != typing.Any - assert rendered_hints["return"] == list[query.QueriedFeature] + assert typing.get_origin(rendered_hints["return"]) is Future + assert typing.get_args(rendered_hints["return"]) == (list[query.QueriedFeature],) source_hints = typing.get_type_hints( render.RenderSessionHandle.query_source_features ) assert source_hints["source_id"] is str - assert source_hints["return"] == list[query.QueriedFeature] + assert typing.get_origin(source_hints["return"]) is Future + assert typing.get_args(source_hints["return"]) == (list[query.QueriedFeature],) response_error_hints = typing.get_type_hints(offline.OfflineRegionResponseError) assert response_error_hints["reason"] is resource.ResourceErrorReason @@ -426,87 +377,12 @@ def test_public_modules_avoid_runtime_annotation_fallbacks() -> None: def test_runtime_handle_context_manager_closes_once() -> None: with mln.RuntimeHandle() as runtime: assert not runtime.closed - runtime.pump() assert runtime.closed runtime.close() assert runtime.closed -def quiesce(runtime: mln.RuntimeHandle) -> None: - """Pump until the runtime is idle, so a park that follows needs a fresh signal.""" - for _ in range(100): - runtime.pump() - if not runtime.drain_events().events: - return - pytest.fail("the runtime kept producing events while idle") - - -def test_parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() -> None: - with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() - quiesce(runtime) - - # The style is malformed, so native reports the failure from its own - # threads and the failure reaches the parked owner thread. - map_handle.set_style_url("unsupported://style.json") - loading_failed = False - load_started = time.monotonic() - for _ in range(20): - runtime.pump(10.0) - assert time.monotonic() - load_started < 5.0, ( - "parks sat out their timeouts while loading was pending" - ) - for event in runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_LOADING_FAILED: - loading_failed = True - if loading_failed: - break - assert loading_failed - - # A source signalled from another thread matches a host's submission path, - # and the park it releases has no other work to end it. The park also - # releases the GIL. - source = runtime.wake_source() - quiesce(runtime) - thread = threading.Thread(target=lambda: (time.sleep(0.02), source.signal())) - thread.start() - park_started = time.monotonic() - runtime.pump(10.0) - assert time.monotonic() - park_started < 5.0, ( - "the parked owner thread timed out instead of taking the signal" - ) - thread.join() - - map_handle.close() - - # A wake source stays usable after its runtime closes, so hosts tear the two - # down in either order. - source.signal() - source.close() - assert source.closed - - -def test_pump_clears_the_wake_flag_it_returns_on() -> None: - with mln.RuntimeHandle() as runtime, runtime.wake_source() as source: - quiesce(runtime) - - source.signal() - signalled_started = time.monotonic() - runtime.pump(10.0) - assert time.monotonic() - signalled_started < 5.0, ( - "a pump waited even though the wake flag was set" - ) - - # The pump above cleared the wake flag, so this one waits its - # full timeout. - idle_started = time.monotonic() - runtime.pump(0.2) - assert time.monotonic() - idle_started >= 0.1, ( - "the first pump left the wake flag set" - ) - - @pytest.mark.skipif( hasattr(sys, "getandroidapilevel"), reason="Android embeds Python and has no standalone interpreter executable", @@ -518,15 +394,18 @@ def test_closed_handle_finalizers_are_quiet_at_interpreter_shutdown() -> None: from maplibre_native_ffi import style runtime = mln.RuntimeHandle() - map_handle = runtime.create_map() - map_handle.set_style_json(b'{"version":8,"sources":{},"layers":[]}') - source = map_handle.add_custom_geometry_source( + map_handle = runtime.create_map().result(timeout=5) + map_handle.set_style_json(b'{"version":8,"sources":{},"layers":[]}').result(timeout=5) + source, completion = map_handle.add_custom_geometry_source( "custom", style.CustomGeometrySourceOptions(max_queued_events=1), ) + completion.result(timeout=5) source.close() map_handle.close() - runtime.close() + # Waiting orders interpreter shutdown after native teardown, which + # otherwise races MapLibre's process-wide state in a short-lived host. + runtime.close().result(timeout=30) """ ) @@ -542,40 +421,53 @@ def test_closed_handle_finalizers_are_quiet_at_interpreter_shutdown() -> None: assert "sys.meta_path is None" not in completed.stderr -def test_duplicate_runtime_reports_invalid_state() -> None: +def test_multiple_runtimes_are_independent() -> None: + first = mln.RuntimeHandle() + second = mln.RuntimeHandle() + try: + assert first is not second + first.barrier().result(timeout=5) + second.barrier().result(timeout=5) + finally: + second.close() + first.close() + + +def test_accepted_native_future_cannot_claim_successful_cancellation() -> None: runtime = mln.RuntimeHandle() try: - with pytest.raises(mln.InvalidStateError) as raised: - mln.RuntimeHandle() + future = runtime.barrier() - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE - assert ( - raised.value.native_status_code - == mln.MaplibreStatus.INVALID_STATE.native_code - ) + assert future.cancel() is False + future.result(timeout=5) finally: - runtime.close() + runtime.close().result(timeout=30) -def test_runtime_close_from_wrong_thread_reports_wrong_thread() -> None: +def test_runtime_and_map_are_usable_across_python_threads() -> None: runtime = mln.RuntimeHandle() - raised_error: list[BaseException] = [] + map_handle = runtime.create_map().result(timeout=5) + failures: list[BaseException] = [] + completions: list[mln.CommandCompletion] = [] - def close_runtime() -> None: + def use_handles() -> None: try: - runtime.close() - except mln.WrongThreadError as error: - raised_error.append(error) - - thread = threading.Thread(target=close_runtime) + assert map_handle.snapshot().generation > 0 + completions.append(map_handle.request_repaint().result(timeout=5)) + runtime.barrier().result(timeout=5) + assert _await(map_handle.get_camera_ordered()).generation > 0 + map_handle.close() + except Exception as error: # noqa: BLE001 - transport worker failures + failures.append(error) + + thread = threading.Thread(target=use_handles) thread.start() thread.join() - - try: - assert len(raised_error) == 1 - assert_wrong_thread_error(raised_error[0]) - finally: - runtime.close() + assert not failures + assert completions[0].disposition == mln.CommandDisposition.COMMITTED + assert completions[0].generation > 0 + assert map_handle.closed + runtime.close() def assert_wrong_thread_error( @@ -590,248 +482,11 @@ def assert_wrong_thread_error( assert error.diagnostic == diagnostic -def test_owner_thread_methods_report_wrong_thread_diagnostics() -> None: - runtime = mln.RuntimeHandle() - map_handle = runtime.create_map() - raised_errors: list[BaseException] = [] - - def call_owner_thread_methods() -> None: - calls: tuple[Callable[[], object], ...] = ( - runtime.pump, - runtime.drain_events, - map_handle.request_repaint, - lambda: runtime.set_event_mask(mln.RuntimeEventMask.ALL), - lambda: map_handle.set_event_mask(mln.RuntimeEventMask.ALL), - ) - for call in calls: - try: - call() - except mln.WrongThreadError as error: - raised_errors.append(error) - - thread = threading.Thread(target=call_owner_thread_methods) - thread.start() - thread.join() - - try: - assert len(raised_errors) == 5 - for error in raised_errors: - assert_wrong_thread_error(error) - finally: - map_handle.close() - runtime.close() - - -def test_resource_transform_registration_reports_wrong_thread_diagnostics() -> None: - runtime = mln.RuntimeHandle() - raised_errors: list[BaseException] = [] - - def transform(request: resource.ResourceTransformRequest) -> str | None: - return request.url - - def call_resource_transform_methods() -> None: - for call in ( - lambda: runtime.set_resource_transform(transform, max_pending_callbacks=1), - runtime.clear_resource_transform, - ): - try: - call() - except mln.WrongThreadError as error: - raised_errors.append(error) - - thread = threading.Thread(target=call_resource_transform_methods) - thread.start() - thread.join() - - try: - assert len(raised_errors) == 2 - for error in raised_errors: - assert_wrong_thread_error(error) - finally: - runtime.close() - - -def test_render_session_methods_propagate_wrong_thread_errors() -> None: - diagnostics = { - "close": "wrong thread while closing render session", - "resize": "wrong thread while resizing render session", - "render_update": "wrong thread while rendering update", - "detach": "wrong thread while detaching render session", - "reduce_memory_use": "wrong thread while reducing memory use", - "clear_data": "wrong thread while clearing data", - "dump_debug_logs": "wrong thread while dumping debug logs", - "texture_image_info": "wrong thread while reading texture info", - "read_premultiplied_rgba8_into": "wrong thread while reading into buffer", - "acquire_metal_owned_texture_frame": "wrong thread while acquiring Metal frame", - "acquire_vulkan_owned_texture_frame": ( - "wrong thread while acquiring Vulkan frame" - ), - "acquire_opengl_owned_texture_frame": ( - "wrong thread while acquiring OpenGL frame" - ), - "query_rendered_features": "wrong thread while querying rendered features", - "query_source_features": "wrong thread while querying source features", - "query_feature_extensions": "wrong thread while querying feature extensions", - } - - class FakeNativeRenderSession: - closed = False - detached = False - - def _wrong_thread(self, method: str) -> None: - raise mln.WrongThreadError( - mln.MaplibreStatus.WRONG_THREAD.native_code, - diagnostics[method], - ) - - def close(self) -> None: - self._wrong_thread("close") - - def resize(self, width: int, height: int, scale_factor: float) -> None: - self._wrong_thread("resize") - - def render_update(self) -> None: - self._wrong_thread("render_update") - - def detach(self) -> None: - self._wrong_thread("detach") - - def reduce_memory_use(self) -> None: - self._wrong_thread("reduce_memory_use") - - def clear_data(self) -> None: - self._wrong_thread("clear_data") - - def dump_debug_logs(self) -> None: - self._wrong_thread("dump_debug_logs") - - def texture_image_info(self) -> None: - self._wrong_thread("texture_image_info") - - def read_premultiplied_rgba8_into(self, buffer: object) -> None: - self._wrong_thread("read_premultiplied_rgba8_into") - - def acquire_metal_owned_texture_frame(self) -> None: - self._wrong_thread("acquire_metal_owned_texture_frame") - - def acquire_vulkan_owned_texture_frame(self) -> None: - self._wrong_thread("acquire_vulkan_owned_texture_frame") - - def acquire_opengl_owned_texture_frame(self) -> None: - self._wrong_thread("acquire_opengl_owned_texture_frame") - - def query_rendered_features( - self, - geometry: object, - layer_ids: tuple[str, ...] | None, - filter_: object, - ) -> None: - self._wrong_thread("query_rendered_features") - - def query_source_features( - self, - source_id: str, - source_layer_ids: tuple[str, ...] | None, - filter_: object, - ) -> None: - self._wrong_thread("query_source_features") - - def query_feature_extensions( - self, - source_id: str, - feature: object, - extension: str, - extension_field: str, - arguments: object, - ) -> None: - self._wrong_thread("query_feature_extensions") - - session = render.RenderSessionHandle._from_native( - FakeNativeRenderSession(), object() - ) - point_query = query.RenderedQueryGeometry.point_geometry( - camera.ScreenPoint(1.0, 2.0) - ) - feature = _json_object( - { - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [2.0, 1.0]}, - "properties": {}, - } - ) - calls = { - "close": session.close, - "resize": lambda: session.resize(128, 64, 1.0), - "render_update": session.render_update, - "detach": session.detach, - "reduce_memory_use": session.reduce_memory_use, - "clear_data": session.clear_data, - "dump_debug_logs": session.dump_debug_logs, - "texture_image_info": session.texture_image_info, - "read_premultiplied_rgba8_into": ( - lambda: session.read_premultiplied_rgba8_into(bytearray(4)) - ), - "acquire_metal_owned_texture_frame": session.acquire_metal_owned_texture_frame, - "acquire_vulkan_owned_texture_frame": ( - session.acquire_vulkan_owned_texture_frame - ), - "acquire_opengl_owned_texture_frame": ( - session.acquire_opengl_owned_texture_frame - ), - "query_rendered_features": lambda: session.query_rendered_features(point_query), - "query_source_features": lambda: session.query_source_features("points"), - "query_feature_extensions": ( - lambda: session.query_feature_extensions( - "points", - feature, - "supercluster", - "leaves", - _json_object({"limit": 10}), - ) - ), - } - raised_errors: dict[str, BaseException] = {} - - def call_render_session_methods() -> None: - for name, call in calls.items(): - try: - call() - except mln.WrongThreadError as error: - raised_errors[name] = error - - thread = threading.Thread(target=call_render_session_methods) - thread.start() - thread.join() - - assert set(raised_errors) == set(calls) - for name, error in raised_errors.items(): - assert_wrong_thread_error(error, diagnostics[name]) - - -def test_render_session_detach_releases_python_map_reference() -> None: - class FakeDetachedSession: - closed = False - - def close(self) -> None: - self.closed = True - - class FakeNativeRenderSession: - closed = False - detached = False - - def detach(self) -> FakeDetachedSession: - self.detached = True - return FakeDetachedSession() - - session = render.RenderSessionHandle._from_native( - FakeNativeRenderSession(), object() - ) - - detached = session.detach() - - assert session.detached - assert session._map is None - detached.close() +def test_phase_three_render_values_preserve_unknown_dispositions() -> None: + assert render.RenderDriver.CORE_WORKER.native_code == 1 + assert render.RenderDriver.CALLER_GRAPHICS_THREAD.native_code == 2 + assert render.RenderResult.DEADLINE_MISSED.native_code == 5 + assert render.RenderResult(777).native_code == 777 def test_map_handle_context_manager_closes_once() -> None: @@ -839,9 +494,11 @@ def test_map_handle_context_manager_closes_once() -> None: with pytest.raises(TypeError, match="RuntimeHandle.create_map"): mln.MapHandle(runtime, mln.MapOptions(width=128, height=64)) - with runtime.create_map(mln.MapOptions(width=128, height=64)) as map_handle: + with runtime.create_map(mln.MapOptions(width=128, height=64)).result( + timeout=5 + ) as map_handle: assert not map_handle.closed - map_handle.request_repaint() + _await(map_handle.request_repaint()) assert map_handle.closed map_handle.close() @@ -853,7 +510,7 @@ def test_map_options_accept_fast_pfor_decoding() -> None: mln.RuntimeHandle() as runtime, runtime.create_map( mln.MapOptions(width=64, height=64, fast_pfor_enabled=True) - ) as map_handle, + ).result(timeout=5) as map_handle, ): assert map_handle.get_size() == (64, 64, 1.0) @@ -861,13 +518,18 @@ def test_map_options_accept_fast_pfor_decoding() -> None: def test_unset_map_options_take_the_c_creation_defaults() -> None: # Unset fields take the C API defaults rather than values this binding # repeats, so mln_map_options_default() stays the single source for them. - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): assert map_handle.get_size() == (256, 256, 1.0) def test_runtime_rejects_close_while_map_is_live() -> None: runtime = mln.RuntimeHandle() - map_handle = runtime.create_map(mln.MapOptions(width=64, height=64)) + map_handle = runtime.create_map(mln.MapOptions(width=64, height=64)).result( + timeout=5 + ) try: with pytest.raises(mln.InvalidStateError) as raised: runtime.close() @@ -882,19 +544,16 @@ def test_runtime_rejects_close_while_map_is_live() -> None: runtime.close() -def test_still_image_request_uses_map_mode_validation() -> None: - with ( - mln.RuntimeHandle() as runtime, - runtime.create_map(mln.MapOptions(mode=mln.MapMode.STATIC)) as static_map, - ): - static_map.request_still_image() +def test_still_image_request_rejects_continuous_map_mode() -> None: with ( mln.RuntimeHandle() as runtime, - runtime.create_map(mln.MapOptions(mode=mln.MapMode.CONTINUOUS)) as map_handle, + runtime.create_map(mln.MapOptions(mode=mln.MapMode.CONTINUOUS)).result( + timeout=5 + ) as map_handle, pytest.raises(mln.InvalidStateError) as raised, ): - map_handle.request_still_image() + _await(map_handle.request_still_image()) assert raised.value.status == mln.MaplibreStatus.INVALID_STATE @@ -905,34 +564,49 @@ def test_map_create_from_closed_runtime_reports_invalid_state() -> None: stale = _native_invalid_network_status_error().diagnostic with pytest.raises(mln.InvalidStateError) as raised: - runtime.create_map() + runtime.create_map().result(timeout=5) assert raised.value.native_status_code is None assert raised.value.diagnostic == "runtime handle is closed" assert raised.value.diagnostic != stale -def test_map_debug_and_status_options_round_trip_public_values() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: +def test_map_debug_and_status_options_round_trip_the_snapshot() -> None: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): debug_options = ( map_module.MapDebugOptions.TILE_BORDERS | map_module.MapDebugOptions.PARSE_STATUS ) - map_handle.set_debug_options(debug_options) + completion = map_handle.set_debug_options(debug_options) map_handle.set_rendering_stats_view_enabled(True) - - assert map_handle.get_debug_options() == debug_options - assert map_handle.get_rendering_stats_view_enabled() is True - assert isinstance(map_handle.is_fully_loaded(), bool) + runtime.barrier().result(timeout=5) + + # Snapshot fence: the commit reports the generation that published its + # effect, and a snapshot at or past that generation observes it. + finished = _await_command_completion(runtime, completion) + assert finished.disposition == mln.CommandDisposition.COMMITTED + snapshot = map_handle.snapshot() + assert snapshot.generation >= finished.generation + assert snapshot.debug_options == debug_options + assert snapshot.rendering_stats_view_enabled is True + assert isinstance(snapshot.fully_loaded, bool) map_handle.set_debug_options(map_module.MapDebugOptions.NONE) map_handle.set_rendering_stats_view_enabled(False) - assert map_handle.get_debug_options() == map_module.MapDebugOptions.NONE - assert map_handle.get_rendering_stats_view_enabled() is False + runtime.barrier().result(timeout=5) + snapshot = map_handle.snapshot() + assert snapshot.debug_options == map_module.MapDebugOptions.NONE + assert snapshot.rendering_stats_view_enabled is False def test_style_url_rejects_embedded_nul_before_native_call() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): stale = _native_invalid_network_status_error().diagnostic with pytest.raises(mln.InvalidArgumentError) as raised: @@ -994,35 +668,43 @@ def prepare() -> None: assert isinstance(prepared, style.GeoJsonSourceDataHandle) with prepared: assert prepared.closed is False - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) - map_handle.add_geojson_source_data("worker-points", prepared) - assert ( - map_handle.get_style_source_type("worker-points") - == style.StyleSourceType.GEOJSON - ) + completion = map_handle.add_geojson_source_data("worker-points", prepared) + runtime.barrier().result(timeout=5) + finished = _await_command_completion(runtime, completion) + assert finished.disposition == mln.CommandDisposition.COMMITTED + info = _await(map_handle.get_style_source_info("worker-points")) + assert info is not None + assert info.source_type == style.StyleSourceType.GEOJSON assert prepared.closed is True def test_geojson_source_data_installs_on_many_sources_and_outlives_release() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) prepared = style.GeoJsonSourceDataHandle(_point_collection("one", "two")) # Install calls borrow the handle, so one prepared value serves any - # number of sources. + # number of sources, and the submit-time lease keeps the prepared + # index alive after the handle is released. map_handle.add_geojson_source_data("points-a", prepared) map_handle.add_geojson_source_data("points-b", prepared) - map_handle.set_geojson_source_data("points-a", prepared) + update_id = map_handle.set_geojson_source_data("points-a", prepared) prepared.close() + runtime.barrier().result(timeout=5) + updated = _await_command_completion(runtime, update_id) + assert updated.disposition == mln.CommandDisposition.COMMITTED # Release never invalidates a source the data was installed on. - assert ( - map_handle.get_style_source_type("points-a") - == style.StyleSourceType.GEOJSON - ) - assert ( - map_handle.get_style_source_type("points-b") - == style.StyleSourceType.GEOJSON - ) + for source_id in ("points-a", "points-b"): + info = _await(map_handle.get_style_source_info(source_id)) + assert info is not None + assert info.source_type == style.StyleSourceType.GEOJSON def test_geojson_source_data_close_is_idempotent_and_blocks_installs() -> None: @@ -1031,7 +713,10 @@ def test_geojson_source_data_close_is_idempotent_and_blocks_installs() -> None: assert prepared.closed is True prepared.close() - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) with pytest.raises(mln.InvalidStateError, match="closed"): map_handle.add_geojson_source_data("points", prepared) @@ -1051,7 +736,10 @@ def test_geojson_source_data_create_validates_cluster_input() -> None: def test_set_geojson_source_data_rejects_mismatched_baked_in_options() -> None: document = _point_collection("one", "two") - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) with style.GeoJsonSourceDataHandle( document, @@ -1062,25 +750,23 @@ def test_set_geojson_source_data_rejects_mismatched_baked_in_options() -> None: ) as clustered: map_handle.add_geojson_source_data("points", clustered) - with ( - style.GeoJsonSourceDataHandle(document) as plain, - pytest.raises(mln.InvalidArgumentError), - ): - map_handle.set_geojson_source_data("points", plain) + # The mismatch is a map-thread validation, so the command fails + # asynchronously with INVALID_ARGUMENT instead of raising at submit. + with style.GeoJsonSourceDataHandle(document) as plain: + rejected_id = map_handle.set_geojson_source_data("points", plain) + _assert_command_failed(rejected_id, mln.MaplibreStatus.INVALID_ARGUMENT) # Different cluster aggregations would change cluster feature # properties under the source's layers, so they are rejected too. - with ( - style.GeoJsonSourceDataHandle( - document, - style.GeoJsonSourceOptions( - cluster=True, - cluster_properties=_json_object({"renamed": ["+", 1]}), - ), - ) as reclustered, - pytest.raises(mln.InvalidArgumentError), - ): - map_handle.set_geojson_source_data("points", reclustered) + with style.GeoJsonSourceDataHandle( + document, + style.GeoJsonSourceOptions( + cluster=True, + cluster_properties=_json_object({"renamed": ["+", 1]}), + ), + ) as reclustered: + reclustered_id = map_handle.set_geojson_source_data("points", reclustered) + _assert_command_failed(reclustered_id, mln.MaplibreStatus.INVALID_ARGUMENT) # Aggregations compare by parsed expression equality, so equivalent # cluster_properties JSON matches regardless of formatting. @@ -1091,18 +777,29 @@ def test_set_geojson_source_data_rejects_mismatched_baked_in_options() -> None: cluster_properties=b' { "names" : ["+", 1] } ', ), ) as matching: - map_handle.set_geojson_source_data("points", matching) + matching_id = map_handle.set_geojson_source_data("points", matching) + runtime.barrier().result(timeout=5) + accepted = _await_command_completion(runtime, matching_id) + assert accepted.disposition == mln.CommandDisposition.COMMITTED def test_set_geojson_source_synchronous_tiling_overrides_at_runtime() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) with style.GeoJsonSourceDataHandle(_point_collection("one")) as prepared: map_handle.add_geojson_source_data("points", prepared) - map_handle.set_geojson_source_synchronous_tiling("points", True) - map_handle.set_geojson_source_synchronous_tiling("points", False) - with pytest.raises(mln.InvalidArgumentError): - map_handle.set_geojson_source_synchronous_tiling("missing", True) + enabled_id = map_handle.set_geojson_source_synchronous_tiling("points", True) + disabled_id = map_handle.set_geojson_source_synchronous_tiling("points", False) + for completion in (_await(enabled_id), _await(disabled_id)): + assert completion.disposition == mln.CommandDisposition.COMMITTED + + # A missing source is a map-thread validation, so the command fails + # asynchronously with INVALID_ARGUMENT instead of raising at submit. + missing_id = map_handle.set_geojson_source_synchronous_tiling("missing", True) + _assert_command_failed(missing_id, mln.MaplibreStatus.INVALID_ARGUMENT) def test_style_source_metadata_enums_preserve_unknown_values() -> None: @@ -1118,27 +815,33 @@ def test_style_source_metadata_enums_preserve_unknown_values() -> None: def test_loaded_style_document_and_url_read_back_what_was_loaded() -> None: style_json = _EMPTY_STYLE_BYTES - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): # Nothing parsed and nothing requested yet. - assert map_handle.get_loaded_style_json() == b"" - assert map_handle.get_style_url() == "" + assert _await(map_handle.get_loaded_style_json()) == b"" + assert _await(map_handle.get_style_url()) == "" # The document reads back byte-for-byte, so it can be reloaded # unchanged. map_handle.set_style_json(style_json) - assert map_handle.get_loaded_style_json() == style_json + assert _await(map_handle.get_loaded_style_json()) == style_json # Inline JSON clears the URL. - assert map_handle.get_style_url() == "" + assert _await(map_handle.get_style_url()) == "" # The URL is request state, recorded before the load can succeed, # while the document still reports the style that last parsed. map_handle.set_style_url("https://example.test/style.json") - assert map_handle.get_style_url() == "https://example.test/style.json" - assert map_handle.get_loaded_style_json() == style_json + assert _await(map_handle.get_style_url()) == "https://example.test/style.json" + assert _await(map_handle.get_loaded_style_json()) == style_json def test_style_source_url_metadata_and_removal_public_api() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) map_handle.add_style_source_json( "style-json-points", @@ -1243,45 +946,23 @@ def test_style_source_url_metadata_and_removal_public_api() -> None: ("https://example.test/dem/{z}/{x}/{y}.png",), ) - assert map_handle.style_source_exists("points") is True - assert map_handle.style_source_exists("missing") is False - assert ( - map_handle.get_style_source_type("style-json-points") - == style.StyleSourceType.GEOJSON - ) - assert ( - map_handle.get_style_source_type("points") == style.StyleSourceType.GEOJSON - ) - assert ( - map_handle.get_style_source_type("inline-points") - == style.StyleSourceType.GEOJSON - ) - assert ( - map_handle.get_style_source_type("vector-tiles") - == style.StyleSourceType.VECTOR - ) - assert ( - map_handle.get_style_source_type("raster-tiles") - == style.StyleSourceType.RASTER - ) - assert ( - map_handle.get_style_source_type("dem-tiles") - == style.StyleSourceType.RASTER_DEM - ) - assert ( - map_handle.get_style_source_type("vector-inline") - == style.StyleSourceType.VECTOR - ) - assert ( - map_handle.get_style_source_type("raster-inline") - == style.StyleSourceType.RASTER - ) - assert ( - map_handle.get_style_source_type("dem-inline") - == style.StyleSourceType.RASTER_DEM - ) - assert map_handle.get_style_source_type("missing") is None - source_ids = map_handle.list_style_source_ids() + def source_type(source_id: str) -> style.StyleSourceType | None: + info = _await(map_handle.get_style_source_info(source_id)) + return info.source_type if info is not None else None + + # The info getter's found flag is the existence check. + assert _await(map_handle.get_style_source_info("points")) is not None + assert _await(map_handle.get_style_source_info("missing")) is None + assert source_type("style-json-points") == style.StyleSourceType.GEOJSON + assert source_type("points") == style.StyleSourceType.GEOJSON + assert source_type("inline-points") == style.StyleSourceType.GEOJSON + assert source_type("vector-tiles") == style.StyleSourceType.VECTOR + assert source_type("raster-tiles") == style.StyleSourceType.RASTER + assert source_type("dem-tiles") == style.StyleSourceType.RASTER_DEM + assert source_type("vector-inline") == style.StyleSourceType.VECTOR + assert source_type("raster-inline") == style.StyleSourceType.RASTER + assert source_type("dem-inline") == style.StyleSourceType.RASTER_DEM + source_ids = _await(map_handle.list_style_source_ids()) assert "style-json-points" in source_ids assert "points" in source_ids assert "inline-points" in source_ids @@ -1292,20 +973,20 @@ def test_style_source_url_metadata_and_removal_public_api() -> None: assert "raster-inline" in source_ids assert "dem-inline" in source_ids - info = map_handle.get_style_source_info("points") + info = _await(map_handle.get_style_source_info("points")) assert info is not None assert info.source_type == style.StyleSourceType.GEOJSON assert info.attribution is None assert info.url == "https://example.test/points.geojson" assert info.tile_json is None - assert map_handle.get_style_source_info("missing") is None + assert _await(map_handle.get_style_source_info("missing")) is None - remote_info = map_handle.get_style_source_info("vector-tiles") + remote_info = _await(map_handle.get_style_source_info("vector-tiles")) assert remote_info is not None assert remote_info.url == "https://example.test/vector.json" assert remote_info.tile_json is None - copied_inline = map_handle.get_style_source_info("vector-inline") + copied_inline = _await(map_handle.get_style_source_info("vector-inline")) assert copied_inline is not None assert copied_inline.url is None assert copied_inline.attribution == "Example attribution" @@ -1323,17 +1004,29 @@ def test_style_source_url_metadata_and_removal_public_api() -> None: geo.LatLng(-5.0, -10.0), geo.LatLng(15.0, 20.0) ) - assert map_handle.remove_style_source("style-json-points") is True - assert map_handle.remove_style_source("points") is True - assert map_handle.remove_style_source("points") is False - assert map_handle.remove_style_source("inline-points") is True - assert map_handle.remove_style_source("vector-tiles") is True - assert map_handle.remove_style_source("raster-tiles") is True - assert map_handle.remove_style_source("dem-tiles") is True - assert map_handle.remove_style_source("vector-inline") is True - assert map_handle.remove_style_source("raster-inline") is True - assert map_handle.remove_style_source("dem-inline") is True - source_ids = map_handle.list_style_source_ids() + removed_id = map_handle.remove_style_source("points") + runtime.barrier().result(timeout=5) + removed = _await_command_completion(runtime, removed_id) + assert removed.disposition == mln.CommandDisposition.COMMITTED + assert _await(map_handle.get_style_source_info("points")) is None + + # Removing the missing source again fails the command with NOT_FOUND. + missing_id = map_handle.remove_style_source("points") + _assert_command_failed(missing_id, mln.MaplibreStatus.NOT_FOUND) + + for source_id in ( + "style-json-points", + "inline-points", + "vector-tiles", + "raster-tiles", + "dem-tiles", + "vector-inline", + "raster-inline", + "dem-inline", + ): + map_handle.remove_style_source(source_id) + runtime.barrier().result(timeout=5) + source_ids = _await(map_handle.list_style_source_ids()) assert "style-json-points" not in source_ids assert "points" not in source_ids assert "inline-points" not in source_ids @@ -1365,7 +1058,10 @@ def test_image_source_url_image_and_coordinates_public_api() -> None: data=bytes([0, 255, 0, 255]), ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) map_handle.add_image_source_url( "overlay-url", @@ -1374,16 +1070,17 @@ def test_image_source_url_image_and_coordinates_public_api() -> None: ) map_handle.add_image_source_image("overlay-inline", coordinates, image) + url_info = _await(map_handle.get_style_source_info("overlay-url")) + inline_info = _await(map_handle.get_style_source_info("overlay-inline")) + assert url_info is not None + assert url_info.source_type == style.StyleSourceType.IMAGE + assert inline_info is not None + assert inline_info.source_type == style.StyleSourceType.IMAGE assert ( - map_handle.get_style_source_type("overlay-url") - == style.StyleSourceType.IMAGE - ) - assert ( - map_handle.get_style_source_type("overlay-inline") - == style.StyleSourceType.IMAGE + _await(map_handle.get_image_source_coordinates("overlay-url")) + == coordinates ) - assert map_handle.get_image_source_coordinates("overlay-url") == coordinates - assert map_handle.get_image_source_coordinates("missing") is None + assert _await(map_handle.get_image_source_coordinates("missing")) is None map_handle.set_image_source_url( "overlay-url", @@ -1392,12 +1089,15 @@ def test_image_source_url_image_and_coordinates_public_api() -> None: map_handle.set_image_source_image("overlay-url", image) map_handle.set_image_source_coordinates("overlay-url", updated_coordinates) assert ( - map_handle.get_image_source_coordinates("overlay-url") + _await(map_handle.get_image_source_coordinates("overlay-url")) == updated_coordinates ) - assert map_handle.remove_style_source("overlay-url") is True - assert map_handle.remove_style_source("overlay-inline") is True + map_handle.remove_style_source("overlay-url") + map_handle.remove_style_source("overlay-inline") + runtime.barrier().result(timeout=5) + assert _await(map_handle.get_style_source_info("overlay-url")) is None + assert _await(map_handle.get_style_source_info("overlay-inline")) is None def test_style_json_light_layer_property_and_filter_public_api() -> None: @@ -1405,7 +1105,10 @@ def test_style_json_light_layer_property_and_filter_public_api() -> None: circle = _json_object({"id": "json-circle", "type": "circle", "source": "points"}) raw_filter = ["==", ["get", "kind"], "park"] filter_value = _json_value(raw_filter) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) map_handle.add_geojson_source_url( "points", @@ -1434,28 +1137,40 @@ def test_style_json_light_layer_property_and_filter_public_api() -> None: map_handle.set_style_light_json(_json_object({"anchor": "viewport"})) map_handle.set_style_light_property("intensity", _json_value(0.5)) - layer_json = map_handle.get_style_layer_json("json-background") + layer_json = _await(map_handle.get_style_layer_json("json-background")) assert layer_json is not None assert json.loads(layer_json) == { "id": "json-background", "type": "background", "paint": {"background-color": ["rgba", 255, 0, 0, 1]}, } - assert map_handle.get_style_layer_json("missing") is None - background_color = map_handle.get_layer_property( - "json-background", - "background-color", + assert _await(map_handle.get_style_layer_json("missing")) is None + background_color = _await( + map_handle.get_layer_property( + "json-background", + "background-color", + ) ) assert json.loads(background_color) == ["rgba", 255, 0, 0, 1] - assert json.loads(map_handle.get_layer_filter("json-circle")) == raw_filter - assert json.loads(map_handle.get_style_light_property("anchor")) == "viewport" - assert json.loads(map_handle.get_style_light_property("intensity")) == 0.5 + assert ( + json.loads(_await(map_handle.get_layer_filter("json-circle"))) == raw_filter + ) + assert ( + json.loads(_await(map_handle.get_style_light_property("anchor"))) + == "viewport" + ) + assert ( + json.loads(_await(map_handle.get_style_light_property("intensity"))) == 0.5 + ) - with pytest.raises(mln.InvalidArgumentError, match="not valid JSON"): - map_handle.set_style_light_property("intensity", b"Infinity") + completion = map_handle.set_style_light_property("intensity", b"Infinity") + _assert_command_failed(completion, mln.MaplibreStatus.INVALID_ARGUMENT) + assert ( + json.loads(_await(map_handle.get_style_light_property("intensity"))) == 0.5 + ) map_handle.set_layer_filter("json-circle", None) - assert map_handle.get_layer_filter("json-circle") is None + assert _await(map_handle.get_layer_filter("json-circle")) is None def test_style_image_metadata_copy_and_removal_public_api() -> None: @@ -1463,7 +1178,10 @@ def test_style_image_metadata_copy_and_removal_public_api() -> None: info=render.TextureImageInfo(width=1, height=1, stride=4, byte_length=4), data=bytes([255, 0, 0, 255]), ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) map_handle.set_style_image( "marker", @@ -1471,9 +1189,7 @@ def test_style_image_metadata_copy_and_removal_public_api() -> None: style.StyleImageOptions(pixel_ratio=2.0, sdf=True), ) - assert map_handle.style_image_exists("marker") is True - assert map_handle.style_image_exists("missing") is False - info = map_handle.get_style_image_info("marker") + info = _await(map_handle.get_style_image_info("marker")) assert info is not None assert info.width == 1 assert info.height == 1 @@ -1481,21 +1197,33 @@ def test_style_image_metadata_copy_and_removal_public_api() -> None: assert info.byte_length == 4 assert info.pixel_ratio == pytest.approx(2.0) assert info.sdf is True - assert map_handle.get_style_image_info("missing") is None + assert _await(map_handle.get_style_image_info("missing")) is None - copied = map_handle.copy_style_image_premultiplied_rgba8("marker") + copied = _await(map_handle.copy_style_image_premultiplied_rgba8("marker")) assert copied is not None assert copied.image == image assert copied.pixel_ratio == pytest.approx(2.0) assert copied.sdf is True - assert map_handle.copy_style_image_premultiplied_rgba8("missing") is None + assert ( + _await(map_handle.copy_style_image_premultiplied_rgba8("missing")) is None + ) - assert map_handle.remove_style_image("marker") is True - assert map_handle.remove_style_image("marker") is False + removed_id = map_handle.remove_style_image("marker") + runtime.barrier().result(timeout=5) + removed = _await_command_completion(runtime, removed_id) + assert removed.disposition == mln.CommandDisposition.COMMITTED + assert _await(map_handle.get_style_image_info("marker")) is None + + # Removing the missing image again fails the command with NOT_FOUND. + missing_id = map_handle.remove_style_image("marker") + _assert_command_failed(missing_id, mln.MaplibreStatus.NOT_FOUND) def test_builtin_style_layers_and_location_indicator_public_api() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) map_handle.add_raster_dem_source_url( "dem", @@ -1521,16 +1249,27 @@ def test_builtin_style_layers_and_location_indicator_public_api() -> None: "marker", ) - assert map_handle.get_style_layer_type("hillshade") == "hillshade" - assert map_handle.get_style_layer_type("relief") == "color-relief" - assert map_handle.get_style_layer_type("location") == "location-indicator" - assert map_handle.remove_style_layer("hillshade") is True - assert map_handle.remove_style_layer("relief") is True - assert map_handle.remove_style_layer("location") is True + def layer_type(layer_id: str) -> str | None: + info = _await(map_handle.get_style_layer_info(layer_id)) + return info.layer_type if info is not None else None + + assert layer_type("hillshade") == "hillshade" + assert layer_type("relief") == "color-relief" + assert layer_type("location") == "location-indicator" + map_handle.remove_style_layer("hillshade") + map_handle.remove_style_layer("relief") + map_handle.remove_style_layer("location") + runtime.barrier().result(timeout=5) + assert _await(map_handle.get_style_layer_info("hillshade")) is None + assert _await(map_handle.get_style_layer_info("relief")) is None + assert _await(map_handle.get_style_layer_info("location")) is None def test_nine_patch_style_image_round_trips_public_api() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) image = render.PremultipliedRgba8Image( render.TextureImageInfo(width=2, height=2, stride=8, byte_length=16), @@ -1544,7 +1283,7 @@ def test_nine_patch_style_image_round_trips_public_api() -> None: ) map_handle.set_style_image("patch", image, options) - info = map_handle.get_style_image_info("patch") + info = _await(map_handle.get_style_image_info("patch")) assert info is not None assert info.stretch_x_count == 1 assert info.stretch_y_count == 2 @@ -1553,7 +1292,7 @@ def test_nine_patch_style_image_round_trips_public_api() -> None: assert info.text_fit_width is None assert info.text_fit_height is style.StyleImageTextFit.PROPORTIONAL - stretches = map_handle.get_style_image_stretches("patch") + stretches = _await(map_handle.get_style_image_stretches("patch")) assert stretches is not None stretch_x, stretch_y = stretches assert stretch_x == (style.ImageStretch(0.0, 1.0),) @@ -1561,10 +1300,9 @@ def test_nine_patch_style_image_round_trips_public_api() -> None: style.ImageStretch(0.0, 1.0), style.ImageStretch(1.0, 2.0), ) - assert map_handle.get_style_image_stretches("missing") is None + assert _await(map_handle.get_style_image_stretches("missing")) is None - # A backwards interval is rejected by C. - with pytest.raises(mln.InvalidArgumentError): + with pytest.raises(mln.InvalidArgumentError, match="positive width"): map_handle.set_style_image( "bad", image, @@ -1577,10 +1315,13 @@ def test_style_transition_options_round_trip_public_api() -> None: b'{"version":8,"transition":{"duration":750,"delay":100},' b'"sources":{},"layers":[]}' ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): # A map with no style yet reports no duration or delay. The # placement flag always reports, because native always holds one. - empty = map_handle.get_style_transition_options() + empty = _await(map_handle.get_style_transition_options()) assert empty.duration_ms is None assert empty.delay_ms is None assert empty.enable_placement_transitions is True @@ -1588,12 +1329,12 @@ def test_style_transition_options_round_trip_public_api() -> None: # The style parser fills in its own 300ms duration for a style that # declares no transition. map_handle.set_style_json(_EMPTY_STYLE_BYTES) - parsed = map_handle.get_style_transition_options() + parsed = _await(map_handle.get_style_transition_options()) assert parsed.duration_ms == 300.0 assert parsed.delay_ms is None map_handle.set_style_json(transition_style_json) - declared = map_handle.get_style_transition_options() + declared = _await(map_handle.get_style_transition_options()) assert declared.duration_ms == 750.0 assert declared.delay_ms == 100.0 assert declared.enable_placement_transitions is True @@ -1605,29 +1346,35 @@ def test_style_transition_options_round_trip_public_api() -> None: enable_placement_transitions=False, ) map_handle.set_style_transition_options(options) - assert map_handle.get_style_transition_options() == options + assert _await(map_handle.get_style_transition_options()) == options # Omitting the flag leaves the cross-fade on rather than clearing it. map_handle.set_style_transition_options( style.StyleTransitionOptions(duration_ms=250.0) ) assert ( - map_handle.get_style_transition_options().enable_placement_transitions + _await( + map_handle.get_style_transition_options() + ).enable_placement_transitions is True ) # Loading a style replaces the override with what that style declares. map_handle.set_style_json(transition_style_json) - assert map_handle.get_style_transition_options() == declared + assert _await(map_handle.get_style_transition_options()) == declared - with pytest.raises(mln.InvalidArgumentError, match="delay_ms"): - map_handle.set_style_transition_options( - style.StyleTransitionOptions(delay_ms=-1.0) - ) + completion = map_handle.set_style_transition_options( + style.StyleTransitionOptions(delay_ms=-1.0) + ) + _assert_command_failed(completion, mln.MaplibreStatus.INVALID_ARGUMENT) + assert _await(map_handle.get_style_transition_options()) == declared def test_layer_base_accessors_round_trip_public_api() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json( b'{"version":8,"sources":{"geo":{"type":"geojson","data":' b'{"type":"FeatureCollection","features":[]}}},"layers":[' @@ -1635,35 +1382,50 @@ def test_layer_base_accessors_round_trip_public_api() -> None: b'{"id":"fill","type":"fill","source":"geo"}]}' ) - assert map_handle.get_layer_source_layer("fill") == "" + assert _await(map_handle.get_layer_source_layer("fill")) == "" map_handle.set_layer_source_layer("fill", "roads") - assert map_handle.get_layer_source_layer("fill") == "roads" - assert map_handle.get_layer_source_id("fill") == "geo" + assert _await(map_handle.get_layer_source_layer("fill")) == "roads" + assert _await(map_handle.get_layer_source_id("fill")) == "geo" - # A layer type that takes no source is rejected, not silently ignored. - with pytest.raises(mln.InvalidArgumentError, match="source-layer"): - map_handle.set_layer_source_layer("bg", "roads") - assert map_handle.get_layer_source_id("bg") == "" + # A layer type that takes no source rejects the accepted command. + completion = map_handle.set_layer_source_layer("bg", "roads") + _assert_command_failed(completion, mln.MaplibreStatus.INVALID_ARGUMENT) + assert _await(map_handle.get_layer_source_id("bg")) == "" # An unset zoom range crosses the boundary as infinities. - assert map_handle.get_layer_min_zoom("fill") == -math.inf - assert map_handle.get_layer_max_zoom("fill") == math.inf - map_handle.set_layer_min_zoom("fill", 4.0) - map_handle.set_layer_max_zoom("fill", 12.5) - assert map_handle.get_layer_min_zoom("fill") == 4.0 - assert map_handle.get_layer_max_zoom("fill") == 12.5 - + info = _await(map_handle.get_style_layer_info("fill")) + assert info is not None + assert info.layer_type == "fill" + assert info.min_zoom == -math.inf + assert info.max_zoom == math.inf + assert info.visibility is style.StyleLayerVisibility.VISIBLE + # The layer-info string sizes gate the source ID and source-layer + # copies, which agree with the scalar accessors. + assert info.source_id == _await(map_handle.get_layer_source_id("fill")) == "geo" assert ( - map_handle.get_layer_visibility("fill") - is style.StyleLayerVisibility.VISIBLE + info.source_layer + == _await(map_handle.get_layer_source_layer("fill")) + == "roads" ) + + map_handle.set_layer_min_zoom("fill", 4.0) + map_handle.set_layer_max_zoom("fill", 12.5) map_handle.set_layer_visibility("fill", style.StyleLayerVisibility.NONE) - assert ( - map_handle.get_layer_visibility("fill") is style.StyleLayerVisibility.NONE - ) + info = _await(map_handle.get_style_layer_info("fill")) + assert info is not None + assert info.min_zoom == 4.0 + assert info.max_zoom == 12.5 + assert info.visibility is style.StyleLayerVisibility.NONE - with pytest.raises(mln.InvalidArgumentError): - map_handle.get_layer_min_zoom("missing") + # A sourceless layer type reports no source strings at all. + background = _await(map_handle.get_style_layer_info("bg")) + assert background is not None + assert background.layer_type == "background" + assert background.source_id is None + assert background.source_layer is None + + # The info getter's found flag reports a missing layer as None. + assert _await(map_handle.get_style_layer_info("missing")) is None def test_style_layer_metadata_move_and_removal_public_api() -> None: @@ -1677,25 +1439,35 @@ def test_style_layer_metadata_move_and_removal_public_api() -> None: ] } """ - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(style_json) - layer_ids = map_handle.list_style_layer_ids() + layer_ids = _await(map_handle.list_style_layer_ids()) assert "background-a" in layer_ids assert "background-b" in layer_ids assert layer_ids.index("background-a") < layer_ids.index("background-b") - assert map_handle.style_layer_exists("background-a") is True - assert map_handle.style_layer_exists("missing") is False - assert map_handle.get_style_layer_type("background-a") == "background" - assert map_handle.get_style_layer_type("missing") is None + info = _await(map_handle.get_style_layer_info("background-a")) + assert info is not None + assert info.layer_type == "background" + assert _await(map_handle.get_style_layer_info("missing")) is None map_handle.move_style_layer("background-b", "background-a") - layer_ids = map_handle.list_style_layer_ids() + layer_ids = _await(map_handle.list_style_layer_ids()) assert layer_ids.index("background-b") < layer_ids.index("background-a") - assert map_handle.remove_style_layer("background-b") is True - assert map_handle.remove_style_layer("background-b") is False - assert "background-b" not in map_handle.list_style_layer_ids() + removed_id = map_handle.remove_style_layer("background-b") + runtime.barrier().result(timeout=5) + removed = _await_command_completion(runtime, removed_id) + assert removed.disposition == mln.CommandDisposition.COMMITTED + assert _await(map_handle.get_style_layer_info("background-b")) is None + assert "background-b" not in _await(map_handle.list_style_layer_ids()) + + # Removing the missing layer again fails the command with NOT_FOUND. + missing_id = map_handle.remove_style_layer("background-b") + _assert_command_failed(missing_id, mln.MaplibreStatus.NOT_FOUND) def test_map_viewport_and_tile_options_round_trip_public_values() -> None: @@ -1714,16 +1486,29 @@ def test_map_viewport_and_tile_options_round_trip_public_values() -> None: lod_mode=map_module.TileLodMode.DEFAULT, ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - map_handle.set_viewport_options(viewport) - map_handle.set_tile_options(tile) + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + viewport_command = map_handle.set_viewport_options(viewport) + tile_command = map_handle.set_tile_options(tile) + runtime.barrier().result(timeout=5) - assert map_handle.get_viewport_options() == viewport - assert map_handle.get_tile_options() == tile + # The new snapshot fields round-trip both committed set commands. + viewport_completion = _await(viewport_command) + tile_completion = _await(tile_command) + snapshot = map_handle.snapshot() + assert snapshot.viewport == viewport + assert snapshot.tile == tile + assert snapshot.generation >= viewport_completion.generation + assert snapshot.generation >= tile_completion.generation def test_camera_snapshot_and_jump_round_trip_public_values() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): target = camera.CameraOptions( center=geo.LatLng(10.0, 20.0), zoom=2.0, @@ -1733,9 +1518,7 @@ def test_camera_snapshot_and_jump_round_trip_public_values() -> None: anchor=camera.ScreenPoint(x=16.0, y=8.0), ) map_handle.jump_to(target) - snapshot = map_handle.get_camera() - map_handle.move_by(1.0, 1.0) - map_handle.cancel_transitions() + snapshot = _await(map_handle.get_camera_ordered()).camera assert snapshot.center is not None assert snapshot.center.latitude == pytest.approx(10.0) @@ -1746,22 +1529,20 @@ def test_camera_snapshot_and_jump_round_trip_public_values() -> None: assert snapshot.padding == target.padding -def test_gesture_in_progress_brackets_host_driven_camera_commands() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - assert map_handle.is_gesture_in_progress() is False - - map_handle.set_gesture_in_progress(True) - map_handle.move_by(8.0, -4.0) - assert map_handle.is_gesture_in_progress() is True - - map_handle.set_gesture_in_progress(False) - assert map_handle.is_gesture_in_progress() is False - - def test_free_camera_and_projection_mode_round_trip_public_values() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - free_camera = map_handle.get_free_camera_options() + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + free_camera = map_handle.snapshot().free_camera assert isinstance(free_camera, camera.FreeCameraOptions) + map_handle.set_free_camera_options( + camera.FreeCameraOptions(orientation=camera.Quaternion(0.0, 0.0, 0.0, 1.0)) + ) + runtime.barrier().result(timeout=5) + updated = map_handle.snapshot().free_camera + assert updated.position is not None + assert updated.orientation is not None projection = camera.ProjectionMode( axonometric=True, @@ -1769,6 +1550,7 @@ def test_free_camera_and_projection_mode_round_trip_public_values() -> None: y_skew=0.2, ) map_handle.set_projection_mode(projection) + runtime.barrier().result(timeout=5) snapshot = map_handle.get_projection_mode() assert snapshot.axonometric is True @@ -1788,7 +1570,10 @@ def test_camera_fit_bounds_and_constraints_public_api() -> None: ) target = camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=1.0) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_bounds( camera.BoundOptions( bounds=camera.Bounded(bounds), @@ -1796,28 +1581,35 @@ def test_camera_fit_bounds_and_constraints_public_api() -> None: max_zoom=10.0, ) ) - constraints = map_handle.get_bounds() - fit_bounds = map_handle.camera_for_lat_lng_bounds(bounds, fit) - fit_coordinates = map_handle.camera_for_lat_lngs( - (bounds.southwest, bounds.northeast), - fit, + runtime.barrier().result(timeout=5) + constraints = map_handle.snapshot().bounds + fit_bounds = _await(map_handle.camera_for_lat_lng_bounds(bounds, fit)) + fit_coordinates = _await( + map_handle.camera_for_lat_lngs( + (bounds.southwest, bounds.northeast), + fit, + ) ) - fit_geometry = map_handle.camera_for_geometry( - _json_object( - { - "type": "LineString", - "coordinates": [ - [bounds.southwest.longitude, bounds.southwest.latitude], - [bounds.northeast.longitude, bounds.northeast.latitude], - ], - } - ), - fit, + fit_geometry = _await( + map_handle.camera_for_geometry( + _json_object( + { + "type": "LineString", + "coordinates": [ + [bounds.southwest.longitude, bounds.southwest.latitude], + [bounds.northeast.longitude, bounds.northeast.latitude], + ], + } + ), + fit, + ) ) - visible_bounds = map_handle.lat_lng_bounds_for_camera(target) - unwrapped_bounds = map_handle.lat_lng_bounds_for_camera( - target, - unwrapped=True, + visible_bounds = _await(map_handle.lat_lng_bounds_for_camera(target)) + unwrapped_bounds = _await( + map_handle.lat_lng_bounds_for_camera( + target, + unwrapped=True, + ) ) assert constraints.bounds == camera.Bounded(bounds) @@ -1834,25 +1626,35 @@ def _jumped_longitude(map_handle: mln.MapHandle, longitude: float) -> float: map_handle.jump_to( camera.CameraOptions(center=geo.LatLng(0.0, longitude), zoom=2.0) ) - center = map_handle.get_camera().center + center = _await(map_handle.get_camera_ordered()).camera.center assert center is not None return center.longitude +def _settled_bounds( + runtime: mln.RuntimeHandle, map_handle: mln.MapHandle +) -> camera.BoundsConstraint | None: + runtime.barrier().result(timeout=5) + return map_handle.snapshot().bounds.bounds + + def test_camera_bounds_distinguish_unbounded_from_world() -> None: world = geo.LatLngBounds( southwest=geo.LatLng(-90.0, -180.0), northeast=geo.LatLng(90.0, 180.0), ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - assert map_handle.get_bounds().bounds == camera.Unbounded() + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + assert _settled_bounds(runtime, map_handle) == camera.Unbounded() # An unbounded map wraps across the antimeridian. assert _jumped_longitude(map_handle, 200.0) == pytest.approx(-160.0, abs=1e-6) map_handle.set_bounds(camera.BoundOptions(bounds=camera.Bounded(world))) - constrained = map_handle.get_bounds().bounds + constrained = _settled_bounds(runtime, map_handle) assert isinstance(constrained, camera.Bounded) assert constrained.bounds.northeast.longitude == pytest.approx(180.0) # World bounds clamp at the antimeridian instead of wrapping. @@ -1860,7 +1662,7 @@ def test_camera_bounds_distinguish_unbounded_from_world() -> None: map_handle.set_bounds(camera.BoundOptions(bounds=camera.Unbounded())) - assert map_handle.get_bounds().bounds == camera.Unbounded() + assert _settled_bounds(runtime, map_handle) == camera.Unbounded() # Releasing the constraint restores antimeridian wrapping. assert _jumped_longitude(map_handle, 200.0) == pytest.approx(-160.0, abs=1e-6) @@ -1873,20 +1675,12 @@ def test_camera_transition_commands_accept_public_values() -> None: easing=camera.UnitBezier(0.0, 0.0, 1.0, 1.0), ) target = camera.CameraOptions(center=geo.LatLng(0.0, 0.0), zoom=1.0) - first = camera.ScreenPoint(0.0, 0.0) - second = camera.ScreenPoint(1.0, 1.0) - - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - map_handle.ease_to(target, animation) - map_handle.fly_to(target, animation) - map_handle.move_by_animated(1.0, 1.0, animation) - map_handle.scale_by(1.0, first) - map_handle.scale_by_animated(1.0, first, animation) - map_handle.rotate_by(first, second) - map_handle.rotate_by_animated(first, second, animation) - map_handle.pitch_by(0.0) - map_handle.pitch_by_animated(0.0, animation) - map_handle.cancel_transitions() + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + assert _await(map_handle.ease_to(target, animation)).generation > 0 + assert _await(map_handle.fly_to(target, animation)).generation > 0 def _drain_runtime_events(runtime: mln.RuntimeHandle) -> list[mln.RuntimeEvent]: @@ -1903,6 +1697,13 @@ def _finished_transition_ids(events: list[mln.RuntimeEvent]) -> list[int]: return ids +def _await_command_completion( + runtime: mln.RuntimeHandle, future +) -> mln.CommandCompletion: + del runtime + return _await(future) + + def _camera_change_modes( events: list[mln.RuntimeEvent], event_type: mln.RuntimeEventType ) -> list[mln.CameraChangeMode]: @@ -1916,12 +1717,18 @@ def _camera_change_modes( def test_zero_duration_ease_reports_transition_finished_once() -> None: target = camera.CameraOptions(center=geo.LatLng(12.0, 34.0), zoom=4.0) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): _drain_runtime_events(runtime) - map_handle.ease_to( - target, - camera.AnimationOptions(duration_ms=0.0, transition_id=101), + _await( + map_handle.ease_to( + target, + camera.AnimationOptions(duration_ms=0.0, transition_id=101), + ) ) + runtime.barrier().result(timeout=5) events = _drain_runtime_events(runtime) assert _finished_transition_ids(events) == [101] @@ -1931,63 +1738,62 @@ def test_zero_duration_ease_reports_transition_finished_once() -> None: def test_superseded_transition_reports_transition_finished_once() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): _drain_runtime_events(runtime) - map_handle.ease_to( - camera.CameraOptions(center=geo.LatLng(20.0, 40.0), zoom=6.0), - camera.AnimationOptions(duration_ms=5_000.0, transition_id=201), + _await( + map_handle.ease_to( + camera.CameraOptions(center=geo.LatLng(20.0, 40.0), zoom=6.0), + camera.AnimationOptions(duration_ms=5_000.0, transition_id=201), + ) ) + runtime.barrier().result(timeout=5) started = _drain_runtime_events(runtime) - map_handle.jump_to( - camera.CameraOptions(center=geo.LatLng(-20.0, -40.0), zoom=2.0) + _await( + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(-20.0, -40.0), zoom=2.0) + ) ) + runtime.barrier().result(timeout=5) superseded = _drain_runtime_events(runtime) assert _finished_transition_ids(started) == [] assert _finished_transition_ids(superseded) == [201] -def test_cancelled_transition_reports_transition_finished_once() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - _drain_runtime_events(runtime) - map_handle.fly_to( - camera.CameraOptions(center=geo.LatLng(30.0, 60.0), zoom=8.0), - camera.AnimationOptions(duration_ms=5_000.0, transition_id=301), - ) - started = _drain_runtime_events(runtime) - map_handle.cancel_transitions() - cancelled = _drain_runtime_events(runtime) - map_handle.cancel_transitions() - cancelled_again = _drain_runtime_events(runtime) - - assert _finished_transition_ids(started) == [] - assert _finished_transition_ids(cancelled) == [301] - assert _finished_transition_ids(cancelled_again) == [] - - def test_completed_ease_reports_transition_finished_once() -> None: target = camera.CameraOptions(center=geo.LatLng(5.0, 10.0), zoom=3.0) deadline = time.monotonic() + 10.0 events: list[mln.RuntimeEvent] = [] - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): _drain_runtime_events(runtime) - map_handle.ease_to( - target, - camera.AnimationOptions(duration_ms=20.0, transition_id=401), + _await( + map_handle.ease_to( + target, + camera.AnimationOptions(duration_ms=20.0, transition_id=401), + ) ) + runtime.barrier().result(timeout=5) while not _finished_transition_ids(events): assert time.monotonic() < deadline, ( - "ease did not finish while the runtime was pumped" + "ease did not finish under autonomous runtime execution" ) - map_handle.request_repaint() - runtime.pump() + _await(map_handle.request_repaint()) + runtime.barrier().result(timeout=5) + time.sleep(0.001) events.extend(_drain_runtime_events(runtime)) trailing: list[mln.RuntimeEvent] = [] for _ in range(8): - map_handle.request_repaint() - runtime.pump() + _await(map_handle.request_repaint()) + runtime.barrier().result(timeout=5) + time.sleep(0.001) trailing.extend(_drain_runtime_events(runtime)) assert _finished_transition_ids(events) == [401] @@ -1997,35 +1803,52 @@ def test_completed_ease_reports_transition_finished_once() -> None: ) -def test_camera_change_events_report_immediate_and_animated_modes() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: +def test_camera_change_events_report_immediate_modes_for_jump_and_zero_duration_ease() -> ( + None +): + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): _drain_runtime_events(runtime) - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0)) + _await( + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0) + ) + ) + runtime.barrier().result(timeout=5) jumped = _drain_runtime_events(runtime) - map_handle.ease_to( - camera.CameraOptions(center=geo.LatLng(-1.0, -2.0), zoom=7.0), - camera.AnimationOptions(duration_ms=5_000.0), + _await( + map_handle.ease_to( + camera.CameraOptions(center=geo.LatLng(-1.0, -2.0), zoom=7.0), + camera.AnimationOptions(duration_ms=0.0), + ) ) + runtime.barrier().result(timeout=5) eased = _drain_runtime_events(runtime) - map_handle.cancel_transitions() - eased.extend(_drain_runtime_events(runtime)) will_change = mln.RuntimeEventType.MAP_CAMERA_WILL_CHANGE did_change = mln.RuntimeEventType.MAP_CAMERA_DID_CHANGE assert _camera_change_modes(jumped, will_change) == [mln.CameraChangeMode.IMMEDIATE] assert _camera_change_modes(jumped, did_change) == [mln.CameraChangeMode.IMMEDIATE] - assert _camera_change_modes(eased, will_change) == [mln.CameraChangeMode.ANIMATED] - assert _camera_change_modes(eased, did_change) == [mln.CameraChangeMode.ANIMATED] + assert _camera_change_modes(eased, will_change) == [mln.CameraChangeMode.IMMEDIATE] + assert _camera_change_modes(eased, did_change) == [mln.CameraChangeMode.IMMEDIATE] def test_transition_finished_precedes_camera_did_change_in_one_batch() -> None: """BND-090: one drain reports more than one event and preserves queue order.""" - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): runtime.drain_events() - map_handle.ease_to( - camera.CameraOptions(center=geo.LatLng(12.0, 34.0), zoom=4.0), - camera.AnimationOptions(duration_ms=0.0, transition_id=707), + _await( + map_handle.ease_to( + camera.CameraOptions(center=geo.LatLng(12.0, 34.0), zoom=4.0), + camera.AnimationOptions(duration_ms=0.0, transition_id=707), + ) ) + runtime.barrier().result(timeout=5) types = [event.event_type for event in runtime.drain_events().events] finished = mln.RuntimeEventType.MAP_CAMERA_TRANSITION_FINISHED @@ -2035,14 +1858,6 @@ def test_transition_finished_precedes_camera_did_change_in_one_batch() -> None: ) -@pytest.mark.parametrize("max_events", [-1, 2**64]) -def test_drain_events_out_of_range_bound_raises_binding_error(max_events: int) -> None: - # PyO3 extracts this as `usize`, so without a range check the caller would - # see a bare OverflowError instead of the binding's error shape. - with mln.RuntimeHandle() as runtime, pytest.raises(mln.InvalidArgumentError): - runtime.drain_events(max_events) - - def test_option_default_masks_keep_native_bits_this_build_does_not_name( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -2060,12 +1875,23 @@ def test_option_default_masks_keep_native_bits_this_build_does_not_name( def test_narrowed_map_mask_drops_the_cleared_event_type() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - map_handle.set_event_mask( - mln.RuntimeEventMask.ALL & ~mln.RuntimeEventMask.MAP_CAMERA_DID_CHANGE + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + _await( + map_handle.set_event_mask( + mln.RuntimeEventMask.ALL & ~mln.RuntimeEventMask.MAP_CAMERA_DID_CHANGE + ) ) + runtime.barrier().result(timeout=5) runtime.drain_events() - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0)) + _await( + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0) + ) + ) + runtime.barrier().result(timeout=5) types = [event.event_type for event in runtime.drain_events().events] assert mln.RuntimeEventType.MAP_CAMERA_WILL_CHANGE in types @@ -2076,45 +1902,24 @@ def test_map_created_with_a_narrowed_mask_never_delivers_the_cleared_type() -> N narrowed = mln.RuntimeEventMask.ALL & ~mln.RuntimeEventMask.MAP_CAMERA_DID_CHANGE options = mln.MapOptions(event_mask=narrowed) - with mln.RuntimeHandle() as runtime, runtime.create_map(options) as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map(options).result(timeout=5) as map_handle, + ): assert map_handle.event_mask == narrowed runtime.drain_events() - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0)) + _await( + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0) + ) + ) + runtime.barrier().result(timeout=5) types = [event.event_type for event in runtime.drain_events().events] assert mln.RuntimeEventType.MAP_CAMERA_WILL_CHANGE in types assert mln.RuntimeEventType.MAP_CAMERA_DID_CHANGE not in types -def test_narrowed_runtime_mask_drops_offline_completion_events(tmp_path: Path) -> None: - cleared = mln.RuntimeEventMask.ALL & ~( - mln.RuntimeEventMask.OFFLINE_OPERATION_COMPLETED - ) - - with mln.RuntimeHandle( - mln.RuntimeOptions(cache_path=str(tmp_path / "masked-cache.db")) - ) as runtime: - # The same drive reports a completion while the type is selected, so the - # narrowed phase below asserts a real negative. - selected = runtime.set_maximum_ambient_cache_size(8 << 20) - _wait_for_offline_operation(runtime, selected) - selected.close() - - runtime.set_event_mask(cleared) - assert runtime.event_mask == cleared - operation = runtime.set_maximum_ambient_cache_size(4 << 20) - completions = 0 - for _ in range(200): - runtime.pump(0.002) - completions += sum( - event.event_type == mln.RuntimeEventType.OFFLINE_OPERATION_COMPLETED - for event in runtime.drain_events().events - ) - operation.close() - - assert completions == 0 - - def test_event_masks_round_trip_on_the_map_and_the_runtime() -> None: created = mln.RuntimeEventMask.ALL & ~( mln.RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED @@ -2123,10 +1928,12 @@ def test_event_masks_round_trip_on_the_map_and_the_runtime() -> None: with mln.RuntimeHandle(mln.RuntimeOptions(event_mask=created)) as runtime: assert runtime.event_mask == created runtime.set_event_mask(mln.RuntimeEventMask.ALL) + runtime.barrier().result(timeout=5) assert runtime.event_mask == mln.RuntimeEventMask.ALL - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_event_mask(mln.RuntimeEventMask.ALL) + runtime.barrier().result(timeout=5) assert map_handle.event_mask == mln.RuntimeEventMask.ALL # A read-modify-write of one bit keeps every other bit, including the @@ -2134,23 +1941,25 @@ def test_event_masks_round_trip_on_the_map_and_the_runtime() -> None: map_handle.set_event_mask( map_handle.event_mask & ~mln.RuntimeEventMask.MAP_IDLE ) + runtime.barrier().result(timeout=5) assert ( map_handle.event_mask == mln.RuntimeEventMask.ALL & ~mln.RuntimeEventMask.MAP_IDLE ) assert mln.RuntimeEventMask.MAP_TILE_ACTION in map_handle.event_mask assert ( - mln.RuntimeEventMask.OFFLINE_OPERATION_COMPLETED + mln.RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED in map_handle.event_mask ) runtime.set_event_mask( - runtime.event_mask & ~mln.RuntimeEventMask.OFFLINE_OPERATION_COMPLETED + runtime.event_mask & ~mln.RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED ) + runtime.barrier().result(timeout=5) assert ( runtime.event_mask == mln.RuntimeEventMask.ALL - & ~mln.RuntimeEventMask.OFFLINE_OPERATION_COMPLETED + & ~mln.RuntimeEventMask.OFFLINE_REGION_STATUS_CHANGED ) @@ -2161,13 +1970,11 @@ def test_empty_creation_mask_queues_nothing() -> None: with mln.RuntimeHandle(options) as runtime: assert runtime.event_mask == mln.RuntimeEventMask.NONE - with runtime.create_map(map_options) as map_handle: + with runtime.create_map(map_options).result(timeout=5) as map_handle: assert map_handle.event_mask == mln.RuntimeEventMask.NONE assert not runtime.drain_events().events - map_handle.set_style_json(_EMPTY_STYLE_BYTES) - for _ in range(64): - runtime.pump() - time.sleep(0.001) + _await(map_handle.set_style_json(_EMPTY_STYLE_BYTES)) + runtime.barrier().result(timeout=5) assert not runtime.drain_events().events @@ -2178,45 +1985,45 @@ def test_event_mask_bit_outside_all_is_rejected() -> None: with pytest.raises(mln.InvalidArgumentError): mln.RuntimeHandle(mln.RuntimeOptions(event_mask=outside)) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises(mln.InvalidArgumentError): - runtime.create_map(mln.MapOptions(event_mask=outside)) + runtime.create_map(mln.MapOptions(event_mask=outside)).result(timeout=5) with pytest.raises(mln.InvalidArgumentError): runtime.set_event_mask(outside) with pytest.raises(mln.InvalidArgumentError): - map_handle.set_event_mask(outside) + _await(map_handle.set_event_mask(outside)) -def test_bounded_drain_reports_remaining_count_until_the_queue_empties() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: +def test_runtime_event_wake_callback_reports_new_events() -> None: + wake = threading.Event() + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): runtime.drain_events() - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0)) - map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(3.0, 4.0), zoom=5.0)) - - bounded = runtime.drain_events(1) - assert len(bounded.events) == 1 - assert bounded.remaining_count > 0 - - # Nothing pumps between the two drains, so the rest is exactly what the - # bounded drain left behind. - rest = runtime.drain_events() - assert len(rest.events) == bounded.remaining_count - assert rest.remaining_count == 0 - assert not runtime.drain_events().events + runtime.set_event_wake_callback(wake.set) + _await(map_handle.request_repaint()) + assert wake.wait(1) + runtime.clear_event_wake_callback() def test_drained_events_stay_equal_after_the_next_drain_and_map_close() -> None: with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() + map_handle = runtime.create_map().result(timeout=5) map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(1.0, 2.0), zoom=3.0)) + runtime.barrier().result(timeout=5) first = runtime.drain_events() snapshot = list(first.events) assert snapshot map_handle.jump_to(camera.CameraOptions(center=geo.LatLng(4.0, 5.0), zoom=6.0)) + runtime.barrier().result(timeout=5) assert runtime.drain_events().events - assert first == mln.RuntimeEventBatch(events=snapshot, remaining_count=0) + assert first == mln.RuntimeEventBatch(events=snapshot) map_handle.close() assert first.events == snapshot @@ -2231,13 +2038,16 @@ def test_drain_steps_by_the_reported_event_size() -> None: def test_drain_returns_a_copied_map_loading_failure() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - with pytest.raises((mln.InvalidArgumentError, mln.NativeError)): - map_handle.set_style_json(b"{") - + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + completion = map_handle.set_style_json(b"{") + _assert_command_failed(completion, mln.MaplibreStatus.NATIVE_ERROR) + events = runtime.drain_events().events failures = [ event - for event in runtime.drain_events().events + for event in events if event.event_type == mln.RuntimeEventType.MAP_LOADING_FAILED ] @@ -2249,8 +2059,11 @@ def test_drain_returns_a_copied_map_loading_failure() -> None: assert loading_failed.message -def test_pump_and_drain_return_a_copied_style_loaded_event() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: +def test_autonomous_runtime_and_drain_return_a_copied_style_loaded_event() -> None: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) style_loaded = _wait_for_runtime_event( runtime, mln.RuntimeEventType.MAP_STYLE_LOADED @@ -2259,7 +2072,7 @@ def test_pump_and_drain_return_a_copied_style_loaded_event() -> None: assert style_loaded.source.source_type == mln.RuntimeEventSourceType.MAP assert style_loaded.source.map_handle is map_handle assert style_loaded.payload is None - runtime.pump() + time.sleep(0.001) assert not runtime.drain_events().events @@ -2272,8 +2085,6 @@ def test_synthetic_batch_decodes_every_payload_and_preserves_unknown_domains() - batch.events ) - assert batch.remaining_count == 3 - assert isinstance(render_frame.payload, mln.RenderFramePayload) assert render_frame.payload.mode == mln.RenderMode.FULL assert render_frame.payload.needs_repaint is True @@ -2381,6 +2192,19 @@ def test_render_descriptors_are_public_python_values() -> None: texture=5, target=0x0DE1, ) + webgpu = render.WebGPUBorrowedTextureDescriptor( + extent=extent, + physical_width=641, + physical_height=481, + context=render.WebGPUContextDescriptor( + instance=pointer, + device=render.NativePointer(0x2345), + queue=render.NativePointer(0x3456), + ), + texture=render.NativePointer(0x4567), + texture_view=render.NativePointer(0x5678), + format=44, + ) # Odd sizes no logical extent maps onto, so these must survive as stated. assert (vulkan.physical_width, vulkan.physical_height) == (641, 481) @@ -2395,53 +2219,13 @@ def test_render_descriptors_are_public_python_values() -> None: assert opengl_wgl.context.device_context.address == 0x1234 assert opengl_wgl.texture == 5 assert opengl_wgl.target == 0x0DE1 + assert (webgpu.physical_width, webgpu.physical_height) == (641, 481) + assert webgpu.context.device.address == 0x2345 + assert webgpu.texture_view.address == 0x5678 + assert webgpu.format == 44 -def test_render_session_query_public_api_returns_queried_features() -> None: - class FakeNativeRenderSession: - closed = False - detached = False - - def __init__(self) -> None: - self.rendered_call = None - self.source_call = None - self.extension_call = None - - def query_rendered_features( - self, - geometry: object, - layer_ids: tuple[str, ...] | None, - filter_: object, - ) -> list[dict[str, object]]: - self.rendered_call = (geometry, layer_ids, filter_) - return [queried_feature_wire] - - def query_source_features( - self, - source_id: str, - source_layer_ids: tuple[str, ...] | None, - filter_: object, - ) -> list[dict[str, object]]: - self.source_call = (source_id, source_layer_ids, filter_) - return [queried_feature_wire] - - def query_feature_extensions( - self, - source_id: str, - feature: object, - extension: str, - extension_field: str, - arguments: object, - ) -> bytes: - self.extension_call = ( - source_id, - feature, - extension, - extension_field, - arguments, - ) - return _json_value(7) - +def test_queried_feature_materializes_native_wire_values() -> None: feature = _json_object( { "type": "Feature", @@ -2451,102 +2235,38 @@ def query_feature_extensions( } ) state = _json_object({"hover": True}) - queried_feature_wire = { - "feature": feature, - "source_id": "points", - "source_layer_id": None, - "state": state, - } - queried = query.QueriedFeature( - feature=feature, - source_id="points", - source_layer_id=None, - state=state, - ) - - fake_native = FakeNativeRenderSession() - with pytest.raises(TypeError, match="created by MapHandle"): - render.RenderSessionHandle(fake_native, object()) - - session = render.RenderSessionHandle._from_native(fake_native, object()) - geometry = query.RenderedQueryGeometry.point_geometry(camera.ScreenPoint(1.0, 2.0)) - rendered_options = query.RenderedFeatureQueryOptions( - layer_ids=("circle",), - filter=_json_value(["==", ["get", "kind"], "park"]), - ) - source_options = query.SourceFeatureQueryOptions( - source_layer_ids=("landuse",), - filter=_json_value(["==", ["get", "kind"], "park"]), - ) - rendered = session.query_rendered_features(geometry, rendered_options) - source = session.query_source_features("points", source_options) - leaves_arguments = _json_object({"limit": 10}) - extension = session.query_feature_extensions( - "points", - feature, - "supercluster", - "leaves", - leaves_arguments, - ) - assert fake_native.rendered_call == ( - {"type": "point", "point": (1.0, 2.0)}, - ("circle",), - _json_value(["==", ["get", "kind"], "park"]), - ) - assert fake_native.source_call[0] == "points" - assert fake_native.source_call[1] == ("landuse",) - assert rendered == [queried] - assert source == [queried] - assert fake_native.extension_call == ( - "points", - feature, - "supercluster", - "leaves", - leaves_arguments, + materialized = render._cast_queried_features( + [ + { + "feature": feature, + "source_id": "points", + "source_layer_id": None, + "state": state, + }, + { + "feature": feature, + "source_id": None, + "source_layer_id": "landuse", + "state": None, + }, + ] ) - assert extension == _json_value(7) - - -def test_opengl_owned_texture_frame_public_api_uses_native_values() -> None: - class FakeNativeFrame: - closed = False - - def frame(self) -> dict[str, object]: - return { - "generation": 1, - "width": 64, - "height": 32, - "scale_factor": 2.0, - "frame_id": 3, - "target": 0x0DE1, - "internal_format": 0x8058, - "format": 0x1908, - "type": 0x1401, - } - - def texture(self) -> int: - return 5 - - def close(self) -> None: - self.closed = True - frame = render.OpenGLOwnedTextureFrameHandle._from_native(FakeNativeFrame()) - - assert frame.frame == render.OpenGLOwnedTextureFrame( - generation=1, - width=64, - height=32, - scale_factor=2.0, - frame_id=3, - target=0x0DE1, - internal_format=0x8058, - format=0x1908, - type=0x1401, - ) - assert frame.texture == 5 - frame.close() - assert frame.closed + assert materialized == [ + query.QueriedFeature( + feature=feature, + source_id="points", + source_layer_id=None, + state=state, + ), + query.QueriedFeature( + feature=feature, + source_id=None, + source_layer_id="landuse", + state=None, + ), + ] def test_map_feature_state_set_get_and_remove() -> None: @@ -2557,15 +2277,20 @@ def test_map_feature_state_set_get_and_remove() -> None: state_key="hover", ) state = _json_object({"hover": True}) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - map_handle.set_feature_state(selector, state) - returned = map_handle.get_feature_state(selector) - map_handle.remove_feature_state(selector) - empty = map_handle.get_feature_state( - query.FeatureStateSelector( - source_id="points", - source_layer_id="symbols", - feature_id="feature-1", + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + _await(map_handle.set_feature_state(selector, state)) + returned = _await(map_handle.get_feature_state(selector)) + _await(map_handle.remove_feature_state(selector)) + empty = _await( + map_handle.get_feature_state( + query.FeatureStateSelector( + source_id="points", + source_layer_id="symbols", + feature_id="feature-1", + ) ) ) @@ -2574,7 +2299,10 @@ def test_map_feature_state_set_get_and_remove() -> None: def test_invalid_render_target_attach_reports_native_status() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises( (mln.InvalidArgumentError, mln.UnsupportedFeatureError) ) as raised: @@ -2587,7 +2315,10 @@ def test_invalid_render_target_attach_reports_native_status() -> None: def test_invalid_opengl_render_target_attach_reports_native_status() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises( (mln.InvalidArgumentError, mln.UnsupportedFeatureError) ) as raised: @@ -2601,14 +2332,37 @@ def test_invalid_opengl_render_target_attach_reports_native_status() -> None: } +def test_invalid_webgpu_render_target_attach_reports_native_status() -> None: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + with pytest.raises( + (mln.InvalidArgumentError, mln.UnsupportedFeatureError) + ) as raised: + map_handle.attach_webgpu_owned_texture( + render.WebGPUOwnedTextureDescriptor() + ) + + assert raised.value.status in { + mln.MaplibreStatus.INVALID_ARGUMENT, + mln.MaplibreStatus.UNSUPPORTED, + } + + def test_map_coordinate_conversions_round_trip_public_values() -> None: coordinate = geo.LatLng(0.0, 0.0) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.jump_to(camera.CameraOptions(center=coordinate, zoom=1.0)) - point = map_handle.pixel_for_lat_lng(coordinate) - projected = map_handle.lat_lng_for_pixel(point) - points = map_handle.pixels_for_lat_lngs((coordinate, geo.LatLng(1.0, 1.0))) - coordinates = map_handle.lat_lngs_for_pixels(points) + point = _await(map_handle.pixel_for_lat_lng(coordinate)) + projected = _await(map_handle.lat_lng_for_pixel(point)) + points = _await( + map_handle.pixels_for_lat_lngs((coordinate, geo.LatLng(1.0, 1.0))) + ) + coordinates = _await(map_handle.lat_lngs_for_pixels(points)) assert isinstance(point, camera.ScreenPoint) assert math.isfinite(projected.latitude) @@ -2628,12 +2382,53 @@ def test_map_projection_converts_coordinates_and_closes() -> None: assert math.isclose(round_tripped.latitude, coordinate.latitude, abs_tol=1e-6) assert math.isclose(round_tripped.longitude, coordinate.longitude, abs_tol=1e-6) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - map_handle.jump_to(camera.CameraOptions(center=coordinate, zoom=1.0)) - with map_handle.create_projection() as projection: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + map_handle.jump_to( + camera.CameraOptions(center=geo.LatLng(10.0, 20.0), zoom=3.0) + ) + with _await(map_handle.create_projection()) as projection: + # A projection created after a camera command observes it. + created_camera = projection.get_camera() + assert created_camera.center is not None + assert created_camera.center.latitude == pytest.approx(10.0) + assert created_camera.center.longitude == pytest.approx(20.0) + assert created_camera.zoom == pytest.approx(3.0) + + # A synchronous conversion round-trips pixel -> latlng -> pixel. point = projection.pixel_for_lat_lng(coordinate) projected = projection.lat_lng_for_pixel(point) + replayed = projection.pixel_for_lat_lng(projected) + assert math.isclose(projected.latitude, coordinate.latitude, abs_tol=1e-6) + assert math.isclose(projected.longitude, coordinate.longitude, abs_tol=1e-6) + assert math.isclose(replayed.x, point.x, abs_tol=1e-6) + assert math.isclose(replayed.y, point.y, abs_tol=1e-6) + + # A setter applies before returning, so it changes later + # conversions. projection.set_camera(camera.CameraOptions(center=coordinate, zoom=2.0)) + recentered = projection.pixel_for_lat_lng(coordinate) + assert (recentered.x, recentered.y) != (point.x, point.y) + moved_camera = projection.get_camera() + assert moved_camera.center is not None + assert moved_camera.center.latitude == pytest.approx(0.0) + assert moved_camera.zoom == pytest.approx(2.0) + + # The projection is usable from a second Python thread. + results: list[geo.LatLng] = [] + thread = threading.Thread( + target=lambda: results.append(projection.lat_lng_for_pixel(recentered)) + ) + thread.start() + thread.join() + assert len(results) == 1 + assert math.isclose(results[0].latitude, coordinate.latitude, abs_tol=1e-6) + assert math.isclose( + results[0].longitude, coordinate.longitude, abs_tol=1e-6 + ) + projection.set_visible_coordinates( (geo.LatLng(-1.0, -1.0), geo.LatLng(1.0, 1.0)), camera.EdgeInsets(), @@ -2647,40 +2442,56 @@ def test_map_projection_converts_coordinates_and_closes() -> None: ), camera.EdgeInsets(), ) - assert not projection.closed assert isinstance(projection.get_camera(), camera.CameraOptions) - assert isinstance(point, camera.ScreenPoint) - assert math.isfinite(projected.latitude) - assert math.isfinite(projected.longitude) + # The close is synchronous, so the handle is retired when it returns + # and a later call is rejected. assert projection.closed + with pytest.raises(mln.InvalidArgumentError): + projection.get_camera() + + +def test_map_projection_outlives_its_source_handles() -> None: + runtime = mln.RuntimeHandle() + map_handle = runtime.create_map().result(timeout=5) + projection = _await(map_handle.create_projection()) + + map_handle.close() + runtime.close() + + assert isinstance(projection.get_camera(), camera.CameraOptions) + projection.close() def test_map_projection_remains_usable_on_another_thread_after_map_close() -> None: runtime = mln.RuntimeHandle() - map_handle = runtime.create_map() - projection = map_handle.create_projection() + map_handle = runtime.create_map().result(timeout=5) + projection = _await(map_handle.create_projection()) map_handle.close() runtime.close() + failures: list[BaseException] = [] + def use_and_close_projection() -> None: - assert projection.get_camera() is not None - projection.close() + try: + assert isinstance(projection.get_camera(), camera.CameraOptions) + projection.close() + except BaseException as error: # noqa: BLE001 - reported by the test thread + failures.append(error) - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - executor.submit(use_and_close_projection).result() + thread = threading.Thread(target=use_and_close_projection) + thread.start() + thread.join() + assert not failures assert projection.closed -def test_offline_region_operation_starts_return_public_handles(tmp_path: Path) -> None: +def test_offline_futures_complete_with_public_values(tmp_path: Path) -> None: definition = offline.OfflineTilePyramidRegionDefinition( style_url="https://example.test/style.json", - bounds=geo.LatLngBounds( - southwest=geo.LatLng(-1.0, -1.0), - northeast=geo.LatLng(1.0, 1.0), - ), + bounds=geo.LatLngBounds(geo.LatLng(-1.0, -2.0), geo.LatLng(1.0, 2.0)), min_zoom=0.0, max_zoom=1.0, pixel_ratio=1.0, @@ -2688,286 +2499,29 @@ def test_offline_region_operation_starts_return_public_handles(tmp_path: Path) - with mln.RuntimeHandle( mln.RuntimeOptions(cache_path=str(tmp_path / "cache.db")) ) as runtime: - operations = [ - runtime.create_offline_region(definition, b"metadata"), - runtime.list_offline_regions(), - runtime.get_offline_region(1), - runtime.update_offline_region_metadata(1, b"updated"), - runtime.get_offline_region_status(1), - runtime.set_offline_region_observed(1, False), - runtime.set_offline_region_download_state( - 1, - offline.OfflineRegionDownloadState.INACTIVE, - ), - runtime.invalidate_offline_region(1), - runtime.delete_offline_region(1), - ] - - for operation in operations: - assert isinstance(operation, offline.OfflineOperationHandle) - assert not operation.closed - operation.close() - assert operation.closed - - -def test_offline_operation_take_results_convert_public_values() -> None: - class FakeNativeRuntime: - def __init__(self) -> None: - self.discarded = [] - - def offline_region_create_take_result( - self, operation_id: int - ) -> dict[str, object]: - assert operation_id == 1 - return region_wire(b"created") - - def offline_region_get_take_result( - self, operation_id: int - ) -> dict[str, object] | None: - assert operation_id == 2 - return region_wire(b"found") - - def offline_regions_list_take_result( - self, operation_id: int - ) -> list[dict[str, object]]: - assert operation_id == 3 - return [region_wire(b"listed")] - - def offline_regions_merge_database_take_result( - self, - operation_id: int, - ) -> list[dict[str, object]]: - assert operation_id == 4 - return [region_wire(b"merged")] - - def offline_region_update_metadata_take_result( - self, - operation_id: int, - ) -> dict[str, object]: - assert operation_id == 5 - return region_wire(b"updated") - - def offline_region_get_status_take_result( - self, operation_id: int - ) -> dict[str, object]: - assert operation_id == 6 - return { - "download_state": 1, - "completed_resource_count": 2, - "completed_resource_size": 3, - "completed_tile_count": 4, - "required_tile_count": 5, - "completed_tile_size": 6, - "required_resource_count": 7, - "required_resource_count_is_precise": True, - "complete": False, - } - - def offline_operation_discard(self, operation_id: int) -> None: - self.discarded.append(operation_id) - - class FakeRuntime: - def __init__(self) -> None: - self._native = FakeNativeRuntime() - self.operations: set[offline.OfflineOperationHandle] = set() - - def _register_offline_operation( - self, operation: offline.OfflineOperationHandle - ) -> None: - self.operations.add(operation) - - def _unregister_offline_operation( - self, operation: offline.OfflineOperationHandle - ) -> None: - self.operations.discard(operation) - - def region_wire(metadata: bytes) -> dict[str, object]: - return { - "id": 42, - "definition": { - "type": "tile_pyramid", - "style_url": "https://example.test/style.json", - "bounds": { - "southwest": {"latitude": -1.0, "longitude": -2.0}, - "northeast": {"latitude": 1.0, "longitude": 2.0}, - }, - "min_zoom": 0.0, - "max_zoom": 10.0, - "pixel_ratio": 1.0, - "include_ideographs": True, - }, - "metadata": metadata, - } - - runtime = FakeRuntime() - - with pytest.raises(TypeError, match="created by RuntimeHandle"): - offline.OfflineOperationHandle(runtime, 99) - - region = offline.OfflineOperationHandle._from_native(runtime, 1).take_region() - optional = offline.OfflineOperationHandle._from_native( - runtime, 2 - ).take_optional_region() - listed = offline.OfflineOperationHandle._from_native(runtime, 3).take_region_list() - merged = offline.OfflineOperationHandle._from_native(runtime, 4).take_region_list( - merge_result=True, - ) - updated = offline.OfflineOperationHandle._from_native( - runtime, 5 - ).take_updated_region() - status = offline.OfflineOperationHandle._from_native(runtime, 6).take_status() - - assert region.id == 42 - assert isinstance(region.definition, offline.OfflineTilePyramidRegionDefinition) - assert region.definition.bounds.southwest == geo.LatLng(-1.0, -2.0) - assert optional is not None - assert optional.metadata == b"found" - assert listed[0].metadata == b"listed" - assert merged[0].metadata == b"merged" - assert updated.metadata == b"updated" - assert status.download_state == offline.OfflineRegionDownloadState.ACTIVE - assert status.completed_resource_count == 2 - assert runtime.operations == set() - - -def test_offline_operation_take_rejects_closed_handles() -> None: - class FakeNativeRuntime: - def __init__(self) -> None: - self.discarded: list[int] = [] - self.status_takes = 0 - - def offline_operation_discard(self, operation_id: int) -> None: - self.discarded.append(operation_id) - - def offline_region_get_status_take_result( - self, - operation_id: int, - ) -> dict[str, object]: - assert operation_id == 10 - self.status_takes += 1 - return { - "download_state": 1, - "completed_resource_count": 0, - "completed_resource_size": 0, - "completed_tile_count": 0, - "required_tile_count": 0, - "completed_tile_size": 0, - "required_resource_count": 0, - "required_resource_count_is_precise": True, - "complete": True, - } - - def offline_region_create_take_result(self, operation_id: int) -> None: - raise AssertionError("closed handle called native take") - - def offline_region_get_take_result(self, operation_id: int) -> None: - raise AssertionError("closed handle called native take") - - def offline_regions_list_take_result(self, operation_id: int) -> None: - raise AssertionError("closed handle called native take") - - def offline_regions_merge_database_take_result(self, operation_id: int) -> None: - raise AssertionError("closed handle called native take") - - def offline_region_update_metadata_take_result(self, operation_id: int) -> None: - raise AssertionError("closed handle called native take") - - class FakeRuntime: - def __init__(self) -> None: - self._native = FakeNativeRuntime() - self.operations: set[offline.OfflineOperationHandle] = set() - - def _register_offline_operation( - self, operation: offline.OfflineOperationHandle - ) -> None: - self.operations.add(operation) - - def _unregister_offline_operation( - self, operation: offline.OfflineOperationHandle - ) -> None: - self.operations.discard(operation) - - runtime = FakeRuntime() - - status_handle = offline.OfflineOperationHandle._from_native(runtime, 10) - assert status_handle.take_status().complete is True - with pytest.raises(mln.InvalidStateError, match="offline operation handle"): - status_handle.take_status() - assert runtime._native.status_takes == 1 - - closed_takes = ( - lambda handle: handle.take_region(), - lambda handle: handle.take_optional_region(), - lambda handle: handle.take_region_list(), - lambda handle: handle.take_region_list(merge_result=True), - lambda handle: handle.take_updated_region(), - lambda handle: handle.take_status(), - ) - for offset, take in enumerate(closed_takes, start=20): - handle = offline.OfflineOperationHandle._from_native(runtime, offset) - handle.close() - with pytest.raises(mln.InvalidStateError, match="offline operation handle"): - take(handle) - - assert runtime._native.discarded == list(range(20, 26)) - assert runtime._native.status_takes == 1 - assert runtime.operations == set() - - -def test_runtime_close_rejects_live_offline_operations(tmp_path: Path) -> None: - runtime = mln.RuntimeHandle(mln.RuntimeOptions(cache_path=str(tmp_path))) - operation = runtime.run_ambient_cache_operation(offline.AmbientCacheOperation.CLEAR) - try: - assert not operation.closed - with pytest.raises(mln.InvalidStateError, match="offline operation"): - runtime.close() - - assert not runtime.closed - assert not operation.closed - finally: - operation.close() - runtime.close() - - -def test_ambient_cache_operation_starts_and_discards_through_public_api( - tmp_path: Path, -) -> None: - with mln.RuntimeHandle(mln.RuntimeOptions(cache_path=str(tmp_path))) as runtime: - operation = runtime.run_ambient_cache_operation( - offline.AmbientCacheOperation.CLEAR, + created = _await(runtime.create_offline_region(definition, b"metadata")) + assert created.metadata == b"metadata" + assert _await(runtime.get_offline_region(created.id)) == created + assert created in _await(runtime.list_offline_regions()) + assert ( + _await(runtime.get_offline_region_status(created.id)).download_state + == offline.OfflineRegionDownloadState.INACTIVE ) + assert _await(runtime.set_offline_region_observed(created.id, False)) is None + assert _await(runtime.delete_offline_region(created.id)) is None - assert isinstance(operation, offline.OfflineOperationHandle) - assert not operation.closed - operation.close() - assert operation.closed - operation.close() - - -def test_set_maximum_ambient_cache_size_starts_and_discards_through_public_api( - tmp_path: Path, -) -> None: - with mln.RuntimeHandle(mln.RuntimeOptions(cache_path=str(tmp_path))) as runtime: - operation = runtime.set_maximum_ambient_cache_size(8 << 20) - - assert isinstance(operation, offline.OfflineOperationHandle) - assert not operation.closed - event = _wait_for_offline_operation(runtime, operation) - completed = event.payload - assert isinstance(completed, offline.OfflineOperationCompleted) - # The completion reports a named kind rather than an unknown value. +def test_ambient_cache_futures_complete_through_public_api(tmp_path: Path) -> None: + with mln.RuntimeHandle( + mln.RuntimeOptions(cache_path=str(tmp_path / "cache.db")) + ) as runtime: assert ( - completed.operation_kind - == offline.OfflineOperationKind.SET_MAXIMUM_AMBIENT_CACHE_SIZE + _await( + runtime.run_ambient_cache_operation(offline.AmbientCacheOperation.CLEAR) + ) + is None ) - assert not completed.operation_kind.is_unknown - - operation.close() - assert operation.closed - - # An out-of-range size stays on the binding's documented error family - # instead of leaking the extension module's OverflowError. + assert _await(runtime.set_maximum_ambient_cache_size(8 << 20)) is None with pytest.raises(mln.InvalidArgumentError): runtime.set_maximum_ambient_cache_size(-1) with pytest.raises(mln.InvalidArgumentError): @@ -2994,15 +2548,6 @@ def test_offline_values_wrap_runtime_event_payload_shape() -> None: required_resource_count_is_precise=True, complete=False, ) - completed = offline.OfflineOperationCompleted._from_runtime_payload( - { - "operation_id": 7, - "operation_kind": offline.OfflineOperationKind.REGION_CREATE.native_code, - "result_kind": offline.OfflineOperationResultKind.REGION, - "result_status": mln.MaplibreStatus.OK.native_code, - "found": True, - } - ) response_error = offline.OfflineRegionResponseError._from_runtime_payload( { "region_id": 8, @@ -3020,8 +2565,6 @@ def test_offline_values_wrap_runtime_event_payload_shape() -> None: definition.definition_type == offline.OfflineRegionDefinitionType.TILE_PYRAMID ) assert status.download_state == offline.OfflineRegionDownloadState.ACTIVE - assert completed.operation_kind == offline.OfflineOperationKind.REGION_CREATE - assert completed.result_kind == offline.OfflineOperationResultKind.REGION unknown_state = offline.OfflineRegionDownloadState(999_001) assert unknown_state.native_code == 999_001 with pytest.raises(mln.InvalidArgumentError, match="cannot be set"): @@ -3083,10 +2626,13 @@ def test_process_global_logging_receiver_copies_native_records() -> None: receiver = log.set_log_callback(max_queued_records=8, consume=True) try: log.set_async_log_severity_mask(log.LogSeverityMask.DEFAULT) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - with pytest.raises((mln.InvalidArgumentError, mln.NativeError)): - map_handle.set_style_json(b"{") + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + map_handle.set_style_json(b"{") map_handle.dump_debug_logs() + runtime.barrier().result(timeout=5) records = [] while (record := receiver.poll_record()) is not None: @@ -3104,10 +2650,13 @@ def test_process_global_logging_receiver_copies_native_records() -> None: def test_log_receiver_reports_dropped_records() -> None: receiver = log.set_log_callback(max_queued_records=1, consume=True) try: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - with pytest.raises((mln.InvalidArgumentError, mln.NativeError)): - map_handle.set_style_json(b"{") + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): + map_handle.set_style_json(b"{") map_handle.dump_debug_logs() + runtime.barrier().result(timeout=5) assert receiver.poll_record() is not None assert receiver.dropped_record_count >= 0 @@ -3251,11 +2800,11 @@ def provider( def test_offline_download_state_setter_rejects_unknown_before_native_call() -> None: class FakeRuntimeNative: - def offline_region_set_download_state_start( + def set_offline_region_download_state( self, region_id: int, state: int, - ) -> int: + ) -> None: raise AssertionError((region_id, state)) runtime = mln.RuntimeHandle.__new__(mln.RuntimeHandle) @@ -3375,7 +2924,7 @@ def transform(request: resource.ResourceTransformRequest) -> str | None: with mln.RuntimeHandle() as runtime: runtime.set_resource_transform(transform, max_pending_callbacks=1) - with runtime.create_map(): + with runtime.create_map().result(timeout=5): runtime.set_resource_transform(transform, max_pending_callbacks=1) runtime.clear_resource_transform() @@ -3421,7 +2970,7 @@ def provider( with _online_network(), _http_style_server() as (style_url, served): with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url(style_url) _wait_for_runtime_event(runtime, mln.RuntimeEventType.MAP_STYLE_LOADED) @@ -3450,7 +2999,7 @@ def transform(request: resource.ResourceTransformRequest) -> str | None: _online_network(), _http_style_server() as (rewritten_style_url, served), mln.RuntimeHandle() as runtime, - runtime.create_map() as map_handle, + runtime.create_map().result(timeout=5) as map_handle, ): runtime.set_resource_transform(transform, max_pending_callbacks=4) map_handle.set_style_url("http://example.invalid/original-style.json") @@ -3473,7 +3022,7 @@ def transform(request: resource.ResourceTransformRequest) -> str: _online_network(), _http_style_server() as (style_url, served), mln.RuntimeHandle() as runtime, - runtime.create_map() as map_handle, + runtime.create_map().result(timeout=5) as map_handle, ): runtime.set_resource_transform(transform, max_pending_callbacks=1) runtime.clear_resource_transform() @@ -3507,7 +3056,7 @@ def load_unservable_style( # style URL proves the request reached the network file source. map_handle.set_style_url(style_url) for _ in range(5000): - runtime.pump() + time.sleep(0.001) for event in runtime.drain_events().events: if ( event.event_type == mln.RuntimeEventType.MAP_LOADING_FAILED @@ -3523,7 +3072,7 @@ def load_unservable_style( runtime.set_resource_provider( counting_provider(first_urls), max_pending_callbacks=4 ) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: load_unservable_style(runtime, map_handle, "jar:file:/packaged/first.json") assert "jar:file:/packaged/first.json" in first_urls @@ -3560,8 +3109,8 @@ def provider( return resource.ResourceProviderDecision.HANDLE with mln.RuntimeHandle() as runtime: - runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + _await(runtime.set_resource_provider(provider, max_pending_callbacks=4)) + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("maplibre://maps/style") _wait_for_runtime_event(runtime, mln.RuntimeEventType.MAP_STYLE_LOADED) @@ -3584,7 +3133,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://inline-style.json") _wait_for_runtime_event(runtime, mln.RuntimeEventType.MAP_STYLE_LOADED) @@ -3609,7 +3158,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://deferred-style.json") handle = _wait_for_provider_handle(runtime, handles) @@ -3652,7 +3201,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://cross-thread-style.json") handle = _wait_for_provider_handle(runtime, handles) @@ -3692,7 +3241,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://error-style.json") event = _wait_for_runtime_event( runtime, @@ -3734,37 +3283,28 @@ def provider( with mln.RuntimeHandle( mln.RuntimeOptions(cache_path=str(tmp_path / "offline-cache.db")) ) as runtime: - runtime.set_resource_provider(provider, max_pending_callbacks=4) - create = runtime.create_offline_region(definition, b"metadata") - _wait_for_offline_operation(runtime, create) - region = create.take_region() + _await(runtime.set_resource_provider(provider, max_pending_callbacks=4)) + region = _await(runtime.create_offline_region(definition, b"metadata")) - observe = runtime.set_offline_region_observed(region.id, True) - _wait_for_offline_operation(runtime, observe) - observe.close() + _await(runtime.set_offline_region_observed(region.id, True)) - activate = runtime.set_offline_region_download_state( - region.id, - offline.OfflineRegionDownloadState.ACTIVE, + _await( + runtime.set_offline_region_download_state( + region.id, + offline.OfflineRegionDownloadState.ACTIVE, + ) ) - try: - event = _wait_for_runtime_event( - runtime, - mln.RuntimeEventType.OFFLINE_REGION_RESPONSE_ERROR, + event = _wait_for_runtime_event( + runtime, + mln.RuntimeEventType.OFFLINE_REGION_RESPONSE_ERROR, + ) + _await( + runtime.set_offline_region_download_state( + region.id, + offline.OfflineRegionDownloadState.INACTIVE, ) - finally: - activate.close() - - deactivate = runtime.set_offline_region_download_state( - region.id, - offline.OfflineRegionDownloadState.INACTIVE, ) - _wait_for_offline_operation(runtime, deactivate) - deactivate.close() - - unobserve = runtime.set_offline_region_observed(region.id, False) - _wait_for_offline_operation(runtime, unobserve) - unobserve.close() + _await(runtime.set_offline_region_observed(region.id, False)) assert isinstance(event.payload, offline.OfflineRegionResponseError) assert event.payload.region_id == region.id @@ -3786,13 +3326,13 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - map_handle = runtime.create_map() + map_handle = runtime.create_map().result(timeout=5) map_handle.set_style_url("custom://cancelled-style.json") handle = _wait_for_provider_handle(runtime, handles) map_handle.close() for _ in range(5000): - runtime.pump() + time.sleep(0.001) if handle.is_cancelled(): break time.sleep(0.001) @@ -3823,7 +3363,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://stale-style-1.json") stale_handle = _wait_for_provider_handle(runtime, handles) stale_handle.close() @@ -3857,7 +3397,7 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_url("custom://release-race-style.json") handle = _wait_for_provider_handle(runtime, handles) @@ -3889,15 +3429,19 @@ def probe_cancelled() -> None: def test_custom_geometry_source_scaffolding_queues_copied_events() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_geometry_source( + source, _ = map_handle.add_custom_geometry_source( "custom", style.CustomGeometrySourceOptions( has_cancel_tile=True, max_queued_events=1, ), ) + runtime.barrier().result(timeout=5) source._native.push_fetch_for_test(1, 2, 3) source._native.push_cancel_for_test(4, 5, 6) @@ -3951,12 +3495,13 @@ def provider( with mln.RuntimeHandle() as runtime: runtime.set_resource_provider(provider, max_pending_callbacks=4) - with runtime.create_map() as map_handle: + with runtime.create_map().result(timeout=5) as map_handle: map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_geometry_source( + source, _ = map_handle.add_custom_geometry_source( "custom", style.CustomGeometrySourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) # The inline load above queued a style-loaded event, so drain it to # keep the wait below tied to the URL load. @@ -3985,7 +3530,7 @@ def _load_style_and_collect_types( map_handle.set_style_json(_EMPTY_STYLE_BYTES) types: set[mln.RuntimeEventType] = set() for _ in range(500): - runtime.pump() + time.sleep(0.001) types.update(event.event_type for event in runtime.drain_events().events) if until is not None and until in types: break @@ -3998,21 +3543,26 @@ def test_style_json_load_releases_custom_geometry_state_with_style_loaded_cleare ): style_loaded = mln.RuntimeEventType.MAP_STYLE_LOADED - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): # The same drive reports a style-loaded event while the type is # selected, so the cleared phase below asserts a real negative. assert style_loaded in _load_style_and_collect_types( runtime, map_handle, style_loaded ) - source = map_handle.add_custom_geometry_source( + source, _ = map_handle.add_custom_geometry_source( "custom-masked", style.CustomGeometrySourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) assert source.closed is False map_handle.set_event_mask( mln.RuntimeEventMask.ALL & ~mln.RuntimeEventMask.MAP_STYLE_LOADED ) + runtime.barrier().result(timeout=5) types = _load_style_and_collect_types(runtime, map_handle, None) # The C API releases the dropped source itself, so the release does not @@ -4022,14 +3572,23 @@ def test_style_json_load_releases_custom_geometry_state_with_style_loaded_cleare def test_remove_style_source_releases_custom_geometry_handle() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_geometry_source( + source, _ = map_handle.add_custom_geometry_source( "custom-remove", style.CustomGeometrySourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) - assert map_handle.remove_style_source("custom-remove") is True + completion = map_handle.remove_style_source("custom-remove") + runtime.barrier().result(timeout=5) + assert ( + _await_command_completion(runtime, completion).disposition + == mln.CommandDisposition.COMMITTED + ) assert source.closed source._native.push_fetch_for_test(1, 2, 3) assert source.poll_event() is None @@ -4037,14 +3596,16 @@ def test_remove_style_source_releases_custom_geometry_handle() -> None: def test_map_close_releases_custom_geometry_handle() -> None: with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() + map_handle = runtime.create_map().result(timeout=5) map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_geometry_source( + source, _ = map_handle.add_custom_geometry_source( "custom-close", style.CustomGeometrySourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) map_handle.close() + runtime.barrier().result(timeout=5) assert source.closed source._native.push_fetch_for_test(1, 2, 3) @@ -4053,7 +3614,10 @@ def test_map_close_releases_custom_geometry_handle() -> None: def test_custom_geometry_source_rejects_empty_queue_capacity() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) with pytest.raises(mln.InvalidArgumentError): map_handle.add_custom_geometry_source( @@ -4063,15 +3627,19 @@ def test_custom_geometry_source_rejects_empty_queue_capacity() -> None: def test_custom_mvt_vector_source_scaffolding_queues_copied_events() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_mvt_vector_source( + source, _ = map_handle.add_custom_mvt_vector_source( "custom-mvt", style.CustomMvtVectorSourceOptions( has_cancel_tile=True, max_queued_events=1, ), ) + runtime.barrier().result(timeout=5) source._native.push_fetch_for_test(1, 2, 3) source._native.push_cancel_for_test(4, 5, 6) @@ -4090,23 +3658,31 @@ def test_custom_mvt_vector_source_scaffolding_queues_copied_events() -> None: "custom-mvt", tile, "tile missing" ) map_handle.invalidate_custom_mvt_vector_source_tile("custom-mvt", tile) - assert ( - map_handle.get_style_source_type("custom-mvt") - == style.StyleSourceType.CUSTOM_MVT_VECTOR - ) + info = map_handle.get_style_source_info("custom-mvt").result(timeout=5) + assert info is not None + assert info.source_type == style.StyleSourceType.CUSTOM_MVT_VECTOR source.close() assert source.closed def test_remove_style_source_releases_custom_mvt_vector_handle() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_mvt_vector_source( + source, _ = map_handle.add_custom_mvt_vector_source( "custom-mvt-remove", style.CustomMvtVectorSourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) - assert map_handle.remove_style_source("custom-mvt-remove") is True + completion = map_handle.remove_style_source("custom-mvt-remove") + runtime.barrier().result(timeout=5) + assert ( + _await_command_completion(runtime, completion).disposition + == mln.CommandDisposition.COMMITTED + ) assert source.closed source._native.push_fetch_for_test(1, 2, 3) assert source.poll_event() is None @@ -4114,14 +3690,16 @@ def test_remove_style_source_releases_custom_mvt_vector_handle() -> None: def test_map_close_releases_custom_mvt_vector_handle() -> None: with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() + map_handle = runtime.create_map().result(timeout=5) map_handle.set_style_json(_EMPTY_STYLE_BYTES) - source = map_handle.add_custom_mvt_vector_source( + source, _ = map_handle.add_custom_mvt_vector_source( "custom-mvt-close", style.CustomMvtVectorSourceOptions(max_queued_events=1), ) + runtime.barrier().result(timeout=5) map_handle.close() + runtime.barrier().result(timeout=5) assert source.closed source._native.push_fetch_for_test(1, 2, 3) @@ -4130,7 +3708,10 @@ def test_map_close_releases_custom_mvt_vector_handle() -> None: def test_custom_mvt_vector_source_rejects_empty_queue_capacity() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): map_handle.set_style_json(_EMPTY_STYLE_BYTES) with pytest.raises(mln.InvalidArgumentError): map_handle.add_custom_mvt_vector_source( @@ -4147,12 +3728,15 @@ def test_set_bounds_rejects_unsupported_constraint() -> None: northeast=geo.LatLng(90.0, 180.0), ) - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises(mln.InvalidArgumentError): map_handle.set_bounds( camera.BoundOptions(bounds=world) # type: ignore[arg-type] ) - assert map_handle.get_bounds().bounds == camera.Unbounded() + assert map_handle.snapshot().bounds.bounds == camera.Unbounded() @pytest.mark.parametrize("transition_id", [-1, 2**64]) @@ -4161,7 +3745,7 @@ def test_transition_id_out_of_range_raises_binding_error(transition_id: int) -> # would see a bare OverflowError instead of the binding's error shape. with ( mln.RuntimeHandle() as runtime, - runtime.create_map() as map_handle, + runtime.create_map().result(timeout=5) as map_handle, pytest.raises(mln.InvalidArgumentError), ): map_handle.ease_to( @@ -4173,13 +3757,13 @@ def test_transition_id_out_of_range_raises_binding_error(transition_id: int) -> def test_released_map_id_replayed_after_a_new_map_reports_it_stale() -> None: """BND-045.""" with mln.RuntimeHandle() as runtime: - first = runtime.create_map() + first = runtime.create_map().result(timeout=5) released = first._native.id() first.close() # The released slot is the one the next map takes, so the replayed id # names a retired generation of a slot that is live again. - second = runtime.create_map() + second = runtime.create_map().result(timeout=5) with pytest.raises(mln.InvalidArgumentError) as excinfo: _native.map_size_by_id_for_test(released) @@ -4190,41 +3774,18 @@ def test_released_map_id_replayed_after_a_new_map_reports_it_stale() -> None: second.close() -def test_map_id_passed_to_a_runtime_operation_reports_invalid_argument() -> None: - """BND-047.""" - with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() - - with pytest.raises(mln.InvalidArgumentError) as excinfo: - _native.pump_runtime_with_map_id_for_test(map_handle._native.id()) - message = str(excinfo.value) - assert "map" in message - assert "runtime" in message - - map_handle.close() - - -def test_live_map_id_called_from_another_thread_reports_wrong_thread() -> None: +def test_live_map_snapshot_is_any_thread() -> None: """BND-049.""" with mln.RuntimeHandle() as runtime: - map_handle = runtime.create_map() + map_handle = runtime.create_map().result(timeout=5) live = map_handle._native.id() + results: list[tuple[int, int, float]] = [] - failures: list[BaseException] = [] - - def call_from_other_thread() -> None: - try: - _native.map_size_by_id_for_test(live) - except BaseException as error: # noqa: BLE001 - recorded for the assert - failures.append(error) - - thread = threading.Thread(target=call_from_other_thread) + thread = threading.Thread( + target=lambda: results.append(_native.map_size_by_id_for_test(live)) + ) thread.start() thread.join() - # The id is live, so the owner-thread rule decides rather than identity. - assert len(failures) == 1 - assert isinstance(failures[0], mln.WrongThreadError) - assert "stale" not in str(failures[0]) - + assert results == [map_handle.get_size()] map_handle.close() diff --git a/bindings/python/tests/test_render_metal_attach.py b/bindings/python/tests/test_render_metal_attach.py index bdccde9f4..f6054fd62 100644 --- a/bindings/python/tests/test_render_metal_attach.py +++ b/bindings/python/tests/test_render_metal_attach.py @@ -10,8 +10,12 @@ EMPTY_STYLE_JSON, RED_BACKGROUND_STYLE_JSON, RED_PIXEL, + close_session, + finish_attach, + finish_render_operation, render_until, render_until_update, + request_and_finish_frame, skip_or_fail_fixture_setup, ) @@ -120,8 +124,10 @@ def _is_painted_red(texture: MetalBorrowedTexture) -> bool: def _assert_public_session_shape(session: render.RenderSessionHandle) -> None: assert isinstance(session, render.RenderSessionHandle) assert session.closed is False - assert session.detached is False - assert callable(session.render_update) + assert session.snapshot.state == render.RenderSessionState.ATTACHED + assert isinstance(session.capabilities, render.RenderSessionCapabilities) + assert callable(session.request_frame) + assert callable(session.drain_frame_results) assert callable(session.close) @@ -137,7 +143,10 @@ def _descriptor_snapshot( def test_invalid_metal_surface_attach_reports_native_status() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises( (mln.InvalidArgumentError, mln.UnsupportedFeatureError) ) as raised: @@ -156,16 +165,17 @@ def test_metal_surface_attach_reports_public_render_session_shape() -> None: mln.RuntimeHandle() as runtime, runtime.create_map( mln.MapOptions(width=surface.width, height=surface.height) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_surface(surface.descriptor()) + session, attach = map_handle.attach_metal_surface(surface.descriptor()) try: + finish_attach(session, attach) _assert_public_session_shape(session) map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) render_until_update(runtime, session) finally: - session.close() + close_session(session) def test_metal_borrowed_texture_attach_reports_public_render_session_shape() -> None: @@ -179,10 +189,11 @@ def test_metal_borrowed_texture_attach_reports_public_render_session_shape() -> width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_borrowed_texture(descriptor) + session, attach = map_handle.attach_metal_borrowed_texture(descriptor) try: + finish_attach(session, attach) _assert_public_session_shape(session) map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) @@ -192,7 +203,7 @@ def test_metal_borrowed_texture_attach_reports_public_render_session_shape() -> session.acquire_metal_owned_texture_frame() assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED finally: - session.close() + close_session(session) def test_metal_borrowed_texture_session_close_preserves_caller_resources() -> None: @@ -208,11 +219,12 @@ def test_metal_borrowed_texture_session_close_preserves_caller_resources() -> No width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_borrowed_texture(descriptor) + session, attach = map_handle.attach_metal_borrowed_texture(descriptor) + finish_attach(session, attach) _assert_public_session_shape(session) - session.close() + close_session(session) assert _descriptor_snapshot(descriptor) == before_descriptor assert texture.texture is before_texture @@ -239,9 +251,10 @@ def test_metal_borrowed_texture_set_target_renders_into_the_replacement() -> Non width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_borrowed_texture(descriptor) + session, attach = map_handle.attach_metal_borrowed_texture(descriptor) + finish_attach(session, attach) try: map_handle.set_style_json(RED_BACKGROUND_STYLE_JSON.encode()) render_until( @@ -252,7 +265,7 @@ def test_metal_borrowed_texture_set_target_renders_into_the_replacement() -> Non ) with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.resize(96, 48, 1.0) + session.resize(render.RenderTargetExtent(96, 48, 1.0)) assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED with _metal_borrowed_texture( @@ -260,7 +273,14 @@ def test_metal_borrowed_texture_set_target_renders_into_the_replacement() -> Non ) as replacement: assert not any(replacement.read_rgba()) - session.set_metal_borrowed_texture_target(replacement.descriptor()) + finish_render_operation( + session, + session.set_metal_borrowed_texture_target( + replacement.descriptor() + ), + ) + map_handle.resize(96, 48, 1.0) + runtime.barrier().result(timeout=5) # The session kept its renderer and paints the # texture it was handed, at the extent handed with @@ -277,7 +297,7 @@ def test_metal_borrowed_texture_set_target_renders_into_the_replacement() -> Non pytest.approx(1.0), ) finally: - session.close() + close_session(session) def test_metal_surface_set_target_presents_through_a_new_surface() -> None: @@ -294,15 +314,20 @@ def test_metal_surface_set_target_presents_through_a_new_surface() -> None: mln.RuntimeHandle() as runtime, runtime.create_map( mln.MapOptions(width=surface.width, height=surface.height) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_surface(surface.descriptor()) + session, attach = map_handle.attach_metal_surface(surface.descriptor()) + finish_attach(session, attach) try: map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) render_until_update(runtime, session) with _metal_surface(context, width=48, height=24) as replacement: - session.set_metal_surface_target(replacement.descriptor()) + finish_render_operation( + session, session.set_metal_surface_target(replacement.descriptor()) + ) + map_handle.resize(48, 24, 1.0) + runtime.barrier().result(timeout=5) render_until( runtime, @@ -310,10 +335,13 @@ def test_metal_surface_set_target_presents_through_a_new_surface() -> None: lambda: map_handle.get_size() == (48, 24, pytest.approx(1.0)), "the map never took the replacement surface extent", ) - assert session.detached is False - assert session.render_update().result == render.RenderResult.RENDERED + assert session.snapshot.state == render.RenderSessionState.ATTACHED + assert ( + request_and_finish_frame(session).disposition + == render.RenderResult.RENDERED + ) finally: - session.close() + close_session(session) def test_metal_set_target_reports_unsupported_for_another_target_kind() -> None: @@ -329,24 +357,32 @@ def test_metal_set_target_reports_unsupported_for_another_target_kind() -> None: mln.RuntimeHandle() as runtime, runtime.create_map( mln.MapOptions(width=surface.width, height=surface.height) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_metal_borrowed_texture(texture.descriptor()) + session, attach = map_handle.attach_metal_borrowed_texture(texture.descriptor()) + finish_attach(session, attach) try: with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.set_metal_surface_target(surface.descriptor()) + finish_render_operation( + session, + session.set_metal_surface_target(surface.descriptor()), + ) assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED # The rejection left the session usable. - session.render_update() + request_and_finish_frame(session) finally: - session.close() + close_session(session) - session = map_handle.attach_metal_surface(surface.descriptor()) + session, attach = map_handle.attach_metal_surface(surface.descriptor()) + finish_attach(session, attach) try: with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.set_metal_borrowed_texture_target(texture.descriptor()) + finish_render_operation( + session, + session.set_metal_borrowed_texture_target(texture.descriptor()), + ) assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED # The rejection left the session usable. - session.render_update() + request_and_finish_frame(session) finally: - session.close() + close_session(session) diff --git a/bindings/python/tests/test_render_metal_owned.py b/bindings/python/tests/test_render_metal_owned.py index ea2bf54b2..ca52774c6 100644 --- a/bindings/python/tests/test_render_metal_owned.py +++ b/bindings/python/tests/test_render_metal_owned.py @@ -1,17 +1,22 @@ from __future__ import annotations -import threading import time from collections.abc import Callable from dataclasses import dataclass import maplibre_native_ffi as mln import pytest -from maplibre_native_ffi import camera, query, render +from maplibre_native_ffi import render from render_backend_helpers.runtime import ( EMPTY_STYLE_JSON, assert_cluster_feature_extensions, assert_geojson_cluster_source, + close_session, + finish_attach, + finish_render_operation, + release_frame, + render_until_update, + request_and_finish_frame, skip_or_fail_fixture_setup, ) @@ -56,12 +61,18 @@ def create( runtime = mln.RuntimeHandle() try: map_handle = runtime.create_map( - mln.MapOptions(width=64, height=64, mode=mln.MapMode.STATIC) - ) + mln.MapOptions( + width=width, + height=height, + scale_factor=scale_factor, + mode=mln.MapMode.CONTINUOUS, + ) + ).result(timeout=5) try: - session = map_handle.attach_metal_owned_texture( + session, attach = map_handle.attach_metal_owned_texture( context.owned_texture_descriptor(width, height, scale_factor) ) + finish_attach(session, attach) except BaseException: map_handle.close() raise @@ -74,7 +85,7 @@ def create( def close(self) -> None: if not self.session.closed: - self.session.close() + close_session(self.session) if not self.map.closed: self.map.close() if not self.runtime.closed: @@ -83,8 +94,9 @@ def close(self) -> None: def render_once(self) -> None: self.map.set_style_json(EMPTY_STYLE_JSON.encode()) + self.runtime.barrier().result(timeout=5) frame = wait_for_metal_frame(self, lambda _: True) - frame.close() + release_frame(frame) @pytest.fixture @@ -96,12 +108,10 @@ def metal_owned_session() -> MetalOwnedSession: fixture.close() -def request_still_image_if_needed(map_handle: mln.MapHandle) -> None: - try: - map_handle.request_still_image() - except mln.InvalidStateError as error: - if "pending still-image request" not in error.diagnostic: - raise +def assert_invalid_state(call: Callable[[], object]) -> None: + with pytest.raises(mln.InvalidStateError) as raised: + call() + assert raised.value.status == mln.MaplibreStatus.INVALID_STATE def wait_for_texture_info( @@ -110,20 +120,13 @@ def wait_for_texture_info( iterations: int = 5000, ) -> render.TextureImageInfo: fixture.map.set_style_json(EMPTY_STYLE_JSON.encode()) - request_still_image_if_needed(fixture.map) - for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass - try: - return fixture.session.texture_image_info() - except mln.InvalidStateError: - time.sleep(0.001) - raise AssertionError("Metal texture readback metadata was not observed") + render_until_update(fixture.runtime, fixture.session) + image = finish_render_operation( + fixture.session, + fixture.session.read_premultiplied_rgba8(), + return_result=True, + ) + return image.info def wait_for_metal_frame( @@ -132,160 +135,176 @@ def wait_for_metal_frame( *, iterations: int = 5000, ) -> render.MetalOwnedTextureFrameHandle: - request_still_image_if_needed(fixture.map) last_frame: render.MetalOwnedTextureFrame | None = None for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass + # Forced rather than render-if-needed: a settled style would otherwise + # report NO_UPDATE forever and never fill a ring slot. + request_and_finish_frame(fixture.session, flags=render.FrameDemandFlag(0)) try: frame = fixture.session.acquire_metal_owned_texture_frame() - except mln.InvalidStateError: + except mln.InvalidStateError, mln.NotReadyError: + # No slot holds a frame this host has not already taken. time.sleep(0.001) continue last_frame = frame.frame if predicate(last_frame): return frame - frame.close() - time.sleep(0.001) + release_frame(frame) raise AssertionError(f"matching Metal frame was not observed; last={last_frame!r}") -def set_target_calls( - session: render.RenderSessionHandle, -) -> tuple[Callable[[], object], ...]: - """Return one `set_*_target` call per backend and target kind. - - Every descriptor here names null backend handles, which is all these tests - need: the acquired-frame guard and the session's owner thread are both - checked before any handle is read. - """ - extent = render.RenderTargetExtent(16, 16, 1.0) - return ( - lambda: session.set_metal_surface_target( - render.MetalSurfaceDescriptor(extent=extent) - ), - lambda: session.set_vulkan_surface_target( - render.VulkanSurfaceDescriptor(extent=extent) - ), - lambda: session.set_opengl_surface_target( - render.OpenGLSurfaceDescriptor(extent=extent) - ), - lambda: session.set_metal_borrowed_texture_target( - render.MetalBorrowedTextureDescriptor( - extent=extent, physical_width=16, physical_height=16 - ) - ), - lambda: session.set_vulkan_borrowed_texture_target( - render.VulkanBorrowedTextureDescriptor( - extent=extent, physical_width=16, physical_height=16 - ) - ), - lambda: session.set_opengl_borrowed_texture_target( - render.OpenGLBorrowedTextureDescriptor( - extent=extent, physical_width=16, physical_height=16 - ) - ), +def test_core_worker_renders_and_releases_owned_metal_frame( + metal_owned_session: MetalOwnedSession, +) -> None: + metal_owned_session.map.set_style_json(EMPTY_STYLE_JSON.encode()) + render_until_update(metal_owned_session.runtime, metal_owned_session.session) + result = metal_owned_session.session.snapshot.latest_result + assert result == render.RenderResult.RENDERED + frame = metal_owned_session.session.acquire_metal_owned_texture_frame() + assert frame.result.disposition == result + assert frame.texture.address != 0 + assert ( + frame.device.address == metal_owned_session.context.descriptor().device.address ) + release_frame(frame) + assert frame.closed + # A rendered frame carries the map's repaint request with its result. A + # static empty style settles, so the signal clears within a few frames + # once nothing asks to draw again. + for token in range(2, 32): + settled = request_and_finish_frame(metal_owned_session.session, token=token) + assert settled.disposition == render.RenderResult.RENDERED + if settled.needs_repaint is False: + break + time.sleep(0.01) + else: + raise AssertionError("needs_repaint never cleared for a static style") -def assert_invalid_state(call: Callable[[], object]) -> None: - with pytest.raises(mln.InvalidStateError) as raised: - call() - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE + +def test_core_worker_reads_owned_metal_texture( + metal_owned_session: MetalOwnedSession, +) -> None: + info = wait_for_texture_info(metal_owned_session) + # Readback metadata describes the attached extent and a row-padded buffer. + assert info.width == 32 + assert info.height == 16 + assert info.stride >= info.width * 4 + assert info.byte_length >= info.stride * info.height + + image = finish_render_operation( + metal_owned_session.session, + metal_owned_session.session.read_premultiplied_rgba8(), + return_result=True, + ) + assert image.info == info + assert len(image.data) == info.byte_length + + +def test_owned_metal_session_reports_core_worker_capabilities( + metal_owned_session: MetalOwnedSession, +) -> None: + capabilities = metal_owned_session.session.capabilities + snapshot = metal_owned_session.session.snapshot + assert capabilities.driver == render.RenderDriver.CORE_WORKER + assert capabilities.texture_ring_depth in (1, 2, 3) + assert snapshot.driver == render.RenderDriver.CORE_WORKER def test_attach_returns_public_render_session_and_rejects_second_session( metal_owned_session: MetalOwnedSession, ) -> None: session = metal_owned_session.session - assert isinstance(session, render.RenderSessionHandle) assert not session.closed - assert not session.detached - with pytest.raises(mln.InvalidStateError) as raised: - metal_owned_session.map.attach_metal_owned_texture( + # A map drives at most one session, so a second attach is rejected and + # leaves the first one usable. + assert_invalid_state( + lambda: metal_owned_session.map.attach_metal_owned_texture( metal_owned_session.context.owned_texture_descriptor(32, 16, 1.0) ) - - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE + ) assert not session.closed -def test_render_update_without_pending_update_reports_no_update_and_keeps_session_live( +def test_detached_session_leaves_the_map_free_to_close( + metal_owned_session: MetalOwnedSession, +) -> None: + session = metal_owned_session.session + assert_invalid_state(metal_owned_session.map.close) + + close_session(session) + assert session.closed + metal_owned_session.map.close() + + +def test_frame_demand_without_a_newer_update_reports_no_update( metal_owned_session: MetalOwnedSession, ) -> None: - update = metal_owned_session.session.render_update() - assert update.result == render.RenderResult.NO_UPDATE - assert update.needs_repaint is False + metal_owned_session.render_once() + # Draining the settled style leaves nothing newer, so a render-if-needed + # demand terminates without drawing and keeps the session live. + for token in range(2, 64): + result = request_and_finish_frame(metal_owned_session.session, token=token) + if result.disposition == render.RenderResult.NO_UPDATE: + break + time.sleep(0.01) + else: + raise AssertionError("a settled style never reported NO_UPDATE") + + assert result.needs_repaint is False assert not metal_owned_session.session.closed - metal_owned_session.session.resize(32, 16, 1.0) + finish_render_operation( + metal_owned_session.session, + metal_owned_session.session.resize(render.RenderTargetExtent(32, 16, 1.0)), + ) -def test_resize_updates_metal_owned_texture_frame_extent( +def test_resize_updates_owned_metal_texture_frame_extent( metal_owned_session: MetalOwnedSession, ) -> None: metal_owned_session.render_once() - metal_owned_session.session.resize(16, 8, 2.0) - # The map applies the new logical size on its next pump, and a static map - # renders only on request, so pump the resize through before requesting the - # still image. A render before that pump reports the pending size. - assert ( - metal_owned_session.session.render_update().result - == render.RenderResult.SIZE_PENDING + finish_render_operation( + metal_owned_session.session, + metal_owned_session.session.resize(render.RenderTargetExtent(16, 8, 2.0)), ) - metal_owned_session.runtime.pump() + # The session-owned texture is sized in device pixels, so a 16x8 logical + # extent at scale factor 2 keeps the 32x16 physical ring. frame = wait_for_metal_frame( metal_owned_session, - lambda info: ( - info.width == 32 and info.height == 16 and info.scale_factor == 2.0 - ), + lambda info: info.scale_factor == pytest.approx(2.0), ) try: info = frame.frame assert info.width == 32 assert info.height == 16 assert info.scale_factor == pytest.approx(2.0) - assert info.generation >= 2 + assert info.generation >= 1 finally: - frame.close() + release_frame(frame) -def test_cpu_readback_metadata_capacity_and_reusable_buffer( +def test_map_size_follows_attach_and_session_resize( metal_owned_session: MetalOwnedSession, ) -> None: - info = wait_for_texture_info(metal_owned_session) + # Attachment sizes the map from the target rather than from map creation. + assert metal_owned_session.map.get_size() == (32, 16, pytest.approx(1.0)) - assert info.width == 32 - assert info.height == 16 - assert info.stride >= info.width * 4 - assert info.byte_length >= info.stride * info.height - - undersized = bytearray([0x7F] * (info.byte_length - 1)) - with pytest.raises(mln.InvalidArgumentError) as raised: - metal_owned_session.session.read_premultiplied_rgba8_into(undersized) - assert raised.value.status == mln.MaplibreStatus.INVALID_ARGUMENT - assert set(undersized) == {0x7F} - - reusable = bytearray(info.byte_length) - copied = metal_owned_session.session.read_premultiplied_rgba8_into(reusable) - assert copied == info - assert len(reusable) == info.byte_length + # An applied resize updates the map viewport, scale factor included. + finish_render_operation( + metal_owned_session.session, + metal_owned_session.session.resize(render.RenderTargetExtent(48, 24, 2.0)), + ) + assert metal_owned_session.map.get_size() == (48, 24, pytest.approx(2.0)) -def test_metal_frame_acquire_release_and_backend_handles( +def test_metal_frame_exposes_backend_handles_only_while_the_lease_is_live( metal_owned_session: MetalOwnedSession, ) -> None: - metal_owned_session.render_once() - - frame = metal_owned_session.session.acquire_metal_owned_texture_frame() + frame = wait_for_metal_frame(metal_owned_session, lambda _: True) assert isinstance(frame, render.MetalOwnedTextureFrameHandle) info = frame.frame assert info.width == 32 @@ -302,183 +321,87 @@ def test_metal_frame_acquire_release_and_backend_handles( assert texture.address != 0 assert device.address == metal_owned_session.context.descriptor().device.address - frame.close() + release_frame(frame) assert frame.closed assert_invalid_state(lambda: frame.texture) assert_invalid_state(lambda: frame.device) -def test_metal_frame_release_failure_leaves_frame_live_for_later_release() -> None: - class FakeNativeFrame: - closed = False - close_calls = 0 - - def frame(self) -> dict[str, object]: - return { - "generation": 1, - "width": 32, - "height": 16, - "scale_factor": 1.0, - "frame_id": 7, - "texture": 0x1000, - "device": 0x2000, - "pixel_format": 80, - } - - def texture_address(self) -> int: - if self.closed: - raise mln.InvalidStateError( - None, "MetalOwnedTextureFrameHandle is closed" - ) - return 0x1000 - - def device_address(self) -> int: - if self.closed: - raise mln.InvalidStateError( - None, "MetalOwnedTextureFrameHandle is closed" - ) - return 0x2000 - - def close(self) -> None: - self.close_calls += 1 - if self.close_calls == 1: - raise mln.InvalidStateError(None, "frame release failed") - self.closed = True - - native = FakeNativeFrame() - frame = render.MetalOwnedTextureFrameHandle._from_native(native) - - assert frame.texture.address == 0x1000 - with pytest.raises(mln.InvalidStateError, match="frame release failed"): - frame.close() - - assert not frame.closed - assert frame.texture.address == 0x1000 - assert native.close_calls == 1 - - frame.close() - assert frame.closed - assert native.close_calls == 2 - assert_invalid_state(lambda: frame.texture) - - -def test_active_metal_frame_rejects_nested_acquire_and_session_operations( - metal_owned_session: MetalOwnedSession, -) -> None: - metal_owned_session.render_once() - - frame = metal_owned_session.session.acquire_metal_owned_texture_frame() - point_query = query.RenderedQueryGeometry.point_geometry( - camera.ScreenPoint(0.0, 0.0) - ) - feature = b'{"type":"Feature","geometry":null,"properties":{}}' - - calls: tuple[Callable[[], object], ...] = ( - lambda: metal_owned_session.session.resize(16, 16, 1.0), - metal_owned_session.session.render_update, - metal_owned_session.session.detach, - metal_owned_session.session.reduce_memory_use, - metal_owned_session.session.clear_data, - metal_owned_session.session.dump_debug_logs, - metal_owned_session.session.texture_image_info, - lambda: metal_owned_session.session.read_premultiplied_rgba8_into(bytearray(4)), - metal_owned_session.session.acquire_metal_owned_texture_frame, - metal_owned_session.session.acquire_vulkan_owned_texture_frame, - metal_owned_session.session.acquire_opengl_owned_texture_frame, - lambda: metal_owned_session.session.query_rendered_features(point_query), - lambda: metal_owned_session.session.query_source_features("point"), - lambda: metal_owned_session.session.query_feature_extensions( - "point", - feature, - "x", - "y", - ), - metal_owned_session.session.close, - *set_target_calls(metal_owned_session.session), - ) - try: - for call in calls: - assert_invalid_state(call) - assert not metal_owned_session.session.closed - finally: - frame.close() - - def test_stale_metal_frame_handles_cannot_expose_backend_handles_after_reuse( metal_owned_session: MetalOwnedSession, ) -> None: - metal_owned_session.render_once() - - stale_frame = metal_owned_session.session.acquire_metal_owned_texture_frame() + stale_frame = wait_for_metal_frame(metal_owned_session, lambda _: True) stale_texture = stale_frame.texture stale_device = stale_frame.device - stale_frame.close() + release_frame(stale_frame) for pointer in (stale_texture, stale_device): assert_invalid_state(lambda pointer=pointer: pointer.address) - assert_invalid_state(lambda: stale_frame.texture) - assert_invalid_state(lambda: stale_frame.device) - next_frame = metal_owned_session.session.acquire_metal_owned_texture_frame() + # The ring may hand the same backend object to the next lease, so the + # retired pointers must stay unreadable rather than alias it. + next_frame = wait_for_metal_frame(metal_owned_session, lambda _: True) try: assert next_frame.texture.address != 0 for pointer in (stale_texture, stale_device): assert_invalid_state(lambda pointer=pointer: pointer.address) finally: - next_frame.close() + release_frame(next_frame) -def test_real_metal_render_session_reports_wrong_thread_errors( +def test_session_close_is_rejected_while_a_frame_lease_is_held( metal_owned_session: MetalOwnedSession, ) -> None: - calls: tuple[Callable[[], object], ...] = ( - lambda: metal_owned_session.session.resize(16, 16, 1.0), - metal_owned_session.session.render_update, - metal_owned_session.session.acquire_metal_owned_texture_frame, - metal_owned_session.session.close, - *set_target_calls(metal_owned_session.session), - ) + session = metal_owned_session.session + frame = wait_for_metal_frame(metal_owned_session, lambda _: True) + try: + assert session.snapshot.acquired_frame_count == 1 + # The host still holds a ring slot, so the session cannot retire. + assert_invalid_state(session.close) + assert not session.closed + finally: + release_frame(frame) - def run_call(call: Callable[[], object], observed: list[Exception]) -> None: - try: - call() - except mln.WrongThreadError as error: - observed.append(error) + assert session.snapshot.acquired_frame_count == 0 - for call in calls: - observed: list[Exception] = [] - thread = threading.Thread(target=run_call, args=(call, observed)) - thread.start() - thread.join() - assert len(observed) == 1 - assert isinstance(observed[0], mln.WrongThreadError) - assert observed[0].status == mln.MaplibreStatus.WRONG_THREAD - assert not metal_owned_session.session.closed +def test_metal_frame_release_failure_leaves_the_lease_live_for_a_later_release() -> ( + None +): + class FakeNativeFrame: + closed = False + release_calls = 0 + def texture_address(self) -> int: + if self.closed: + raise mln.InvalidStateError( + None, "MetalOwnedTextureFrameHandle is closed" + ) + return 0x1000 -def test_set_target_reports_unsupported_for_a_session_owned_texture( - metal_owned_session: MetalOwnedSession, -) -> None: - """Spec coverage: BND-176. - - A session-owned texture is sized and replaced by its session, so it takes a - resize rather than a replacement. The session's target kind is checked - before the descriptor, so the null texture below is never read. - """ - with pytest.raises(mln.UnsupportedFeatureError) as raised: - metal_owned_session.session.set_metal_borrowed_texture_target( - render.MetalBorrowedTextureDescriptor( - extent=render.RenderTargetExtent(32, 16, 1.0), - physical_width=32, - physical_height=16, - ) - ) - assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED + def release(self, kind: int, address: int, value: int) -> None: + self.release_calls += 1 + if self.release_calls == 1: + raise mln.InvalidStateError(None, "frame release failed") + self.closed = True - # The rejection left the session rendering into the texture it owns. - metal_owned_session.session.resize(32, 16, 1.0) - metal_owned_session.render_once() + native = FakeNativeFrame() + frame = render.MetalOwnedTextureFrameHandle._from_native(native) + + assert frame.texture.address == 0x1000 + with pytest.raises(mln.InvalidStateError, match="frame release failed"): + release_frame(frame) + + # A rejected release keeps the host's claim on the slot, so the address + # stays readable and a later release still retires it. + assert not frame.closed + assert frame.texture.address == 0x1000 + assert native.release_calls == 1 + + release_frame(frame) + assert frame.closed + assert native.release_calls == 2 + assert_invalid_state(lambda: frame.texture) def test_cluster_feature_extension_queries_resolve_unsigned_cluster_id_and_limit( diff --git a/bindings/python/tests/test_render_opengl_egl.py b/bindings/python/tests/test_render_opengl_egl.py index f18dc17d3..7509e3dc5 100644 --- a/bindings/python/tests/test_render_opengl_egl.py +++ b/bindings/python/tests/test_render_opengl_egl.py @@ -1,6 +1,5 @@ from __future__ import annotations -import threading import time from collections.abc import Callable, Iterator from contextlib import contextmanager @@ -8,14 +7,19 @@ import maplibre_native_ffi as mln import pytest -from maplibre_native_ffi import camera, query, render +from maplibre_native_ffi import render from render_backend_helpers.runtime import ( EMPTY_STYLE_JSON, RED_BACKGROUND_STYLE_JSON, + RED_PIXEL, assert_cluster_feature_extensions, assert_geojson_cluster_source, - render_until, + close_session, + finish_attach, + finish_render_operation, + release_frame, render_until_update, + request_and_finish_frame, skip_or_fail_fixture_setup, ) @@ -69,12 +73,18 @@ def create( runtime = mln.RuntimeHandle() try: map_handle = runtime.create_map( - mln.MapOptions(width=64, height=64, mode=mln.MapMode.STATIC) - ) + mln.MapOptions( + width=width, + height=height, + scale_factor=scale_factor, + mode=mln.MapMode.CONTINUOUS, + ) + ).result(timeout=5) try: - session = map_handle.attach_opengl_owned_texture( + session, attach = map_handle.attach_opengl_owned_texture( context.owned_texture_descriptor(width, height, scale_factor) ) + finish_attach(session, attach) except BaseException: map_handle.close() raise @@ -87,7 +97,7 @@ def create( def close(self) -> None: if not self.session.closed: - self.session.close() + close_session(self.session) if not self.map.closed: self.map.close() if not self.runtime.closed: @@ -97,7 +107,7 @@ def close(self) -> None: def render_once(self) -> None: self.map.set_style_json(EMPTY_STYLE_JSON.encode()) frame = wait_for_opengl_frame(self, lambda _: True) - frame.close() + release_frame(frame) @pytest.fixture @@ -181,12 +191,10 @@ def _egl_borrowed_texture( texture.close() -def request_still_image_if_needed(map_handle: mln.MapHandle) -> None: - try: - map_handle.request_still_image() - except mln.InvalidStateError as error: - if "pending still-image request" not in error.diagnostic: - raise +def assert_invalid_state(call: Callable[[], object]) -> None: + with pytest.raises(mln.InvalidStateError) as raised: + call() + assert raised.value.status == mln.MaplibreStatus.INVALID_STATE def wait_for_texture_info( @@ -195,20 +203,13 @@ def wait_for_texture_info( iterations: int = 5000, ) -> render.TextureImageInfo: fixture.map.set_style_json(EMPTY_STYLE_JSON.encode()) - request_still_image_if_needed(fixture.map) - for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass - try: - return fixture.session.texture_image_info() - except mln.InvalidStateError: - time.sleep(0.001) - raise AssertionError("OpenGL texture readback metadata was not observed") + render_until_update(fixture.runtime, fixture.session) + image = finish_render_operation( + fixture.session, + fixture.session.read_premultiplied_rgba8(), + return_result=True, + ) + return image.info def wait_for_opengl_frame( @@ -217,130 +218,81 @@ def wait_for_opengl_frame( *, iterations: int = 5000, ) -> render.OpenGLOwnedTextureFrameHandle: - request_still_image_if_needed(fixture.map) last_frame: render.OpenGLOwnedTextureFrame | None = None for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass + # Forced rather than render-if-needed: a settled style would otherwise + # report NO_UPDATE forever and never fill a ring slot. + request_and_finish_frame(fixture.session, flags=render.FrameDemandFlag(0)) try: frame = fixture.session.acquire_opengl_owned_texture_frame() - except mln.InvalidStateError: + except mln.InvalidStateError, mln.NotReadyError: + # No slot holds a frame this host has not already taken. time.sleep(0.001) continue last_frame = frame.frame if predicate(last_frame): return frame - frame.close() - time.sleep(0.001) + release_frame(frame) raise AssertionError(f"matching OpenGL frame was not observed; last={last_frame!r}") -def assert_invalid_state(call: Callable[[], object]) -> None: - with pytest.raises(mln.InvalidStateError) as raised: - call() - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE - - -def _assert_public_session_shape(session: render.RenderSessionHandle) -> None: - assert isinstance(session, render.RenderSessionHandle) - assert session.closed is False - assert session.detached is False - assert callable(session.render_update) - assert callable(session.close) - - -def _borrowed_descriptor_snapshot( - descriptor: render.OpenGLBorrowedTextureDescriptor, -) -> tuple[object, ...]: - context = descriptor.context - assert isinstance(context, render.EglContextDescriptor) - return ( - descriptor.extent.width, - descriptor.extent.height, - descriptor.extent.scale_factor, - context.display.address, - context.config.address, - context.share_context.address, - context.get_proc_address.address, - descriptor.texture, - descriptor.target, - ) - - -def test_invalid_opengl_surface_attach_reports_native_status() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: - with pytest.raises( - (mln.InvalidArgumentError, mln.UnsupportedFeatureError) - ) as raised: - map_handle.attach_opengl_surface(render.OpenGLSurfaceDescriptor()) - - assert raised.value.status in { - mln.MaplibreStatus.INVALID_ARGUMENT, - mln.MaplibreStatus.UNSUPPORTED, - } - - -def test_egl_pbuffer_surface_attach_reports_public_session_shape() -> None: - with ( - _egl_context() as context, - _egl_pbuffer_surface(context) as surface, - mln.RuntimeHandle() as runtime, - runtime.create_map( - mln.MapOptions(width=surface.width, height=surface.height) - ) as map_handle, - ): - session = map_handle.attach_opengl_surface(surface.descriptor()) - try: - _assert_public_session_shape(session) +def test_caller_driver_renders_and_releases_owned_opengl_frame( + opengl_owned_session: OpenGLOwnedSession, +) -> None: + opengl_owned_session.map.set_style_json(EMPTY_STYLE_JSON.encode()) + render_until_update(opengl_owned_session.runtime, opengl_owned_session.session) + result = opengl_owned_session.session.snapshot.latest_result + assert result == render.RenderResult.RENDERED + frame = opengl_owned_session.session.acquire_opengl_owned_texture_frame() + assert frame.result.disposition == result + assert frame.texture.value != 0 + release_frame(frame) + assert frame.closed - map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) - render_until_update(runtime, session) - finally: - session.close() +def test_caller_driver_reads_owned_opengl_texture( + opengl_owned_session: OpenGLOwnedSession, +) -> None: + info = wait_for_texture_info(opengl_owned_session) + # Readback metadata describes the attached extent and a row-padded buffer. + assert info.width == 32 + assert info.height == 16 + assert info.stride >= info.width * 4 + assert info.byte_length >= info.stride * info.height -def test_dedicated_egl_surface_renders_and_keeps_its_context_current() -> None: - with ( - _egl_context() as context, - _egl_pbuffer_surface(context) as surface, - mln.RuntimeHandle() as runtime, - runtime.create_map( - mln.MapOptions(width=surface.width, height=surface.height) - ) as map_handle, - ): - session = map_handle.attach_opengl_surface(surface.dedicated_descriptor()) - try: - map_handle.set_style_json(RED_BACKGROUND_STYLE_JSON.encode()) - render_until_update(runtime, session) + image = finish_render_operation( + opengl_owned_session.session, + opengl_owned_session.session.read_premultiplied_rgba8(), + return_result=True, + ) + assert image.info == info + assert len(image.data) == info.byte_length - # The session owns this thread, so it keeps its context current - # between renders rather than restoring what it found. - assert current_context_address() != 0 - finally: - session.close() - # Closing the session releases the thread it had taken over. - assert current_context_address() == 0 +def test_owned_opengl_session_is_always_caller_driven( + opengl_owned_session: OpenGLOwnedSession, +) -> None: + capabilities = opengl_owned_session.session.capabilities + snapshot = opengl_owned_session.session.snapshot + assert capabilities.driver == render.RenderDriver.CALLER_GRAPHICS_THREAD + assert capabilities.texture_ring_depth in (1, 2, 3) + assert snapshot.driver == render.RenderDriver.CALLER_GRAPHICS_THREAD def test_attach_returns_public_render_session_and_rejects_second_session( opengl_owned_session: OpenGLOwnedSession, ) -> None: session = opengl_owned_session.session + assert isinstance(session, render.RenderSessionHandle) + assert not session.closed - _assert_public_session_shape(session) - - with pytest.raises(mln.InvalidStateError) as raised: - opengl_owned_session.map.attach_opengl_owned_texture( + # A map drives at most one session, so a second attach is rejected and + # leaves the first one usable. + assert_invalid_state( + lambda: opengl_owned_session.map.attach_opengl_owned_texture( opengl_owned_session.context.owned_texture_descriptor(32, 16, 1.0) ) - - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE + ) assert not session.closed @@ -348,84 +300,79 @@ def test_detached_session_leaves_the_map_free_to_close( opengl_owned_session: OpenGLOwnedSession, ) -> None: session = opengl_owned_session.session - assert_invalid_state(opengl_owned_session.map.close) - detached = session.detach() - opengl_owned_session.map.close() - detached.close() - + close_session(session) assert session.closed + opengl_owned_session.map.close() -def test_render_update_without_pending_update_reports_no_update_and_keeps_session_live( +def test_frame_demand_without_a_newer_update_reports_no_update( opengl_owned_session: OpenGLOwnedSession, ) -> None: - update = opengl_owned_session.session.render_update() - assert update.result == render.RenderResult.NO_UPDATE - assert update.needs_repaint is False + opengl_owned_session.render_once() + # Draining the settled style leaves nothing newer, so a render-if-needed + # demand terminates without drawing and keeps the session live. + for token in range(2, 64): + result = request_and_finish_frame(opengl_owned_session.session, token=token) + if result.disposition == render.RenderResult.NO_UPDATE: + break + time.sleep(0.01) + else: + raise AssertionError("a settled style never reported NO_UPDATE") + + assert result.needs_repaint is False assert not opengl_owned_session.session.closed - opengl_owned_session.session.resize(32, 16, 1.0) + finish_render_operation( + opengl_owned_session.session, + opengl_owned_session.session.resize(render.RenderTargetExtent(32, 16, 1.0)), + ) -def test_resize_updates_opengl_owned_texture_frame_extent( +def test_resize_updates_owned_opengl_texture_frame_extent( opengl_owned_session: OpenGLOwnedSession, ) -> None: opengl_owned_session.render_once() - opengl_owned_session.session.resize(16, 8, 2.0) - # The map applies the new logical size on its own thread, so a render - # before the next pump reports the pending size. - assert ( - opengl_owned_session.session.render_update().result - == render.RenderResult.SIZE_PENDING + finish_render_operation( + opengl_owned_session.session, + opengl_owned_session.session.resize(render.RenderTargetExtent(16, 8, 2.0)), ) + # The session-owned texture is sized in device pixels, so a 16x8 logical + # extent at scale factor 2 keeps the 32x16 physical ring. frame = wait_for_opengl_frame( opengl_owned_session, - lambda info: ( - info.width == 32 and info.height == 16 and info.scale_factor == 2.0 - ), + lambda info: info.scale_factor == pytest.approx(2.0), ) try: info = frame.frame assert info.width == 32 assert info.height == 16 assert info.scale_factor == pytest.approx(2.0) - assert info.generation >= 2 + assert info.generation >= 1 finally: - frame.close() + release_frame(frame) -def test_cpu_readback_metadata_capacity_and_reusable_buffer( +def test_map_size_follows_attach_and_session_resize( opengl_owned_session: OpenGLOwnedSession, ) -> None: - info = wait_for_texture_info(opengl_owned_session) - - assert info.width == 32 - assert info.height == 16 - assert info.stride >= info.width * 4 - assert info.byte_length >= info.stride * info.height - - undersized = bytearray([0x7F] * (info.byte_length - 1)) - with pytest.raises(mln.InvalidArgumentError) as raised: - opengl_owned_session.session.read_premultiplied_rgba8_into(undersized) - assert raised.value.status == mln.MaplibreStatus.INVALID_ARGUMENT - assert set(undersized) == {0x7F} + # Attachment sizes the map from the target rather than from map creation. + assert opengl_owned_session.map.get_size() == (32, 16, pytest.approx(1.0)) - reusable = bytearray(info.byte_length) - copied = opengl_owned_session.session.read_premultiplied_rgba8_into(reusable) - assert copied == info - assert len(reusable) == info.byte_length + # An applied resize updates the map viewport, scale factor included. + finish_render_operation( + opengl_owned_session.session, + opengl_owned_session.session.resize(render.RenderTargetExtent(48, 24, 2.0)), + ) + assert opengl_owned_session.map.get_size() == (48, 24, pytest.approx(2.0)) -def test_opengl_frame_acquire_release_and_backend_texture_scope( +def test_opengl_frame_exposes_its_texture_name_only_while_the_lease_is_live( opengl_owned_session: OpenGLOwnedSession, ) -> None: - opengl_owned_session.render_once() - - frame = opengl_owned_session.session.acquire_opengl_owned_texture_frame() - assert isinstance(frame, render.OpenGLOwnedTextureFrameHandle) + frame = wait_for_opengl_frame(opengl_owned_session, lambda _: True) info = frame.frame assert info.width == 32 assert info.height == 16 @@ -433,275 +380,176 @@ def test_opengl_frame_acquire_release_and_backend_texture_scope( assert info.generation >= 1 assert info.frame_id >= 0 assert info.target != 0 - assert info.internal_format != 0 - assert info.format != 0 - assert info.type != 0 texture = frame.texture - assert isinstance(texture, render.FrameOpenGLTextureName) assert texture.value != 0 - frame.close() - assert frame.closed - assert_invalid_state(lambda: frame.texture) - assert_invalid_state(lambda: texture.value) - - -def test_opengl_frame_release_failure_leaves_frame_live_for_later_release() -> None: - class FakeNativeFrame: - closed = False - close_calls = 0 - - def frame(self) -> dict[str, object]: - return { - "generation": 1, - "width": 32, - "height": 16, - "scale_factor": 1.0, - "frame_id": 7, - "target": 0x0DE1, - "internal_format": 0x8058, - "format": 0x1908, - "type": 0x1401, - } - - def texture(self) -> int: - if self.closed: - raise mln.InvalidStateError(None, "OpenGL frame is closed") - return 5 - - def close(self) -> None: - self.close_calls += 1 - if self.close_calls == 1: - raise mln.InvalidStateError(None, "frame release failed") - self.closed = True - - native = FakeNativeFrame() - frame = render.OpenGLOwnedTextureFrameHandle._from_native(native) - - assert frame.texture.value == 5 - with pytest.raises(mln.InvalidStateError, match="frame release failed"): - frame.close() - - assert not frame.closed - assert frame.texture.value == 5 - assert native.close_calls == 1 - - frame.close() + release_frame(frame) assert frame.closed - assert native.close_calls == 2 assert_invalid_state(lambda: frame.texture) -def test_active_opengl_frame_rejects_nested_acquire_and_session_operations( - opengl_owned_session: OpenGLOwnedSession, -) -> None: - opengl_owned_session.render_once() - - frame = opengl_owned_session.session.acquire_opengl_owned_texture_frame() - point_query = query.RenderedQueryGeometry.point_geometry( - camera.ScreenPoint(0.0, 0.0) - ) - feature = b'{"type":"Feature","geometry":null,"properties":{}}' - - calls: tuple[Callable[[], object], ...] = ( - lambda: opengl_owned_session.session.resize(16, 16, 1.0), - opengl_owned_session.session.render_update, - opengl_owned_session.session.detach, - opengl_owned_session.session.reduce_memory_use, - opengl_owned_session.session.clear_data, - opengl_owned_session.session.dump_debug_logs, - opengl_owned_session.session.texture_image_info, - lambda: opengl_owned_session.session.read_premultiplied_rgba8_into( - bytearray(4) - ), - opengl_owned_session.session.acquire_metal_owned_texture_frame, - opengl_owned_session.session.acquire_vulkan_owned_texture_frame, - opengl_owned_session.session.acquire_opengl_owned_texture_frame, - lambda: opengl_owned_session.session.query_rendered_features(point_query), - lambda: opengl_owned_session.session.query_source_features("point"), - lambda: opengl_owned_session.session.query_feature_extensions( - "point", - feature, - "x", - "y", - ), - opengl_owned_session.session.close, - ) - try: - for call in calls: - assert_invalid_state(call) - assert not opengl_owned_session.session.closed - finally: - frame.close() - - def test_stale_opengl_texture_names_cannot_expose_value_after_reuse( opengl_owned_session: OpenGLOwnedSession, ) -> None: - opengl_owned_session.render_once() - - stale_frame = opengl_owned_session.session.acquire_opengl_owned_texture_frame() + stale_frame = wait_for_opengl_frame(opengl_owned_session, lambda _: True) stale_texture = stale_frame.texture - stale_frame.close() + release_frame(stale_frame) assert_invalid_state(lambda: stale_texture.value) - assert_invalid_state(lambda: stale_frame.texture) - next_frame = opengl_owned_session.session.acquire_opengl_owned_texture_frame() + # The ring may hand the same texture name to the next lease, so the + # retired name must stay unreadable rather than alias it. + next_frame = wait_for_opengl_frame(opengl_owned_session, lambda _: True) try: assert next_frame.texture.value != 0 assert_invalid_state(lambda: stale_texture.value) finally: - next_frame.close() + release_frame(next_frame) -def test_real_opengl_render_session_reports_wrong_thread_errors( +def test_session_close_is_rejected_while_a_frame_lease_is_held( opengl_owned_session: OpenGLOwnedSession, ) -> None: - calls: tuple[Callable[[], object], ...] = ( - lambda: opengl_owned_session.session.resize(16, 16, 1.0), - opengl_owned_session.session.render_update, - opengl_owned_session.session.acquire_opengl_owned_texture_frame, - opengl_owned_session.session.close, - ) - - def run_call(call: Callable[[], object], observed: list[Exception]) -> None: - try: - call() - except mln.WrongThreadError as error: - observed.append(error) + session = opengl_owned_session.session + frame = wait_for_opengl_frame(opengl_owned_session, lambda _: True) + try: + assert session.snapshot.acquired_frame_count == 1 + # The host still holds a ring slot, so the session cannot retire. + assert_invalid_state(session.close) + assert not session.closed + finally: + release_frame(frame) - for call in calls: - observed: list[Exception] = [] - thread = threading.Thread(target=run_call, args=(call, observed)) - thread.start() - thread.join() + assert session.snapshot.acquired_frame_count == 0 - assert len(observed) == 1 - assert isinstance(observed[0], mln.WrongThreadError) - assert observed[0].status == mln.MaplibreStatus.WRONG_THREAD - assert not opengl_owned_session.session.closed +def test_invalid_opengl_surface_attach_reports_native_status() -> None: + with mln.RuntimeHandle() as runtime: + map_handle = runtime.create_map().result(timeout=5) + try: + with pytest.raises( + (mln.InvalidArgumentError, mln.UnsupportedFeatureError) + ) as raised: + map_handle.attach_opengl_surface(render.OpenGLSurfaceDescriptor()) + assert raised.value.status in { + mln.MaplibreStatus.INVALID_ARGUMENT, + mln.MaplibreStatus.UNSUPPORTED, + } + finally: + map_handle.close() -def test_egl_borrowed_texture_session_close_preserves_caller_resources() -> None: - with _egl_context() as context, _egl_borrowed_texture(context) as texture: - descriptor = texture.descriptor() - before_descriptor = _borrowed_descriptor_snapshot(descriptor) - before_texture = texture.texture - with ( - mln.RuntimeHandle() as runtime, - runtime.create_map( - mln.MapOptions( - width=descriptor.extent.width, - height=descriptor.extent.height, - ) - ) as map_handle, - ): - session = map_handle.attach_opengl_borrowed_texture(descriptor) +def test_egl_pbuffer_surface_session_attaches_and_renders() -> None: + with _egl_context() as context, _egl_pbuffer_surface(context) as surface: + runtime = mln.RuntimeHandle() + map_handle = runtime.create_map( + mln.MapOptions(width=surface.width, height=surface.height) + ).result(timeout=5) + try: + session, attach = map_handle.attach_opengl_surface(surface.descriptor()) + finish_attach(session, attach) try: - _assert_public_session_shape(session) - + assert not session.closed + assert session.capabilities.driver == ( + render.RenderDriver.CALLER_GRAPHICS_THREAD + ) map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) render_until_update(runtime, session) - - with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.acquire_opengl_owned_texture_frame() - assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED finally: - session.close() + close_session(session) + finally: + map_handle.close() + runtime.close().result(timeout=30) - assert _borrowed_descriptor_snapshot(descriptor) == before_descriptor - assert texture.texture == before_texture - assert texture.exists() - replacement_descriptor = texture.descriptor() - assert _borrowed_descriptor_snapshot(replacement_descriptor) == ( - before_descriptor - ) +def test_dedicated_egl_surface_renders_and_keeps_its_context_current() -> None: + with _egl_context() as context, _egl_pbuffer_surface(context) as surface: + runtime = mln.RuntimeHandle() + map_handle = runtime.create_map( + mln.MapOptions(width=surface.width, height=surface.height) + ).result(timeout=5) + try: + session, attach = map_handle.attach_opengl_surface( + surface.dedicated_descriptor() + ) + finish_attach(session, attach) + try: + map_handle.set_style_json(RED_BACKGROUND_STYLE_JSON.encode()) + render_until_update(runtime, session) + # A dedicated context belongs to the session, so it stays + # current on this thread between renders rather than being + # restored to what the session found. + assert current_context_address() != 0 + finally: + close_session(session) -def test_egl_borrowed_texture_set_target_hands_over_a_replacement() -> None: - """Spec coverage: BND-175, BND-176. + # Detaching hands the thread back with no context current. + assert current_context_address() == 0 + finally: + map_handle.close() + runtime.close().result(timeout=30) - A caller-owned texture is sized by its owner, so a host that follows a - resize allocates a texture at the new size and hands it over instead of - resizing this session. - """ + +def test_egl_borrowed_texture_session_close_preserves_caller_resources() -> None: with _egl_context() as context, _egl_borrowed_texture(context) as texture: - descriptor = texture.descriptor() + runtime = mln.RuntimeHandle() + map_handle = runtime.create_map(mln.MapOptions(width=32, height=16)).result( + timeout=5 + ) + try: + session, attach = map_handle.attach_opengl_borrowed_texture( + texture.descriptor() + ) + finish_attach(session, attach) + map_handle.set_style_json(RED_BACKGROUND_STYLE_JSON.encode()) + render_until_update(runtime, session) + close_session(session) - with ( - mln.RuntimeHandle() as runtime, - runtime.create_map( - mln.MapOptions( - width=descriptor.extent.width, - height=descriptor.extent.height, - ) - ) as map_handle, - ): - session = map_handle.attach_opengl_borrowed_texture(descriptor) + # The texture is caller-owned, so the session leaves it alive for + # the host to read and destroy. + assert texture.exists() + assert texture.read_rgba()[:4] == RED_PIXEL + finally: + map_handle.close() + runtime.close().result(timeout=30) + + +def test_egl_borrowed_texture_set_target_hands_over_a_replacement() -> None: + with ( + _egl_context() as context, + _egl_borrowed_texture(context) as first, + _egl_borrowed_texture(context) as second, + ): + runtime = mln.RuntimeHandle() + map_handle = runtime.create_map(mln.MapOptions(width=32, height=16)).result( + timeout=5 + ) + try: + session, attach = map_handle.attach_opengl_borrowed_texture( + first.descriptor() + ) + finish_attach(session, attach) try: map_handle.set_style_json(RED_BACKGROUND_STYLE_JSON.encode()) render_until_update(runtime, session) - with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.resize(48, 24, 1.0) - assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED - - with _egl_borrowed_texture(context, width=48, height=24) as replacement: - # Freshly allocated, so anything drawn into it - # later came from this session after the handoff. - assert not any(replacement.read_rgba()) - - # The C API replaces the target on the calling - # thread, so the host context is current for it. - context.make_current(replacement.surface) - try: - session.set_opengl_borrowed_texture_target( - replacement.descriptor() - ) - finally: - context.clear_current() - - # The session kept its renderer and paints the - # texture it was handed, at the extent handed with - # it, once the map catches up. - render_until( - runtime, - session, - lambda: ( - map_handle.get_size() == (48, 24, pytest.approx(1.0)) - and any(replacement.read_rgba()) - ), - "the replacement texture was never rendered into", - ) - assert ( - session.render_update().result == render.RenderResult.RENDERED - ) - - # Both textures belong to their owner: the session - # neither released the outgoing one nor took over - # the replacement's lifetime. - assert texture.exists() - assert replacement.exists() - - # A surface descriptor names a target this session - # does not have. - with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.set_opengl_surface_target( - render.OpenGLSurfaceDescriptor( - extent=replacement.descriptor().extent, - context=context.descriptor(), - surface=render.NativePointer(0x1), - ) - ) - assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED - # The rejection left the session usable. - session.render_update() + finish_render_operation( + session, + session.set_opengl_borrowed_texture_target(second.descriptor()), + ) + render_until_update(runtime, session) + + # Rendering moved to the replacement, and the retired texture + # stays the caller's to destroy. + assert second.read_rgba()[:4] == RED_PIXEL + assert first.exists() finally: - session.close() + close_session(session) + finally: + map_handle.close() + runtime.close().result(timeout=30) def test_cluster_feature_extension_queries_resolve_unsigned_cluster_id_and_limit( diff --git a/bindings/python/tests/test_render_vulkan_attach.py b/bindings/python/tests/test_render_vulkan_attach.py index 3e3047b17..7ea77f750 100644 --- a/bindings/python/tests/test_render_vulkan_attach.py +++ b/bindings/python/tests/test_render_vulkan_attach.py @@ -8,8 +8,12 @@ from maplibre_native_ffi import render from render_backend_helpers.runtime import ( EMPTY_STYLE_JSON, + close_session, + finish_attach, + finish_render_operation, render_until, render_until_update, + request_and_finish_frame, skip_or_fail_fixture_setup, ) @@ -77,8 +81,10 @@ def _vulkan_borrowed_image( def _assert_public_session_shape(session: render.RenderSessionHandle) -> None: assert isinstance(session, render.RenderSessionHandle) assert session.closed is False - assert session.detached is False - assert callable(session.render_update) + assert session.snapshot.state == render.RenderSessionState.ATTACHED + assert isinstance(session.capabilities, render.RenderSessionCapabilities) + assert callable(session.request_frame) + assert callable(session.drain_frame_results) assert callable(session.close) @@ -106,7 +112,10 @@ def _descriptor_snapshot( def test_invalid_vulkan_surface_attach_reports_native_status() -> None: - with mln.RuntimeHandle() as runtime, runtime.create_map() as map_handle: + with ( + mln.RuntimeHandle() as runtime, + runtime.create_map().result(timeout=5) as map_handle, + ): with pytest.raises( (mln.InvalidArgumentError, mln.UnsupportedFeatureError) ) as raised: @@ -131,10 +140,11 @@ def test_vulkan_borrowed_texture_attach_reports_public_render_session_shape() -> width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_vulkan_borrowed_texture(descriptor) + session, attach = map_handle.attach_vulkan_borrowed_texture(descriptor) try: + finish_attach(session, attach) _assert_public_session_shape(session) map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) @@ -144,7 +154,7 @@ def test_vulkan_borrowed_texture_attach_reports_public_render_session_shape() -> session.acquire_vulkan_owned_texture_frame() assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED finally: - session.close() + close_session(session) def test_vulkan_borrowed_texture_session_close_preserves_caller_resources() -> None: @@ -162,11 +172,12 @@ def test_vulkan_borrowed_texture_session_close_preserves_caller_resources() -> N width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_vulkan_borrowed_texture(descriptor) + session, attach = map_handle.attach_vulkan_borrowed_texture(descriptor) + finish_attach(session, attach) _assert_public_session_shape(session) - session.close() + close_session(session) assert _descriptor_snapshot(descriptor) == before_descriptor assert (image.image, image.image_view, image.memory) == before_resources @@ -196,25 +207,33 @@ def test_vulkan_borrowed_texture_set_target_hands_over_a_replacement() -> None: width=descriptor.extent.width, height=descriptor.extent.height, ) - ) as map_handle, + ).result(timeout=5) as map_handle, ): - session = map_handle.attach_vulkan_borrowed_texture(descriptor) + session, attach = map_handle.attach_vulkan_borrowed_texture(descriptor) + finish_attach(session, attach) try: map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) render_until_update(runtime, session) with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.resize(48, 24, 1.0) + session.resize(render.RenderTargetExtent(48, 24, 1.0)) assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED with _vulkan_borrowed_image( context, width=48, height=24 ) as replacement: replacement_descriptor = replacement.descriptor() - session.set_vulkan_borrowed_texture_target(replacement_descriptor) + finish_render_operation( + session, + session.set_vulkan_borrowed_texture_target( + replacement_descriptor + ), + ) + map_handle.resize(48, 24, 1.0) + runtime.barrier().result(timeout=5) # The session kept its renderer and renders at the - # extent it was handed, once the map catches up. + # extent it was handed, once the map has caught up. render_until( runtime, session, @@ -222,21 +241,30 @@ def test_vulkan_borrowed_texture_set_target_hands_over_a_replacement() -> None: "the map never took the replacement image extent", ) assert ( - session.render_update().result == render.RenderResult.RENDERED + request_and_finish_frame(session).disposition + == render.RenderResult.RENDERED ) # A surface descriptor names a target this session # does not have. - with pytest.raises(mln.UnsupportedFeatureError) as raised: - session.set_vulkan_surface_target( - render.VulkanSurfaceDescriptor( - extent=replacement_descriptor.extent, - context=context.descriptor(), - surface=render.NativePointer(0x1), - ) + with pytest.raises( + (mln.InvalidArgumentError, mln.UnsupportedFeatureError) + ) as raised: + finish_render_operation( + session, + session.set_vulkan_surface_target( + render.VulkanSurfaceDescriptor( + extent=replacement_descriptor.extent, + context=context.descriptor(), + surface=render.NativePointer(0x1), + ) + ), ) - assert raised.value.status == mln.MaplibreStatus.UNSUPPORTED + assert raised.value.status in { + mln.MaplibreStatus.INVALID_ARGUMENT, + mln.MaplibreStatus.UNSUPPORTED, + } # The rejection left the session usable. - session.render_update() + request_and_finish_frame(session) finally: - session.close() + close_session(session) diff --git a/bindings/python/tests/test_render_vulkan_owned.py b/bindings/python/tests/test_render_vulkan_owned.py index b5f523cef..4552cac51 100644 --- a/bindings/python/tests/test_render_vulkan_owned.py +++ b/bindings/python/tests/test_render_vulkan_owned.py @@ -1,17 +1,22 @@ from __future__ import annotations -import threading import time from collections.abc import Callable from dataclasses import dataclass import maplibre_native_ffi as mln import pytest -from maplibre_native_ffi import camera, query, render +from maplibre_native_ffi import render from render_backend_helpers.runtime import ( EMPTY_STYLE_JSON, assert_cluster_feature_extensions, assert_geojson_cluster_source, + close_session, + finish_attach, + finish_render_operation, + release_frame, + render_until_update, + request_and_finish_frame, skip_or_fail_fixture_setup, ) @@ -56,12 +61,18 @@ def create( runtime = mln.RuntimeHandle() try: map_handle = runtime.create_map( - mln.MapOptions(width=64, height=64, mode=mln.MapMode.STATIC) - ) + mln.MapOptions( + width=width, + height=height, + scale_factor=scale_factor, + mode=mln.MapMode.CONTINUOUS, + ) + ).result(timeout=5) try: - session = map_handle.attach_vulkan_owned_texture( + session, attach = map_handle.attach_vulkan_owned_texture( context.owned_texture_descriptor(width, height, scale_factor) ) + finish_attach(session, attach) except BaseException: map_handle.close() raise @@ -74,7 +85,7 @@ def create( def close(self) -> None: if not self.session.closed: - self.session.close() + close_session(self.session) if not self.map.closed: self.map.close() if not self.runtime.closed: @@ -83,8 +94,9 @@ def close(self) -> None: def render_once(self) -> None: self.map.set_style_json(EMPTY_STYLE_JSON.encode()) + self.runtime.barrier().result(timeout=5) frame = wait_for_vulkan_frame(self, lambda _: True) - frame.close() + release_frame(frame) @pytest.fixture @@ -96,12 +108,10 @@ def vulkan_owned_session() -> VulkanOwnedSession: fixture.close() -def request_still_image_if_needed(map_handle: mln.MapHandle) -> None: - try: - map_handle.request_still_image() - except mln.InvalidStateError as error: - if "pending still-image request" not in error.diagnostic: - raise +def assert_invalid_state(call: Callable[[], object]) -> None: + with pytest.raises(mln.InvalidStateError) as raised: + call() + assert raised.value.status == mln.MaplibreStatus.INVALID_STATE def wait_for_texture_info( @@ -110,20 +120,13 @@ def wait_for_texture_info( iterations: int = 5000, ) -> render.TextureImageInfo: fixture.map.set_style_json(EMPTY_STYLE_JSON.encode()) - request_still_image_if_needed(fixture.map) - for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass - try: - return fixture.session.texture_image_info() - except mln.InvalidStateError: - time.sleep(0.001) - raise AssertionError("texture readback metadata was not observed") + render_until_update(fixture.runtime, fixture.session) + image = finish_render_operation( + fixture.session, + fixture.session.read_premultiplied_rgba8(), + return_result=True, + ) + return image.info def wait_for_vulkan_frame( @@ -132,50 +135,97 @@ def wait_for_vulkan_frame( *, iterations: int = 5000, ) -> render.VulkanOwnedTextureFrameHandle: - request_still_image_if_needed(fixture.map) last_frame: render.VulkanOwnedTextureFrame | None = None for _ in range(iterations): - fixture.runtime.pump() - for event in fixture.runtime.drain_events().events: - if event.event_type == mln.RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE: - try: - fixture.session.render_update() - except mln.InvalidStateError: - pass + # Forced rather than render-if-needed: a settled style would otherwise + # report NO_UPDATE forever and never fill a ring slot. + request_and_finish_frame(fixture.session, flags=render.FrameDemandFlag(0)) try: frame = fixture.session.acquire_vulkan_owned_texture_frame() - except mln.InvalidStateError: + except mln.InvalidStateError, mln.NotReadyError: + # No slot holds a frame this host has not already taken. time.sleep(0.001) continue last_frame = frame.frame if predicate(last_frame): return frame - frame.close() - time.sleep(0.001) + release_frame(frame) raise AssertionError(f"matching Vulkan frame was not observed; last={last_frame!r}") -def assert_invalid_state(call: Callable[[], object]) -> None: - with pytest.raises(mln.InvalidStateError) as raised: - call() - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE +def test_core_worker_renders_and_releases_owned_vulkan_frame( + vulkan_owned_session: VulkanOwnedSession, +) -> None: + vulkan_owned_session.map.set_style_json(EMPTY_STYLE_JSON.encode()) + render_until_update(vulkan_owned_session.runtime, vulkan_owned_session.session) + result = vulkan_owned_session.session.snapshot.latest_result + assert result == render.RenderResult.RENDERED + frame = vulkan_owned_session.session.acquire_vulkan_owned_texture_frame() + assert frame.result.disposition == result + assert frame.image.address != 0 + assert frame.image_view.address != 0 + assert ( + frame.device.address == vulkan_owned_session.context.descriptor().device.address + ) + release_frame(frame) + assert frame.closed + + # A rendered frame carries the map's repaint request with its result. A + # static empty style settles, so the signal clears within a few frames + # once nothing asks to draw again. + for token in range(2, 32): + settled = request_and_finish_frame(vulkan_owned_session.session, token=token) + assert settled.disposition == render.RenderResult.RENDERED + if settled.needs_repaint is False: + break + time.sleep(0.01) + else: + raise AssertionError("needs_repaint never cleared for a static style") + + +def test_core_worker_reads_owned_vulkan_texture( + vulkan_owned_session: VulkanOwnedSession, +) -> None: + info = wait_for_texture_info(vulkan_owned_session) + # Readback metadata describes the attached extent and a row-padded buffer. + assert info.width == 32 + assert info.height == 16 + assert info.stride >= info.width * 4 + assert info.byte_length >= info.stride * info.height + + image = finish_render_operation( + vulkan_owned_session.session, + vulkan_owned_session.session.read_premultiplied_rgba8(), + return_result=True, + ) + assert image.info == info + assert len(image.data) == info.byte_length + + +def test_owned_vulkan_session_reports_core_worker_capabilities( + vulkan_owned_session: VulkanOwnedSession, +) -> None: + capabilities = vulkan_owned_session.session.capabilities + snapshot = vulkan_owned_session.session.snapshot + assert capabilities.driver == render.RenderDriver.CORE_WORKER + assert capabilities.texture_ring_depth in (1, 2, 3) + assert snapshot.driver == render.RenderDriver.CORE_WORKER def test_attach_returns_public_render_session_and_rejects_second_session( vulkan_owned_session: VulkanOwnedSession, ) -> None: session = vulkan_owned_session.session - assert isinstance(session, render.RenderSessionHandle) assert not session.closed - assert not session.detached - with pytest.raises(mln.InvalidStateError) as raised: - vulkan_owned_session.map.attach_vulkan_owned_texture( + # A map drives at most one session, so a second attach is rejected and + # leaves the first one usable. + assert_invalid_state( + lambda: vulkan_owned_session.map.attach_vulkan_owned_texture( vulkan_owned_session.context.owned_texture_descriptor(32, 16, 1.0) ) - - assert raised.value.status == mln.MaplibreStatus.INVALID_STATE + ) assert not session.closed @@ -183,166 +233,79 @@ def test_detached_session_leaves_the_map_free_to_close( vulkan_owned_session: VulkanOwnedSession, ) -> None: session = vulkan_owned_session.session - assert_invalid_state(vulkan_owned_session.map.close) - detached = session.detach() - vulkan_owned_session.map.close() - detached.close() - + close_session(session) assert session.closed + vulkan_owned_session.map.close() -def test_render_update_without_pending_update_reports_no_update_and_keeps_session_live( +def test_frame_demand_without_a_newer_update_reports_no_update( vulkan_owned_session: VulkanOwnedSession, ) -> None: - update = vulkan_owned_session.session.render_update() - assert update.result == render.RenderResult.NO_UPDATE - assert update.needs_repaint is False + vulkan_owned_session.render_once() + # Draining the settled style leaves nothing newer, so a render-if-needed + # demand terminates without drawing and keeps the session live. + for token in range(2, 64): + result = request_and_finish_frame(vulkan_owned_session.session, token=token) + if result.disposition == render.RenderResult.NO_UPDATE: + break + time.sleep(0.01) + else: + raise AssertionError("a settled style never reported NO_UPDATE") + + assert result.needs_repaint is False assert not vulkan_owned_session.session.closed - vulkan_owned_session.session.resize(32, 16, 1.0) + finish_render_operation( + vulkan_owned_session.session, + vulkan_owned_session.session.resize(render.RenderTargetExtent(32, 16, 1.0)), + ) -def test_resize_updates_vulkan_owned_texture_frame_extent( +def test_resize_updates_owned_vulkan_texture_frame_extent( vulkan_owned_session: VulkanOwnedSession, ) -> None: vulkan_owned_session.render_once() - vulkan_owned_session.session.resize(16, 8, 2.0) - # The map applies the new logical size on its next pump, and a static map - # renders only on request, so pump the resize through before requesting the - # still image. A render before that pump reports the pending size. - assert ( - vulkan_owned_session.session.render_update().result - == render.RenderResult.SIZE_PENDING + finish_render_operation( + vulkan_owned_session.session, + vulkan_owned_session.session.resize(render.RenderTargetExtent(16, 8, 2.0)), ) - vulkan_owned_session.runtime.pump() + # The session-owned texture is sized in device pixels, so a 16x8 logical + # extent at scale factor 2 keeps the 32x16 physical ring. frame = wait_for_vulkan_frame( vulkan_owned_session, - lambda info: ( - info.width == 32 and info.height == 16 and info.scale_factor == 2.0 - ), + lambda info: info.scale_factor == pytest.approx(2.0), ) try: info = frame.frame assert info.width == 32 assert info.height == 16 assert info.scale_factor == pytest.approx(2.0) - assert info.generation >= 2 + assert info.generation >= 1 finally: - frame.close() + release_frame(frame) -def test_map_size_follows_attach_and_resize_and_keeps_the_creation_scale_factor( +def test_map_size_follows_attach_and_session_resize( vulkan_owned_session: VulkanOwnedSession, ) -> None: - # A session enqueues the map size for the map's owner thread rather than - # setting it in place, so the map keeps its previous size until pumped. - assert vulkan_owned_session.map.get_size() == (64, 64, pytest.approx(1.0)) - vulkan_owned_session.runtime.pump() + # Attachment sizes the map from the target rather than from map creation. assert vulkan_owned_session.map.get_size() == (32, 16, pytest.approx(1.0)) - # Resizing at a different scale factor leaves the map's own pixel ratio. - vulkan_owned_session.session.resize(48, 24, 2.0) - vulkan_owned_session.runtime.pump() - assert vulkan_owned_session.map.get_size() == (48, 24, pytest.approx(1.0)) - - -def test_a_worker_thread_attaches_its_own_session_and_renders() -> None: - """Spec coverage: BND-193, BND-195. - - A session is owned by the thread that attaches it, so a worker thread can - attach and drive its own session against a map owned by the main thread. - """ - import threading - - if not mln.supported_render_backends() & mln.RenderBackend.VULKAN: - pytest.skip("native library does not support Vulkan render sessions") - try: - context = VulkanContext.create() - except VulkanUnavailableError as error: - pytest.skip(f"Vulkan fixture creation is unavailable: {error}") - - runtime = mln.RuntimeHandle() - failure: list[BaseException] = [] - rendered: list[bool] = [] - - def attach_render_close(map_handle: mln.MapHandle) -> None: - try: - session = map_handle.attach_vulkan_owned_texture( - context.owned_texture_descriptor(32, 16, 1.0) - ) - try: - # The map applies its logical size on its own thread, so the - # first renders report a pending size until the main thread - # pumps. - deadline = time.monotonic() + 5.0 - while time.monotonic() < deadline: - if session.render_update().result == render.RenderResult.RENDERED: - rendered.append(True) - break - time.sleep(0.002) - finally: - # Closing here proves the session is destroyed on the thread - # that attached it, which is what frees the map to close. - session.close() - except BaseException as error: # noqa: BLE001 - failure.append(error) - - try: - map_handle = runtime.create_map(mln.MapOptions(width=64, height=64)) - try: - map_handle.set_style_json(EMPTY_STYLE_JSON.encode()) - worker = threading.Thread(target=attach_render_close, args=(map_handle,)) - worker.start() - while worker.is_alive(): - # A short park rather than zero: this waits on the worker, so - # spinning would burn the deadline before it made progress. - runtime.pump(0.002) - runtime.drain_events() - worker.join() - assert not failure, failure - assert rendered, "worker thread should render the map" - finally: - map_handle.close() - finally: - runtime.close() - context.close() - - -def test_cpu_readback_metadata_capacity_and_reusable_buffer( - vulkan_owned_session: VulkanOwnedSession, -) -> None: - info = wait_for_texture_info(vulkan_owned_session) - - assert info.width == 32 - assert info.height == 16 - assert info.stride >= info.width * 4 - assert info.byte_length >= info.stride * info.height - - undersized = bytearray([0x7F] * (info.byte_length - 1)) - with pytest.raises(mln.InvalidArgumentError) as raised: - vulkan_owned_session.session.read_premultiplied_rgba8_into(undersized) - assert raised.value.status == mln.MaplibreStatus.INVALID_ARGUMENT - assert set(undersized) == {0x7F} - - # An empty destination is a caller error here, not the C size probe. - with pytest.raises(mln.InvalidArgumentError): - vulkan_owned_session.session.read_premultiplied_rgba8_into(bytearray()) - - reusable = bytearray(info.byte_length) - copied = vulkan_owned_session.session.read_premultiplied_rgba8_into(reusable) - assert copied == info - assert len(reusable) == info.byte_length + # An applied resize updates the map viewport, scale factor included. + finish_render_operation( + vulkan_owned_session.session, + vulkan_owned_session.session.resize(render.RenderTargetExtent(48, 24, 2.0)), + ) + assert vulkan_owned_session.map.get_size() == (48, 24, pytest.approx(2.0)) -def test_vulkan_frame_acquire_release_and_backend_handles( +def test_vulkan_frame_exposes_backend_handles_only_while_the_lease_is_live( vulkan_owned_session: VulkanOwnedSession, ) -> None: - vulkan_owned_session.render_once() - - frame = vulkan_owned_session.session.acquire_vulkan_owned_texture_frame() + frame = wait_for_vulkan_frame(vulkan_owned_session, lambda _: True) assert isinstance(frame, render.VulkanOwnedTextureFrameHandle) info = frame.frame assert info.width == 32 @@ -362,31 +325,58 @@ def test_vulkan_frame_acquire_release_and_backend_handles( assert image_view.address != 0 assert device.address == vulkan_owned_session.context.descriptor().device.address - frame.close() + release_frame(frame) assert frame.closed assert_invalid_state(lambda: frame.image) assert_invalid_state(lambda: frame.image_view) assert_invalid_state(lambda: frame.device) -def test_vulkan_frame_release_failure_leaves_frame_live_for_later_release() -> None: +def test_stale_vulkan_frame_handles_cannot_expose_backend_handles_after_reuse( + vulkan_owned_session: VulkanOwnedSession, +) -> None: + stale_frame = wait_for_vulkan_frame(vulkan_owned_session, lambda _: True) + stale_image = stale_frame.image + stale_image_view = stale_frame.image_view + stale_device = stale_frame.device + release_frame(stale_frame) + + for pointer in (stale_image, stale_image_view, stale_device): + assert_invalid_state(lambda pointer=pointer: pointer.address) + + # The ring may hand the same backend object to the next lease, so the + # retired pointers must stay unreadable rather than alias it. + next_frame = wait_for_vulkan_frame(vulkan_owned_session, lambda _: True) + try: + assert next_frame.image.address != 0 + for pointer in (stale_image, stale_image_view, stale_device): + assert_invalid_state(lambda pointer=pointer: pointer.address) + finally: + release_frame(next_frame) + + +def test_session_close_is_rejected_while_a_frame_lease_is_held( + vulkan_owned_session: VulkanOwnedSession, +) -> None: + session = vulkan_owned_session.session + frame = wait_for_vulkan_frame(vulkan_owned_session, lambda _: True) + try: + assert session.snapshot.acquired_frame_count == 1 + # The host still holds a ring slot, so the session cannot retire. + assert_invalid_state(session.close) + assert not session.closed + finally: + release_frame(frame) + + assert session.snapshot.acquired_frame_count == 0 + + +def test_vulkan_frame_release_failure_leaves_the_lease_live_for_a_later_release() -> ( + None +): class FakeNativeFrame: closed = False - close_calls = 0 - - def frame(self) -> dict[str, object]: - return { - "generation": 1, - "width": 32, - "height": 16, - "scale_factor": 1.0, - "frame_id": 7, - "image": 0x1000, - "image_view": 0x2000, - "device": 0x3000, - "format": 44, - "layout": 55, - } + release_calls = 0 def image_address(self) -> int: if self.closed: @@ -395,23 +385,9 @@ def image_address(self) -> int: ) return 0x1000 - def image_view_address(self) -> int: - if self.closed: - raise mln.InvalidStateError( - None, "VulkanOwnedTextureFrameHandle is closed" - ) - return 0x2000 - - def device_address(self) -> int: - if self.closed: - raise mln.InvalidStateError( - None, "VulkanOwnedTextureFrameHandle is closed" - ) - return 0x3000 - - def close(self) -> None: - self.close_calls += 1 - if self.close_calls == 1: + def release(self, kind: int, address: int, value: int) -> None: + self.release_calls += 1 + if self.release_calls == 1: raise mln.InvalidStateError(None, "frame release failed") self.closed = True @@ -420,115 +396,20 @@ def close(self) -> None: assert frame.image.address == 0x1000 with pytest.raises(mln.InvalidStateError, match="frame release failed"): - frame.close() + release_frame(frame) + # A rejected release keeps the host's claim on the slot, so the address + # stays readable and a later release still retires it. assert not frame.closed assert frame.image.address == 0x1000 - assert native.close_calls == 1 + assert native.release_calls == 1 - frame.close() + release_frame(frame) assert frame.closed - assert native.close_calls == 2 + assert native.release_calls == 2 assert_invalid_state(lambda: frame.image) -def test_active_vulkan_frame_rejects_nested_acquire_and_session_operations( - vulkan_owned_session: VulkanOwnedSession, -) -> None: - vulkan_owned_session.render_once() - - frame = vulkan_owned_session.session.acquire_vulkan_owned_texture_frame() - point_query = query.RenderedQueryGeometry.point_geometry( - camera.ScreenPoint(0.0, 0.0) - ) - feature = b'{"type":"Feature","geometry":null,"properties":{}}' - - calls: tuple[Callable[[], object], ...] = ( - lambda: vulkan_owned_session.session.resize(16, 16, 1.0), - vulkan_owned_session.session.render_update, - vulkan_owned_session.session.detach, - vulkan_owned_session.session.reduce_memory_use, - vulkan_owned_session.session.clear_data, - vulkan_owned_session.session.dump_debug_logs, - vulkan_owned_session.session.texture_image_info, - lambda: vulkan_owned_session.session.read_premultiplied_rgba8_into( - bytearray(4) - ), - vulkan_owned_session.session.acquire_metal_owned_texture_frame, - vulkan_owned_session.session.acquire_vulkan_owned_texture_frame, - vulkan_owned_session.session.acquire_opengl_owned_texture_frame, - lambda: vulkan_owned_session.session.query_rendered_features(point_query), - lambda: vulkan_owned_session.session.query_source_features("point"), - lambda: vulkan_owned_session.session.query_feature_extensions( - "point", - feature, - "x", - "y", - ), - vulkan_owned_session.session.close, - ) - try: - for call in calls: - assert_invalid_state(call) - assert not vulkan_owned_session.session.closed - finally: - frame.close() - - -def test_stale_vulkan_frame_handles_cannot_expose_backend_handles_after_reuse( - vulkan_owned_session: VulkanOwnedSession, -) -> None: - vulkan_owned_session.render_once() - - stale_frame = vulkan_owned_session.session.acquire_vulkan_owned_texture_frame() - stale_image = stale_frame.image - stale_image_view = stale_frame.image_view - stale_device = stale_frame.device - stale_frame.close() - - for pointer in (stale_image, stale_image_view, stale_device): - assert_invalid_state(lambda pointer=pointer: pointer.address) - assert_invalid_state(lambda: stale_frame.image) - assert_invalid_state(lambda: stale_frame.image_view) - assert_invalid_state(lambda: stale_frame.device) - - next_frame = vulkan_owned_session.session.acquire_vulkan_owned_texture_frame() - try: - assert next_frame.image.address != 0 - for pointer in (stale_image, stale_image_view, stale_device): - assert_invalid_state(lambda pointer=pointer: pointer.address) - finally: - next_frame.close() - - -def test_real_vulkan_render_session_reports_wrong_thread_errors( - vulkan_owned_session: VulkanOwnedSession, -) -> None: - calls: tuple[Callable[[], object], ...] = ( - lambda: vulkan_owned_session.session.resize(16, 16, 1.0), - vulkan_owned_session.session.render_update, - vulkan_owned_session.session.acquire_vulkan_owned_texture_frame, - vulkan_owned_session.session.close, - ) - - def run_call(call: Callable[[], object], observed: list[Exception]) -> None: - try: - call() - except mln.WrongThreadError as error: - observed.append(error) - - for call in calls: - observed: list[Exception] = [] - thread = threading.Thread(target=run_call, args=(call, observed)) - thread.start() - thread.join() - - assert len(observed) == 1 - assert isinstance(observed[0], mln.WrongThreadError) - assert observed[0].status == mln.MaplibreStatus.WRONG_THREAD - assert not vulkan_owned_session.session.closed - - def test_cluster_feature_extension_queries_resolve_unsigned_cluster_id_and_limit( vulkan_owned_session: VulkanOwnedSession, ) -> None: diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/camera.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/camera.rs index 84de0fb69..95f132b4d 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/camera.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/camera.rs @@ -136,6 +136,131 @@ impl AnimationOptions { } } +/// Transition behavior for one atomic camera update. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum CameraUpdateMode { + #[default] + Jump, + Ease, + Fly, +} + +impl CameraUpdateMode { + fn to_native(self) -> u32 { + match self { + Self::Jump => sys::MLN_CAMERA_UPDATE_MODE_JUMP, + Self::Ease => sys::MLN_CAMERA_UPDATE_MODE_EASE, + Self::Fly => sys::MLN_CAMERA_UPDATE_MODE_FLY, + } + } +} + +/// Gesture boundary carried atomically with a camera update. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum GesturePhase { + #[default] + None, + Begin, + Update, + End, + Cancel, +} + +impl GesturePhase { + fn to_native(self) -> u32 { + match self { + Self::None => sys::MLN_GESTURE_PHASE_NONE, + Self::Begin => sys::MLN_GESTURE_PHASE_BEGIN, + Self::Update => sys::MLN_GESTURE_PHASE_UPDATE, + Self::End => sys::MLN_GESTURE_PHASE_END, + Self::Cancel => sys::MLN_GESTURE_PHASE_CANCEL, + } + } +} + +/// One copied, atomic camera command. +#[derive(Debug, Clone, PartialEq, Default)] +#[non_exhaustive] +pub struct CameraUpdate { + pub mode: CameraUpdateMode, + pub camera: CameraOptions, + pub animation: AnimationOptions, + pub gesture_phase: GesturePhase, +} + +impl CameraUpdate { + fn to_native(&self) -> sys::mln_camera_update { + // SAFETY: the default constructor initializes this ABI version's size. + let mut raw = unsafe { sys::mln_camera_update_default() }; + raw.mode = self.mode.to_native(); + raw.camera = self.camera.to_native(); + raw.animation = self.animation.to_native(); + raw.gesture_phase = self.gesture_phase.to_native(); + raw + } +} + +/// Relative camera operation kind. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum CameraDeltaKind { + #[default] + Move, + Scale, + Bearing, + Pitch, +} + +/// One relative camera operation. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct CameraDelta { + pub kind: CameraDeltaKind, + pub offset: ScreenPoint, + pub amount: f64, + pub anchor: Option, + pub animation: AnimationOptions, +} + +impl Default for CameraDelta { + fn default() -> Self { + Self { + kind: CameraDeltaKind::Move, + offset: ScreenPoint::new(0.0, 0.0), + amount: 0.0, + anchor: None, + animation: AnimationOptions::default(), + } + } +} + +impl CameraDelta { + fn to_native(&self) -> sys::mln_camera_delta { + // SAFETY: the default constructor initializes this ABI version's size. + let mut raw = unsafe { sys::mln_camera_delta_default() }; + raw.kind = match self.kind { + CameraDeltaKind::Move => sys::MLN_CAMERA_DELTA_MOVE, + CameraDeltaKind::Scale => sys::MLN_CAMERA_DELTA_SCALE, + CameraDeltaKind::Bearing => sys::MLN_CAMERA_DELTA_BEARING, + CameraDeltaKind::Pitch => sys::MLN_CAMERA_DELTA_PITCH, + }; + raw.offset = screen_point_to_native(self.offset); + raw.amount = self.amount; + if let Some(anchor) = self.anchor { + raw.has_anchor = true; + raw.anchor = screen_point_to_native(anchor); + } + raw.animation = self.animation.to_native(); + raw + } +} + +/// Camera and publication generation returned by a snapshot or ordered query. +#[derive(Debug, Clone, PartialEq)] +pub struct CameraSnapshot { + pub generation: u64, + pub camera: CameraOptions, +} + /// Optional fitting controls for camera-for-viewport queries. #[derive(Debug, Clone, PartialEq, Default)] #[non_exhaustive] @@ -354,6 +479,28 @@ pub fn projection_mode_from_native(raw: sys::mln_projection_mode) -> ProjectionM ProjectionMode::from_native(raw) } +#[doc(hidden)] +pub trait CameraUpdateNativeExt { + fn to_native(&self) -> sys::mln_camera_update; +} + +#[doc(hidden)] +pub trait CameraDeltaNativeExt { + fn to_native(&self) -> sys::mln_camera_delta; +} + +impl CameraDeltaNativeExt for CameraDelta { + fn to_native(&self) -> sys::mln_camera_delta { + CameraDelta::to_native(self) + } +} + +impl CameraUpdateNativeExt for CameraUpdate { + fn to_native(&self) -> sys::mln_camera_update { + CameraUpdate::to_native(self) + } +} + #[doc(hidden)] pub trait CameraOptionsNativeExt { fn to_native(&self) -> sys::mln_camera_options; diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/enums.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/enums.rs index cdb2ead5d..9f4fb7624 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/enums.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/enums.rs @@ -25,16 +25,17 @@ bitflags::bitflags! { } } -/// How a session's OpenGL context relates to the thread that attached it. +/// How a session's OpenGL context relates to its driver thread and host graphics +/// state. /// /// A shared session leaves the thread as it found it: every render makes the /// session context current and restores whatever was current before. The /// session context joins the host share group the descriptor names, so a host /// may hand the session a texture and sample it from its own context. /// -/// A dedicated session owns the thread. It makes its context current once and -/// keeps it current between renders, and it joins no share group. Use this when -/// a thread exists to drive one render session and runs no other graphics work. +/// A dedicated session owns its driver thread's context. It keeps the context +/// current between renders and joins no share group. The driver may be a native +/// core worker or a dedicated host thread. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum OpenGLContextOwnership { @@ -162,8 +163,6 @@ bitflags::bitflags! { sys::MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR; const OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = sys::MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED; - const OFFLINE_OPERATION_COMPLETED = - sys::MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED; /// Selects every map-originated event type this version defines. const ALL_MAP_EVENTS = sys::MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS; /// Selects every runtime-originated event type this version defines. @@ -198,103 +197,6 @@ impl AmbientCacheOperation { } } -/// Offline database operation kind reported by completion events. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum OfflineOperationKind { - AmbientCache, - RegionCreate, - RegionGet, - RegionsList, - RegionsMergeDatabase, - RegionUpdateMetadata, - RegionGetStatus, - RegionSetObserved, - RegionSetDownloadState, - RegionInvalidate, - RegionDelete, - SetMaximumAmbientCacheSize, - Unknown(u32), -} - -impl OfflineOperationKind { - pub fn from_raw(raw: u32) -> Self { - match raw { - sys::MLN_OFFLINE_OPERATION_AMBIENT_CACHE => Self::AmbientCache, - sys::MLN_OFFLINE_OPERATION_REGION_CREATE => Self::RegionCreate, - sys::MLN_OFFLINE_OPERATION_REGION_GET => Self::RegionGet, - sys::MLN_OFFLINE_OPERATION_REGIONS_LIST => Self::RegionsList, - sys::MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE => Self::RegionsMergeDatabase, - sys::MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA => Self::RegionUpdateMetadata, - sys::MLN_OFFLINE_OPERATION_REGION_GET_STATUS => Self::RegionGetStatus, - sys::MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED => Self::RegionSetObserved, - sys::MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE => Self::RegionSetDownloadState, - sys::MLN_OFFLINE_OPERATION_REGION_INVALIDATE => Self::RegionInvalidate, - sys::MLN_OFFLINE_OPERATION_REGION_DELETE => Self::RegionDelete, - sys::MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE => { - Self::SetMaximumAmbientCacheSize - } - _ => Self::Unknown(raw), - } - } - - pub fn raw_value(self) -> u32 { - match self { - Self::AmbientCache => sys::MLN_OFFLINE_OPERATION_AMBIENT_CACHE, - Self::RegionCreate => sys::MLN_OFFLINE_OPERATION_REGION_CREATE, - Self::RegionGet => sys::MLN_OFFLINE_OPERATION_REGION_GET, - Self::RegionsList => sys::MLN_OFFLINE_OPERATION_REGIONS_LIST, - Self::RegionsMergeDatabase => sys::MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE, - Self::RegionUpdateMetadata => sys::MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA, - Self::RegionGetStatus => sys::MLN_OFFLINE_OPERATION_REGION_GET_STATUS, - Self::RegionSetObserved => sys::MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED, - Self::RegionSetDownloadState => sys::MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE, - Self::RegionInvalidate => sys::MLN_OFFLINE_OPERATION_REGION_INVALIDATE, - Self::RegionDelete => sys::MLN_OFFLINE_OPERATION_REGION_DELETE, - Self::SetMaximumAmbientCacheSize => { - sys::MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE - } - Self::Unknown(raw) => raw, - } - } -} - -/// Offline database operation result kind reported by completion events. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum OfflineOperationResultKind { - None, - Region, - OptionalRegion, - RegionList, - RegionStatus, - Unknown(u32), -} - -impl OfflineOperationResultKind { - pub fn from_raw(raw: u32) -> Self { - match raw { - sys::MLN_OFFLINE_OPERATION_RESULT_NONE => Self::None, - sys::MLN_OFFLINE_OPERATION_RESULT_REGION => Self::Region, - sys::MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION => Self::OptionalRegion, - sys::MLN_OFFLINE_OPERATION_RESULT_REGION_LIST => Self::RegionList, - sys::MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS => Self::RegionStatus, - _ => Self::Unknown(raw), - } - } - - pub fn raw_value(self) -> u32 { - match self { - Self::None => sys::MLN_OFFLINE_OPERATION_RESULT_NONE, - Self::Region => sys::MLN_OFFLINE_OPERATION_RESULT_REGION, - Self::OptionalRegion => sys::MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION, - Self::RegionList => sys::MLN_OFFLINE_OPERATION_RESULT_REGION_LIST, - Self::RegionStatus => sys::MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS, - Self::Unknown(raw) => raw, - } - } -} - /// Style source type values returned by native style source metadata. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] @@ -681,7 +583,6 @@ pub enum RuntimeEventType { OfflineRegionStatusChanged, OfflineRegionResponseError, OfflineRegionTileCountLimitExceeded, - OfflineOperationCompleted, Unknown(u32), } @@ -718,7 +619,6 @@ impl RuntimeEventType { sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED => { Self::OfflineRegionTileCountLimitExceeded } - sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED => Self::OfflineOperationCompleted, _ => Self::Unknown(raw), } } @@ -757,7 +657,6 @@ impl RuntimeEventType { Self::OfflineRegionTileCountLimitExceeded => { sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED } - Self::OfflineOperationCompleted => sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED, Self::Unknown(raw) => raw, } } @@ -805,35 +704,6 @@ impl RenderMode { } } -/// Outcome of a successful render-update call. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum RenderResult { - /// The call rendered a frame into the render target. - Rendered, - /// The call produced no frame. Wait for a render-update-available event. - NoUpdate, - /// The map has not applied the session's current size yet. Wait for the - /// next render-update-available event. - SizePending, - /// The render target had no frame to draw into. Wait for a host event that - /// changes the target, or back off and retry. - TargetNotReady, - Unknown(u32), -} - -impl RenderResult { - pub fn from_raw(raw: u32) -> Self { - match raw { - sys::MLN_RENDER_RESULT_RENDERED => Self::Rendered, - sys::MLN_RENDER_RESULT_NO_UPDATE => Self::NoUpdate, - sys::MLN_RENDER_RESULT_SIZE_PENDING => Self::SizePending, - sys::MLN_RENDER_RESULT_TARGET_NOT_READY => Self::TargetNotReady, - _ => Self::Unknown(raw), - } - } -} - /// Tile operation reported by tile observer events. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/error.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/error.rs index d8dccfa6a..93622ba63 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/error.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/error.rs @@ -13,6 +13,11 @@ pub enum ErrorKind { InvalidState, WrongThread, Unsupported, + Cancelled, + Busy, + TargetLost, + NotReady, + NotFound, NativeError, AbiVersionMismatch, UnknownStatus, @@ -98,6 +103,11 @@ pub fn kind_for_status(status: i32) -> ErrorKind { sys::MLN_STATUS_INVALID_STATE => ErrorKind::InvalidState, sys::MLN_STATUS_WRONG_THREAD => ErrorKind::WrongThread, sys::MLN_STATUS_UNSUPPORTED => ErrorKind::Unsupported, + sys::MLN_STATUS_CANCELLED => ErrorKind::Cancelled, + sys::MLN_STATUS_BUSY => ErrorKind::Busy, + sys::MLN_STATUS_TARGET_LOST => ErrorKind::TargetLost, + sys::MLN_STATUS_NOT_READY => ErrorKind::NotReady, + sys::MLN_STATUS_NOT_FOUND => ErrorKind::NotFound, sys::MLN_STATUS_NATIVE_ERROR => ErrorKind::NativeError, _ => ErrorKind::UnknownStatus, } diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/events.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/events.rs index 8e176a1c3..55069d7f2 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/events.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/events.rs @@ -7,8 +7,7 @@ use maplibre_native_ffi_sys as sys; use crate::{Error, Result}; use crate::{ - OfflineOperationKind, OfflineOperationResultKind, OfflineRegionDownloadState, RenderMode, - ResourceErrorReason, RuntimeEventType, TileOperation, + OfflineRegionDownloadState, RenderMode, ResourceErrorReason, RuntimeEventType, TileOperation, }; /// Byte offset of the payload union inside a native event record. Every ABI @@ -172,19 +171,6 @@ pub struct OfflineRegionTileCountLimitEvent { pub limit: u64, } -/// Offline operation-completion event payload. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub struct OfflineOperationCompletedEvent { - pub operation_id: u64, - pub operation_kind: OfflineOperationKind, - pub raw_operation_kind: u32, - pub result_kind: OfflineOperationResultKind, - pub raw_result_kind: u32, - pub result_status: i32, - pub found: bool, -} - /// Camera transition-finished event payload. /// /// A transition reports its end once for every terminal outcome, and the @@ -197,6 +183,28 @@ pub struct CameraTransitionFinishedEvent { pub transition_id: u64, } +/// Terminal disposition of an accepted command. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CommandDisposition { + Committed, + Superseded, + Failed, + Cancelled, + Unknown(u32), +} + +impl CommandDisposition { + pub fn from_native(raw: u32) -> Self { + match raw { + sys::MLN_COMMAND_DISPOSITION_COMMITTED => Self::Committed, + sys::MLN_COMMAND_DISPOSITION_SUPERSEDED => Self::Superseded, + sys::MLN_COMMAND_DISPOSITION_FAILED => Self::Failed, + sys::MLN_COMMAND_DISPOSITION_CANCELLED => Self::Cancelled, + value => Self::Unknown(value), + } + } +} + /// Payload of a type this version does not define, preserved for forward /// compatibility. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -219,7 +227,6 @@ pub enum RuntimeEventPayload { OfflineRegionStatus(OfflineRegionStatusEvent), OfflineRegionResponseError(OfflineRegionResponseErrorEvent), OfflineRegionTileCountLimit(OfflineRegionTileCountLimitEvent), - OfflineOperationCompleted(OfflineOperationCompletedEvent), CameraTransitionFinished(CameraTransitionFinishedEvent), Unknown(UnknownRuntimeEventPayload), } @@ -232,8 +239,7 @@ pub struct CopiedRuntimeEvent { pub source: RawRuntimeEventSource, /// Secondary event detail whose meaning `event_type` selects. Camera /// change events decode as - /// `CameraChangeMode::from_raw(code as u32)`, offline operation-completion - /// events carry the operation's native status, and map loading-failure + /// `CameraChangeMode::from_raw(code as u32)`, and map loading-failure /// events carry a load error ordinal whose text is in `message`. pub code: i32, pub message: Option, @@ -255,7 +261,7 @@ pub struct NativeEventView<'a> { /// Iterator over the events of one drained batch, in queue order. pub struct NativeEventViews<'a> { - batch: sys::mln_runtime_event_batch, + batch: sys::mln_runtime_event_batch_view, index: usize, _storage: PhantomData<&'a [u8]>, } @@ -286,9 +292,9 @@ impl ExactSizeIterator for NativeEventViews<'_> {} /// /// # Safety /// -/// `batch` must be a batch that `mln_runtime_drain_events` filled, whose event -/// and message storage stays valid for `'a`. -pub unsafe fn event_views<'a>(batch: &sys::mln_runtime_event_batch) -> NativeEventViews<'a> { +/// `batch` must be a view that `mln_event_batch_get` filled, whose event and +/// message storage stays valid for `'a`. +pub unsafe fn event_views<'a>(batch: &sys::mln_runtime_event_batch_view) -> NativeEventViews<'a> { NativeEventViews { batch: *batch, index: 0, @@ -303,11 +309,11 @@ pub unsafe fn event_views<'a>(batch: &sys::mln_runtime_event_batch) -> NativeEve /// /// # Safety /// -/// `batch` must be a batch that `mln_runtime_drain_events` filled, whose event -/// and message storage stays valid for `'a`, and `index` must be below +/// `batch` must be a view that `mln_event_batch_get` filled, whose event and +/// message storage stays valid for `'a`, and `index` must be below /// `batch.event_count`. pub unsafe fn event_view<'a>( - batch: &sys::mln_runtime_event_batch, + batch: &sys::mln_runtime_event_batch_view, index: usize, ) -> NativeEventView<'a> { let stride = batch.event_size as usize; @@ -352,10 +358,10 @@ pub unsafe fn event_view<'a>( /// /// # Safety /// -/// `batch` must be a batch that `mln_runtime_drain_events` filled, whose event -/// and message storage stays valid for the returned iterator's lifetime. +/// `batch` must be a view that `mln_event_batch_get` filled, whose event and +/// message storage stays valid for the returned iterator's lifetime. pub unsafe fn drain_batch<'a>( - batch: &sys::mln_runtime_event_batch, + batch: &sys::mln_runtime_event_batch_view, ) -> impl Iterator> + 'a { // SAFETY: The caller promised the batch describes live storage. unsafe { event_views(batch) }.map(|view| @@ -443,18 +449,6 @@ pub unsafe fn payload_from_view(view: &NativeEventView<'_>) -> RuntimeEventPaylo limit: payload.limit, }) } - sys::MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED => { - let payload = raw.payload.offline_operation_completed; - RuntimeEventPayload::OfflineOperationCompleted(OfflineOperationCompletedEvent { - operation_id: payload.operation_id, - operation_kind: OfflineOperationKind::from_raw(payload.operation_kind), - raw_operation_kind: payload.operation_kind, - result_kind: OfflineOperationResultKind::from_raw(payload.result_kind), - raw_result_kind: payload.result_kind, - result_status: payload.result_status, - found: payload.found, - }) - } sys::MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED => { RuntimeEventPayload::CameraTransitionFinished(CameraTransitionFinishedEvent { transition_id: raw.payload.camera_transition_finished.transition_id, @@ -508,7 +502,7 @@ mod tests { } fn push(&mut self, mut event: sys::mln_runtime_event, message: &[u8]) { - event.message_offset = u32::try_from(self.messages.len()).unwrap(); + event.message_offset = u64::try_from(self.messages.len()).unwrap(); event.message_size = u32::try_from(message.len()).unwrap(); self.messages.extend_from_slice(message); self.messages.push(0); @@ -525,15 +519,14 @@ mod tests { self.count += 1; } - fn raw(&self) -> sys::mln_runtime_event_batch { - sys::mln_runtime_event_batch { - size: mem::size_of::() as u32, + fn raw(&self) -> sys::mln_runtime_event_batch_view { + sys::mln_runtime_event_batch_view { + size: mem::size_of::() as u32, event_size: u32::try_from(self.stride).unwrap(), events: self.records.as_ptr().cast(), event_count: self.count, messages: self.messages.as_ptr().cast(), messages_size: self.messages.len(), - remaining_count: 0, } } } diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/handle.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/handle.rs index 681c85ca9..ca5a028d1 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/handle.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/handle.rs @@ -38,7 +38,9 @@ native_handle!( sys::mln_map, sys::mln_map_projection, sys::mln_render_session, - sys::mln_wake_source, + sys::mln_event_batch, + sys::mln_render_frame_batch, + sys::mln_acquired_frame, sys::mln_resource_request_handle, sys::mln_geojson_source_data, sys::mln_offline_region_snapshot, @@ -89,7 +91,7 @@ pub fn report_leak(leak: NativeHandleLeak) { /// Tracks whether a native handle is still live and centralizes close-once /// behavior. Public handle policy such as `!Send`, parent retention, and -/// owner-thread checks lives above this. +/// thread-affinity checks lives above this. #[derive(Debug)] pub struct NativeHandleState { id: Cell>, @@ -148,7 +150,7 @@ impl NativeHandleState { /// Closes the handle with a status-returning destroy function. /// /// The state remains live when the destroy function reports an error, which - /// lets callers retry an owner-thread close later. + /// lets callers retry a graphics-thread-affine close later. /// /// # Safety /// diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/lib.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/lib.rs index 586937f8e..3457130a3 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/lib.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/lib.rs @@ -32,18 +32,16 @@ pub use camera::{ pub use enums::{ AmbientCacheOperation, CameraChangeMode, ConstrainMode, LocationIndicatorImageKind, LogEvent, LogSeverity, LogSeverityMask, MapDebugOptions, MapMode, NetworkStatus, NorthOrientation, - OfflineOperationKind, OfflineOperationResultKind, OfflineRegionDownloadState, OpenGLClientApi, - OpenGLContextOwnership, OpenGLContextProviderMask, RasterDemEncoding, RenderBackendMask, - RenderMode, RenderResult, ResourceErrorReason, ResourceKind, ResourceLoadingMethod, - ResourcePriority, ResourceResponseStatus, ResourceStoragePolicy, ResourceUsage, - RuntimeEventMask, RuntimeEventType, SourceType, StyleImageTextFit, StyleLayerVisibility, - TileLodMode, TileOperation, TileScheme, VectorTileEncoding, ViewportMode, + OfflineRegionDownloadState, OpenGLClientApi, OpenGLContextOwnership, OpenGLContextProviderMask, + RasterDemEncoding, RenderBackendMask, RenderMode, ResourceErrorReason, ResourceKind, + ResourceLoadingMethod, ResourcePriority, ResourceResponseStatus, ResourceStoragePolicy, + ResourceUsage, RuntimeEventMask, RuntimeEventType, SourceType, StyleImageTextFit, + StyleLayerVisibility, TileLodMode, TileOperation, TileScheme, VectorTileEncoding, ViewportMode, }; pub use error::{Error, ErrorKind, Result, check}; pub use events::{ CameraTransitionFinishedEvent, CopiedRuntimeEvent, NativeEventView, NativeEventViews, - OfflineOperationCompletedEvent, OfflineRegionResponseErrorEvent, OfflineRegionStatus, - OfflineRegionStatusEvent, OfflineRegionTileCountLimitEvent, RawRuntimeEventSource, + OfflineRegionResponseErrorEvent, OfflineRegionStatus, OfflineRegionStatusEvent, RenderFrameEvent, RenderMapEvent, RenderingStats, RuntimeEventPayload, TileActionEvent, TileId, UnknownRuntimeEventPayload, }; @@ -63,7 +61,7 @@ pub use runtime::{ }; pub use style::{ GeoJsonSourceOptions, ImageContent, ImageStretch, SourceInfo, StyleImage, StyleImageOptions, - StyleTransitionOptions, TileJsonInfo, TileSourceOptions, + StyleLayerInfo, StyleTransitionOptions, TileJsonInfo, TileSourceOptions, }; pub use values::{ EdgeInsets, LatLng, LatLngBounds, PremultipliedRgba8Image, ProjectedMeters, Quaternion, diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/options.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/options.rs index 126555979..5e6edbed0 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/options.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/options.rs @@ -42,9 +42,9 @@ impl MapOptions { // SAFETY: Default constructor takes no arguments and initializes size // and default values for this C ABI version. let mut raw = unsafe { sys::mln_map_options_default() }; - raw.width = self.width; - raw.height = self.height; - raw.scale_factor = self.scale_factor; + raw.initial_extent.width = self.width; + raw.initial_extent.height = self.height; + raw.initial_extent.scale_factor = self.scale_factor; raw.map_mode = self.mode.raw_for_set()?; raw.fast_pfor_enabled = self.fast_pfor_enabled; raw.event_mask = self.event_mask.bits(); @@ -58,9 +58,9 @@ impl Default for MapOptions { // for this C ABI version. let raw = unsafe { sys::mln_map_options_default() }; Self { - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, + width: raw.initial_extent.width, + height: raw.initial_extent.height, + scale_factor: raw.initial_extent.scale_factor, mode: MapMode::from_raw(raw.map_mode), fast_pfor_enabled: raw.fast_pfor_enabled, // Retained rather than named, so a newer native library's default @@ -280,9 +280,9 @@ mod tests { let raw = map_options_to_native(&options).unwrap(); assert_eq!(raw.size, std::mem::size_of::() as u32); - assert_eq!(raw.width, 800); - assert_eq!(raw.height, 600); - assert_eq!(raw.scale_factor, 2.0); + assert_eq!(raw.initial_extent.width, 800); + assert_eq!(raw.initial_extent.height, 600); + assert_eq!(raw.initial_extent.scale_factor, 2.0); assert_eq!(raw.map_mode, sys::MLN_MAP_MODE_STATIC); assert!(raw.fast_pfor_enabled); } diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/query.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/query.rs index 56ab7badf..9164400f6 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/query.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/query.rs @@ -389,6 +389,36 @@ pub struct QueriedFeature { pub state: Option>, } +/// Copies borrowed native query hits into owned Rust values. +/// +/// # Safety +/// +/// Every view in `hits` must remain valid for this call. +pub unsafe fn copy_queried_features( + hits: &[sys::mln_queried_feature], +) -> crate::Result> { + hits.iter() + .map(|hit| { + let feature = unsafe { crate::string::copy_string_view_bytes(hit.feature) }?; + let source_id = (hit.fields & sys::MLN_QUERIED_FEATURE_SOURCE_ID != 0) + .then(|| unsafe { crate::string::copy_string_view(hit.source_id) }) + .transpose()?; + let source_layer_id = (hit.fields & sys::MLN_QUERIED_FEATURE_SOURCE_LAYER_ID != 0) + .then(|| unsafe { crate::string::copy_string_view(hit.source_layer_id) }) + .transpose()?; + let state = (hit.fields & sys::MLN_QUERIED_FEATURE_STATE != 0) + .then(|| unsafe { crate::string::copy_string_view_bytes(hit.state) }) + .transpose()?; + Ok(QueriedFeature { + feature, + source_id, + source_layer_id, + state, + }) + }) + .collect() +} + /// Copies an owned native queried-feature list into owned Rust values. /// /// # Safety @@ -406,39 +436,16 @@ pub unsafe fn copy_queried_feature_list( // SAFETY: list is live and count points to writable storage. crate::check(unsafe { sys::mln_queried_feature_list_count(list.handle(), &mut count) })?; - let mut features = Vec::with_capacity(count); + let mut raw_features = Vec::with_capacity(count); for index in 0..count { // SAFETY: Default constructor takes no arguments and initializes size. let mut hit = unsafe { sys::mln_queried_feature_default() }; // SAFETY: list is live, index is in range, and hit is writable. crate::check(unsafe { sys::mln_queried_feature_list_get(list.handle(), index, &mut hit) })?; - // SAFETY: The borrowed view remains valid until the list guard drops. - let feature = unsafe { crate::string::copy_string_view_bytes(hit.feature) }?; - let source_id = if hit.fields & sys::MLN_QUERIED_FEATURE_SOURCE_ID != 0 { - // SAFETY: The borrowed view remains valid until the list guard drops. - Some(unsafe { crate::string::copy_string_view(hit.source_id) }?) - } else { - None - }; - let source_layer_id = if hit.fields & sys::MLN_QUERIED_FEATURE_SOURCE_LAYER_ID != 0 { - // SAFETY: The borrowed view remains valid until the list guard drops. - Some(unsafe { crate::string::copy_string_view(hit.source_layer_id) }?) - } else { - None - }; - let state = if hit.fields & sys::MLN_QUERIED_FEATURE_STATE != 0 { - // SAFETY: The borrowed view remains valid until the list guard drops. - Some(unsafe { crate::string::copy_string_view_bytes(hit.state) }?) - } else { - None - }; - features.push(QueriedFeature { - feature, - source_id, - source_layer_id, - state, - }); + raw_features.push(hit); } + // SAFETY: The list guard keeps every borrowed view alive for this call. + let features = unsafe { copy_queried_features(&raw_features) }?; Ok(features) } diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/render.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/render.rs index d6aa73e49..a41f9b9de 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/render.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/render.rs @@ -56,11 +56,12 @@ pub struct EglContextDescriptorFields { pub ownership: sys::mln_opengl_context_ownership, } -/// A browser host owns its WebGL context and hands the session its Emscripten -/// handle, so there is nothing else for a descriptor to carry. #[derive(Debug, Clone, Copy, PartialEq)] pub struct WebGlContextDescriptorFields { + pub kind: sys::mln_webgl_context_kind, pub context: i32, + pub canvas_selector_data: *const u8, + pub canvas_selector_size: usize, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -244,12 +245,14 @@ fn opengl_context_descriptor_to_native( egl: egl_context_descriptor_to_native(egl), }, }, - // A browser session renders through the host's own context, so it is - // shared only. OpenGLContextDescriptorFields::WebGl(webgl) => sys::mln_opengl_context_descriptor { size: std::mem::size_of::() as u32, platform: sys::MLN_OPENGL_CONTEXT_PLATFORM_WEBGL, - ownership: sys::MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED, + ownership: if webgl.kind == sys::MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS { + sys::MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED + } else { + sys::MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED + }, data: sys::mln_opengl_context_descriptor__bindgen_ty_1 { webgl: webgl_context_descriptor_to_native(webgl), }, @@ -262,7 +265,12 @@ fn webgl_context_descriptor_to_native( ) -> sys::mln_webgl_context_descriptor { sys::mln_webgl_context_descriptor { size: std::mem::size_of::() as u32, + kind: fields.kind, context: fields.context, + canvas_selector: sys::mln_buffer_view { + data: fields.canvas_selector_data.cast(), + size: fields.canvas_selector_size, + }, } } @@ -675,7 +683,10 @@ mod tests { opengl_owned_texture_descriptor_to_native(OpenGLOwnedTextureDescriptorFields { extent: extent(), context: OpenGLContextDescriptorFields::WebGl(WebGlContextDescriptorFields { + kind: sys::MLN_WEBGL_CONTEXT_EXISTING, context: 13, + canvas_selector_data: std::ptr::null(), + canvas_selector_size: 0, }), }); assert_eq!( diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/resource.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/resource.rs index 9b9c6e6f9..7417c5098 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/resource.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/resource.rs @@ -253,6 +253,7 @@ pub fn resource_provider_descriptor( size: std::mem::size_of::() as u32, callback, user_data, + release_user_data: None, } } @@ -271,6 +272,7 @@ pub fn resource_transform_descriptor( size: std::mem::size_of::() as u32, callback, user_data, + release_user_data: None, } } @@ -289,6 +291,7 @@ pub fn http_header_transform_descriptor( size: std::mem::size_of::() as u32, callback, user_data, + release_user_data: None, } } @@ -340,6 +343,11 @@ pub fn status_for_error(error: &Error) -> sys::mln_status { ErrorKind::InvalidState => sys::MLN_STATUS_INVALID_STATE, ErrorKind::WrongThread => sys::MLN_STATUS_WRONG_THREAD, ErrorKind::Unsupported => sys::MLN_STATUS_UNSUPPORTED, + ErrorKind::Cancelled => sys::MLN_STATUS_CANCELLED, + ErrorKind::Busy => sys::MLN_STATUS_BUSY, + ErrorKind::TargetLost => sys::MLN_STATUS_TARGET_LOST, + ErrorKind::NotReady => sys::MLN_STATUS_NOT_READY, + ErrorKind::NotFound => sys::MLN_STATUS_NOT_FOUND, ErrorKind::NativeError | ErrorKind::AbiVersionMismatch | ErrorKind::UnknownStatus => { sys::MLN_STATUS_NATIVE_ERROR } diff --git a/bindings/rust/crates/maplibre-native-ffi-core/src/style.rs b/bindings/rust/crates/maplibre-native-ffi-core/src/style.rs index fbe81c613..c4a5fc8dc 100644 --- a/bindings/rust/crates/maplibre-native-ffi-core/src/style.rs +++ b/bindings/rust/crates/maplibre-native-ffi-core/src/style.rs @@ -4,7 +4,8 @@ use std::ptr; use maplibre_native_ffi_sys as sys; use crate::enums::{ - RasterDemEncoding, SourceType, StyleImageTextFit, TileScheme, VectorTileEncoding, + RasterDemEncoding, SourceType, StyleImageTextFit, StyleLayerVisibility, TileScheme, + VectorTileEncoding, }; use crate::string::{StringView, buffer_view, string_view}; use crate::values::{ @@ -420,6 +421,60 @@ pub fn style_source_info_from_native( } } +/// Copied fixed metadata for one style layer. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct StyleLayerInfo { + /// Style-spec layer type string, such as `fill` or `background`. + pub layer_type: String, + /// Lowest zoom at which the layer draws; `f64::NEG_INFINITY` with no lower + /// bound. + pub min_zoom: f64, + /// Highest zoom at which the layer draws; `f64::INFINITY` with no upper + /// bound. + pub max_zoom: f64, + pub visibility: StyleLayerVisibility, + /// Source ID; `None` for layer types that take no source. + pub source_id: Option, + /// Source-layer ID; `None` when the layer carries none. + pub source_layer: Option, +} + +pub fn empty_style_layer_info() -> sys::mln_style_layer_info { + sys::mln_style_layer_info { + size: std::mem::size_of::() as u32, + fields: 0, + type_: buffer_view(&[]), + min_zoom: 0.0, + max_zoom: 0.0, + visibility: sys::MLN_STYLE_LAYER_VISIBILITY_VISIBLE, + source_id_size: 0, + source_layer_size: 0, + } +} + +/// Converts a filled native layer-info struct plus separately copied strings. +/// +/// # Safety +/// +/// `info.type_` must view readable bytes for this call. +pub unsafe fn style_layer_info_from_native( + info: &sys::mln_style_layer_info, + source_id: Option, + source_layer: Option, +) -> crate::Result { + // SAFETY: The caller promises the type view is readable. + let layer_type = unsafe { crate::string::copy_string_view(info.type_) }?; + Ok(StyleLayerInfo { + layer_type, + min_zoom: info.min_zoom, + max_zoom: info.max_zoom, + visibility: StyleLayerVisibility::from_raw(info.visibility), + source_id, + source_layer, + }) +} + /// Copied runtime style image pixels with style-specific metadata. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] @@ -427,6 +482,11 @@ pub struct StyleImage { pub image: PremultipliedRgba8Image, pub pixel_ratio: f32, pub sdf: bool, + pub stretch_x: Vec, + pub stretch_y: Vec, + pub content: Option, + pub text_fit_width: Option, + pub text_fit_height: Option, } impl StyleImage { @@ -435,6 +495,11 @@ impl StyleImage { image, pixel_ratio, sdf, + stretch_x: Vec::new(), + stretch_y: Vec::new(), + content: None, + text_fit_width: None, + text_fit_height: None, } } } diff --git a/bindings/rust/crates/maplibre-native-ffi-sys/build.rs b/bindings/rust/crates/maplibre-native-ffi-sys/build.rs index 3d00845ad..ca4822d61 100644 --- a/bindings/rust/crates/maplibre-native-ffi-sys/build.rs +++ b/bindings/rust/crates/maplibre-native-ffi-sys/build.rs @@ -55,7 +55,8 @@ fn main() -> Result<(), Box> { .header(header.display().to_string()) .clang_arg("-xc") .clang_arg("-std=c23") - .clang_arg(format!("-I{}", include_dir.display())); + .clang_arg(format!("-I{}", include_dir.display())) + .prepend_enum_name(false); // Cross-build frontends such as cibuildwheel expose the NDK compiler but // do not know bindgen's target-specific environment variable. Derive the @@ -101,11 +102,11 @@ fn main() -> Result<(), Box> { // Every C handle is the same uint64_t; a transparent newtype per handle // keeps a map from being passed where a runtime is expected. .new_type_alias(concat!( - "^mln_(buffer|runtime|map|map_projection|render_session|wake_source", + "^mln_(buffer|runtime|map|map_projection|render_session", + "|event_batch|render_frame_batch|acquired_frame", "|resource_request_handle|geojson_source_data|offline_region_snapshot", "|offline_region_list|style_id_list|style_string_list|queried_feature_list)$" )) - .prepend_enum_name(false) .layout_tests(true) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate()?; diff --git a/bindings/rust/crates/maplibre-native-ffi/src/camera.rs b/bindings/rust/crates/maplibre-native-ffi/src/camera.rs index d8defaad3..2df7323b7 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/camera.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/camera.rs @@ -1,8 +1,9 @@ pub use maplibre_native_ffi_core::camera::{ - AnimationOptions, BoundOptions, BoundsConstraint, CameraFitOptions, CameraOptions, - FreeCameraOptions, ProjectionMode, + AnimationOptions, BoundOptions, BoundsConstraint, CameraDelta, CameraDeltaKind, + CameraFitOptions, CameraOptions, CameraSnapshot, CameraUpdate, CameraUpdateMode, + FreeCameraOptions, GesturePhase, ProjectionMode, }; pub(crate) use maplibre_native_ffi_core::camera::{ - AnimationOptionsNativeExt, BoundOptionsNativeExt, CameraFitOptionsNativeExt, - CameraOptionsNativeExt, FreeCameraOptionsNativeExt, ProjectionModeNativeExt, + BoundOptionsNativeExt, CameraDeltaNativeExt, CameraFitOptionsNativeExt, CameraOptionsNativeExt, + CameraUpdateNativeExt, FreeCameraOptionsNativeExt, ProjectionModeNativeExt, }; diff --git a/bindings/rust/crates/maplibre-native-ffi/src/completion.rs b/bindings/rust/crates/maplibre-native-ffi/src/completion.rs new file mode 100644 index 000000000..e81471d12 --- /dev/null +++ b/bindings/rust/crates/maplibre-native-ffi/src/completion.rs @@ -0,0 +1,461 @@ +use std::any::TypeId; +use std::ffi::c_void; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Condvar, Mutex}; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +use maplibre_native_ffi_core as core; +use maplibre_native_ffi_sys as sys; + +use crate::{Error, Result}; + +/// Metadata reported when an ordered command commits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CommandCompletion { + pub disposition: crate::CommandDisposition, + pub generation: u64, + pub raw_status: i32, + pub diagnostic: String, +} + +struct State { + result: Mutex>>, + terminal: Mutex>, + retained: Mutex>>, + ready: Condvar, + waker: Mutex>, +} + +type Converter = Box Result + Send>; + +struct Bridge { + state: Arc>, + convert: Mutex>>, + accept_error_status: bool, +} + +/// A one-shot native completion exposed as a Rust future. +pub struct NativeFuture { + state: Arc>, +} + +impl std::fmt::Debug for NativeFuture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NativeFuture") + .field("ready", &self.is_ready()) + .finish() + } +} + +impl NativeFuture { + pub(crate) fn retain(&self, value: Arc) + where + U: Send + Sync + 'static, + { + let terminal = self + .state + .terminal + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if terminal.is_none() { + *self + .state + .retained + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(value); + } + } + + /// Reports whether native has delivered the one terminal result. + pub fn is_ready(&self) -> bool { + self.state + .result + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .is_some() + } + + /// Blocks for at most `timeout`, for hosts that must pump another service + /// loop while waiting rather than run an async executor. + pub fn wait(&self, timeout: Duration) -> Result { + let result = self + .state + .result + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if result.is_some() { + return Ok(true); + } + Ok(self + .state + .ready + .wait_timeout_while(result, timeout, |result| result.is_none()) + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .0 + .is_some()) + } + + /// Consumes a completed future without an async executor. + pub fn take(&self) -> Result { + let mut result = self + .state + .result + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + result.take().ok_or_else(|| { + Error::new( + crate::ErrorKind::InvalidState, + None, + "native future has not completed", + ) + })? + } + + #[cfg(test)] + pub(crate) fn is_completed(&self) -> Result { + Ok(self.is_ready()) + } + + #[cfg(test)] + pub(crate) fn terminal_status(&self) -> Result { + let terminal = self + .state + .terminal + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match terminal.as_ref() { + Some((status, _)) => Ok(*status), + None => Err(Error::new( + crate::ErrorKind::NotReady, + None, + "native future has not completed", + )), + } + } + + #[cfg(test)] + pub(crate) fn diagnostic(&self) -> Result { + let terminal = self + .state + .terminal + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match terminal.as_ref() { + Some((_, diagnostic)) => Ok(diagnostic.clone()), + None => Err(Error::new( + crate::ErrorKind::NotReady, + None, + "native future has not completed", + )), + } + } + + #[cfg(test)] + pub(crate) fn finish(&self) -> Result { + self.take() + } + + #[cfg(test)] + pub(crate) fn release(self) {} +} + +#[cfg(test)] +pub(crate) fn blocking(future: Result>) -> T { + let future = future.expect("native submission failed"); + assert!( + future + .wait(Duration::from_secs(30)) + .expect("native wait failed") + ); + future.take().expect("native completion failed") +} + +impl Future for NativeFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + let mut result = self + .state + .result + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(result) = result.take() { + return Poll::Ready(result); + } + *self + .state + .waker + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(context.waker().clone()); + Poll::Pending + } +} + +unsafe extern "C" fn complete( + user_data: *mut c_void, + result: *const sys::mln_completion_result, +) { + if user_data.is_null() || result.is_null() { + return; + } + // SAFETY: native owns Bridge from successful submission through release. + let bridge = unsafe { &*user_data.cast::>() }; + // SAFETY: completion result is borrowed for this callback. + let raw = unsafe { &*result }; + *bridge + .state + .terminal + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = + Some((raw.status, copy_diagnostic(raw))); + let converted = if raw.status == sys::MLN_STATUS_OK || bridge.accept_error_status { + let convert = bridge + .convert + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); + match convert { + Some(convert) => convert(raw), + None => Err(Error::new( + crate::ErrorKind::InvalidState, + None, + "native completion ran more than once", + )), + } + } else { + let diagnostic = if raw.diagnostic.data.is_null() || raw.diagnostic.size == 0 { + String::new() + } else { + // SAFETY: diagnostic is borrowed and valid for this callback. + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts(raw.diagnostic.data.cast::(), raw.diagnostic.size) + }) + .into_owned() + }; + Err(Error::from_status_and_diagnostic(raw.status, diagnostic)) + }; + *bridge + .state + .result + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(converted); + bridge.state.ready.notify_all(); + if let Some(waker) = bridge + .state + .waker + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take() + { + waker.wake(); + } +} + +unsafe extern "C" fn release(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: native calls release once after accepted completion delivery. + let bridge = unsafe { Box::from_raw(user_data.cast::>()) }; + bridge + .state + .retained + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); + drop(bridge); + } +} + +pub(crate) fn submit(submit: S, convert: C) -> Result> +where + T: Send + 'static, + S: FnOnce(*const sys::mln_completion) -> sys::mln_status, + C: FnOnce(&sys::mln_completion_result) -> Result + Send + 'static, +{ + let state = Arc::new(State { + result: Mutex::new(None), + terminal: Mutex::new(None), + retained: Mutex::new(None), + ready: Condvar::new(), + waker: Mutex::new(None), + }); + let bridge = Box::new(Bridge { + state: Arc::clone(&state), + convert: Mutex::new(Some(Box::new(convert))), + accept_error_status: TypeId::of::() == TypeId::of::(), + }); + let bridge = Box::into_raw(bridge); + let completion = sys::mln_completion { + size: std::mem::size_of::() as u32, + callback: Some(complete::), + user_data: bridge.cast(), + release_user_data: Some(release::), + }; + let status = submit(&completion); + if status != sys::MLN_STATUS_OK { + // SAFETY: rejected submissions invoke neither callback and retain no pointer. + drop(unsafe { Box::from_raw(bridge) }); + return core::check(status).map(|()| unreachable!()); + } + Ok(NativeFuture { state }) +} + +/// An already-complete future, for a call that finished without submitting. +pub(crate) fn ready(value: T) -> NativeFuture { + NativeFuture { + state: Arc::new(State { + result: Mutex::new(Some(Ok(value))), + terminal: Mutex::new(None), + retained: Mutex::new(None), + ready: Condvar::new(), + waker: Mutex::new(None), + }), + } +} + +pub(crate) fn unit(result: &sys::mln_completion_result) -> Result<()> { + if !result.value.is_null() || result.value_count != 0 { + return Err(Error::new( + crate::ErrorKind::NativeError, + None, + "unit completion returned a value", + )); + } + Ok(()) +} + +pub(crate) fn command(result: &sys::mln_completion_result) -> Result { + unit(result)?; + Ok(CommandCompletion { + disposition: crate::CommandDisposition::from_native(result.disposition), + generation: result.generation, + raw_status: result.status, + diagnostic: copy_diagnostic(result), + }) +} + +fn copy_diagnostic(result: &sys::mln_completion_result) -> String { + if result.diagnostic.data.is_null() || result.diagnostic.size == 0 { + return String::new(); + } + // SAFETY: the diagnostic is borrowed for this callback and copied here. + String::from_utf8_lossy(unsafe { + std::slice::from_raw_parts(result.diagnostic.data.cast::(), result.diagnostic.size) + }) + .into_owned() +} + +pub(crate) fn copy_value(result: &sys::mln_completion_result) -> Result { + if result.value.is_null() || result.value_count != 1 { + return Err(Error::new( + crate::ErrorKind::NativeError, + None, + "native completion returned no value", + )); + } + // SAFETY: the submitting API defines value as one T for this callback. + Ok(unsafe { result.value.cast::().read_unaligned() }) +} + +pub(crate) fn copy_slice(result: &sys::mln_completion_result) -> Result> { + if result.value_count == 0 { + return Ok(Vec::new()); + } + if result.value.is_null() { + return Err(Error::new( + crate::ErrorKind::NativeError, + None, + "native completion returned a null slice", + )); + } + // SAFETY: the submitting API defines value_count T elements for this callback. + Ok( + unsafe { + std::slice::from_raw_parts(result.value.cast::(), result.value_count).to_vec() + }, + ) +} + +pub(crate) fn optional_value(result: &sys::mln_completion_result) -> Result> { + if result.value_count == 0 { + return Ok(None); + } + copy_value(result).map(Some) +} + +pub(crate) fn buffer(result: &sys::mln_completion_result) -> Result> { + let view = copy_value::(result)?; + // SAFETY: the view is borrowed for this completion callback. + unsafe { core::string::copy_string_view_bytes(view) } +} + +pub(crate) fn optional_buffer(result: &sys::mln_completion_result) -> Result>> { + match optional_value::(result)? { + Some(view) => { + // SAFETY: the view is borrowed for this completion callback. + unsafe { core::string::copy_string_view_bytes(view) }.map(Some) + } + None => Ok(None), + } +} + +pub(crate) fn string(result: &sys::mln_completion_result) -> Result { + let bytes = buffer(result)?; + String::from_utf8(bytes).map_err(|error| { + Error::new( + crate::ErrorKind::NativeError, + None, + format!("native completion returned invalid UTF-8: {error}"), + ) + }) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicBool, Ordering}; + + use super::*; + + struct DropProbe(Arc); + + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.store(true, Ordering::Release); + } + } + + #[test] + fn native_bridge_retains_value_after_future_is_dropped() { + let mut callback = None; + let mut user_data = std::ptr::null_mut(); + let mut release_user_data = None; + let future = submit( + |completion| { + // SAFETY: submit borrows a complete descriptor for this call. + let completion = unsafe { &*completion }; + callback = completion.callback; + user_data = completion.user_data; + release_user_data = completion.release_user_data; + sys::MLN_STATUS_OK + }, + unit, + ) + .unwrap(); + + let dropped = Arc::new(AtomicBool::new(false)); + future.retain(Arc::new(DropProbe(Arc::clone(&dropped)))); + drop(future); + assert!(!dropped.load(Ordering::Acquire)); + + let mut result = unsafe { std::mem::zeroed::() }; + result.size = std::mem::size_of::() as u32; + result.status = sys::MLN_STATUS_OK; + // SAFETY: the captured descriptor owns user_data through release. + unsafe { callback.unwrap()(user_data, &result) }; + assert!(!dropped.load(Ordering::Acquire)); + // SAFETY: release belongs to the same accepted descriptor. + unsafe { release_user_data.unwrap()(user_data) }; + assert!(dropped.load(Ordering::Acquire)); + } +} diff --git a/bindings/rust/crates/maplibre-native-ffi/src/custom_geometry.rs b/bindings/rust/crates/maplibre-native-ffi/src/custom_geometry.rs index 0fe210b63..2ddc6eed1 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/custom_geometry.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/custom_geometry.rs @@ -41,10 +41,9 @@ impl CanonicalTileId { type TileCallback = dyn Fn(CanonicalTileId) + Send + Sync + 'static; /// Options used when adding a custom geometry source. -/// /// Custom geometry callbacks may run on native worker threads. Keep callbacks -/// quick, and hand work back to the map owner thread before calling map APIs -/// such as `set_custom_geometry_source_tile_data` or invalidation helpers. +/// quick; map commands may be submitted directly from any thread outside the +/// callback. #[non_exhaustive] pub struct CustomGeometrySourceOptions { fetch_tile: Box, diff --git a/bindings/rust/crates/maplibre-native-ffi/src/custom_mvt_vector.rs b/bindings/rust/crates/maplibre-native-ffi/src/custom_mvt_vector.rs index f833be587..c063bad14 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/custom_mvt_vector.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/custom_mvt_vector.rs @@ -14,9 +14,9 @@ type TileCallback = dyn Fn(CanonicalTileId) + Send + Sync + 'static; /// Options used when adding a custom MVT vector source. /// /// Custom MVT vector callbacks may run on native worker threads. Keep callbacks -/// quick, and hand work back to the map owner thread before calling map APIs -/// such as `set_custom_mvt_vector_source_tile_data` or -/// `set_custom_mvt_vector_source_tile_error`. +/// quick; map commands such as `set_custom_mvt_vector_source_tile_data` or +/// `set_custom_mvt_vector_source_tile_error` may be submitted directly from any +/// thread outside the callback. #[non_exhaustive] pub struct CustomMvtVectorSourceOptions { fetch_tile: Box, diff --git a/bindings/rust/crates/maplibre-native-ffi/src/events.rs b/bindings/rust/crates/maplibre-native-ffi/src/events.rs index 47b644a93..f48676cf8 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/events.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/events.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::marker::PhantomData; use std::str; use maplibre_native_ffi_core as maplibre_core; @@ -7,7 +6,7 @@ use maplibre_native_ffi_sys as sys; use crate::{Error, Result}; pub use maplibre_core::events::{ - CameraTransitionFinishedEvent, OfflineOperationCompletedEvent, OfflineRegionResponseErrorEvent, + CameraTransitionFinishedEvent, CommandDisposition, OfflineRegionResponseErrorEvent, OfflineRegionStatus, OfflineRegionStatusEvent, OfflineRegionTileCountLimitEvent, RenderFrameEvent, RenderMapEvent, RenderingStats, RuntimeEventPayload, TileActionEvent, TileId, UnknownRuntimeEventPayload, @@ -69,38 +68,47 @@ pub struct RuntimeEvent { pub event_type: RuntimeEventType, pub source: RuntimeEventSource, /// Secondary event detail whose meaning `event_type` selects. Camera - /// change events decode as - /// `CameraChangeMode::from_raw(code as u32)`, offline operation-completion - /// events carry the operation's native status, and map loading-failure - /// events carry a load error ordinal whose text is in `message`. + /// change events decode as `CameraChangeMode::from_raw(code as u32)`, and + /// map loading-failure events carry a load error ordinal whose text is in + /// `message`. pub code: i32, pub message: Option, pub payload: RuntimeEventPayload, } -/// Batch of runtime events borrowed from runtime-owned storage. +/// Owned batch of runtime events. /// -/// A batch borrows the [`RuntimeHandle`](crate::RuntimeHandle) it came from, so -/// the next drain is a compile error while the batch lives, and an event read -/// out of a batch borrows the batch. Take [`RuntimeEventRef::to_owned`] for a -/// value that outlives either. -pub struct RuntimeEventBatch<'a> { - raw: sys::mln_runtime_event_batch, - _storage: PhantomData<&'a [u8]>, +/// Native storage remains stable until this batch is dropped, including across +/// later drains from the same runtime. +pub struct RuntimeEventBatch { + handle: sys::mln_event_batch, + raw: sys::mln_runtime_event_batch_view, } -impl<'a> RuntimeEventBatch<'a> { - /// Reports one drained batch's runtime-owned storage. +impl RuntimeEventBatch { + /// Acquires the immutable view for an owned native batch. /// /// # Safety /// - /// `raw` must be a batch that `mln_runtime_drain_events` filled, whose - /// event and message storage stays readable for `'a`. - pub(crate) unsafe fn new(raw: sys::mln_runtime_event_batch) -> Self { - Self { - raw, - _storage: PhantomData, + /// `handle` must be an owned batch returned by `mln_runtime_drain_events`. + pub(crate) unsafe fn new(handle: sys::mln_event_batch) -> Result { + let mut raw = sys::mln_runtime_event_batch_view { + size: std::mem::size_of::() as u32, + event_size: 0, + events: std::ptr::null(), + event_count: 0, + messages: std::ptr::null(), + messages_size: 0, + }; + // SAFETY: `handle` is live and `raw` is writable for this ABI version. + if let Err(error) = + maplibre_core::check(unsafe { sys::mln_event_batch_get(handle, &mut raw) }) + { + // SAFETY: This function owns the handle on every path. + unsafe { sys::mln_event_batch_release(handle) }; + return Err(error); } + Ok(Self { handle, raw }) } /// Returns how many events this batch reports. @@ -113,29 +121,11 @@ impl<'a> RuntimeEventBatch<'a> { self.len() == 0 } - /// Returns how many events stayed queued after this batch. A nonzero count - /// means another drain reports more events. - pub fn remaining(&self) -> usize { - self.raw.remaining_count - } - /// Walks this batch's events in queue order. - /// - /// Each event borrows this batch, so safe code cannot read one after the - /// batch is gone: - /// - /// ```compile_fail,E0505 - /// # use maplibre_native_ffi::{RuntimeHandle, RuntimeOptions}; - /// let mut runtime = RuntimeHandle::with_options(&RuntimeOptions::default()).unwrap(); - /// let batch = runtime.drain_events(0).unwrap(); - /// let events = batch.iter().collect::>(); - /// drop(batch); - /// let _ = events.first().map(|event| event.message_bytes()); - /// ``` pub fn iter(&self) -> impl Iterator> { (0..self.len()).map(move |index| { - // SAFETY: This batch's storage stays readable while it is borrowed, - // and index names one of its events. + // SAFETY: The owned batch keeps its immutable storage live, and + // index names one of its events. RuntimeEventRef { view: unsafe { maplibre_core::events::event_view(&self.raw, index) }, } @@ -143,16 +133,22 @@ impl<'a> RuntimeEventBatch<'a> { } } -impl fmt::Debug for RuntimeEventBatch<'_> { +impl Drop for RuntimeEventBatch { + fn drop(&mut self) { + // SAFETY: This wrapper owns exactly one native batch handle. + unsafe { sys::mln_event_batch_release(self.handle) }; + } +} + +impl fmt::Debug for RuntimeEventBatch { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RuntimeEventBatch") .field("len", &self.len()) - .field("remaining", &self.remaining()) .finish() } } -/// One event of a [`RuntimeEventBatch`], read from runtime-owned storage. +/// One event of a [`RuntimeEventBatch`], read from batch-owned storage. #[derive(Clone, Copy)] pub struct RuntimeEventRef<'a> { view: maplibre_core::NativeEventView<'a>, @@ -265,7 +261,7 @@ impl SynthesizedBatch { } pub(crate) fn push(&mut self, mut event: sys::mln_runtime_event, message: &[u8]) { - event.message_offset = u32::try_from(self.messages.len()).unwrap(); + event.message_offset = u64::try_from(self.messages.len()).unwrap(); event.message_size = u32::try_from(message.len()).unwrap(); self.messages.extend_from_slice(message); self.messages.push(0); @@ -283,22 +279,26 @@ impl SynthesizedBatch { self.count += 1; } - pub(crate) fn raw(&self) -> sys::mln_runtime_event_batch { - sys::mln_runtime_event_batch { - size: std::mem::size_of::() as u32, + pub(crate) fn raw(&self) -> sys::mln_runtime_event_batch_view { + sys::mln_runtime_event_batch_view { + size: std::mem::size_of::() as u32, event_size: u32::try_from(self.stride).unwrap(), events: self.records.as_ptr().cast(), event_count: self.count, messages: self.messages.as_ptr().cast(), messages_size: self.messages.len(), - remaining_count: 0, } } - pub(crate) fn batch(&self) -> RuntimeEventBatch<'_> { - // SAFETY: This fixture's records and arena are laid out the way a drain - // fills them, and they outlive the borrow the batch takes. - unsafe { RuntimeEventBatch::new(self.raw()) } + pub(crate) fn iter(&self) -> impl Iterator> { + let raw = self.raw(); + (0..self.count).map(move |index| { + // SAFETY: This fixture owns the event records and message arena for + // the iterator's lifetime, and index names one of its records. + RuntimeEventRef { + view: unsafe { maplibre_core::events::event_view(&raw, index) }, + } + }) } } @@ -312,8 +312,7 @@ mod tests { // Spec coverage: BND-086. fn a_batch_applies_the_rust_source_policy_to_every_source_kind() { let mut batch = SynthesizedBatch::new(); - let mut runtime_event = - SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED); + let mut runtime_event = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_IDLE); runtime_event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME; batch.push(runtime_event, b""); let mut map_event = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED); @@ -327,8 +326,6 @@ mod tests { unknown_source.source_type = 999_003; unknown_source.source = 0x0300_0000_0000_0063; batch.push(unknown_source, b""); - let batch = batch.batch(); - let sources = batch.iter().map(|event| event.source()).collect::>(); assert_eq!( @@ -371,9 +368,7 @@ mod tests { let mut records = SynthesizedBatch::new(); records.push(status_event, b""); records.push(error_event, b"offline failed"); - let batch = records.batch(); - - let events = batch.iter().collect::>(); + let events = records.iter().collect::>(); assert_eq!( events[0].event_type(), @@ -414,8 +409,7 @@ mod tests { transition_id: 0x0102_0304_0506_0708, }; records.push(event, b"future payload"); - let batch = records.batch(); - let borrowed = batch.iter().next().unwrap(); + let borrowed = records.iter().next().unwrap(); let owned = borrowed.to_owned().unwrap(); assert_eq!(borrowed.message_bytes(), b"future payload"); @@ -448,8 +442,7 @@ mod tests { SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED), b"loaded", ); - let batch = records.batch(); - let events = batch.iter().collect::>(); + let events = records.iter().collect::>(); let error = events[0].message().unwrap_err(); assert_eq!(error.kind(), crate::ErrorKind::InvalidArgument); diff --git a/bindings/rust/crates/maplibre-native-ffi/src/geojson.rs b/bindings/rust/crates/maplibre-native-ffi/src/geojson.rs index 92b72a79d..631a5e6e3 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/geojson.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/geojson.rs @@ -13,8 +13,8 @@ use crate::{GeoJsonSourceOptions, Result}; /// [`Self::new`] parses one complete UTF-8 GeoJSON document and tiles or /// clusters it into the index a GeoJSON source consumes, which is the /// expensive part of a data update. It needs no runtime or map and runs on -/// any thread, so a host prepares data on a worker thread and installs it on -/// the map owner thread through +/// any thread, so a host can prepare data concurrently with map work and +/// install it through /// [`crate::MapHandle::add_geojson_source_data`] or /// [`crate::MapHandle::set_geojson_source_data`]. /// diff --git a/bindings/rust/crates/maplibre-native-ffi/src/handle.rs b/bindings/rust/crates/maplibre-native-ffi/src/handle.rs index 0af9ee997..4d17cac13 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/handle.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/handle.rs @@ -1,139 +1,116 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::Mutex; - -use maplibre_native_ffi_core::{ - self as maplibre_core, - handle::{NativeHandle, NativeHandleState}, -}; -use maplibre_native_ffi_sys as sys; +use maplibre_native_ffi_core::{self as maplibre_core, handle::NativeHandle}; use crate::{Error, Result}; #[derive(Debug)] -pub(crate) struct ThreadAffineNativeHandle { - state: NativeHandleState, - destroy: unsafe extern "C" fn(T) -> sys::mln_status, - _thread_affine: PhantomData>, +enum ConcurrentHandleState { + Live(T), + Closing, + Closed, } +static CLOSE_FINISHED: std::sync::Condvar = std::sync::Condvar::new(); + #[derive(Debug)] pub(crate) struct ConcurrentNativeHandle { - state: Mutex>, - destroy: unsafe extern "C" fn(T) -> sys::mln_status, + state: std::sync::Mutex>, + type_name: &'static str, } impl ConcurrentNativeHandle { - /// Takes ownership of a native handle that the C API accepts from any thread. + /// Takes ownership of a native handle whose registry and control state are + /// safe to inspect from any thread. /// /// # Safety /// - /// `handle` must be a live handle of the matching native type owned by the - /// caller. `destroy` must release exactly that handle type. - pub(crate) unsafe fn from_handle( - handle: T, - destroy: unsafe extern "C" fn(T) -> sys::mln_status, - type_name: &'static str, - ) -> Result { + /// `handle` must be a live owned handle of the matching native type. + pub(crate) unsafe fn from_handle(handle: T, type_name: &'static str) -> Result { + if handle.to_raw() == 0 { + return Err(Error::invalid_argument(format!( + "{type_name} handle must not be zero" + ))); + } Ok(Self { - // SAFETY: The caller promises handle is an owned live handle of the - // matching native type. - state: Mutex::new(unsafe { NativeHandleState::from_handle(handle, type_name) }?), - destroy, + state: std::sync::Mutex::new(ConcurrentHandleState::Live(handle)), + type_name, }) } pub(crate) fn live_handle(&self) -> Option { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .live_handle() - } - - pub(crate) fn is_closed(&self) -> bool { - self.state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .is_closed() - } - - pub(crate) fn close(&self) -> Result<()> { - let state = self + match *self .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - // SAFETY: from_handle binds this state to its matching destroy function. - unsafe { state.close_status(self.destroy) } - } -} - -impl Drop for ConcurrentNativeHandle { - fn drop(&mut self) { - let state = self - .state - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let id = state.id().unwrap_or_default(); - // SAFETY: from_handle binds this state to its matching destroy function. - if unsafe { state.close_status(self.destroy) }.is_err() { - maplibre_core::handle::report_leak(maplibre_core::handle::NativeHandleLeak { - type_name: state.type_name(), - id, - }); + .unwrap_or_else(|poisoned| poisoned.into_inner()) + { + ConcurrentHandleState::Live(handle) => Some(handle), + ConcurrentHandleState::Closing | ConcurrentHandleState::Closed => None, } } -} - -impl ThreadAffineNativeHandle { - /// Takes ownership of a native thread-affine handle. - /// - /// # Safety - /// - /// `handle` must be a live handle of the matching native type owned by the - /// caller. `destroy` must be the C API function that releases exactly that - /// handle type and returns a status without taking ownership on failure. - pub(crate) unsafe fn from_handle( - handle: T, - destroy: unsafe extern "C" fn(T) -> sys::mln_status, - type_name: &'static str, - ) -> Result { - Ok(Self { - // SAFETY: The caller promises handle is an owned live handle of the - // matching native type. - state: unsafe { NativeHandleState::from_handle(handle, type_name) }?, - destroy, - _thread_affine: PhantomData, - }) - } - - pub(crate) fn live_handle(&self) -> Option { - self.state.live_handle() - } pub(crate) fn is_closed(&self) -> bool { - self.state.is_closed() + matches!( + *self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()), + ConcurrentHandleState::Closed + ) } - pub(crate) fn close(&self) -> Result<()> { - // SAFETY: from_handle binds this Rust handle to the matching C API - // destroy function for its owned native handle. - unsafe { self.state.close_status(self.destroy) } + pub(crate) fn close_with(&self, close: impl FnOnce(T) -> Result) -> Result> { + let mut close = Some(close); + loop { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match *state { + ConcurrentHandleState::Live(handle) => { + *state = ConcurrentHandleState::Closing; + drop(state); + let result = close.take().expect("close callback runs once")(handle); + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + *state = if result.is_ok() { + ConcurrentHandleState::Closed + } else { + ConcurrentHandleState::Live(handle) + }; + CLOSE_FINISHED.notify_all(); + return result.map(Some); + } + ConcurrentHandleState::Closing => { + state = CLOSE_FINISHED + .wait(state) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + drop(state); + } + ConcurrentHandleState::Closed => return Ok(None), + } + } } -} -impl Drop for ThreadAffineNativeHandle { - fn drop(&mut self) { - // Drop cannot return an error and must not panic, so a destroy that - // reports failure leaves the handle live and goes to the leak channel. - let id = self.state.id().unwrap_or_default(); - // SAFETY: from_handle binds this Rust handle to the matching C API - // destroy function for its owned native handle. - if unsafe { self.state.close_status(self.destroy) }.is_err() { - maplibre_core::handle::report_leak(maplibre_core::handle::NativeHandleLeak { - type_name: self.state.type_name(), - id, - }); - } + pub(crate) fn leak_for_report(&self) { + let handle = { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match *state { + ConcurrentHandleState::Live(handle) => { + *state = ConcurrentHandleState::Closed; + Some(handle) + } + ConcurrentHandleState::Closing | ConcurrentHandleState::Closed => None, + } + }; + let Some(handle) = handle else { return }; + maplibre_core::handle::report_leak(maplibre_core::handle::NativeHandleLeak { + type_name: self.type_name, + id: handle.to_raw(), + }); } } @@ -150,64 +127,54 @@ pub(crate) fn out_handle( #[cfg(test)] mod tests { - use std::sync::Mutex; - use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier, mpsc}; + use std::time::Duration; - use super::*; - - static DESTROY_COUNT: AtomicUsize = AtomicUsize::new(0); - static DESTROY_STATUS: AtomicI32 = AtomicI32::new(sys::MLN_STATUS_OK); - static DESTROY_TEST_LOCK: Mutex<()> = Mutex::new(()); - - /// A synthetic map handle for close-once tests. It reaches only - /// `count_destroy` below, never the C API. The kind byte matches a map so - /// a value escaping into a diagnostic reads as one. - const TEST_HANDLE: sys::mln_map = sys::mln_map(0x0200_0000_0000_002a); - - unsafe extern "C" fn count_destroy(handle: sys::mln_map) -> sys::mln_status { - if handle.0 != 0 { - DESTROY_COUNT.fetch_add(1, Ordering::SeqCst); - } - DESTROY_STATUS.load(Ordering::SeqCst) - } + use maplibre_native_ffi_sys as sys; - fn test_handle() -> ThreadAffineNativeHandle { - DESTROY_COUNT.store(0, Ordering::SeqCst); - DESTROY_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst); - - // SAFETY: count_destroy only records calls and never reaches native. - unsafe { ThreadAffineNativeHandle::from_handle(TEST_HANDLE, count_destroy, "test_handle") } - .unwrap() - } - - #[test] - // Spec coverage: BND-040. - fn close_is_internally_idempotent_after_success() { - let _guard = DESTROY_TEST_LOCK.lock().unwrap(); - let handle = test_handle(); - - handle.close().unwrap(); - handle.close().unwrap(); - - assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), 1); - assert!(handle.live_handle().is_none()); - } + use super::*; #[test] - // Spec coverage: BND-041. - fn failed_close_leaves_handle_live_for_later_close() { - let _guard = DESTROY_TEST_LOCK.lock().unwrap(); - let handle = test_handle(); - DESTROY_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst); - - let error = handle.close().unwrap_err(); - assert_eq!(error.kind(), crate::ErrorKind::InvalidState); - assert_eq!(handle.live_handle().unwrap().0, TEST_HANDLE.0); - - DESTROY_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst); - handle.close().unwrap(); - - assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), 2); + fn concurrent_close_calls_native_once_and_waits_for_the_winner() { + let handle = Arc::new(unsafe { + ConcurrentNativeHandle::from_handle(sys::mln_render_session(1), "test session").unwrap() + }); + let entered = Arc::new(Barrier::new(2)); + let finish = Arc::new(Barrier::new(2)); + let calls = Arc::new(AtomicUsize::new(0)); + + let first_handle = Arc::clone(&handle); + let first_entered = Arc::clone(&entered); + let first_finish = Arc::clone(&finish); + let first_calls = Arc::clone(&calls); + let first = std::thread::spawn(move || { + first_handle + .close_with(|_| { + first_calls.fetch_add(1, Ordering::Relaxed); + first_entered.wait(); + first_finish.wait(); + Ok(()) + }) + .unwrap() + }); + + entered.wait(); assert!(handle.live_handle().is_none()); + let second_handle = Arc::clone(&handle); + let (sender, receiver) = mpsc::channel(); + let second = std::thread::spawn(move || { + sender + .send(second_handle.close_with(|_| Ok(())).unwrap()) + .unwrap(); + }); + assert!(receiver.recv_timeout(Duration::from_millis(20)).is_err()); + finish.wait(); + + assert_eq!(first.join().unwrap(), Some(())); + assert_eq!(receiver.recv_timeout(Duration::from_secs(1)).unwrap(), None); + second.join().unwrap(); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert!(handle.is_closed()); } } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/lib.rs b/bindings/rust/crates/maplibre-native-ffi/src/lib.rs index 3f7bb1abd..029fafbc7 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/lib.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/lib.rs @@ -1,13 +1,14 @@ //! Safe Rust binding for the MapLibre Native C API. //! -//! This crate owns Rust-specific ergonomics and safety policy: thread-affine -//! public handles, parent retention, owner-thread `Drop`, Rust errors, -//! callback closure APIs, and lifetime-scoped render resources. Shared C ABI +//! Runtime and map control handles are any-thread; graphics sessions retain +//! their backend thread affinity. This crate also owns parent retention, Rust +//! errors, callback closures, and render-resource lifetimes. Shared C ABI //! adaptation lives in `maplibre-native-ffi-core`. #![deny(unsafe_op_in_unsafe_fn)] mod camera; +mod completion; mod custom_geometry; mod custom_mvt_vector; mod events; @@ -27,61 +28,60 @@ use maplibre_native_ffi_core as maplibre_core; use maplibre_native_ffi_sys as sys; pub use camera::{ - AnimationOptions, BoundOptions, BoundsConstraint, CameraFitOptions, CameraOptions, - FreeCameraOptions, ProjectionMode, + AnimationOptions, BoundOptions, BoundsConstraint, CameraDelta, CameraDeltaKind, + CameraFitOptions, CameraOptions, CameraSnapshot, CameraUpdate, CameraUpdateMode, + FreeCameraOptions, GesturePhase, ProjectionMode, }; +pub use completion::{CommandCompletion, NativeFuture}; pub use custom_geometry::{CanonicalTileId, CustomGeometrySourceOptions}; pub use custom_mvt_vector::CustomMvtVectorSourceOptions; pub use events::{ - CameraTransitionFinishedEvent, MapId, OfflineOperationCompletedEvent, - OfflineRegionResponseErrorEvent, OfflineRegionStatus, OfflineRegionStatusEvent, - OfflineRegionTileCountLimitEvent, RenderFrameEvent, RenderMapEvent, RenderingStats, - RuntimeEvent, RuntimeEventBatch, RuntimeEventPayload, RuntimeEventRef, RuntimeEventSource, - TileActionEvent, TileId, UnknownRuntimeEventPayload, + CameraTransitionFinishedEvent, CommandDisposition, MapId, OfflineRegionResponseErrorEvent, + OfflineRegionStatus, OfflineRegionStatusEvent, OfflineRegionTileCountLimitEvent, + RenderFrameEvent, RenderMapEvent, RenderingStats, RuntimeEvent, RuntimeEventBatch, + RuntimeEventPayload, RuntimeEventRef, RuntimeEventSource, TileActionEvent, TileId, + UnknownRuntimeEventPayload, }; pub use geojson::GeoJsonSourceDataHandle; pub use logging::{LogRecord, clear_log_callback, set_async_log_severity_mask, set_log_callback}; pub use map::{ - GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, MapAttachRef, - MapHandle, RasterDemEncoding, SourceInfo, SourceType, StyleImage, StyleImageInfo, - StyleImageOptions, StyleImageTextFit, StyleLayerVisibility, StyleTransitionOptions, - TileJsonInfo, TileScheme, TileSourceOptions, VectorTileEncoding, + GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, LogicalExtent, + MapHandle, MapSnapshot, SourceInfo, SourceType, StyleImage, StyleImageInfo, StyleImageOptions, + StyleImageStretches, StyleImageTextFit, StyleLayerInfo, StyleLayerVisibility, + StyleTransitionOptions, TileJsonInfo, TileScheme, TileSourceOptions, VectorTileEncoding, }; pub use maplibre_core::{ AmbientCacheOperation, CameraChangeMode, ConstrainMode, Error, ErrorKind, LogEvent, LogSeverity, LogSeverityMask, MapDebugOptions, MapMode, MapOptions, MapTileOptions, - MapViewportOptions, NetworkStatus, NorthOrientation, OfflineOperationKind, - OfflineOperationResultKind, OfflineRegionDownloadState, OpenGLClientApi, - OpenGLContextOwnership, OpenGLContextProviderMask, RenderBackendMask, RenderMode, RenderResult, - ResourceErrorReason, ResourceKind, ResourceLoadingMethod, ResourcePriority, - ResourceResponseStatus, ResourceStoragePolicy, ResourceUsage, Result, RuntimeEventMask, - RuntimeEventType, TileLodMode, TileOperation, ViewportMode, + MapViewportOptions, NetworkStatus, NorthOrientation, OfflineRegionDownloadState, + OpenGLClientApi, OpenGLContextOwnership, OpenGLContextProviderMask, RasterDemEncoding, + RenderBackendMask, RenderMode, ResourceErrorReason, ResourceKind, ResourceLoadingMethod, + ResourcePriority, ResourceResponseStatus, ResourceStoragePolicy, ResourceUsage, Result, + RuntimeEventMask, RuntimeEventType, TileLodMode, TileOperation, ViewportMode, }; pub use maplibre_native_ffi_core::handle::{NativeHandleLeak, set_leak_reporter}; pub use projection::MapProjectionHandle; pub use render::{ - DetachedRenderSessionHandle, EglContextDescriptor, FeatureStateSelector, FrameNativePointer, - FrameOpenGLTextureName, MetalBorrowedTextureDescriptor, MetalContextDescriptor, - MetalOwnedTextureDescriptor, MetalOwnedTextureFrame, MetalOwnedTextureFrameHandle, - MetalSurfaceDescriptor, NativePointer, OpenGLBorrowedTextureDescriptor, + AcquiredFrameHandle, EglContextDescriptor, FeatureStateSelector, FrameDemand, FrameDisposition, + FrameGpuSync, FrameNativePointer, FrameOpenGLTextureName, GpuSync, GpuSyncKind, + MetalBorrowedTextureDescriptor, MetalContextDescriptor, MetalOwnedTextureDescriptor, + MetalOwnedTextureFrame, MetalSurfaceDescriptor, NativePointer, OpenGLBorrowedTextureDescriptor, OpenGLContextDescriptor, OpenGLOwnedTextureDescriptor, OpenGLOwnedTextureFrame, - OpenGLOwnedTextureFrameHandle, OpenGLSurfaceDescriptor, PremultipliedRgba8Image, - QueriedFeature, RenderSessionHandle, RenderTargetExtent, RenderUpdate, - RenderedFeatureQueryOptions, RenderedQueryGeometry, SourceFeatureQueryOptions, - TextureImageInfo, VulkanBorrowedTextureDescriptor, VulkanContextDescriptor, - VulkanOwnedTextureDescriptor, VulkanOwnedTextureFrame, VulkanOwnedTextureFrameHandle, - VulkanSurfaceDescriptor, WebGlContextDescriptor, WebGpuBorrowedTextureDescriptor, - WebGpuContextDescriptor, WebGpuOwnedTextureDescriptor, WebGpuOwnedTextureFrame, - WebGpuOwnedTextureFrameHandle, WebGpuSurfaceDescriptor, WglContextDescriptor, + OpenGLSurfaceDescriptor, PremultipliedRgba8Image, QueriedFeature, RenderAbandonResult, + RenderDriverKind, RenderFrameBatch, RenderFrameResult, RenderSessionAttachOptions, + RenderSessionAttachment, RenderSessionCapabilities, RenderSessionHandle, + RenderSessionLifecycle, RenderSessionSnapshot, RenderTargetExtent, RenderedFeatureQueryOptions, + RenderedQueryGeometry, SourceFeatureQueryOptions, TextureImageInfo, + VulkanBorrowedTextureDescriptor, VulkanContextDescriptor, VulkanOwnedTextureDescriptor, + VulkanOwnedTextureFrame, VulkanSurfaceDescriptor, WebGlContextDescriptor, + WebGpuBorrowedTextureDescriptor, WebGpuContextDescriptor, WebGpuOwnedTextureDescriptor, + WebGpuOwnedTextureFrame, WebGpuSurfaceDescriptor, WglContextDescriptor, }; pub use resource::{ ByteRange, HttpHeader, HttpHeaderTransformRequest, ResourceProviderDecision, ResourceRequest, ResourceRequestHandle, ResourceResponse, ResourceTransformRequest, }; -pub use runtime::{ - OfflineOperationHandle, OfflineRegionDefinition, OfflineRegionInfo, RuntimeHandle, - RuntimeOptions, WakeSource, -}; +pub use runtime::{OfflineRegionDefinition, OfflineRegionInfo, RuntimeHandle, RuntimeOptions}; pub use values::{ EdgeInsets, LatLng, LatLngBounds, ProjectedMeters, Quaternion, ScreenBox, ScreenPoint, UnitBezier, Vec3, @@ -144,72 +144,6 @@ impl std::fmt::Display for HandleOperationError { impl std::error::Error for HandleOperationError {} -/// Error returned by offline operation result transfers. -#[derive(Debug)] -pub enum OfflineOperationTakeError { - /// The native transfer failed before consuming the operation result. - Retryable(HandleOperationError), - /// The native result was consumed, but copying it into Rust-owned data failed. - Consumed(Error), -} - -impl OfflineOperationTakeError { - pub(crate) fn retryable(error: Error, handle: T) -> Self { - Self::Retryable(HandleOperationError::new(error, handle)) - } - - pub(crate) fn consumed(error: Error) -> Self { - Self::Consumed(error) - } - - /// Returns the operation error. - pub fn error(&self) -> &Error { - match self { - Self::Retryable(error) => error.error(), - Self::Consumed(error) => error, - } - } - - /// Returns the stable category for the operation error. - pub fn kind(&self) -> ErrorKind { - self.error().kind() - } - - /// Returns the raw C status for native operation errors, when available. - pub fn raw_status(&self) -> Option { - self.error().raw_status() - } - - /// Returns the copied diagnostic message for the operation error. - pub fn diagnostic(&self) -> &str { - self.error().diagnostic() - } - - /// Returns the retryable error and still-live handle, if the operation was not consumed. - pub fn into_retryable(self) -> Option> { - match self { - Self::Retryable(error) => Some(error), - Self::Consumed(_) => None, - } - } - - /// Returns the operation error, dropping any retryable handle. - pub fn into_error(self) -> Error { - match self { - Self::Retryable(error) => error.into_error(), - Self::Consumed(error) => error, - } - } -} - -impl std::fmt::Display for OfflineOperationTakeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.error().fmt(f) - } -} - -impl std::error::Error for OfflineOperationTakeError {} - /// Returns the native C ABI contract version. pub fn c_version() -> u32 { // SAFETY: mln_c_version takes no arguments and returns the process-global C @@ -282,16 +216,16 @@ mod tests { use super::*; - assert_not_impl_any!(RuntimeHandle: Send, Sync); - assert_not_impl_any!(MapHandle: Send, Sync); + assert_impl_all!(RuntimeHandle: Send, Sync); + assert_impl_all!(MapHandle: Send, Sync); assert_impl_all!(MapProjectionHandle: Send, Sync); + assert_impl_all!(RenderSessionHandle: Send, Sync); + assert_impl_all!(AcquiredFrameHandle: Send, Sync); assert_not_impl_any!(NativePointer: Send, Sync); assert_not_impl_any!(FrameNativePointer<'static>: Send, Sync); - assert_not_impl_any!(RenderSessionHandle: Send, Sync); - assert_not_impl_any!(DetachedRenderSessionHandle: Send, Sync); - // The one map value that crosses threads: a copied handle id that lets a - // render thread name a map owned elsewhere. - assert_impl_all!(MapAttachRef: Send, Sync); + assert_not_impl_any!(FrameOpenGLTextureName<'static>: Send, Sync); + assert_not_impl_any!(GpuSync: Send, Sync); + assert_not_impl_any!(FrameGpuSync<'static>: Send, Sync); #[test] // Spec coverage: BND-103. diff --git a/bindings/rust/crates/maplibre-native-ffi/src/logging.rs b/bindings/rust/crates/maplibre-native-ffi/src/logging.rs index 978a7c594..7dcc187f0 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/logging.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/logging.rs @@ -1,6 +1,5 @@ use std::ffi::{c_char, c_void}; use std::panic::{self, AssertUnwindSafe}; -use std::sync::{Arc, Mutex, MutexGuard}; use crate::{Result, sys}; use maplibre_core::LogSeverityMask; @@ -14,14 +13,6 @@ struct CallbackState { callback: Box, } -static LOG_CALLBACK_STATE: Mutex>> = Mutex::new(None); - -fn lock_log_callback_state() -> MutexGuard<'static, Option>> { - LOG_CALLBACK_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) -} - /// Installs or replaces the process-global MapLibre Native log callback. /// /// MapLibre Native may invoke the callback from logging or worker threads. The @@ -32,36 +23,31 @@ pub fn set_log_callback(callback: F) -> Result<()> where F: Fn(LogRecord) -> bool + Send + Sync + 'static, { - let replacement = Arc::new(CallbackState { + let replacement = Box::new(CallbackState { callback: Box::new(callback), }); - let user_data = Arc::as_ptr(&replacement).cast_mut().cast::(); - let previous = { - let mut current = lock_log_callback_state(); - // SAFETY: log_callback_trampoline has the C callback ABI. user_data - // points at replacement, which is retained in current until the native - // callback is replaced or cleared. - maplibre_core::check(unsafe { - sys::mln_log_set_callback(Some(log_callback_trampoline), user_data) - })?; - - current.replace(replacement) - }; - drop(previous); - Ok(()) + let user_data = Box::into_raw(replacement).cast::(); + // SAFETY: native owns user_data after success and releases it through + // release_log_callback_state after the final callback returns. + let result = maplibre_core::check(unsafe { + sys::mln_log_set_callback( + Some(log_callback_trampoline), + user_data, + Some(release_log_callback_state), + ) + }); + if result.is_err() { + // SAFETY: a failed registration does not take ownership. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } + result } /// Clears the process-global MapLibre Native log callback. pub fn clear_log_callback() -> Result<()> { // SAFETY: mln_log_clear_callback takes no arguments and clears native's // process-global callback slot. - let previous = { - let mut current = lock_log_callback_state(); - maplibre_core::check(unsafe { sys::mln_log_clear_callback() })?; - current.take() - }; - drop(previous); - Ok(()) + maplibre_core::check(unsafe { sys::mln_log_clear_callback() }) } /// Configures severities that MapLibre Native may dispatch asynchronously. @@ -82,13 +68,18 @@ unsafe extern "C" fn log_callback_trampoline( return 0; } - // SAFETY: set_log_callback installs Arc::as_ptr(&CallbackState) as - // user_data and retains the current Arc until native replacement or clear - // succeeds, so the pointer remains valid for native dispatch. + // SAFETY: native owns the boxed CallbackState until replacement or clear. let state = unsafe { &*user_data.cast::() }; invoke_callback(state, severity, event, code, message) } +unsafe extern "C" fn release_log_callback_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: native calls this once for the Box transferred on install. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + fn invoke_callback( state: &CallbackState, raw_severity: u32, @@ -167,27 +158,24 @@ mod tests { let calls = Arc::new(AtomicUsize::new(0)); let test_calls = calls.clone(); - set_log_callback(move |record| { - test_calls.fetch_add(1, Ordering::SeqCst); - if record.code == 42 { - assert_eq!(record.severity, LogSeverity::Warning); - assert_eq!(record.severity.raw_value(), sys::MLN_LOG_SEVERITY_WARNING); - assert_eq!(record.event, LogEvent::Render); - assert_eq!(record.event.raw_value(), sys::MLN_LOG_EVENT_RENDER); - assert_eq!(record.message, "hello"); - return true; - } - false - }) - .unwrap(); + let state = CallbackState { + callback: Box::new(move |record| { + test_calls.fetch_add(1, Ordering::SeqCst); + if record.code == 42 { + assert_eq!(record.severity, LogSeverity::Warning); + assert_eq!(record.severity.raw_value(), sys::MLN_LOG_SEVERITY_WARNING); + assert_eq!(record.event, LogEvent::Render); + assert_eq!(record.event.raw_value(), sys::MLN_LOG_EVENT_RENDER); + assert_eq!(record.message, "hello"); + return true; + } + false + }), + }; let baseline_calls = calls.load(Ordering::SeqCst); let message = CString::new("hello").unwrap(); - let current = { - let state = lock_log_callback_state(); - state.as_ref().unwrap().clone() - }; - let user_data = Arc::as_ptr(¤t).cast_mut().cast::(); + let user_data = std::ptr::from_ref(&state).cast_mut().cast::(); assert_eq!( unsafe { log_callback_trampoline( @@ -202,8 +190,8 @@ mod tests { ); assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1); + set_log_callback(|_| true).unwrap(); clear_log_callback().unwrap(); - assert!(lock_log_callback_state().is_none()); assert_eq!( unsafe { log_callback_trampoline( @@ -231,6 +219,7 @@ mod tests { true }) .unwrap(); + assert_eq!(Arc::strong_count(&first_calls), 2); let second_callback_calls = Arc::clone(&second_calls); set_log_callback(move |_| { @@ -238,44 +227,26 @@ mod tests { true }) .unwrap(); - - let current = { - let state = lock_log_callback_state(); - state.as_ref().unwrap().clone() - }; - let message = CString::new("replacement").unwrap(); - assert_eq!( - invoke_callback( - ¤t, - sys::MLN_LOG_SEVERITY_INFO, - sys::MLN_LOG_EVENT_GENERAL, - 7, - message.as_ptr(), - ), - 1 - ); - + assert_eq!(Arc::strong_count(&first_calls), 1); assert_eq!(first_calls.load(Ordering::SeqCst), 0); - assert_eq!(second_calls.load(Ordering::SeqCst), 1); + assert_eq!(second_calls.load(Ordering::SeqCst), 0); clear_log_callback().unwrap(); - assert!(lock_log_callback_state().is_none()); + assert_eq!(Arc::strong_count(&second_calls), 1); } #[test] // Spec coverage: BND-121. fn invalid_utf8_log_messages_are_not_consumed() { let _guard = LoggingTestGuard::new(); - set_log_callback(|_| true).unwrap(); let invalid = b"\xff\0"; - let current = { - let state = lock_log_callback_state(); - state.as_ref().unwrap().clone() + let state = CallbackState { + callback: Box::new(|_| true), }; assert_eq!( invoke_callback( - ¤t, + &state, sys::MLN_LOG_SEVERITY_ERROR, sys::MLN_LOG_EVENT_GENERAL, 0, @@ -283,25 +254,20 @@ mod tests { ), 0 ); - - clear_log_callback().unwrap(); } #[test] // Spec coverage: BND-121. fn log_callback_panics_are_not_consumed() { let _guard = LoggingTestGuard::new(); - set_log_callback(|_| panic!("contained panic")).unwrap(); - let message = CString::new("boom").unwrap(); - let current = { - let state = lock_log_callback_state(); - state.as_ref().unwrap().clone() + let state = CallbackState { + callback: Box::new(|_| panic!("contained panic")), }; assert_eq!( invoke_callback( - ¤t, + &state, sys::MLN_LOG_SEVERITY_ERROR, sys::MLN_LOG_EVENT_GENERAL, 0, @@ -309,8 +275,6 @@ mod tests { ), 0 ); - - clear_log_callback().unwrap(); } #[test] diff --git a/bindings/rust/crates/maplibre-native-ffi/src/map.rs b/bindings/rust/crates/maplibre-native-ffi/src/map.rs index a2de552fe..efcc674f2 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/map.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/map.rs @@ -1,67 +1,62 @@ -use std::cell::RefCell; use std::fmt; -use std::rc::Rc; +use std::sync::{Arc, Mutex}; use maplibre_native_ffi_core as maplibre_core; -use maplibre_native_ffi_core::ptr::{const_ptr_or_null, mut_ptr_or_null, option_ptr}; -use maplibre_native_ffi_core::values::{ - empty_lat_lng, empty_lat_lng_bounds as empty_bounds, empty_screen_point, lat_lngs_to_native, - screen_points_to_native, -}; +use maplibre_native_ffi_core::ptr::{const_ptr_or_null, option_ptr}; +use maplibre_native_ffi_core::values::{lat_lngs_to_native, screen_points_to_native}; use maplibre_native_ffi_sys as sys; #[cfg(test)] use crate::PremultipliedRgba8Image; use crate::camera::{ - AnimationOptionsNativeExt, BoundOptionsNativeExt, CameraFitOptionsNativeExt, - CameraOptionsNativeExt, FreeCameraOptionsNativeExt, ProjectionModeNativeExt, + BoundOptionsNativeExt, CameraDeltaNativeExt, CameraFitOptionsNativeExt, CameraOptionsNativeExt, + CameraUpdateNativeExt, FreeCameraOptionsNativeExt, ProjectionModeNativeExt, }; +use crate::completion; #[cfg(test)] use crate::custom_geometry::CanonicalTileId; use crate::events::MapId; -use crate::handle::{ThreadAffineNativeHandle, closed_handle_error}; +use crate::handle::{ConcurrentNativeHandle, closed_handle_error}; use crate::options::{MapOptionsNativeExt, MapTileOptionsNativeExt, MapViewportOptionsNativeExt}; use crate::render::{ MetalBorrowedTextureDescriptor, MetalOwnedTextureDescriptor, MetalSurfaceDescriptor, OpenGLBorrowedTextureDescriptor, OpenGLOwnedTextureDescriptor, OpenGLSurfaceDescriptor, - RenderSessionHandle, VulkanBorrowedTextureDescriptor, VulkanOwnedTextureDescriptor, - VulkanSurfaceDescriptor, WebGpuBorrowedTextureDescriptor, WebGpuOwnedTextureDescriptor, - WebGpuSurfaceDescriptor, + RenderSessionAttachOptions, RenderSessionAttachment, RenderSessionHandle, + VulkanBorrowedTextureDescriptor, VulkanOwnedTextureDescriptor, VulkanSurfaceDescriptor, + WebGpuBorrowedTextureDescriptor, WebGpuOwnedTextureDescriptor, WebGpuSurfaceDescriptor, }; use crate::runtime::{RuntimeHandle, RuntimeState}; use crate::values::NativeValue; use crate::{ - AnimationOptions, BoundOptions, CameraFitOptions, CameraOptions, Error, ErrorKind, - FreeCameraOptions, HandleOperationError, LatLng, LatLngBounds, MapDebugOptions, MapOptions, - MapProjectionHandle, MapTileOptions, MapViewportOptions, ProjectionMode, Result, - RuntimeEventMask, ScreenPoint, + BoundOptions, CameraDelta, CameraFitOptions, CameraOptions, CameraSnapshot, CameraUpdate, + Error, FreeCameraOptions, HandleOperationError, LatLng, LatLngBounds, MapDebugOptions, + MapOptions, MapProjectionHandle, MapTileOptions, MapViewportOptions, NativeFuture, + ProjectionMode, Result, RuntimeEventMask, ScreenPoint, }; mod style; pub use style::{ - GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, - RasterDemEncoding, SourceInfo, SourceType, StyleImage, StyleImageInfo, StyleImageOptions, - StyleImageTextFit, StyleLayerVisibility, StyleTransitionOptions, TileJsonInfo, TileScheme, - TileSourceOptions, VectorTileEncoding, + GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, SourceInfo, + SourceType, StyleImage, StyleImageInfo, StyleImageOptions, StyleImageStretches, + StyleImageTextFit, StyleLayerInfo, StyleLayerVisibility, StyleTransitionOptions, TileJsonInfo, + TileScheme, TileSourceOptions, VectorTileEncoding, }; #[derive(Debug)] pub(crate) struct MapState { - handle: ThreadAffineNativeHandle, - runtime: RefCell>>, + handle: ConcurrentNativeHandle, + runtime: Mutex>>, id: MapId, } impl MapState { - fn new(native: sys::mln_map, runtime: Rc, id: MapId) -> Result { - // SAFETY: native came from successful mln_map_create and is paired with - // the matching map destroy function. - let handle = unsafe { - ThreadAffineNativeHandle::from_handle(native, sys::mln_map_destroy, "mln_map") - }?; + fn new(native: sys::mln_map, runtime: Arc, id: MapId) -> Result { + // SAFETY: native came from a successful typed creation take and map + // control state supports calls from any thread. + let handle = unsafe { ConcurrentNativeHandle::from_handle(native, "mln_map") }?; Ok(Self { handle, - runtime: RefCell::new(Some(runtime)), + runtime: Mutex::new(Some(runtime)), id, }) } @@ -76,27 +71,71 @@ impl MapState { self.handle.is_closed() } - fn close(&self) -> Result<()> { - // The destroy releases the callback state of this map's custom geometry - // sources before it returns. - self.handle.close()?; - self.runtime.borrow_mut().take(); - Ok(()) + fn close(&self) -> Result> { + let teardown = self.handle.close_with(|map| { + // SAFETY: map is live and native consumes it only on success. A + // rejected release leaves the completion state owned by submit. + completion::submit( + |completion| unsafe { sys::mln_map_release(map, completion) }, + completion::unit, + ) + })?; + self.runtime + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); + Ok(teardown.unwrap_or_else(|| completion::ready(()))) } } impl Drop for MapState { fn drop(&mut self) { - self.runtime.borrow_mut().take(); - // A failed destroy, such as one with a render session still attached, - // is reported through the leak channel by the handle's own `Drop`. - let _ = self.handle.close(); + if self.close().is_err() { + self.handle.leak_for_report(); + } + self.runtime + .get_mut() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); } } -/// Owner-thread map handle bound to a retained runtime. +/// Any-thread map control handle. pub struct MapHandle { - pub(crate) inner: Rc, + pub(crate) inner: MapState, +} + +/// Logical map extent in UI pixels and device-pixel scale. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LogicalExtent { + pub width: u32, + pub height: u32, + pub scale_factor: f64, +} + +/// Immutable map state copied from the latest worker publication. +/// +/// Every committed map command publishes a new generation in its completion, +/// so a snapshot whose `generation` is at or past that value observes the +/// commit. +#[derive(Debug, Clone, PartialEq)] +pub struct MapSnapshot { + pub generation: u64, + /// Debug overlay mask committed by [`MapHandle::set_debug_options`]. + pub debug_options: MapDebugOptions, + pub camera: CameraOptions, + pub logical_extent: LogicalExtent, + pub projection_mode: ProjectionMode, + pub viewport: MapViewportOptions, + /// True once every requested style and tile resource finished loading. + pub fully_loaded: bool, + pub rendering_stats_view_enabled: bool, + pub repaint_demand: bool, + pub event_mask: RuntimeEventMask, + pub latest_render_update_generation: u64, + pub tile: MapTileOptions, + pub bounds: BoundOptions, + pub free_camera: FreeCameraOptions, } impl fmt::Debug for MapHandle { @@ -108,23 +147,29 @@ impl fmt::Debug for MapHandle { } impl MapHandle { - /// Creates a map with explicit map options on the runtime owner thread. - pub fn with_options(runtime: &RuntimeHandle, options: &MapOptions) -> Result { + /// Creates a map on the runtime worker. + pub fn with_options( + runtime: &RuntimeHandle, + options: &MapOptions, + ) -> Result> { let runtime_ptr = runtime.inner.native()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); let raw_options = options.to_native()?; - - // SAFETY: runtime_ptr is a live runtime handle. raw_options is a - // materialized map descriptor with size filled by the binding. out is a - // valid null-initialized out-pointer owned by this call. - maplibre_core::check(unsafe { - sys::mln_map_create(runtime_ptr, &raw_options, out.as_mut_ptr()) - })?; - let native = out.get(); - let id = MapId::new(native.0); - let state = Rc::new(MapState::new(native, Rc::clone(&runtime.inner), id)?); - - Ok(Self { inner: state }) + let runtime_state = Arc::clone(&runtime.inner); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_create(runtime_ptr, &raw_options, completion) }, + move |result| { + let native = crate::completion::copy_value::(result)?; + if native.0 == 0 { + return Err(Error::invalid_argument( + "map creation returned a null handle", + )); + } + let id = MapId::new(native.0); + Ok(Self { + inner: MapState::new(native, runtime_state, id)?, + }) + }, + ) } /// Returns this map's runtime-local event source identity. @@ -132,846 +177,616 @@ impl MapHandle { self.inner.id } - /// Explicitly destroys the map. A failed destroy leaves the native handle - /// live so child handles can keep retaining and closing the map. - /// - /// Closing discards this map's queued runtime events and its recorded - /// loading failure, with no flush and no terminal event. Dropping the - /// handle ends the event stream the same way. - pub fn close(self) -> std::result::Result<(), HandleOperationError> { + /// Explicitly releases the map's public handle. + pub fn close(self) -> std::result::Result, HandleOperationError> { if self.inner.is_closed() { - return Ok(()); - } - if Rc::strong_count(&self.inner) > 1 { - return Err(HandleOperationError::new( - Error::new( - ErrorKind::InvalidState, - None, - "MapHandle cannot close while child handles are live", - ), - self, - )); + return Ok(completion::ready(())); } self.inner .close() .map_err(|error| HandleOperationError::new(error, self)) } - /// Requests a repaint for a continuous map. - pub fn request_repaint(&self) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is a live map handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_map_request_repaint(map) }) - } - - /// Requests one still image for a static or tile map. - pub fn request_still_image(&self) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is a live map handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_map_request_still_image(map) }) - } - - /// Selects which map-originated event types this map queues. - /// - /// A map reads the bits in - /// [`RuntimeEventMask::ALL_MAP_EVENTS`](crate::RuntimeEventMask::ALL_MAP_EVENTS), - /// so [`RuntimeEventMask::ALL`](crate::RuntimeEventMask::ALL) selects every - /// map-originated type. Narrowing gates later events and keeps queued ones. - /// A bit outside `ALL` is an invalid-argument error. - pub fn set_event_mask(&self, mask: RuntimeEventMask) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is live. The C API validates unknown mask bits. - maplibre_core::check(unsafe { sys::mln_map_set_event_mask(map, mask.bits()) }) - } - - /// Reports which map-originated event types this map queues, starting from - /// the mask its creation options selected. - pub fn event_mask(&self) -> Result { - let map = self.inner.native()?; - let mut raw = 0; - // SAFETY: map is live and out_mask points to writable u64 storage. - maplibre_core::check(unsafe { sys::mln_map_get_event_mask(map, &mut raw) })?; - Ok(RuntimeEventMask::from_bits_retain(raw)) - } - - /// Applies MapLibre debug overlay mask bits. - pub fn set_debug_options(&self, options: MapDebugOptions) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is live. The C API validates unknown mask bits. - maplibre_core::check(unsafe { sys::mln_map_set_debug_options(map, options.bits()) }) - } - - /// Reads MapLibre debug overlay mask bits. - pub fn debug_options(&self) -> Result { - let map = self.inner.native()?; - let mut raw = 0; - // SAFETY: map is live and out_options points to writable u32 storage. - maplibre_core::check(unsafe { sys::mln_map_get_debug_options(map, &mut raw) })?; - Ok(MapDebugOptions::from_bits_retain(raw)) - } - - /// Enables or disables MapLibre's rendering stats overlay view. - pub fn set_rendering_stats_view_enabled(&self, enabled: bool) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is live and enabled is passed by value. - maplibre_core::check(unsafe { sys::mln_map_set_rendering_stats_view_enabled(map, enabled) }) + /// Closes this map and waits for retirement when the test target permits a + /// synchronous wait. + #[cfg(test)] + pub(crate) fn close_and_wait(self) { + let completion = self + .close() + .map_err(HandleOperationError::into_error) + .expect("native close submission failed"); + #[cfg(not(target_os = "emscripten"))] + completion::blocking(Ok(completion)); + // Emscripten graphics teardown can require the test's pthread. The + // browser runner isolates every integration test in its own process, + // which provides the resource boundary without blocking that thread. + #[cfg(target_os = "emscripten")] + drop(completion); + } + + /// Copies the latest immutable map publication. + pub fn snapshot(&self) -> Result { + let map = self.inner.native()?; + let mut raw = sys::mln_map_snapshot { + size: std::mem::size_of::() as u32, + debug_options: 0, + generation: 0, + // SAFETY: these constructors initialize the nested ABI descriptors. + camera: unsafe { sys::mln_camera_options_default() }, + logical_extent: sys::mln_logical_extent { + width: 0, + height: 0, + scale_factor: 0.0, + }, + projection_mode: unsafe { sys::mln_projection_mode_default() }, + viewport: unsafe { sys::mln_map_viewport_options_default() }, + fully_loaded: false, + rendering_stats_view_enabled: false, + repaint_demand: false, + reserved_flags: 0, + event_mask: 0, + latest_render_update_generation: 0, + tile: unsafe { sys::mln_map_tile_options_default() }, + bounds: unsafe { sys::mln_bound_options_default() }, + free_camera: unsafe { sys::mln_free_camera_options_default() }, + }; + // SAFETY: map is live and raw is a size-tagged writable descriptor. + maplibre_core::check(unsafe { sys::mln_map_snapshot_get(map, &mut raw) })?; + Ok(MapSnapshot { + generation: raw.generation, + debug_options: MapDebugOptions::from_bits_retain(raw.debug_options), + camera: CameraOptions::from_native(raw.camera), + logical_extent: LogicalExtent { + width: raw.logical_extent.width, + height: raw.logical_extent.height, + scale_factor: raw.logical_extent.scale_factor, + }, + projection_mode: ProjectionMode::from_native(raw.projection_mode), + viewport: MapViewportOptions::from_native(raw.viewport), + fully_loaded: raw.fully_loaded, + rendering_stats_view_enabled: raw.rendering_stats_view_enabled, + repaint_demand: raw.repaint_demand, + event_mask: RuntimeEventMask::from_bits_retain(raw.event_mask), + latest_render_update_generation: raw.latest_render_update_generation, + tile: MapTileOptions::from_native(raw.tile), + bounds: BoundOptions::from_native(raw.bounds), + free_camera: FreeCameraOptions::from_native(raw.free_camera), + }) } - /// Reads whether MapLibre's rendering stats overlay view is enabled. - pub fn rendering_stats_view_enabled(&self) -> Result { + /// Submits the map's logical extent update. + pub fn resize(&self, extent: LogicalExtent) -> Result> { let map = self.inner.native()?; - let mut enabled = false; - // SAFETY: map is live and out_enabled points to writable bool storage. - maplibre_core::check(unsafe { - sys::mln_map_get_rendering_stats_view_enabled(map, &mut enabled) - })?; - Ok(enabled) + let raw = sys::mln_logical_extent { + width: extent.width, + height: extent.height, + scale_factor: extent.scale_factor, + }; + crate::completion::submit( + move |completion| unsafe { sys::mln_map_resize(map, raw, completion) }, + crate::completion::command, + ) } - /// Reads whether MapLibre currently considers the map fully loaded. - pub fn is_fully_loaded(&self) -> Result { + /// Requests a continuous-map repaint and returns its completion future. + pub fn request_repaint(&self) -> Result> { let map = self.inner.native()?; - let mut loaded = false; - // SAFETY: map is live and out_loaded points to writable bool storage. - maplibre_core::check(unsafe { sys::mln_map_is_fully_loaded(map, &mut loaded) })?; - Ok(loaded) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_request_repaint(map, completion) }, + crate::completion::command, + ) } - /// Dumps map debug logs through MapLibre Native logging. - pub fn dump_debug_logs(&self) -> Result<()> { + /// Starts one noncoalescing still-image operation. + pub fn request_still_image(&self) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live. - maplibre_core::check(unsafe { sys::mln_map_dump_debug_logs(map) }) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_request_still_image(map, completion) }, + crate::completion::unit, + ) } - /// Reads the map's logical viewport size in UI pixels and its pixel ratio. - /// The scale factor is fixed for the lifetime of the map and independent of - /// any render target's scale factor. - pub fn size(&self) -> Result<(u32, u32, f64)> { + /// Copies the latest published camera without entering the runtime queue. + pub fn camera_snapshot(&self) -> Result { let map = self.inner.native()?; - let mut width = 0u32; - let mut height = 0u32; - let mut scale_factor = 0f64; - // SAFETY: map is live and all three out pointers reference live locals - // for the duration of the call. + // SAFETY: the constructor initializes this ABI version's descriptor. + let mut camera = unsafe { sys::mln_camera_options_default() }; + let mut generation = 0; + // SAFETY: map is live and both outputs are writable. maplibre_core::check(unsafe { - sys::mln_map_get_size(map, &mut width, &mut height, &mut scale_factor) + sys::mln_map_camera_snapshot_get(map, &mut camera, &mut generation) })?; - Ok((width, height, scale_factor)) - } - - /// Reads live viewport and render-transform controls. - pub fn viewport_options(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_map_viewport_options_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_viewport_options(map, &mut raw) })?; - Ok(MapViewportOptions::from_native(raw)) - } - - /// Applies selected live viewport and render-transform controls. - pub fn set_viewport_options(&self, options: &MapViewportOptions) -> Result<()> { - let map = self.inner.native()?; - let raw = options.to_native(); - // SAFETY: map is live and raw is a materialized descriptor valid for - // the duration of this call. - maplibre_core::check(unsafe { sys::mln_map_set_viewport_options(map, &raw) }) - } - - /// Reads tile prefetch and LOD tuning controls. - pub fn tile_options(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_map_tile_options_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_tile_options(map, &mut raw) })?; - Ok(MapTileOptions::from_native(raw)) - } - - /// Applies selected tile prefetch and LOD tuning controls. - pub fn set_tile_options(&self, options: &MapTileOptions) -> Result<()> { - let map = self.inner.native()?; - let raw = options.to_native(); - // SAFETY: map is live and raw is a materialized descriptor valid for - // the duration of this call. - maplibre_core::check(unsafe { sys::mln_map_set_tile_options(map, &raw) }) - } - - /// Reads the current camera snapshot. - pub fn camera(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_camera_options_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_camera(map, &mut raw) })?; - Ok(CameraOptions::from_native(raw)) - } - - /// Applies a camera jump command. - pub fn jump_to(&self, camera: &CameraOptions) -> Result<()> { - let map = self.inner.native()?; - let raw = camera.to_native(); - // SAFETY: map is live and raw is a materialized descriptor valid for - // the duration of this call. - maplibre_core::check(unsafe { sys::mln_map_jump_to(map, &raw) }) - } - - /// Applies a camera ease transition command. An absent `animation`, or one - /// with no duration, reaches the target before this call returns. - pub fn ease_to( - &self, - camera: &CameraOptions, - animation: Option<&AnimationOptions>, - ) -> Result<()> { - let map = self.inner.native()?; - let raw_camera = camera.to_native(); - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and descriptors are valid for this call. A null - // animation pointer requests native defaults. - maplibre_core::check(unsafe { - sys::mln_map_ease_to(map, &raw_camera, option_ptr(raw_animation.as_ref())) + Ok(CameraSnapshot { + generation, + camera: CameraOptions::from_native(camera), }) } - /// Applies a camera fly transition command. Fly animates by default, so the - /// camera is still en route when this call returns and advances as the - /// runtime is pumped. - pub fn fly_to( + /// Submits one copied atomic camera update. + pub fn update_camera( &self, - camera: &CameraOptions, - animation: Option<&AnimationOptions>, - ) -> Result<()> { - let map = self.inner.native()?; - let raw_camera = camera.to_native(); - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and descriptors are valid for this call. A null - // animation pointer requests native defaults. - maplibre_core::check(unsafe { - sys::mln_map_fly_to(map, &raw_camera, option_ptr(raw_animation.as_ref())) - }) - } - - /// Applies a screen-space pan command. - pub fn move_by(&self, delta_x: f64, delta_y: f64) -> Result<()> { + update: &CameraUpdate, + ) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live. The C API validates numeric values. - maplibre_core::check(unsafe { sys::mln_map_move_by(map, delta_x, delta_y) }) + let raw = update.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_update_camera(map, &raw, completion) }, + crate::completion::command, + ) } - /// Applies an animated screen-space pan command. - /// An absent `animation`, or one with no duration, applies the pan - /// instantly. - pub fn move_by_animated( + /// Submits one relative camera update. + pub fn apply_camera_delta( &self, - delta_x: f64, - delta_y: f64, - animation: Option<&AnimationOptions>, - ) -> Result<()> { - let map = self.inner.native()?; - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and the optional animation descriptor is valid - // for this call. The C API validates numeric values. - maplibre_core::check(unsafe { - sys::mln_map_move_by_animated(map, delta_x, delta_y, option_ptr(raw_animation.as_ref())) - }) - } - - /// Applies a screen-space zoom command. - pub fn scale_by(&self, scale: f64, anchor: Option) -> Result<()> { + delta: &CameraDelta, + ) -> Result> { + let map = self.inner.native()?; + let raw = delta.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_apply_camera_delta(map, &raw, completion) }, + crate::completion::command, + ) + } + + /// Starts an ordered camera query. + pub fn camera_query(&self) -> Result> { + let map = self.inner.native()?; + crate::completion::submit( + move |completion| unsafe { sys::mln_map_camera_query(map, completion) }, + |result| { + let value = crate::completion::copy_value::(result)?; + Ok(CameraSnapshot { + generation: value.generation, + camera: CameraOptions::from_native(value.camera), + }) + }, + ) + } + + /// Selects which map-originated event types this map queues and returns the + /// completion future. + pub fn set_event_mask( + &self, + mask: RuntimeEventMask, + ) -> Result> { let map = self.inner.native()?; - let raw_anchor = anchor.map(ScreenPoint::to_native); - // SAFETY: map is live and the optional anchor pointer is valid for this - // call. The C API validates numeric values. - maplibre_core::check(unsafe { - sys::mln_map_scale_by(map, scale, option_ptr(raw_anchor.as_ref())) - }) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_event_mask(map, mask.bits(), completion) }, + crate::completion::command, + ) } - /// Applies an animated screen-space zoom command. - /// An absent `animation`, or one with no duration, applies the zoom - /// instantly. - pub fn scale_by_animated( + /// Submits a debug-overlay command. + /// + /// The committed mask is visible as [`MapSnapshot::debug_options`]. + pub fn set_debug_options( &self, - scale: f64, - anchor: Option, - animation: Option<&AnimationOptions>, - ) -> Result<()> { + options: MapDebugOptions, + ) -> Result> { let map = self.inner.native()?; - let raw_anchor = anchor.map(ScreenPoint::to_native); - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and optional descriptors are valid for this call. - // The C API validates numeric values. - maplibre_core::check(unsafe { - sys::mln_map_scale_by_animated( - map, - scale, - option_ptr(raw_anchor.as_ref()), - option_ptr(raw_animation.as_ref()), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_debug_options(map, options.bits(), completion) + }, + crate::completion::command, + ) } - /// Applies a screen-space rotate command. - pub fn rotate_by(&self, first: ScreenPoint, second: ScreenPoint) -> Result<()> { + /// Submits a rendering-stats visibility command. + /// + /// The committed value is visible as + /// [`MapSnapshot::rendering_stats_view_enabled`]. + pub fn set_rendering_stats_view_enabled( + &self, + enabled: bool, + ) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live. Points are passed by value and validated by C. - maplibre_core::check(unsafe { - sys::mln_map_rotate_by(map, first.to_native(), second.to_native()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_rendering_stats_view_enabled(map, enabled, completion) + }, + crate::completion::command, + ) } - /// Applies an animated screen-space rotate command. - /// An absent `animation`, or one with no duration, applies the rotation - /// instantly. - pub fn rotate_by_animated( + /// Submits a viewport-options command; the committed options are visible + /// as [`MapSnapshot::viewport`]. + pub fn set_viewport_options( &self, - first: ScreenPoint, - second: ScreenPoint, - animation: Option<&AnimationOptions>, - ) -> Result<()> { + options: &MapViewportOptions, + ) -> Result> { let map = self.inner.native()?; - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and optional animation descriptor is valid for - // this call. Points are passed by value and validated by C. - maplibre_core::check(unsafe { - sys::mln_map_rotate_by_animated( - map, - first.to_native(), - second.to_native(), - option_ptr(raw_animation.as_ref()), - ) - }) + let raw = options.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_viewport_options(map, &raw, completion) }, + crate::completion::command, + ) } - /// Applies a pitch delta command. - pub fn pitch_by(&self, pitch: f64) -> Result<()> { + /// Submits a tile-options command; the committed options are visible as + /// [`MapSnapshot::tile`]. + pub fn set_tile_options( + &self, + options: &MapTileOptions, + ) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live. The C API validates numeric values. - maplibre_core::check(unsafe { sys::mln_map_pitch_by(map, pitch) }) + let raw = options.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_tile_options(map, &raw, completion) }, + crate::completion::command, + ) } - /// Applies an animated pitch delta command. - /// An absent `animation`, or one with no duration, applies the pitch - /// instantly. - pub fn pitch_by_animated( + /// Submits a camera-constraint command; the committed constraints are + /// visible as [`MapSnapshot::bounds`]. + pub fn set_bounds( &self, - pitch: f64, - animation: Option<&AnimationOptions>, - ) -> Result<()> { + options: &BoundOptions, + ) -> Result> { let map = self.inner.native()?; - let raw_animation = animation.map(AnimationOptions::to_native); - // SAFETY: map is live and optional animation descriptor is valid for - // this call. The C API validates numeric values. - maplibre_core::check(unsafe { - sys::mln_map_pitch_by_animated(map, pitch, option_ptr(raw_animation.as_ref())) - }) + let raw = options.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_bounds(map, &raw, completion) }, + crate::completion::command, + ) } - /// Cancels active camera transitions. - pub fn cancel_transitions(&self) -> Result<()> { + /// Submits a free-camera command; the committed options are visible as + /// [`MapSnapshot::free_camera`]. + pub fn set_free_camera_options( + &self, + options: &FreeCameraOptions, + ) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live. - maplibre_core::check(unsafe { sys::mln_map_cancel_transitions(map) }) + let raw = options.to_native(); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_free_camera_options(map, &raw, completion) + }, + crate::completion::command, + ) } - /// Marks whether a host-driven gesture is in progress. The flag stays set - /// until the host clears it, so pair every `true` with a `false`. - pub fn set_gesture_in_progress(&self, in_progress: bool) -> Result<()> { - let map = self.inner.native()?; - // SAFETY: map is live and in_progress is passed by value. - maplibre_core::check(unsafe { sys::mln_map_set_gesture_in_progress(map, in_progress) }) + pub fn projection_mode(&self) -> Result { + Ok(self.snapshot()?.projection_mode) } - /// Reads whether a host-driven gesture is currently in progress. - pub fn is_gesture_in_progress(&self) -> Result { + pub fn set_projection_mode( + &self, + mode: &ProjectionMode, + ) -> Result> { let map = self.inner.native()?; - let mut in_progress = false; - // SAFETY: map is live and out_in_progress points to writable bool - // storage. - maplibre_core::check(unsafe { - sys::mln_map_is_gesture_in_progress(map, &mut in_progress) - })?; - Ok(in_progress) + let raw = mode.to_native(); + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_projection_mode(map, &raw, completion) }, + crate::completion::command, + ) } - /// Computes a camera that fits geographic bounds in the current viewport. pub fn camera_for_lat_lng_bounds( &self, bounds: LatLngBounds, fit_options: Option<&CameraFitOptions>, - ) -> Result { - let map = self.inner.native()?; - let raw_fit = fit_options.map(CameraFitOptions::to_native); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw_camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: map is live, bounds is passed by value, optional fit options - // are valid for this call, and raw_camera is writable. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_lat_lng_bounds( - map, - bounds.to_native(), - option_ptr(raw_fit.as_ref()), - &mut raw_camera, - ) - })?; - Ok(CameraOptions::from_native(raw_camera)) + ) -> Result> { + let map = self.inner.native()?; + let fit = fit_options.map(CameraFitOptions::to_native); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_camera_for_lat_lng_bounds( + map, + bounds.to_native(), + option_ptr(fit.as_ref()), + completion, + ) + }, + |result| { + Ok(CameraOptions::from_native(crate::completion::copy_value( + result, + )?)) + }, + ) } - /// Computes a camera that fits geographic coordinates in the current viewport. pub fn camera_for_lat_lngs( &self, coordinates: &[LatLng], fit_options: Option<&CameraFitOptions>, - ) -> Result { - let map = self.inner.native()?; + ) -> Result> { if coordinates.is_empty() { return Err(Error::invalid_argument( "camera_for_lat_lngs requires at least one coordinate", )); } - let raw_coordinates = lat_lngs_to_native(coordinates); - let raw_fit = fit_options.map(CameraFitOptions::to_native); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw_camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: map is live, arrays are valid for coordinate_count non-empty - // entries, optional fit options are valid, and raw_camera is writable. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_lat_lngs( - map, - const_ptr_or_null(&raw_coordinates), - raw_coordinates.len(), - option_ptr(raw_fit.as_ref()), - &mut raw_camera, - ) - })?; - Ok(CameraOptions::from_native(raw_camera)) + let map = self.inner.native()?; + let coordinates = lat_lngs_to_native(coordinates); + let fit = fit_options.map(CameraFitOptions::to_native); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_camera_for_lat_lngs( + map, + const_ptr_or_null(&coordinates), + coordinates.len(), + option_ptr(fit.as_ref()), + completion, + ) + }, + |result| { + Ok(CameraOptions::from_native(crate::completion::copy_value( + result, + )?)) + }, + ) } - /// Computes a camera that fits a geometry in the current viewport. pub fn camera_for_geometry( &self, geometry: &[u8], fit_options: Option<&CameraFitOptions>, - ) -> Result { - let map = self.inner.native()?; - let native_geometry = maplibre_core::string::buffer_view(geometry); - let raw_fit = fit_options.map(CameraFitOptions::to_native); - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw_camera = unsafe { sys::mln_camera_options_default() }; - // SAFETY: map is live, native_geometry owns backing storage for the - // duration of this call, optional fit options are valid, and raw_camera - // is writable. - maplibre_core::check(unsafe { - sys::mln_map_camera_for_geometry( - map, - native_geometry, - option_ptr(raw_fit.as_ref()), - &mut raw_camera, - ) - })?; - Ok(CameraOptions::from_native(raw_camera)) - } - - /// Computes geographic bounds for a camera from two viewport corners. - /// - /// The box is the hull of the top-left and bottom-right screen corners for - /// that camera in the current viewport. When bearing and pitch are zero, the - /// box equals the visible area. Those corners are the northwest and - /// southeast of the viewport. Longitudes stay in -180 to 180. - pub fn lat_lng_bounds_for_camera(&self, camera: &CameraOptions) -> Result { - let map = self.inner.native()?; - let raw_camera = camera.to_native(); - let mut raw_bounds = empty_bounds(); - // SAFETY: map is live, raw_camera is a valid descriptor for this call, - // and raw_bounds points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_lat_lng_bounds_for_camera(map, &raw_camera, &mut raw_bounds) - })?; - Ok(LatLngBounds::from_native(raw_bounds)) + ) -> Result> { + let map = self.inner.native()?; + let fit = fit_options.map(CameraFitOptions::to_native); + let geometry = geometry.to_vec(); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_camera_for_geometry( + map, + maplibre_core::string::buffer_view(&geometry), + option_ptr(fit.as_ref()), + completion, + ) + }, + |result| { + Ok(CameraOptions::from_native(crate::completion::copy_value( + result, + )?)) + }, + ) + } + + fn bounds_for_camera( + &self, + camera: &CameraOptions, + unwrapped: bool, + ) -> Result> { + let map = self.inner.native()?; + let camera = camera.to_native(); + crate::completion::submit( + move |completion| unsafe { + if unwrapped { + sys::mln_map_lat_lng_bounds_for_camera_unwrapped(map, &camera, completion) + } else { + sys::mln_map_lat_lng_bounds_for_camera(map, &camera, completion) + } + }, + |result| { + Ok(LatLngBounds::from_native(crate::completion::copy_value( + result, + )?)) + }, + ) + } + + pub fn lat_lng_bounds_for_camera( + &self, + camera: &CameraOptions, + ) -> Result> { + self.bounds_for_camera(camera, false) } - /// Computes geographic bounds for a camera from the four viewport corners. - /// - /// The axis-aligned hull of all four screen corners and the center - /// encompasses the projected viewport. Longitudes unwrap onto the shortest - /// path through the center. A viewport that crosses the antimeridian reports - /// values outside -180 to 180. pub fn lat_lng_bounds_for_camera_unwrapped( &self, camera: &CameraOptions, - ) -> Result { - let map = self.inner.native()?; - let raw_camera = camera.to_native(); - let mut raw_bounds = empty_bounds(); - // SAFETY: map is live, raw_camera is a valid descriptor for this call, - // and raw_bounds points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_lat_lng_bounds_for_camera_unwrapped(map, &raw_camera, &mut raw_bounds) - })?; - Ok(LatLngBounds::from_native(raw_bounds)) - } - - /// Reads map camera constraint options. - pub fn bounds(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_bound_options_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_bounds(map, &mut raw) })?; - Ok(BoundOptions::from_native(raw)) - } - - /// Applies selected map camera constraint options. - pub fn set_bounds(&self, options: &BoundOptions) -> Result<()> { - let map = self.inner.native()?; - let raw = options.to_native(); - // SAFETY: map is live and raw is a valid descriptor for this call. - maplibre_core::check(unsafe { sys::mln_map_set_bounds(map, &raw) }) - } - - /// Reads the current free camera position and orientation. - pub fn free_camera_options(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_free_camera_options_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_free_camera_options(map, &mut raw) })?; - Ok(FreeCameraOptions::from_native(raw)) - } - - /// Applies selected free camera position and orientation fields. - pub fn set_free_camera_options(&self, options: &FreeCameraOptions) -> Result<()> { - let map = self.inner.native()?; - let raw = options.to_native(); - // SAFETY: map is live and raw is a valid descriptor for this call. - maplibre_core::check(unsafe { sys::mln_map_set_free_camera_options(map, &raw) }) - } - - /// Reads current axonometric rendering options. - pub fn projection_mode(&self) -> Result { - let map = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut raw = unsafe { sys::mln_projection_mode_default() }; - // SAFETY: map is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_get_projection_mode(map, &mut raw) })?; - Ok(ProjectionMode::from_native(raw)) + ) -> Result> { + self.bounds_for_camera(camera, true) } - /// Applies selected axonometric rendering option fields. - pub fn set_projection_mode(&self, mode: &ProjectionMode) -> Result<()> { + pub fn pixel_for_lat_lng(&self, coordinate: LatLng) -> Result> { let map = self.inner.native()?; - let raw = mode.to_native(); - // SAFETY: map is live and raw is a valid descriptor for this call. - maplibre_core::check(unsafe { sys::mln_map_set_projection_mode(map, &raw) }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_pixel_for_lat_lng(map, coordinate.to_native(), completion) + }, + |result| { + Ok(ScreenPoint::from_native(crate::completion::copy_value( + result, + )?)) + }, + ) } - /// Converts a geographic world coordinate to a screen point for the current map. - pub fn pixel_for_lat_lng(&self, coordinate: LatLng) -> Result { + pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result> { let map = self.inner.native()?; - let mut raw_point = empty_screen_point(); - // SAFETY: map is live, coordinate is passed by value, and raw_point is - // writable storage for the output. - maplibre_core::check(unsafe { - sys::mln_map_pixel_for_lat_lng(map, coordinate.to_native(), &mut raw_point) - })?; - Ok(ScreenPoint::from_native(raw_point)) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_lat_lng_for_pixel(map, point.to_native(), completion) + }, + |result| Ok(LatLng::from_native(crate::completion::copy_value(result)?)), + ) } - /// Converts a screen point to a geographic world coordinate for the current map. - pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result { - let map = self.inner.native()?; - let mut raw_coordinate = empty_lat_lng(); - // SAFETY: map is live, point is passed by value, and raw_coordinate is - // writable storage for the output. - maplibre_core::check(unsafe { - sys::mln_map_lat_lng_for_pixel(map, point.to_native(), &mut raw_coordinate) - })?; - Ok(LatLng::from_native(raw_coordinate)) - } - - /// Converts geographic world coordinates to screen points for the current map. - pub fn pixels_for_lat_lngs(&self, coordinates: &[LatLng]) -> Result> { - let map = self.inner.native()?; - let raw_coordinates = lat_lngs_to_native(coordinates); - let mut raw_points = vec![empty_screen_point(); coordinates.len()]; - // SAFETY: map is live. Input and output arrays are valid for len - // entries, or null when len is 0. - maplibre_core::check(unsafe { - sys::mln_map_pixels_for_lat_lngs( - map, - const_ptr_or_null(&raw_coordinates), - raw_coordinates.len(), - mut_ptr_or_null(&mut raw_points), - ) - })?; - Ok(raw_points - .into_iter() - .map(ScreenPoint::from_native) - .collect()) - } - - /// Converts screen points to geographic world coordinates for the current map. - pub fn lat_lngs_for_pixels(&self, points: &[ScreenPoint]) -> Result> { - let map = self.inner.native()?; - let raw_points = screen_points_to_native(points); - let mut raw_coordinates = vec![empty_lat_lng(); points.len()]; - // SAFETY: map is live. Input and output arrays are valid for len - // entries, or null when len is 0. - maplibre_core::check(unsafe { - sys::mln_map_lat_lngs_for_pixels( - map, - const_ptr_or_null(&raw_points), - raw_points.len(), - mut_ptr_or_null(&mut raw_coordinates), - ) - })?; - Ok(raw_coordinates - .into_iter() - .map(LatLng::from_native) - .collect()) + pub fn pixels_for_lat_lngs( + &self, + coordinates: &[LatLng], + ) -> Result>> { + let map = self.inner.native()?; + let coordinates = lat_lngs_to_native(coordinates); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_pixels_for_lat_lngs( + map, + const_ptr_or_null(&coordinates), + coordinates.len(), + completion, + ) + }, + |result| { + Ok( + crate::completion::copy_slice::(result)? + .into_iter() + .map(ScreenPoint::from_native) + .collect(), + ) + }, + ) + } + + pub fn lat_lngs_for_pixels(&self, points: &[ScreenPoint]) -> Result>> { + let map = self.inner.native()?; + let points = screen_points_to_native(points); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_lat_lngs_for_pixels( + map, + const_ptr_or_null(&points), + points.len(), + completion, + ) + }, + |result| { + Ok(crate::completion::copy_slice::(result)? + .into_iter() + .map(LatLng::from_native) + .collect()) + }, + ) } /// Creates a standalone projection snapshot from the current map transform. - pub fn create_projection(&self) -> Result { + pub fn create_projection(&self) -> Result> { MapProjectionHandle::new(self) } - - /// Produces a [`Send`] reference to this map for attaching a render session. - /// A render session is owned by the thread that attaches it, which need not - /// be the map's owner thread. - pub fn attach_ref(&self) -> Result { - Ok(MapAttachRef { - map: self.inner.native()?, - }) - } -} - -/// A reference to a map for the sole purpose of attaching a render session, -/// produced by [`MapHandle::attach_ref`]. Attaching is the one map operation -/// that runs on the render session's thread instead of the map's. -/// -/// This is a copied handle value and carries no Rust retention of the map. -/// Native destroys a map only once no session is attached, and rejects a -/// reference that outlives its map rather than binding to a later one. -/// -/// Close the session before the map: dropping a [`MapHandle`] with a session -/// still attached leaks the native map, reports it through -/// [`set_leak_reporter`](crate::set_leak_reporter), and leaves the runtime -/// undestroyable. -#[derive(Clone, Copy)] -pub struct MapAttachRef { - map: sys::mln_map, -} - -impl fmt::Debug for MapAttachRef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MapAttachRef").finish() - } } -impl MapAttachRef { - /// The handle of the map this reference names. - pub(crate) fn map(&self) -> sys::mln_map { - self.map - } - - /// Attaches a Metal native surface render target to the map. - /// - /// The layer and optional device pointers are backend-native handles. They - /// must name valid Metal objects for this session and remain usable on the - /// owner thread until the session is detached or closed. +impl MapHandle { pub fn attach_metal_surface( &self, - descriptor: &MetalSurfaceDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_metal_surface_attach(map, &raw, out) } + value: &MetalSurfaceDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_metal_surface_attach(map, &raw, options, session, operation) }) } - /// Attaches a Vulkan native surface render target to the map. - /// - /// Vulkan handles are borrowed. They must remain valid and externally - /// synchronized until the session is detached or closed. pub fn attach_vulkan_surface( &self, - descriptor: &VulkanSurfaceDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_vulkan_surface_attach(map, &raw, out) } + value: &VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_vulkan_surface_attach(map, &raw, options, session, operation) }) } - /// Attaches a WebGPU native surface render target to the map. - /// - /// The surface, device, and instance are borrowed. They must stay valid - /// until the session detaches or closes. pub fn attach_webgpu_surface( &self, - descriptor: &WebGpuSurfaceDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_webgpu_surface_attach(map, &raw, out) } + value: &WebGpuSurfaceDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_webgpu_surface_attach(map, &raw, options, session, operation) }) } - /// Attaches an OpenGL native surface render target to the map. - /// - /// OpenGL context provider and surface handles are borrowed. They must - /// remain valid and externally synchronized until the session is detached - /// or closed. pub fn attach_opengl_surface( &self, - descriptor: &OpenGLSurfaceDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_opengl_surface_attach(map, &raw, out) } + value: &OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_opengl_surface_attach(map, &raw, options, session, operation) }) } - /// Attaches a Metal session-owned texture render target to the map. - /// - /// The device pointer must name a valid Metal device that remains usable on - /// the owner thread until the session is detached or closed. pub fn attach_metal_owned_texture( &self, - descriptor: &MetalOwnedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_metal_owned_texture_attach(map, &raw, out) } + value: &MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_metal_owned_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches a Metal caller-owned texture render target to the map. - /// - /// The texture pointer is borrowed. The caller owns the texture, keeps it - /// valid until detach or close, and synchronizes use outside this session. pub fn attach_metal_borrowed_texture( &self, - descriptor: &MetalBorrowedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_metal_borrowed_texture_attach(map, &raw, out) } + value: &MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_metal_borrowed_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches a Vulkan session-owned texture render target to the map. - /// - /// Vulkan device and queue handles are borrowed. They must remain valid and - /// externally synchronized until the session is detached or closed. pub fn attach_vulkan_owned_texture( &self, - descriptor: &VulkanOwnedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_vulkan_owned_texture_attach(map, &raw, out) } + value: &VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_vulkan_owned_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches a Vulkan caller-owned texture render target to the map. - /// - /// Vulkan handles, image, and image view are borrowed. The caller owns the - /// image resources, keeps them valid until detach or close, and handles - /// queue-family ownership and synchronization outside this session. pub fn attach_vulkan_borrowed_texture( &self, - descriptor: &VulkanBorrowedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_vulkan_borrowed_texture_attach(map, &raw, out) } + value: &VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_vulkan_borrowed_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches a WebGPU session-owned texture render target to the map. - /// - /// The WebGPU device and queue are borrowed. They must remain valid until - /// the session is detached or closed, and the session renders on the thread - /// that owns them. pub fn attach_webgpu_owned_texture( &self, - descriptor: &WebGpuOwnedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_webgpu_owned_texture_attach(map, &raw, out) } + value: &WebGpuOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_webgpu_owned_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches a WebGPU caller-owned texture render target to the map. - /// - /// The texture and its view are borrowed. The caller owns them, keeps them - /// valid until detach or close, and creates both from the descriptor's - /// device. pub fn attach_webgpu_borrowed_texture( &self, - descriptor: &WebGpuBorrowedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_webgpu_borrowed_texture_attach(map, &raw, out) } + value: &WebGpuBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_webgpu_borrowed_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches an OpenGL session-owned texture render target to the map. - /// - /// The context provider handles are borrowed. They must remain valid until - /// the session is detached or closed. Host sampling must use a context in - /// the same share group while the acquired frame remains open. pub fn attach_opengl_owned_texture( &self, - descriptor: &OpenGLOwnedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_opengl_owned_texture_attach(map, &raw, out) } + value: &OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_opengl_owned_texture_attach(map, &raw, options, session, operation) }) } - /// Attaches an OpenGL caller-owned texture render target to the map. - /// - /// The context provider handles and texture object are borrowed. The caller - /// owns the texture, keeps it valid until detach or close, and synchronizes - /// use outside this session. pub fn attach_opengl_borrowed_texture( &self, - descriptor: &OpenGLBorrowedTextureDescriptor, - ) -> Result { - let raw = descriptor.to_native(); - RenderSessionHandle::attach(self, |map, out| { - // SAFETY: map is live, raw is a materialized descriptor valid for - // this call, and out is a null-initialized out-pointer. - unsafe { sys::mln_opengl_borrowed_texture_attach(map, &raw, out) } + value: &OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions, + ) -> Result { + let raw = value.to_native(); + RenderSessionHandle::attach(self, options, |map, options, session, operation| unsafe { + sys::mln_opengl_borrowed_texture_attach(map, &raw, options, session, operation) }) } } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/map/style.rs b/bindings/rust/crates/maplibre-native-ffi/src/map/style.rs index 4aacd7a70..cb39fb8ca 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/map/style.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/map/style.rs @@ -6,10 +6,10 @@ pub(crate) use maplibre_core::style::{ TileSourceOptionsNativeExt, }; pub use maplibre_core::{ - GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, - RasterDemEncoding, SourceInfo, SourceType, StyleImage, StyleImageInfo, StyleImageOptions, - StyleImageTextFit, StyleLayerVisibility, StyleTransitionOptions, TileJsonInfo, TileScheme, - TileSourceOptions, VectorTileEncoding, + GeoJsonSourceOptions, ImageContent, ImageStretch, LocationIndicatorImageKind, SourceInfo, + SourceType, StyleImage, StyleImageInfo, StyleImageOptions, StyleImageTextFit, StyleLayerInfo, + StyleLayerVisibility, StyleTransitionOptions, TileJsonInfo, TileScheme, TileSourceOptions, + VectorTileEncoding, }; use maplibre_native_ffi_core as maplibre_core; use maplibre_native_ffi_core::ptr::const_ptr_or_null; @@ -17,14 +17,16 @@ use maplibre_native_ffi_core::query::FeatureStateSelectorNativeExt; use maplibre_native_ffi_core::values::lat_lngs_to_native; use maplibre_native_ffi_sys as sys; -use crate::custom_geometry::{CanonicalTileId, CustomGeometrySourceState}; -use crate::custom_mvt_vector::CustomMvtVectorSourceState; +use crate::custom_geometry::{ + CanonicalTileId, CustomGeometrySourceOptions, CustomGeometrySourceState, +}; +use crate::custom_mvt_vector::{CustomMvtVectorSourceOptions, CustomMvtVectorSourceState}; use crate::render::PremultipliedRgba8Image; use crate::values::NativeValue; -use crate::{ - CustomGeometrySourceOptions, CustomMvtVectorSourceOptions, Error, ErrorKind, - FeatureStateSelector, LatLng, LatLngBounds, Result, -}; +use crate::{CommandCompletion, FeatureStateSelector, LatLng, LatLngBounds, NativeFuture, Result}; + +/// Horizontal and vertical stretch intervals for one style image. +pub type StyleImageStretches = (Vec, Vec); impl super::MapHandle { /// Loads a style URL through MapLibre Native style APIs. @@ -32,73 +34,103 @@ impl super::MapHandle { /// Loading is asynchronous: a style that fails to fetch or parse still /// returns `Ok` here and reports through a later loading-failed runtime /// event. Watch the event stream for the load outcome. - pub fn set_style_url(&self, url: &str) -> Result<()> { + pub fn set_style_url(&self, url: &str) -> Result> { let map = self.inner.native()?; let url = maplibre_core::string::c_string(url)?; // SAFETY: map is live and url is a NUL-terminated UTF-8 string the C // API consumes before returning. - maplibre_core::check(unsafe { sys::mln_map_set_style_url(map, url.as_ptr()) })?; - Ok(()) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_style_url(map, url.as_ptr(), completion) }, + crate::completion::command, + ) } /// Loads inline style JSON through MapLibre Native style APIs. /// /// A parse failure is reported twice: this call returns the error, and the /// same message arrives as a loading-failed runtime event. - pub fn set_style_json(&self, json: &[u8]) -> Result<()> { + pub fn set_style_json(&self, json: &[u8]) -> Result> { let map = self.inner.native()?; let json = maplibre_core::string::buffer_view(json); // SAFETY: map is live and json is valid for the call. Style replacement // completes before a successful return, so the C API has already // released the callback state of the sources this load dropped. - maplibre_core::check(unsafe { sys::mln_map_set_style_json(map, json) }) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_set_style_json(map, json, completion) }, + crate::completion::command, + ) } - /// Sets per-feature state on this map. - pub fn set_feature_state(&self, selector: &FeatureStateSelector, state: &[u8]) -> Result<()> { + /// Sets per-feature state on this map. The state bytes must contain one + /// UTF-8 JSON object and are copied before the call returns. + pub fn set_feature_state( + &self, + selector: &FeatureStateSelector, + state: &[u8], + ) -> Result> { let map = self.inner.native()?; let selector = selector.to_native(); let state = maplibre_core::string::buffer_view(state); - // SAFETY: map is live and all borrowed storage remains valid for the call. - maplibre_core::check(unsafe { - sys::mln_map_set_feature_state(map, selector.as_ptr(), state) - }) + // SAFETY: map is live and all borrowed storage remains valid for the + // synchronous submission; the C API copies selector and state bytes + // before return. + crate::completion::submit( + |completion| unsafe { + sys::mln_map_set_feature_state(map, selector.as_ptr(), state, completion) + }, + crate::completion::command, + ) } - /// Copies per-feature state from this map. - pub fn get_feature_state(&self, selector: &FeatureStateSelector) -> Result> { + /// Copies per-feature state from this map. The read observes every map + /// command accepted before it, and missing state is an empty JSON object. + pub fn get_feature_state( + &self, + selector: &FeatureStateSelector, + ) -> Result>> { let map = self.inner.native()?; let selector = selector.to_native(); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live, selector storage remains valid, and out is writable. - maplibre_core::check(unsafe { - sys::mln_map_get_feature_state(map, selector.as_ptr(), out.as_mut_ptr()) - })?; - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(out.get()) } + // SAFETY: map is live and selector storage remains valid for the + // synchronous submission; the C API copies selector bytes before + // return. + crate::completion::submit( + |completion| unsafe { + sys::mln_map_get_feature_state(map, selector.as_ptr(), completion) + }, + crate::completion::buffer, + ) } - /// Removes per-feature state selected on this map. - pub fn remove_feature_state(&self, selector: &FeatureStateSelector) -> Result<()> { + /// Removes per-feature state selected on this map. A selector with a state + /// key removes one key, one with only a feature ID removes that feature's + /// state, and one with neither removes all state for the source. + pub fn remove_feature_state( + &self, + selector: &FeatureStateSelector, + ) -> Result> { let map = self.inner.native()?; let selector = selector.to_native(); - // SAFETY: map is live and selector storage remains valid for the call. - maplibre_core::check(unsafe { sys::mln_map_remove_feature_state(map, selector.as_ptr()) }) + // SAFETY: map is live and selector storage remains valid for the + // synchronous submission; the C API copies selector bytes before + // return. + crate::completion::submit( + |completion| unsafe { + sys::mln_map_remove_feature_state(map, selector.as_ptr(), completion) + }, + crate::completion::command, + ) } /// Copies the style document this map's style was last parsed from: the /// string given to [`Self::set_style_json`] or the body fetched for /// [`Self::set_style_url`], byte for byte. Runtime mutations do not change /// it. An empty buffer means no document has been parsed. - pub fn loaded_style_json(&self) -> Result> { + pub fn loaded_style_json(&self) -> Result>> { let map = self.inner.native()?; - // SAFETY: map is live, and each call writes only through the pointers - // it is given. - unsafe { - copy_bytes(|text, capacity, out_size| { - sys::mln_map_copy_loaded_style_json(map, text, capacity, out_size) - }) - } + crate::completion::submit( + move |completion| unsafe { sys::mln_map_loaded_style_json(map, completion) }, + crate::completion::buffer, + ) } /// Copies the URL this map's style was last requested from. @@ -107,15 +139,12 @@ impl super::MapHandle { /// the response arrives, and [`Self::set_style_json`] clears it, so this can /// disagree with [`Self::loaded_style_json`] while a load is in flight. An /// empty string means no URL bytes are available. - pub fn style_url(&self) -> Result { + pub fn style_url(&self) -> Result> { let map = self.inner.native()?; - // SAFETY: map is live, and each call writes only through the pointers - // it is given. - unsafe { - copy_text(|url, capacity, out_size| { - sys::mln_map_copy_style_url(map, url, capacity, out_size) - }) - } + crate::completion::submit( + move |completion| unsafe { sys::mln_map_style_url(map, completion) }, + crate::completion::string, + ) } /// Adds a custom geometry source to the current style. @@ -123,13 +152,13 @@ impl super::MapHandle { /// The callback state is scoped to this map's current style. The C API /// frees it once it stops referencing it, whether the source is removed, /// dropped by a style load, or retired with the map. Native may invoke - /// callbacks from worker threads, so queue owner-thread work before calling - /// map APIs. + /// callbacks from worker threads, so schedule host-context work before + /// calling map APIs. pub fn add_custom_geometry_source( &self, source_id: &str, options: CustomGeometrySourceOptions, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id_view = maplibre_core::string::string_view(source_id); let state = CustomGeometrySourceState::new(options); @@ -139,16 +168,22 @@ impl super::MapHandle { let state = Box::into_raw(state); // SAFETY: map is live, source_id_view is valid for this call, and // descriptor names callback state that lives until the release callback. - let status = unsafe { - sys::mln_map_add_custom_geometry_source(map, source_id_view.raw(), &descriptor) - }; - if let Err(error) = maplibre_core::check(status) { - // SAFETY: A rejected add releases nothing, so this box is still - // this call's to free. + let result = crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_custom_geometry_source( + map, + source_id_view.raw(), + &descriptor, + completion, + ) + }, + crate::completion::command, + ); + if result.is_err() { + // SAFETY: rejected submissions retain ownership of callback state. drop(unsafe { Box::from_raw(state) }); - return Err(error); } - Ok(()) + result } /// Sets custom geometry source data for one canonical tile. @@ -157,20 +192,24 @@ impl super::MapHandle { source_id: &str, tile_id: CanonicalTileId, data: &[u8], - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let data = maplibre_core::string::buffer_view(data); // SAFETY: map is live, source_id is valid for this call, tile_id is // passed by value, and data remains valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_custom_geometry_source_tile_data( - map, - source_id.raw(), - tile_id.to_native(), - data, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_custom_geometry_source_tile_data( + map, + source_id.raw(), + tile_id.to_native(), + data, + completion, + ) + }, + crate::completion::command, + ) } /// Invalidates custom geometry source data for one canonical tile. @@ -178,18 +217,22 @@ impl super::MapHandle { &self, source_id: &str, tile_id: CanonicalTileId, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live, source_id is valid for this call, and tile_id is // passed by value. - maplibre_core::check(unsafe { - sys::mln_map_invalidate_custom_geometry_source_tile( - map, - source_id.raw(), - tile_id.to_native(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_invalidate_custom_geometry_source_tile( + map, + source_id.raw(), + tile_id.to_native(), + completion, + ) + }, + crate::completion::command, + ) } /// Invalidates custom geometry source data inside a geographic region. @@ -197,18 +240,22 @@ impl super::MapHandle { &self, source_id: &str, bounds: LatLngBounds, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live, source_id is valid for this call, and bounds is // passed by value. - maplibre_core::check(unsafe { - sys::mln_map_invalidate_custom_geometry_source_region( - map, - source_id.raw(), - bounds.to_native(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_invalidate_custom_geometry_source_region( + map, + source_id.raw(), + bounds.to_native(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds a custom MVT vector source to the current style. @@ -216,13 +263,13 @@ impl super::MapHandle { /// The callback state is scoped to this map's current style. The C API /// frees it once it stops referencing it, whether the source is removed, /// dropped by a style load, or retired with the map. Native may invoke - /// callbacks from worker threads, so queue owner-thread work before calling - /// map APIs. + /// callbacks from worker threads, so schedule host-context work before + /// calling map APIs. pub fn add_custom_mvt_vector_source( &self, source_id: &str, options: CustomMvtVectorSourceOptions, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id_view = maplibre_core::string::string_view(source_id); let state = CustomMvtVectorSourceState::new(options); @@ -232,16 +279,22 @@ impl super::MapHandle { let state = Box::into_raw(state); // SAFETY: map is live, source_id_view is valid for this call, and // descriptor names callback state that lives until the release callback. - let status = unsafe { - sys::mln_map_add_custom_mvt_vector_source(map, source_id_view.raw(), &descriptor) - }; - if let Err(error) = maplibre_core::check(status) { - // SAFETY: A rejected add releases nothing, so this box is still - // this call's to free. + let result = crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_custom_mvt_vector_source( + map, + source_id_view.raw(), + &descriptor, + completion, + ) + }, + crate::completion::command, + ); + if result.is_err() { + // SAFETY: rejected submissions retain ownership of callback state. drop(unsafe { Box::from_raw(state) }); - return Err(error); } - Ok(()) + result } /// Sets custom MVT vector source data for one canonical tile. @@ -253,20 +306,24 @@ impl super::MapHandle { source_id: &str, tile_id: CanonicalTileId, data: &[u8], - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let data = maplibre_core::string::buffer_view(data); // SAFETY: map is live, source_id is valid for this call, tile_id is // passed by value, and data remains valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_custom_mvt_vector_source_tile_data( - map, - source_id.raw(), - tile_id.to_native(), - data, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_custom_mvt_vector_source_tile_data( + map, + source_id.raw(), + tile_id.to_native(), + data, + completion, + ) + }, + crate::completion::command, + ) } /// Reports a custom MVT vector source error for one canonical tile. @@ -275,20 +332,24 @@ impl super::MapHandle { source_id: &str, tile_id: CanonicalTileId, message: &str, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let message = maplibre_core::string::string_view(message); // SAFETY: map is live, source_id and message are valid for this call, // and tile_id is passed by value. - maplibre_core::check(unsafe { - sys::mln_map_set_custom_mvt_vector_source_tile_error( - map, - source_id.raw(), - tile_id.to_native(), - message.raw(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_custom_mvt_vector_source_tile_error( + map, + source_id.raw(), + tile_id.to_native(), + message.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Invalidates custom MVT vector source data for one canonical tile. @@ -296,30 +357,41 @@ impl super::MapHandle { &self, source_id: &str, tile_id: CanonicalTileId, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live, source_id is valid for this call, and tile_id is // passed by value. - maplibre_core::check(unsafe { - sys::mln_map_invalidate_custom_mvt_vector_source_tile( - map, - source_id.raw(), - tile_id.to_native(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_invalidate_custom_mvt_vector_source_tile( + map, + source_id.raw(), + tile_id.to_native(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds one style source from a style-spec source JSON object. - pub fn add_style_source_json(&self, source_id: &str, source_json: &[u8]) -> Result<()> { + pub fn add_style_source_json( + &self, + source_id: &str, + source_json: &[u8], + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let source_json = maplibre_core::string::buffer_view(source_json); // SAFETY: map is live, source_id and source_json are explicit-length // views valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_style_source_json(map, source_id.raw(), source_json) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_style_source_json(map, source_id.raw(), source_json, completion) + }, + crate::completion::command, + ) } /// Adds a vector source with a TileJSON URL. @@ -328,7 +400,7 @@ impl super::MapHandle { source_id: &str, url: &str, options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); @@ -338,9 +410,18 @@ impl super::MapHandle { .map_or(ptr::null(), NativeTileSourceOptions::as_ptr); // SAFETY: map is live, source_id and url are valid for this call, and // options_ptr is null or points to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_vector_source_url(map, source_id.raw(), url.raw(), options_ptr) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_vector_source_url( + map, + source_id.raw(), + url.raw(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a vector source with inline tile URLs. @@ -349,7 +430,7 @@ impl super::MapHandle { source_id: &str, tiles: &[S], options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let raw_tiles = NativeTileUrls::new(tiles); @@ -360,15 +441,19 @@ impl super::MapHandle { // SAFETY: map is live, source_id is valid for this call, raw_tiles // points to call-scoped string views, and options_ptr is null or points // to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_vector_source_tiles( - map, - source_id.raw(), - raw_tiles.as_ptr(), - raw_tiles.len(), - options_ptr, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_vector_source_tiles( + map, + source_id.raw(), + raw_tiles.as_ptr(), + raw_tiles.len(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a raster source with a TileJSON URL. @@ -377,7 +462,7 @@ impl super::MapHandle { source_id: &str, url: &str, options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); @@ -387,9 +472,18 @@ impl super::MapHandle { .map_or(ptr::null(), NativeTileSourceOptions::as_ptr); // SAFETY: map is live, source_id and url are valid for this call, and // options_ptr is null or points to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_raster_source_url(map, source_id.raw(), url.raw(), options_ptr) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_raster_source_url( + map, + source_id.raw(), + url.raw(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a raster source with inline tile URLs. @@ -398,7 +492,7 @@ impl super::MapHandle { source_id: &str, tiles: &[S], options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let raw_tiles = NativeTileUrls::new(tiles); @@ -409,15 +503,19 @@ impl super::MapHandle { // SAFETY: map is live, source_id is valid for this call, raw_tiles // points to call-scoped string views, and options_ptr is null or points // to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_raster_source_tiles( - map, - source_id.raw(), - raw_tiles.as_ptr(), - raw_tiles.len(), - options_ptr, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_raster_source_tiles( + map, + source_id.raw(), + raw_tiles.as_ptr(), + raw_tiles.len(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a raster DEM source with a TileJSON URL. @@ -426,7 +524,7 @@ impl super::MapHandle { source_id: &str, url: &str, options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); @@ -436,9 +534,18 @@ impl super::MapHandle { .map_or(ptr::null(), NativeTileSourceOptions::as_ptr); // SAFETY: map is live, source_id and url are valid for this call, and // options_ptr is null or points to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_raster_dem_source_url(map, source_id.raw(), url.raw(), options_ptr) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_raster_dem_source_url( + map, + source_id.raw(), + url.raw(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a raster DEM source with inline tile URLs. @@ -447,7 +554,7 @@ impl super::MapHandle { source_id: &str, tiles: &[S], options: Option<&TileSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let raw_tiles = NativeTileUrls::new(tiles); @@ -458,15 +565,19 @@ impl super::MapHandle { // SAFETY: map is live, source_id is valid for this call, raw_tiles // points to call-scoped string views, and options_ptr is null or points // to call-scoped native options. - maplibre_core::check(unsafe { - sys::mln_map_add_raster_dem_source_tiles( - map, - source_id.raw(), - raw_tiles.as_ptr(), - raw_tiles.len(), - options_ptr, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_raster_dem_source_tiles( + map, + source_id.raw(), + raw_tiles.as_ptr(), + raw_tiles.len(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds an image source that loads its image from a URL. @@ -479,7 +590,7 @@ impl super::MapHandle { source_id: &str, coordinates: &[LatLng; 4], url: &str, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let coordinates = lat_lngs_to_native(coordinates); @@ -487,15 +598,19 @@ impl super::MapHandle { // SAFETY: map is live, source_id and url are explicit-length views // valid for this call, and coordinates points to call-scoped native // coordinate storage. Native validates coordinate contents. - maplibre_core::check(unsafe { - sys::mln_map_add_image_source_url( - map, - source_id.raw(), - const_ptr_or_null(&coordinates), - coordinates.len(), - url.raw(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_image_source_url( + map, + source_id.raw(), + const_ptr_or_null(&coordinates), + coordinates.len(), + url.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds an image source with inline premultiplied RGBA8 pixels. @@ -508,7 +623,7 @@ impl super::MapHandle { source_id: &str, coordinates: &[LatLng; 4], image: &PremultipliedRgba8Image, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let coordinates = lat_lngs_to_native(coordinates); @@ -516,27 +631,38 @@ impl super::MapHandle { // SAFETY: map is live, source_id is an explicit-length view valid for // this call, coordinates points to call-scoped native coordinate // storage, and image points into the borrowed Rust image for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_image_source_image( - map, - source_id.raw(), - const_ptr_or_null(&coordinates), - coordinates.len(), - &image, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_image_source_image( + map, + source_id.raw(), + const_ptr_or_null(&coordinates), + coordinates.len(), + &image, + completion, + ) + }, + crate::completion::command, + ) } /// Updates an image source to load its image from a URL. - pub fn set_image_source_url(&self, source_id: &str, url: &str) -> Result<()> { + pub fn set_image_source_url( + &self, + source_id: &str, + url: &str, + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); // SAFETY: map is live, and source_id and url are explicit-length views // valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_image_source_url(map, source_id.raw(), url.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_image_source_url(map, source_id.raw(), url.raw(), completion) + }, + crate::completion::command, + ) } /// Updates an image source with inline premultiplied RGBA8 pixels. @@ -544,15 +670,18 @@ impl super::MapHandle { &self, source_id: &str, image: &PremultipliedRgba8Image, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let image = maplibre_core::values::premultiplied_rgba8_image_to_native(image); // SAFETY: map is live, source_id is an explicit-length view valid for // this call, and image points into the borrowed Rust image for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_image_source_image(map, source_id.raw(), &image) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_image_source_image(map, source_id.raw(), &image, completion) + }, + crate::completion::command, + ) } /// Updates image source coordinates. @@ -564,86 +693,70 @@ impl super::MapHandle { &self, source_id: &str, coordinates: &[LatLng; 4], - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let coordinates = lat_lngs_to_native(coordinates); // SAFETY: map is live, source_id is an explicit-length view valid for // this call, and coordinates points to call-scoped native coordinate // storage. Native validates coordinate contents. - maplibre_core::check(unsafe { - sys::mln_map_set_image_source_coordinates( - map, - source_id.raw(), - const_ptr_or_null(&coordinates), - coordinates.len(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_image_source_coordinates( + map, + source_id.raw(), + const_ptr_or_null(&coordinates), + coordinates.len(), + completion, + ) + }, + crate::completion::command, + ) } /// Copies image source coordinates into owned Rust values. - pub fn image_source_coordinates(&self, source_id: &str) -> Result> { + pub fn image_source_coordinates( + &self, + source_id: &str, + ) -> Result>> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); - let mut coordinates = [sys::mln_lat_lng { - latitude: 0.0, - longitude: 0.0, - }; 4]; - let mut coordinate_count = 0; - let mut found = false; - // SAFETY: map is live, source_id is an explicit-length view valid for - // this call, coordinates has capacity for four native coordinates, and - // output pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_image_source_coordinates( - map, - source_id.raw(), - coordinates.as_mut_ptr(), - coordinates.len(), - &mut coordinate_count, - &mut found, - ) - })?; - if !found { - return Ok(None); - } - if coordinate_count != coordinates.len() { - return Err(Error::new( - ErrorKind::NativeError, - None, - "native image source coordinate count did not match Rust image source invariant", - )); - } - Ok(Some(coordinates.map(LatLng::from_native))) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_image_source_coordinates(map, source_id.raw(), completion) + }, + |result| { + let values = crate::completion::copy_slice::(result)?; + if values.is_empty() { + return Ok(None); + } + let values: [sys::mln_lat_lng; 4] = values.try_into().map_err(|_| { + crate::Error::new( + crate::ErrorKind::NativeError, + None, + "image coordinates completion did not contain four coordinates", + ) + })?; + Ok(Some(values.map(LatLng::from_native))) + }, + ) } /// Removes one style source by ID. /// - /// Returns whether a source existed and was removed. Native returns an - /// error when a layer still uses the source. - pub fn remove_style_source(&self, source_id: &str) -> Result { + /// The command's finished event reports `Failed` with a not-found status + /// code when no source has the ID, and `Failed` with an invalid-state + /// status code when a layer still uses the source. + pub fn remove_style_source(&self, source_id: &str) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); - let mut removed = false; - // SAFETY: map is live, source_id is an explicit-length view valid for - // this call, and removed points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_remove_style_source(map, source_id.raw(), &mut removed) - })?; - Ok(removed) - } - - /// Reports whether a style source ID exists. - pub fn style_source_exists(&self, source_id: &str) -> Result { - let map = self.inner.native()?; - let source_id = maplibre_core::string::string_view(source_id); - let mut exists = false; - // SAFETY: map is live, source_id is an explicit-length view valid for - // this call, and exists points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_style_source_exists(map, source_id.raw(), &mut exists) - })?; - Ok(exists) + // SAFETY: map is live and source_id is valid for this call. + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_remove_style_source(map, source_id.raw(), completion) + }, + crate::completion::command, + ) } /// Adds or replaces one runtime style image. @@ -652,7 +765,7 @@ impl super::MapHandle { image_id: &str, image: &PremultipliedRgba8Image, options: Option<&StyleImageOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let image_id = maplibre_core::string::string_view(image_id); let image = maplibre_core::values::premultiplied_rgba8_image_to_native(image); @@ -663,296 +776,52 @@ impl super::MapHandle { // SAFETY: map is live, image_id is an explicit-length view valid for // this call, image points into the borrowed Rust image for this call, // and options_ptr is either null or points to call-scoped options. - maplibre_core::check(unsafe { - sys::mln_map_set_style_image(map, image_id.raw(), &image, options_ptr) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_style_image(map, image_id.raw(), &image, options_ptr, completion) + }, + crate::completion::command, + ) } /// Removes one runtime style image by ID. /// - /// Returns whether an image existed and was removed. - pub fn remove_style_image(&self, image_id: &str) -> Result { + /// The command's finished event reports `Failed` with a not-found status + /// code when no runtime image has the ID. + pub fn remove_style_image(&self, image_id: &str) -> Result> { let map = self.inner.native()?; let image_id = maplibre_core::string::string_view(image_id); - let mut removed = false; - // SAFETY: map is live, image_id is an explicit-length view valid for - // this call, and removed points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_remove_style_image(map, image_id.raw(), &mut removed) - })?; - Ok(removed) - } - - /// Reports whether a runtime style image ID exists. - pub fn style_image_exists(&self, image_id: &str) -> Result { - let map = self.inner.native()?; - let image_id = maplibre_core::string::string_view(image_id); - let mut exists = false; - // SAFETY: map is live, image_id is an explicit-length view valid for - // this call, and exists points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_style_image_exists(map, image_id.raw(), &mut exists) - })?; - Ok(exists) - } - - /// Copies fixed metadata for one runtime style image. - pub fn style_image_info(&self, image_id: &str) -> Result> { - let map = self.inner.native()?; - let image_id = maplibre_core::string::string_view(image_id); - let mut info = maplibre_core::style::empty_style_image_info(); - let mut found = false; - // SAFETY: map is live, image_id is an explicit-length view valid for - // this call, info has its ABI size initialized, and found points to - // writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_image_info(map, image_id.raw(), &mut info, &mut found) - })?; - Ok(found.then(|| maplibre_core::values::style_image_info_from_native(&info))) + // SAFETY: map is live and image_id is valid for this call. + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_remove_style_image(map, image_id.raw(), completion) + }, + crate::completion::command, + ) } - /// Copies one runtime style image into owned tightly packed premultiplied RGBA8 pixels. - pub fn copy_style_image_premultiplied_rgba8( - &self, - image_id: &str, - ) -> Result> { + /// Copies one runtime style image and all of its metadata. + pub fn style_image(&self, image_id: &str) -> Result>> { let map = self.inner.native()?; let image_id = maplibre_core::string::string_view(image_id); - let mut raw_info = maplibre_core::style::empty_style_image_info(); - let mut info_found = false; - // SAFETY: map is live, image_id is an explicit-length view valid for - // this call, raw_info has its ABI size initialized, and info_found - // points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_image_info(map, image_id.raw(), &mut raw_info, &mut info_found) - })?; - if !info_found { - return Ok(None); - } - let info = maplibre_core::values::style_image_info_from_native(&raw_info); - - let mut data = vec![0u8; info.byte_length]; - let mut copied_size = 0; - let mut found = false; - let pixels = if data.is_empty() { - ptr::null_mut() - } else { - data.as_mut_ptr() - }; - // SAFETY: map is live, image_id remains valid for this call, data is - // writable for info.byte_length bytes (or null with zero capacity), and - // output pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_premultiplied_rgba8( - map, - image_id.raw(), - pixels, - data.len(), - &mut copied_size, - &mut found, - ) - })?; - if !found { - return Ok(None); - } - maplibre_core::style::style_image_from_copied_premultiplied_rgba8(info, data, copied_size) - .map(Some) - } - - /// Gets one style source type. - pub fn style_source_type(&self, source_id: &str) -> Result> { - let map = self.inner.native()?; - let source_id = maplibre_core::string::string_view(source_id); - let mut raw_source_type = sys::MLN_STYLE_SOURCE_TYPE_UNKNOWN; - let mut found = false; - // SAFETY: map is live, source_id is an explicit-length view valid for - // this call, and output pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_type( - map, - source_id.raw(), - &mut raw_source_type, - &mut found, - ) - })?; - Ok(found.then(|| SourceType::from_raw(raw_source_type))) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_style_image_info(map, image_id.raw(), completion) + }, + copy_style_image, + ) } /// Copies retained metadata for one style source. - pub fn style_source_info(&self, source_id: &str) -> Result> { + pub fn style_source_info(&self, source_id: &str) -> Result>> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); - let mut info = maplibre_core::style::empty_style_source_info(); - let mut found = false; - // SAFETY: map is live, source_id is an explicit-length view valid for - // this call, info has its ABI size initialized, and found points to - // writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_info(map, source_id.raw(), &mut info, &mut found) - })?; - if !found { - return Ok(None); - } - - let attribution = if info.has_attribution { - match self.copy_style_source_attribution(map, source_id.raw(), info.attribution_size)? { - Some(attribution) => Some(attribution), - None => return Ok(None), - } - } else { - None - }; - - let url = if info.fields & sys::MLN_STYLE_SOURCE_INFO_URL != 0 { - match self.copy_style_source_url(map, source_id.raw(), info.url_size)? { - Some(url) => Some(url), - None => return Ok(None), - } - } else { - None - }; - - let tiles = if info.fields & sys::MLN_STYLE_SOURCE_INFO_TILEJSON != 0 { - match self.copy_style_source_tile_urls(map, source_id.raw())? { - Some(tiles) => tiles, - None => return Ok(None), - } - } else { - Vec::new() - }; - - Ok(Some(maplibre_core::style::style_source_info_from_native( - &info, - attribution, - url, - tiles, - ))) - } - - fn copy_style_source_attribution( - &self, - map: sys::mln_map, - source_id: sys::mln_buffer_view, - attribution_size: usize, - ) -> Result> { - if attribution_size == 0 { - let mut copied_size = 0; - let mut found = false; - // SAFETY: map is live, source_id remains valid for this call, - // capacity is zero so the output buffer may be null, and output - // pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_source_attribution( - map, - source_id, - ptr::null_mut(), - 0, - &mut copied_size, - &mut found, - ) - })?; - return Ok(found.then(String::new)); - } - - let mut buffer = vec![0u8; attribution_size]; - let mut copied_size = 0; - let mut found = false; - // SAFETY: map is live, source_id remains valid for this call, buffer is - // writable for attribution_size bytes, and output pointers refer to - // writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_source_attribution( - map, - source_id, - buffer.as_mut_ptr().cast(), - buffer.len(), - &mut copied_size, - &mut found, - ) - })?; - if !found { - return Ok(None); - } - if copied_size > buffer.len() { - return Err(Error::new( - ErrorKind::NativeError, - None, - "native style source attribution size exceeded caller buffer", - )); - } - buffer.truncate(copied_size); - String::from_utf8(buffer).map(Some).map_err(|error| { - Error::invalid_argument(format!( - "native style source attribution was not valid UTF-8: {error}" - )) - }) - } - - fn copy_style_source_url( - &self, - map: sys::mln_map, - source_id: sys::mln_buffer_view, - url_size: usize, - ) -> Result> { - let mut buffer = vec![0u8; url_size]; - let mut copied_size = 0; - let mut found = false; - // SAFETY: map and source_id remain live for this call, the buffer is - // writable for url_size bytes or null-equivalent when empty, and the - // output pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_source_url( - map, - source_id, - if buffer.is_empty() { - ptr::null_mut() - } else { - buffer.as_mut_ptr().cast() - }, - buffer.len(), - &mut copied_size, - &mut found, - ) - })?; - if !found { - return Ok(None); - } - if copied_size > buffer.len() { - return Err(Error::new( - ErrorKind::NativeError, - None, - "native style source URL size exceeded caller buffer", - )); - } - buffer.truncate(copied_size); - String::from_utf8(buffer).map(Some).map_err(|error| { - Error::invalid_argument(format!( - "native style source URL was not valid UTF-8: {error}" - )) - }) - } - - fn copy_style_source_tile_urls( - &self, - map: sys::mln_map, - source_id: sys::mln_buffer_view, - ) -> Result>> { - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut found = false; - // SAFETY: map and source_id remain live for this call, out is a - // null-initialized output handle, and found points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_source_tile_urls(map, source_id, out.as_mut_ptr(), &mut found) - })?; - if !found { - return Ok(None); - } - // SAFETY: A found source returns an owned style string list; core - // copies every borrowed view and releases the list on all paths. - unsafe { - maplibre_core::style::copy_style_string_list(out.into_live("mln_style_string_list")?) - .map(Some) - } + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_style_source_info(map, source_id.raw(), completion) + }, + copy_source_info, + ) } /// Adds a GeoJSON source that loads data from a URL. @@ -962,7 +831,7 @@ impl super::MapHandle { source_id: &str, url: &str, options: Option<&GeoJsonSourceOptions>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); @@ -975,9 +844,18 @@ impl super::MapHandle { // SAFETY: map is live, source_id and url are valid for this call, and // options_ptr is null or points to call-scoped native options that keep // the cluster-properties buffer alive. - maplibre_core::check(unsafe { - sys::mln_map_add_geojson_source_url(map, source_id.raw(), url.raw(), options_ptr) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_geojson_source_url( + map, + source_id.raw(), + url.raw(), + options_ptr, + completion, + ) + }, + crate::completion::command, + ) } /// Adds a GeoJSON source with prepared inline data. @@ -988,27 +866,42 @@ impl super::MapHandle { &self, source_id: &str, data: &crate::GeoJsonSourceDataHandle, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live, source_id is valid for this call, and data is a // live prepared-data handle the call only borrows. - maplibre_core::check(unsafe { - sys::mln_map_add_geojson_source_data(map, source_id.raw(), data.native()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_geojson_source_data( + map, + source_id.raw(), + data.native(), + completion, + ) + }, + crate::completion::command, + ) } /// Updates one GeoJSON source to load data from a URL. /// /// The source keeps the options it was added with. - pub fn set_geojson_source_url(&self, source_id: &str, url: &str) -> Result<()> { + pub fn set_geojson_source_url( + &self, + source_id: &str, + url: &str, + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); let url = maplibre_core::string::string_view(url); // SAFETY: map is live and source_id and url are valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_url(map, source_id.raw(), url.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_geojson_source_url(map, source_id.raw(), url.raw(), completion) + }, + crate::completion::command, + ) } /// Updates one GeoJSON source with prepared inline data. @@ -1021,14 +914,22 @@ impl super::MapHandle { &self, source_id: &str, data: &crate::GeoJsonSourceDataHandle, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live, source_id is valid for this call, and data is a // live prepared-data handle the call only borrows. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_data(map, source_id.raw(), data.native()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_geojson_source_data( + map, + source_id.raw(), + data.native(), + completion, + ) + }, + crate::completion::command, + ) } /// Overrides one GeoJSON source's synchronous tiling at runtime. @@ -1036,18 +937,26 @@ impl super::MapHandle { /// While enabled, the source slices requested tiles inline during the /// update pass, as if its options had set `synchronous_tiling`; disabling /// restores the option the source was added with. The override applies to - /// update passes after this call returns. + /// update passes after the command commits. pub fn set_geojson_source_synchronous_tiling( &self, source_id: &str, enabled: bool, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live and source_id is valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_geojson_source_synchronous_tiling(map, source_id.raw(), enabled) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_geojson_source_synchronous_tiling( + map, + source_id.raw(), + enabled, + completion, + ) + }, + crate::completion::command, + ) } /// Adds one style layer from a full style-spec layer JSON object. @@ -1055,15 +964,23 @@ impl super::MapHandle { &self, layer_json: &[u8], before_layer_id: Option<&str>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_json = maplibre_core::string::buffer_view(layer_json); let before_layer_id = maplibre_core::string::string_view(before_layer_id.unwrap_or("")); // SAFETY: map is live, and layer_json and before_layer_id are // explicit-length views valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_style_layer_json(map, layer_json, before_layer_id.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_style_layer_json( + map, + layer_json, + before_layer_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds a hillshade layer for a raster DEM source. @@ -1072,20 +989,24 @@ impl super::MapHandle { layer_id: &str, source_id: &str, before_layer_id: Option<&str>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let source_id = maplibre_core::string::string_view(source_id); let before_layer_id = maplibre_core::string::string_view(before_layer_id.unwrap_or("")); // SAFETY: map is live, and all string views are valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_hillshade_layer( - map, - layer_id.raw(), - source_id.raw(), - before_layer_id.raw(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_hillshade_layer( + map, + layer_id.raw(), + source_id.raw(), + before_layer_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds a color-relief layer for a raster DEM source. @@ -1094,20 +1015,24 @@ impl super::MapHandle { layer_id: &str, source_id: &str, before_layer_id: Option<&str>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let source_id = maplibre_core::string::string_view(source_id); let before_layer_id = maplibre_core::string::string_view(before_layer_id.unwrap_or("")); // SAFETY: map is live, and all string views are valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_color_relief_layer( - map, - layer_id.raw(), - source_id.raw(), - before_layer_id.raw(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_color_relief_layer( + map, + layer_id.raw(), + source_id.raw(), + before_layer_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Adds a source-free location indicator layer. @@ -1115,14 +1040,22 @@ impl super::MapHandle { &self, layer_id: &str, before_layer_id: Option<&str>, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let before_layer_id = maplibre_core::string::string_view(before_layer_id.unwrap_or("")); // SAFETY: map is live, and string views are valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_add_location_indicator_layer(map, layer_id.raw(), before_layer_id.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_add_location_indicator_layer( + map, + layer_id.raw(), + before_layer_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Sets a location indicator layer location. @@ -1131,29 +1064,45 @@ impl super::MapHandle { layer_id: &str, coordinate: LatLng, altitude: f64, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live, layer_id is valid for this call, and coordinate // is passed by value. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_location( - map, - layer_id.raw(), - coordinate.to_native(), - altitude, - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_location_indicator_location( + map, + layer_id.raw(), + coordinate.to_native(), + altitude, + completion, + ) + }, + crate::completion::command, + ) } /// Sets a location indicator layer bearing in degrees. - pub fn set_location_indicator_bearing(&self, layer_id: &str, bearing: f64) -> Result<()> { + pub fn set_location_indicator_bearing( + &self, + layer_id: &str, + bearing: f64, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live and layer_id is valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_bearing(map, layer_id.raw(), bearing) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_location_indicator_bearing( + map, + layer_id.raw(), + bearing, + completion, + ) + }, + crate::completion::command, + ) } /// Sets a location indicator layer accuracy radius in meters. @@ -1161,13 +1110,21 @@ impl super::MapHandle { &self, layer_id: &str, radius: f64, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live and layer_id is valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_accuracy_radius(map, layer_id.raw(), radius) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_location_indicator_accuracy_radius( + map, + layer_id.raw(), + radius, + completion, + ) + }, + crate::completion::command, + ) } /// Sets one location indicator image-name property. @@ -1176,96 +1133,118 @@ impl super::MapHandle { layer_id: &str, image_kind: LocationIndicatorImageKind, image_id: &str, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let image_id = maplibre_core::string::string_view(image_id); // SAFETY: map is live, string views are valid for this call, and // image_kind is a valid C enum value. - maplibre_core::check(unsafe { - sys::mln_map_set_location_indicator_image_name( - map, - layer_id.raw(), - image_kind.raw_value(), - image_id.raw(), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_location_indicator_image_name( + map, + layer_id.raw(), + image_kind.raw_value(), + image_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Copies one style layer as a full style-spec JSON object. - pub fn style_layer_json(&self, layer_id: &str) -> Result>> { + pub fn style_layer_json(&self, layer_id: &str) -> Result>>> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut found = false; - // SAFETY: map is live, layer_id is valid for this call, out is a - // null-initialized out-pointer, and found points to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_style_layer_json(map, layer_id.raw(), out.as_mut_ptr(), &mut found) - })?; - if !found { - return Ok(None); - } - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(out.get()) }.map(Some) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_style_layer_json(map, layer_id.raw(), completion) + }, + crate::completion::optional_buffer, + ) } /// Sets the style light from a style-spec light JSON object. - pub fn set_style_light_json(&self, light_json: &[u8]) -> Result<()> { + pub fn set_style_light_json( + &self, + light_json: &[u8], + ) -> Result> { let map = self.inner.native()?; let light_json = maplibre_core::string::buffer_view(light_json); // SAFETY: map is live and light_json remains valid for this call. - maplibre_core::check(unsafe { sys::mln_map_set_style_light_json(map, light_json) }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_style_light_json(map, light_json, completion) + }, + crate::completion::command, + ) } /// Sets one style light property. - pub fn set_style_light_property(&self, property_name: &str, value: &[u8]) -> Result<()> { + pub fn set_style_light_property( + &self, + property_name: &str, + value: &[u8], + ) -> Result> { let map = self.inner.native()?; let property_name = maplibre_core::string::string_view(property_name); let value = maplibre_core::string::buffer_view(value); // SAFETY: map is live, and property_name and value remain valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_style_light_property(map, property_name.raw(), value) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_style_light_property(map, property_name.raw(), value, completion) + }, + crate::completion::command, + ) } /// Copies one style light property as a style-spec JSON value. - pub fn style_light_property(&self, property_name: &str) -> Result>> { + pub fn style_light_property( + &self, + property_name: &str, + ) -> Result>>> { let map = self.inner.native()?; let property_name = maplibre_core::string::string_view(property_name); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live, property_name is valid for this call, and out is - // a null-initialized out-pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_style_light_property(map, property_name.raw(), out.as_mut_ptr()) - })?; - let Some(buffer) = out.into_option() else { - return Ok(None); - }; - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(buffer) }.map(Some) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_style_light_property(map, property_name.raw(), completion) + }, + crate::completion::optional_buffer, + ) } /// Sets the style's global transition options. This replaces the whole /// configuration rather than merging, and loading a style replaces it /// again, so apply an override after the style loads. - pub fn set_style_transition_options(&self, options: &StyleTransitionOptions) -> Result<()> { + pub fn set_style_transition_options( + &self, + options: &StyleTransitionOptions, + ) -> Result> { let map = self.inner.native()?; let raw = maplibre_core::style::style_transition_options_to_native(options); // SAFETY: map is live and raw is a fully initialized options struct // borrowed for this call. - maplibre_core::check(unsafe { sys::mln_map_set_style_transition_options(map, &raw) }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_style_transition_options(map, &raw, completion) + }, + crate::completion::command, + ) } /// Reads the style's global transition options. - pub fn style_transition_options(&self) -> Result { - let map = self.inner.native()?; - let mut raw = maplibre_core::style::empty_style_transition_options(); - // SAFETY: map is live and raw has its ABI size initialized. - maplibre_core::check(unsafe { sys::mln_map_get_style_transition_options(map, &mut raw) })?; - Ok(maplibre_core::style::style_transition_options_from_native( - &raw, - )) + pub fn style_transition_options(&self) -> Result> { + let map = self.inner.native()?; + crate::completion::submit( + move |completion| unsafe { sys::mln_map_get_style_transition_options(map, completion) }, + |result| { + let raw = crate::completion::copy_value(result)?; + Ok(maplibre_core::style::style_transition_options_from_native( + &raw, + )) + }, + ) } /// Sets one layer style property. @@ -1274,237 +1253,194 @@ impl super::MapHandle { layer_id: &str, property_name: &str, value: &[u8], - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let property_name = maplibre_core::string::string_view(property_name); let value = maplibre_core::string::buffer_view(value); // SAFETY: map is live, and all string and buffer views remain valid for // this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_property(map, layer_id.raw(), property_name.raw(), value) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_property( + map, + layer_id.raw(), + property_name.raw(), + value, + completion, + ) + }, + crate::completion::command, + ) } /// Copies one layer style property as a style-spec JSON value. - pub fn layer_property(&self, layer_id: &str, property_name: &str) -> Result>> { + pub fn layer_property( + &self, + layer_id: &str, + property_name: &str, + ) -> Result>>> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let property_name = maplibre_core::string::string_view(property_name); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live, string views are valid for this call, and out is - // a null-initialized out-pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_property( - map, - layer_id.raw(), - property_name.raw(), - out.as_mut_ptr(), - ) - })?; - let Some(buffer) = out.into_option() else { - return Ok(None); - }; - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(buffer) }.map(Some) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_layer_property( + map, + layer_id.raw(), + property_name.raw(), + completion, + ) + }, + crate::completion::optional_buffer, + ) } /// Sets or clears one layer filter. - pub fn set_layer_filter(&self, layer_id: &str, filter: Option<&[u8]>) -> Result<()> { + pub fn set_layer_filter( + &self, + layer_id: &str, + filter: Option<&[u8]>, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let native_filter = filter.map(maplibre_core::string::buffer_view); // SAFETY: map is live, layer_id is valid for this call, and the // optional filter descriptor is either null or valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_filter( - map, - layer_id.raw(), - native_filter.as_ref().map_or(ptr::null(), ptr::from_ref), - ) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_filter( + map, + layer_id.raw(), + native_filter.as_ref().map_or(ptr::null(), ptr::from_ref), + completion, + ) + }, + crate::completion::command, + ) } /// Copies one layer filter as a style-spec JSON value. - pub fn layer_filter(&self, layer_id: &str) -> Result>> { + pub fn layer_filter(&self, layer_id: &str) -> Result>>> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live, layer_id is valid for this call, and out is a - // null-initialized out-pointer. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_filter(map, layer_id.raw(), out.as_mut_ptr()) - })?; - let Some(buffer) = out.into_option() else { - return Ok(None); - }; - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(buffer) }.map(Some) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_layer_filter(map, layer_id.raw(), completion) + }, + crate::completion::optional_buffer, + ) } /// Copies one runtime style image's stretchable intervals. /// /// Returns `None` when no image carries `image_id`. - pub fn style_image_stretches( - &self, - image_id: &str, - ) -> Result, Vec)>> { - let map = self.inner.native()?; - let image_id = maplibre_core::string::string_view(image_id); - let mut x_count = 0; - let mut y_count = 0; - let mut found = false; - // SAFETY: map is live, image_id stays valid for this call, both arrays - // are null with zero capacity so this is a size probe, and the output - // pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_stretches( - map, - image_id.raw(), - ptr::null_mut(), - 0, - &mut x_count, - ptr::null_mut(), - 0, - &mut y_count, - &mut found, - ) - })?; - if !found { - return Ok(None); - } - - let mut stretch_x = vec![sys::mln_image_stretch { from: 0.0, to: 0.0 }; x_count]; - let mut stretch_y = vec![sys::mln_image_stretch { from: 0.0, to: 0.0 }; y_count]; - // SAFETY: each buffer is writable for its reported count, and the output - // pointers refer to writable storage. - maplibre_core::check(unsafe { - sys::mln_map_copy_style_image_stretches( - map, - image_id.raw(), - stretch_x.as_mut_ptr(), - stretch_x.len(), - &mut x_count, - stretch_y.as_mut_ptr(), - stretch_y.len(), - &mut y_count, - &mut found, - ) - })?; - let to_public = |stretches: &[sys::mln_image_stretch]| -> Vec { - stretches - .iter() - .map(|stretch| ImageStretch::new(stretch.from, stretch.to)) - .collect() - }; - Ok(Some((to_public(&stretch_x), to_public(&stretch_y)))) - } - /// Sets one layer's source-layer ID. /// /// Layer types that take no source, such as background, are rejected. - pub fn set_layer_source_layer(&self, layer_id: &str, source_layer: &str) -> Result<()> { + pub fn set_layer_source_layer( + &self, + layer_id: &str, + source_layer: &str, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let source_layer = maplibre_core::string::string_view(source_layer); // SAFETY: map is live and both string views stay valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_source_layer(map, layer_id.raw(), source_layer.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_source_layer( + map, + layer_id.raw(), + source_layer.raw(), + completion, + ) + }, + crate::completion::command, + ) } /// Copies one layer's source-layer ID, empty when the layer carries none. - pub fn layer_source_layer(&self, layer_id: &str) -> Result { + pub fn layer_source_layer(&self, layer_id: &str) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); - // SAFETY: map is live, layer_id stays valid for both calls, and each - // call writes only through the pointers it is given. - unsafe { - copy_text(|text, capacity, out_size| { - sys::mln_map_copy_layer_source_layer(map, layer_id.raw(), text, capacity, out_size) - }) - } + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_copy_layer_source_layer(map, layer_id.raw(), completion) + }, + crate::completion::string, + ) } /// Sets one layer's source ID. /// /// Layer types that take no source, such as background, are rejected. The /// named source need not exist yet. - pub fn set_layer_source_id(&self, layer_id: &str, source_id: &str) -> Result<()> { + pub fn set_layer_source_id( + &self, + layer_id: &str, + source_id: &str, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); let source_id = maplibre_core::string::string_view(source_id); // SAFETY: map is live and both string views stay valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_source_id(map, layer_id.raw(), source_id.raw()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_source_id(map, layer_id.raw(), source_id.raw(), completion) + }, + crate::completion::command, + ) } /// Copies one layer's source ID, empty when the layer carries none. - pub fn layer_source_id(&self, layer_id: &str) -> Result { + pub fn layer_source_id(&self, layer_id: &str) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); - // SAFETY: map is live, layer_id stays valid for both calls, and each - // call writes only through the pointers it is given. - unsafe { - copy_text(|text, capacity, out_size| { - sys::mln_map_copy_layer_source_id(map, layer_id.raw(), text, capacity, out_size) - }) - } + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_copy_layer_source_id(map, layer_id.raw(), completion) + }, + crate::completion::string, + ) } /// Sets the lowest zoom at which one layer draws. /// /// Pass `f64::NEG_INFINITY` for no lower bound. - pub fn set_layer_min_zoom(&self, layer_id: &str, min_zoom: f64) -> Result<()> { + pub fn set_layer_min_zoom( + &self, + layer_id: &str, + min_zoom: f64, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live and layer_id stays valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_min_zoom(map, layer_id.raw(), min_zoom) - }) - } - - /// Reads the lowest zoom at which one layer draws. - /// - /// A layer with no lower bound reports `f64::NEG_INFINITY`. - pub fn layer_min_zoom(&self, layer_id: &str) -> Result { - let map = self.inner.native()?; - let layer_id = maplibre_core::string::string_view(layer_id); - let mut min_zoom = 0.0; - // SAFETY: map is live, layer_id stays valid for this call, and min_zoom - // is writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_min_zoom(map, layer_id.raw(), &mut min_zoom) - })?; - Ok(min_zoom) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_min_zoom(map, layer_id.raw(), min_zoom, completion) + }, + crate::completion::command, + ) } /// Sets the highest zoom at which one layer draws. /// /// Pass `f64::INFINITY` for no upper bound. - pub fn set_layer_max_zoom(&self, layer_id: &str, max_zoom: f64) -> Result<()> { + pub fn set_layer_max_zoom( + &self, + layer_id: &str, + max_zoom: f64, + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live and layer_id stays valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_max_zoom(map, layer_id.raw(), max_zoom) - }) - } - - /// Reads the highest zoom at which one layer draws. - /// - /// A layer with no upper bound reports `f64::INFINITY`. - pub fn layer_max_zoom(&self, layer_id: &str) -> Result { - let map = self.inner.native()?; - let layer_id = maplibre_core::string::string_view(layer_id); - let mut max_zoom = 0.0; - // SAFETY: map is live, layer_id stays valid for this call, and max_zoom - // is writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_max_zoom(map, layer_id.raw(), &mut max_zoom) - })?; - Ok(max_zoom) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_max_zoom(map, layer_id.raw(), max_zoom, completion) + }, + crate::completion::command, + ) } /// Sets whether one layer draws. @@ -1512,116 +1448,207 @@ impl super::MapHandle { &self, layer_id: &str, visibility: StyleLayerVisibility, - ) -> Result<()> { + ) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); // SAFETY: map is live and layer_id stays valid for this call. - maplibre_core::check(unsafe { - sys::mln_map_set_layer_visibility(map, layer_id.raw(), visibility.raw_value()) - }) + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_set_layer_visibility( + map, + layer_id.raw(), + visibility.raw_value(), + completion, + ) + }, + crate::completion::command, + ) + } + + /// Copies current style source IDs into owned Rust strings. + pub fn style_source_ids(&self) -> Result>> { + let map = self.inner.native()?; + crate::completion::submit( + move |completion| unsafe { sys::mln_map_list_style_source_ids(map, completion) }, + copy_string_views, + ) } - /// Reads whether one layer draws. - pub fn layer_visibility(&self, layer_id: &str) -> Result { + /// Copies current style layer IDs into owned Rust strings. + pub fn style_layer_ids(&self) -> Result>> { + let map = self.inner.native()?; + crate::completion::submit( + move |completion| unsafe { sys::mln_map_list_style_layer_ids(map, completion) }, + copy_string_views, + ) + } + /// Removes one style layer by ID. + /// + /// The command's finished event reports `Failed` with a not-found status + /// code when no layer has the ID. + pub fn remove_style_layer(&self, layer_id: &str) -> Result> { let map = self.inner.native()?; let layer_id = maplibre_core::string::string_view(layer_id); - let mut visibility = 0; - // SAFETY: map is live, layer_id stays valid for this call, and - // visibility is writable storage. - maplibre_core::check(unsafe { - sys::mln_map_get_layer_visibility(map, layer_id.raw(), &mut visibility) - })?; - Ok(StyleLayerVisibility::from_raw(visibility)) + // SAFETY: map is live and layer_id is valid for this call. + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_remove_style_layer(map, layer_id.raw(), completion) + }, + crate::completion::command, + ) } - /// Copies current style source IDs into owned Rust strings. - pub fn style_source_ids(&self) -> Result> { + /// Copies fixed metadata for one style layer. + /// + /// The operation resolves the layer's type, zoom range, visibility, and + /// source IDs together; its take returns `None` when no layer carries + /// `layer_id`. + pub fn style_layer_info(&self, layer_id: &str) -> Result>> { let map = self.inner.native()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live and out is a null-initialized out-pointer owned by - // this call. On success the returned handle is wrapped and destroyed by - // the copying helper below. - maplibre_core::check(unsafe { sys::mln_map_list_style_source_ids(map, out.as_mut_ptr()) })?; - // SAFETY: On success, the C API returns an owned style ID list handle; - // core copies and releases it. - unsafe { maplibre_core::style::copy_style_id_list(out.into_live("mln_style_id_list")?) } + let layer_id = maplibre_core::string::string_view(layer_id); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_get_style_layer_info(map, layer_id.raw(), completion) + }, + copy_layer_info, + ) } - /// Copies current style layer IDs into owned Rust strings. - pub fn style_layer_ids(&self) -> Result> { + pub fn move_style_layer( + &self, + layer_id: &str, + before_layer_id: Option<&str>, + ) -> Result> { let map = self.inner.native()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map is live and out is a null-initialized out-pointer owned by - // this call. On success the returned handle is wrapped and destroyed by - // the copying helper below. - maplibre_core::check(unsafe { sys::mln_map_list_style_layer_ids(map, out.as_mut_ptr()) })?; - // SAFETY: On success, the C API returns an owned style ID list handle; - // core copies and releases it. - unsafe { maplibre_core::style::copy_style_id_list(out.into_live("mln_style_id_list")?) } + let layer_id = maplibre_core::string::string_view(layer_id); + let before_layer_id = maplibre_core::string::string_view(before_layer_id.unwrap_or("")); + crate::completion::submit( + move |completion| unsafe { + sys::mln_map_move_style_layer( + map, + layer_id.raw(), + before_layer_id.raw(), + completion, + ) + }, + crate::completion::command, + ) } } -/// Probes the required byte length, then copies the text into an owned `String`. -/// -/// # Safety -/// -/// `copy` must forward its arguments to a C entry point that writes at most -/// `capacity` bytes through the text pointer and the required length through the -/// size pointer. -unsafe fn copy_text( - copy: impl Fn(*mut std::os::raw::c_char, usize, *mut usize) -> sys::mln_status, -) -> Result { - let mut required = 0; - maplibre_core::check(copy(ptr::null_mut(), 0, &mut required))?; - if required == 0 { - return Ok(String::new()); - } - - let mut buffer = vec![0u8; required]; - let mut copied = 0; - maplibre_core::check(copy(buffer.as_mut_ptr().cast(), buffer.len(), &mut copied))?; - if copied > buffer.len() { - return Err(Error::new( - ErrorKind::NativeError, - None, - "native text size exceeded caller buffer", - )); - } - buffer.truncate(copied); - String::from_utf8(buffer).map_err(|_| { - Error::new( - ErrorKind::NativeError, - None, - "native text was not valid UTF-8", - ) - }) +fn copy_string_views(result: &sys::mln_completion_result) -> Result> { + crate::completion::copy_slice::(result)? + .into_iter() + .map(|view| { + // SAFETY: each view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(view) } + }) + .collect() } -/// Probes the required byte length, then copies the bytes into owned storage. -/// -/// # Safety -/// -/// `copy` must write at most `capacity` bytes and report the required length -/// through the size pointer. -unsafe fn copy_bytes( - copy: impl Fn(*mut u8, usize, *mut usize) -> sys::mln_status, -) -> Result> { - let mut required = 0; - maplibre_core::check(copy(ptr::null_mut(), 0, &mut required))?; - if required == 0 { - return Ok(Vec::new()); - } - - let mut buffer = vec![0u8; required]; - let mut copied = 0; - maplibre_core::check(copy(buffer.as_mut_ptr(), buffer.len(), &mut copied))?; - if copied > buffer.len() { - return Err(Error::new( - ErrorKind::NativeError, - None, - "native byte size exceeded caller buffer", - )); - } - buffer.truncate(copied); - Ok(buffer) +fn copy_source_info(result: &sys::mln_completion_result) -> Result> { + let Some(raw) = crate::completion::optional_value::(result)? + else { + return Ok(None); + }; + let attribution = raw + .info + .has_attribution + .then(|| { + // SAFETY: the view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(raw.attribution) } + }) + .transpose()?; + let url = (raw.info.fields & sys::MLN_STYLE_SOURCE_INFO_URL != 0) + .then(|| { + // SAFETY: the view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(raw.url) } + }) + .transpose()?; + let tiles = if raw.tile_url_count == 0 { + Vec::new() + } else { + if raw.tile_urls.is_null() { + return Err(crate::Error::new( + crate::ErrorKind::NativeError, + None, + "source completion returned a null tile URL array", + )); + } + // SAFETY: the array is borrowed for this completion callback. + unsafe { std::slice::from_raw_parts(raw.tile_urls, raw.tile_url_count) } + .iter() + .map(|view| { + // SAFETY: each view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(*view) } + }) + .collect::>>()? + }; + Ok(Some(maplibre_core::style::style_source_info_from_native( + &raw.info, + attribution, + url, + tiles, + ))) +} + +fn copy_layer_info(result: &sys::mln_completion_result) -> Result> { + let Some(raw) = crate::completion::optional_value::(result)? + else { + return Ok(None); + }; + let source_id = (raw.info.fields & sys::MLN_STYLE_LAYER_INFO_SOURCE_ID != 0) + .then(|| { + // SAFETY: the view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(raw.source_id) } + }) + .transpose()?; + let source_layer = (raw.info.fields & sys::MLN_STYLE_LAYER_INFO_SOURCE_LAYER != 0) + .then(|| { + // SAFETY: the view is borrowed for this completion callback. + unsafe { maplibre_core::string::copy_string_view(raw.source_layer) } + }) + .transpose()?; + // SAFETY: the result's type view is borrowed for this callback. + unsafe { + maplibre_core::style::style_layer_info_from_native(&raw.info, source_id, source_layer) + } + .map(Some) +} + +fn copy_style_image(result: &sys::mln_completion_result) -> Result> { + let Some(raw) = crate::completion::optional_value::(result)? + else { + return Ok(None); + }; + // SAFETY: pixels are borrowed for this completion callback. + let pixels = unsafe { maplibre_core::string::copy_string_view_bytes(raw.pixels) }?; + let info = maplibre_core::values::style_image_info_from_native(&raw.info); + let mut image = maplibre_core::style::style_image_from_copied_premultiplied_rgba8( + info, + pixels, + raw.pixels.size, + )?; + let copy_stretches = |values: *const sys::mln_image_stretch, count: usize| { + if count == 0 { + return Ok(Vec::new()); + } + if values.is_null() { + return Err(crate::Error::new( + crate::ErrorKind::NativeError, + None, + "native style image returned a null stretch array", + )); + } + Ok(unsafe { std::slice::from_raw_parts(values, count) } + .iter() + .map(|value| ImageStretch::new(value.from, value.to)) + .collect()) + }; + image.stretch_x = copy_stretches(raw.stretch_x, raw.stretch_x_count)?; + image.stretch_y = copy_stretches(raw.stretch_y, raw.stretch_y_count)?; + image.content = info.content; + image.text_fit_width = info.text_fit_width; + image.text_fit_height = info.text_fit_height; + Ok(Some(image)) } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/map/tests.rs b/bindings/rust/crates/maplibre-native-ffi/src/map/tests.rs index ae859264e..18e4fd930 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/map/tests.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/map/tests.rs @@ -4,11 +4,11 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; -use crate::events::{RuntimeEventPayload, RuntimeEventSource, RuntimeEventType}; +use crate::custom_geometry::CustomGeometrySourceOptions; +use crate::events::{CommandDisposition, RuntimeEventSource, RuntimeEventType}; use crate::{ - BoundsConstraint, CameraChangeMode, CustomGeometrySourceOptions, CustomMvtVectorSourceOptions, - EdgeInsets, ErrorKind, MapMode, ResourceKind, ResourceProviderDecision, ResourceResponse, - RuntimeEventMask, TextureImageInfo, + CommandCompletion, CustomMvtVectorSourceOptions, ErrorKind, NativeFuture, RasterDemEncoding, + ResourceKind, ResourceProviderDecision, ResourceResponse, RuntimeEventMask, TextureImageInfo, }; const VALID_STYLE_JSON: &str = r#"{"version":8,"sources":{},"layers":[]}"#; @@ -18,11 +18,61 @@ const STYLE_WITH_TRANSITION_JSON: &str = const STYLE_WITH_DELAY_ONLY_TRANSITION_JSON: &str = r#"{"version":8,"transition":{"delay":100},"sources":{},"layers":[]}"#; +macro_rules! operation_result { + ($operation:expr) => {{ + let operation = $operation.unwrap(); + assert!(operation.wait(Duration::from_secs(5)).unwrap()); + let result = operation.take(); + drop(operation); + result + }}; +} +fn await_runtime_barrier(runtime: &RuntimeHandle) { + let barrier = runtime.barrier().unwrap(); + assert!(barrier.wait(Duration::from_secs(5)).unwrap()); + barrier.finish().unwrap(); + barrier.release(); +} +/// Waits for exactly `expected` source callback-state releases. +/// +/// A command's completion reports the command, not the release of the callback +/// state it dropped, and native may free that state on another thread, so the +/// count is only eventually consistent with the commands the test submitted. +fn await_release_count(runtime: &RuntimeHandle, releases: &Arc, expected: usize) { + let deadline = std::time::Instant::now() + Duration::from_secs(5); + loop { + await_runtime_barrier(runtime); + let observed = releases.load(Ordering::SeqCst); + if observed >= expected { + assert_eq!(observed, expected); + return; + } + assert!( + std::time::Instant::now() < deadline, + "expected {expected} releases, observed {observed}" + ); + std::thread::sleep(Duration::from_millis(5)); + } +} +fn assert_command_disposition( + _runtime: &RuntimeHandle, + completion: NativeFuture, + expected: CommandDisposition, +) -> (Option, i32, Option) { + assert!(completion.wait(Duration::from_secs(5)).unwrap()); + let completion = completion.take().unwrap(); + assert_eq!(completion.disposition, expected); + let status = completion.raw_status; + let diagnostic = (!completion.diagnostic.is_empty()).then(|| completion.diagnostic.clone()); + (Some(completion), status, diagnostic) +} + #[test] // Spec coverage: BND-105. fn nine_patch_style_image_round_trips_stretch_content_and_text_fit() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let image = @@ -43,11 +93,13 @@ fn nine_patch_style_image_round_trips_stretch_content_and_text_fit() { map.set_style_image("patch", &image, Some(&options)) .unwrap(); - let info = map.style_image_info("patch").unwrap().unwrap(); - assert_eq!(info.stretch_x_count, 1); - assert_eq!(info.stretch_y_count, 2); + let saved = operation_result!(map.style_image("patch")) + .unwrap() + .unwrap(); + assert_eq!(saved.stretch_x, vec![ImageStretch::new(0.0, 1.0)]); + assert_eq!(saved.stretch_y.len(), 2); assert_eq!( - info.content, + saved.content, Some(ImageContent { left: 0.5, top: 0.5, @@ -56,119 +108,140 @@ fn nine_patch_style_image_round_trips_stretch_content_and_text_fit() { }) ); // An absent text fit stays distinguishable from a present default. - assert_eq!(info.text_fit_width, None); - assert_eq!(info.text_fit_height, Some(StyleImageTextFit::Proportional)); - - let (stretch_x, stretch_y) = map.style_image_stretches("patch").unwrap().unwrap(); - assert_eq!(stretch_x, vec![ImageStretch::new(0.0, 1.0)]); - assert_eq!( - stretch_y, - vec![ImageStretch::new(0.0, 1.0), ImageStretch::new(1.0, 2.0)] + assert_eq!(saved.text_fit_width, None); + assert_eq!(saved.text_fit_height, Some(StyleImageTextFit::Proportional)); + assert!( + operation_result!(map.style_image("missing")) + .unwrap() + .is_none() ); - assert!(map.style_image_stretches("missing").unwrap().is_none()); let mut bad = StyleImageOptions::default(); bad.stretch_x = Some(vec![ImageStretch::new(2.0, 1.0)]); let error = map.set_style_image("bad", &image, Some(&bad)).unwrap_err(); assert_eq!(error.kind(), ErrorKind::InvalidArgument); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-105. fn layer_base_accessors_round_trip_through_real_c_abi() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(STYLE_WITH_IDS_JSON.as_bytes()).unwrap(); - assert_eq!(map.layer_source_layer("geo-fill").unwrap(), ""); + assert_eq!( + operation_result!(map.layer_source_layer("geo-fill")).unwrap(), + "" + ); map.set_layer_source_layer("geo-fill", "roads").unwrap(); - assert_eq!(map.layer_source_layer("geo-fill").unwrap(), "roads"); - assert_eq!(map.layer_source_id("geo-fill").unwrap(), "geo"); + assert_eq!( + operation_result!(map.layer_source_layer("geo-fill")).unwrap(), + "roads" + ); + assert_eq!( + operation_result!(map.layer_source_id("geo-fill")).unwrap(), + "geo" + ); - let error = map - .set_layer_source_layer("background", "roads") - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert!(error.diagnostic().contains("source-layer")); - assert_eq!(map.layer_source_id("background").unwrap(), ""); + let rejected_command = map.set_layer_source_layer("background", "roads").unwrap(); + assert_command_disposition(&runtime, rejected_command, CommandDisposition::Failed); + assert_eq!( + operation_result!(map.layer_source_id("background")).unwrap(), + "" + ); + + // An unset zoom range crosses the boundary as infinities, and the info + // aggregate resolves source IDs alongside the scalar fields. + let info = operation_result!(map.style_layer_info("geo-fill")) + .unwrap() + .expect("styled layer should report info"); + assert_eq!(info.layer_type, "fill"); + assert_eq!(info.min_zoom, f64::NEG_INFINITY); + assert_eq!(info.max_zoom, f64::INFINITY); + assert_eq!(info.visibility, StyleLayerVisibility::Visible); + assert_eq!(info.source_id.as_deref(), Some("geo")); + assert_eq!(info.source_layer.as_deref(), Some("roads")); - // An unset zoom range crosses the boundary as infinities. - assert_eq!(map.layer_min_zoom("geo-fill").unwrap(), f64::NEG_INFINITY); - assert_eq!(map.layer_max_zoom("geo-fill").unwrap(), f64::INFINITY); map.set_layer_min_zoom("geo-fill", 4.0).unwrap(); map.set_layer_max_zoom("geo-fill", 12.5).unwrap(); - assert_eq!(map.layer_min_zoom("geo-fill").unwrap(), 4.0); - assert_eq!(map.layer_max_zoom("geo-fill").unwrap(), 12.5); - - assert_eq!( - map.layer_visibility("geo-fill").unwrap(), - StyleLayerVisibility::Visible - ); map.set_layer_visibility("geo-fill", StyleLayerVisibility::None) .unwrap(); - assert_eq!( - map.layer_visibility("geo-fill").unwrap(), - StyleLayerVisibility::None - ); + let info = operation_result!(map.style_layer_info("geo-fill")) + .unwrap() + .unwrap(); + assert_eq!(info.min_zoom, 4.0); + assert_eq!(info.max_zoom, 12.5); + assert_eq!(info.visibility, StyleLayerVisibility::None); - let error = map + // A sourceless layer reports absent source IDs rather than empty ones. + let info = operation_result!(map.style_layer_info("background")) + .unwrap() + .unwrap(); + assert_eq!(info.layer_type, "background"); + assert_eq!(info.source_id, None); + assert_eq!(info.source_layer, None); + + let rejected_command = map .set_layer_visibility("geo-fill", StyleLayerVisibility::Unknown(900)) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); + .unwrap(); + assert_command_disposition(&runtime, rejected_command, CommandDisposition::Failed); - let error = map.layer_min_zoom("missing").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); + // The not-found path reports None instead of an error. + assert!( + operation_result!(map.style_layer_info("missing")) + .unwrap() + .is_none() + ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } fn object_member<'a>(value: &'a JsonValue, key: &str) -> Option<&'a JsonValue> { value.as_object()?.get(key) } -fn assert_lat_lng_close(actual: LatLng, expected: LatLng) { - assert!((actual.latitude - expected.latitude).abs() < 1e-7); - assert!((actual.longitude - expected.longitude).abs() < 1e-7); -} - #[test] -// Spec coverage: BND-040 and BND-100. +// Spec coverage: BND-040, BND-050, and BND-100. fn map_close_consumes_handle_and_drop_stays_idempotent() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-042. fn map_retains_runtime_after_runtime_handle_is_dropped() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); drop(runtime); - map.close().unwrap(); + map.close_and_wait(); } #[test] // Spec coverage: BND-024, BND-101, and BND-105. fn style_setters_accept_valid_input_and_reject_embedded_nul() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); - let _ = map.style_source_ids().unwrap(); - let _ = map.style_layer_ids().unwrap(); + let _ = operation_result!(map.style_source_ids()).unwrap(); + let _ = operation_result!(map.style_layer_ids()).unwrap(); map.set_style_json(STYLE_WITH_IDS_JSON.as_bytes()).unwrap(); - let source_ids = map.style_source_ids().unwrap(); - let layer_ids = map.style_layer_ids().unwrap(); + let source_ids = operation_result!(map.style_source_ids()).unwrap(); + let layer_ids = operation_result!(map.style_layer_ids()).unwrap(); assert!(source_ids.iter().any(|id| id == "geo")); assert!(layer_ids.iter().any(|id| id == "background")); assert!(layer_ids.iter().any(|id| id == "geo-fill")); @@ -182,52 +255,59 @@ fn style_setters_accept_valid_input_and_reject_embedded_nul() { assert_eq!(error.raw_status(), None); assert!(error.diagnostic().contains("embedded NUL")); - let error = map.set_style_json(b"{").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::NativeError); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR)); - assert!(!error.diagnostic().trim().is_empty()); + let malformed_id = map.set_style_json(b"{").unwrap(); + assert_command_disposition(&runtime, malformed_id, CommandDisposition::Failed); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-101. fn loaded_style_document_and_url_read_back_what_was_loaded() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); - assert!(map.loaded_style_json().unwrap().is_empty()); - assert_eq!(map.style_url().unwrap(), ""); + assert!( + operation_result!(map.loaded_style_json()) + .unwrap() + .is_empty() + ); + assert_eq!(operation_result!(map.style_url()).unwrap(), ""); // The document reads back byte-for-byte, so it can be reloaded unchanged. map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); assert_eq!( - map.loaded_style_json().unwrap(), + operation_result!(map.loaded_style_json()).unwrap(), VALID_STYLE_JSON.as_bytes() ); // Inline JSON clears the URL. - assert_eq!(map.style_url().unwrap(), ""); + assert_eq!(operation_result!(map.style_url()).unwrap(), ""); // The URL records the request before the load succeeds; the document still // reports the style that last parsed. map.set_style_url("https://example.com/style.json").unwrap(); - assert_eq!(map.style_url().unwrap(), "https://example.com/style.json"); assert_eq!( - map.loaded_style_json().unwrap(), + operation_result!(map.style_url()).unwrap(), + "https://example.com/style.json" + ); + assert_eq!( + operation_result!(map.loaded_style_json()).unwrap(), VALID_STYLE_JSON.as_bytes() ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-105. -fn style_source_exists_and_remove_call_real_c_api() { +fn style_removals_commit_or_fail_with_not_found() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + map.set_style_json(STYLE_WITH_IDS_JSON.as_bytes()).unwrap(); let source = serde_json::to_vec(&json!({ "type": "geojson", @@ -235,17 +315,58 @@ fn style_source_exists_and_remove_call_real_c_api() { })) .unwrap(); - assert!(!map.style_source_exists("owned-source").unwrap()); - assert!(!map.remove_style_source("owned-source").unwrap()); + // Removing a missing source fails with the not-found status code. + let missing = map.remove_style_source("owned-source").unwrap(); + let (finished, code, _) = + assert_command_disposition(&runtime, missing, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_NOT_FOUND); + assert!(finished.is_some()); + // Removing an existing source commits, and the info getter's found flag + // re-checks existence on both sides of the removal. map.add_style_source_json("owned-source", &source).unwrap(); - assert!(map.style_source_exists("owned-source").unwrap()); - assert!(map.remove_style_source("owned-source").unwrap()); - assert!(!map.style_source_exists("owned-source").unwrap()); - assert!(!map.remove_style_source("owned-source").unwrap()); + assert!( + operation_result!(map.style_source_info("owned-source")) + .unwrap() + .is_some() + ); + let removed = map.remove_style_source("owned-source").unwrap(); + let (finished, _, _) = + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + assert_ne!(finished.unwrap().generation, 0); + assert!( + operation_result!(map.style_source_info("owned-source")) + .unwrap() + .is_none() + ); + + // Removing a source a layer still uses fails with invalid state. + let in_use = map.remove_style_source("geo").unwrap(); + let (_, code, diagnostic) = + assert_command_disposition(&runtime, in_use, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_INVALID_STATE); + assert!(diagnostic.unwrap().contains("used by a layer")); - map.close().unwrap(); - runtime.close().unwrap(); + // Layer removal commits and then reports not-found for the same ID. + let removed = map.remove_style_layer("geo-fill").unwrap(); + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + assert!( + operation_result!(map.style_layer_info("geo-fill")) + .unwrap() + .is_none() + ); + let missing = map.remove_style_layer("geo-fill").unwrap(); + let (_, code, _) = assert_command_disposition(&runtime, missing, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_NOT_FOUND); + + map.close_and_wait(); + runtime.close_and_wait(); +} + +fn source_type_of(map: &MapHandle, source_id: &str) -> Option { + operation_result!(map.style_source_info(source_id)) + .unwrap() + .map(|info| info.source_type) } fn test_style_image(data: Vec) -> PremultipliedRgba8Image { @@ -256,7 +377,8 @@ fn test_style_image(data: Vec) -> PremultipliedRgba8Image { // Spec coverage: BND-069 and BND-105. fn style_image_copy_uses_rust_owned_buffer() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let original_pixels = vec![ @@ -266,15 +388,7 @@ fn style_image_copy_uses_rust_owned_buffer() { map.set_style_image("plain", &image, None).unwrap(); image.data.fill(0); - assert!(map.style_image_exists("plain").unwrap()); - let info = map - .style_image_info("plain") - .unwrap() - .expect("added image should have copied metadata"); - assert_eq!(info.width, image.info.width); - assert_eq!(info.height, image.info.height); - let mut copied = map - .copy_style_image_premultiplied_rgba8("plain") + let mut copied = operation_result!(map.style_image("plain")) .unwrap() .expect("added Rust image should copy back through C"); @@ -283,16 +397,25 @@ fn style_image_copy_uses_rust_owned_buffer() { assert_eq!(copied.image.data, original_pixels); copied.image.data.fill(1); assert_eq!( - map.copy_style_image_premultiplied_rgba8("plain") + operation_result!(map.style_image("plain")) .unwrap() .expect("style image copy should not expose native storage") .image .data, original_pixels ); - assert!(map.remove_style_image("plain").unwrap()); - assert!(!map.style_image_exists("plain").unwrap()); - assert!(!map.remove_style_image("plain").unwrap()); + // Image removal commits, the info getter's found flag reports the image + // gone, and a repeat removal fails with the not-found status code. + let removed = map.remove_style_image("plain").unwrap(); + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + assert!( + operation_result!(map.style_image("plain")) + .unwrap() + .is_none() + ); + let missing = map.remove_style_image("plain").unwrap(); + let (_, code, _) = assert_command_disposition(&runtime, missing, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_NOT_FOUND); } fn image_source_coordinates() -> [LatLng; 4] { @@ -308,14 +431,15 @@ fn image_source_coordinates() -> [LatLng; 4] { // Spec coverage: BND-105. fn image_source_helpers_accept_url_and_inline_images() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let coordinates = image_source_coordinates(); map.add_image_source_url("url-image", &coordinates, "https://example.com/image.png") .unwrap(); assert_eq!( - map.image_source_coordinates("url-image").unwrap(), + operation_result!(map.image_source_coordinates("url-image")).unwrap(), Some(coordinates) ); @@ -323,7 +447,7 @@ fn image_source_helpers_accept_url_and_inline_images() { map.add_image_source_image("inline-image", &coordinates, &image) .unwrap(); assert_eq!( - map.style_source_type("inline-image").unwrap(), + source_type_of(&map, "inline-image"), Some(SourceType::Image) ); } @@ -332,15 +456,13 @@ fn image_source_helpers_accept_url_and_inline_images() { // Spec coverage: BND-105. fn tile_source_helpers_call_real_c_api() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); map.add_vector_source_url("vector-url", "https://example.com/vector.json", None) .unwrap(); - assert_eq!( - map.style_source_type("vector-url").unwrap(), - Some(SourceType::Vector) - ); + assert_eq!(source_type_of(&map, "vector-url"), Some(SourceType::Vector)); let mut dem_options = TileSourceOptions::default(); dem_options.raster_dem_encoding = Some(RasterDemEncoding::Terrarium); @@ -351,7 +473,7 @@ fn tile_source_helpers_call_real_c_api() { ) .unwrap(); assert_eq!( - map.style_source_type("dem-tiles").unwrap(), + source_type_of(&map, "dem-tiles"), Some(SourceType::RasterDem) ); } @@ -360,7 +482,8 @@ fn tile_source_helpers_call_real_c_api() { // Spec coverage: BND-060, BND-061, and BND-105. fn geojson_source_helpers_accept_prepared_data_and_keep_options_across_updates() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let mut options = GeoJsonSourceOptions::default(); @@ -378,7 +501,7 @@ fn geojson_source_helpers_accept_prepared_data_and_keep_options_across_updates() ) .unwrap(); assert_eq!( - map.style_source_type("geojson-url").unwrap(), + source_type_of(&map, "geojson-url"), Some(SourceType::GeoJson) ); @@ -398,38 +521,44 @@ fn geojson_source_helpers_accept_prepared_data_and_keep_options_across_updates() map.add_geojson_source_data("geojson-data-2", &data) .unwrap(); assert_eq!( - map.style_source_type("geojson-data").unwrap(), + source_type_of(&map, "geojson-data"), Some(SourceType::GeoJson) ); assert_eq!( - map.style_source_type("geojson-data-2").unwrap(), + source_type_of(&map, "geojson-data-2"), Some(SourceType::GeoJson) ); // Updates install prepared data; the source keeps the options the data it // was added with carried. - map.set_geojson_source_data("geojson-data", &data).unwrap(); - map.set_geojson_source_data("geojson-url", &data).unwrap(); + let update = map.set_geojson_source_data("geojson-data", &data).unwrap(); + assert_command_disposition(&runtime, update, CommandDisposition::Committed); + let update = map.set_geojson_source_data("geojson-url", &data).unwrap(); + assert_command_disposition(&runtime, update, CommandDisposition::Committed); map.set_geojson_source_url("geojson-data-2", "https://example.com/points.geojson") .unwrap(); - // Sources keep their own reference, so releasing the prepared data never - // invalidates them. + // The submit-time lease keeps the prepared index alive, so the handle may + // be released as soon as an install command is submitted, and installed + // sources keep their own reference. + let final_install = map.set_geojson_source_data("geojson-data", &data).unwrap(); data.close(); + assert_command_disposition(&runtime, final_install, CommandDisposition::Committed); assert_eq!( - map.style_source_type("geojson-data").unwrap(), + source_type_of(&map, "geojson-data"), Some(SourceType::GeoJson) ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-060 and BND-061. fn set_geojson_source_data_rejects_mismatched_prepared_options() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let bytes = br#"{"type":"FeatureCollection","features":[]}"#; @@ -444,10 +573,9 @@ fn set_geojson_source_data_rejects_mismatched_prepared_options() { let mut mismatched = options.clone(); mismatched.cluster_radius = Some(80); let mismatched = crate::GeoJsonSourceDataHandle::new(bytes, Some(&mismatched)).unwrap(); - let error = map - .set_geojson_source_data("points", &mismatched) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); + let rejected = map.set_geojson_source_data("points", &mismatched).unwrap(); + let (_, code, _) = assert_command_disposition(&runtime, rejected, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_INVALID_ARGUMENT); // Cluster aggregations are part of the comparison, so data prepared with // different cluster_properties is rejected too. @@ -456,17 +584,19 @@ fn set_geojson_source_data_rejects_mismatched_prepared_options() { Some(serde_json::to_vec(&json!({"weight_sum": ["+", ["get", "weight"]]})).unwrap()); let properties_only = crate::GeoJsonSourceDataHandle::new(bytes, Some(&properties_only)).unwrap(); - let error = map + let rejected = map .set_geojson_source_data("points", &properties_only) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); + .unwrap(); + let (_, code, _) = assert_command_disposition(&runtime, rejected, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_INVALID_ARGUMENT); // The rejected install left the source usable with matching data. let matching = crate::GeoJsonSourceDataHandle::new(bytes, Some(&options)).unwrap(); - map.set_geojson_source_data("points", &matching).unwrap(); + let accepted = map.set_geojson_source_data("points", &matching).unwrap(); + assert_command_disposition(&runtime, accepted, CommandDisposition::Committed); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -535,7 +665,8 @@ fn geojson_source_data_handle_is_send_and_sync() { // Spec coverage: BND-060 and BND-061. fn geojson_data_prepared_off_thread_installs_on_the_map_thread() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let data = std::thread::spawn(|| { @@ -548,43 +679,50 @@ fn geojson_data_prepared_off_thread_installs_on_the_map_thread() { map.add_geojson_source_data("worker-prepared", &data) .unwrap(); assert_eq!( - map.style_source_type("worker-prepared").unwrap(), + source_type_of(&map, "worker-prepared"), Some(SourceType::GeoJson) ); // The prepared value releases off the map thread as well. std::thread::spawn(move || drop(data)).join().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-060 and BND-061. fn synchronous_tiling_override_targets_live_geojson_sources_only() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(STYLE_WITH_IDS_JSON.as_bytes()).unwrap(); - map.set_geojson_source_synchronous_tiling("geo", true) + let enabled = map + .set_geojson_source_synchronous_tiling("geo", true) .unwrap(); - map.set_geojson_source_synchronous_tiling("geo", false) + assert_command_disposition(&runtime, enabled, CommandDisposition::Committed); + let disabled = map + .set_geojson_source_synchronous_tiling("geo", false) .unwrap(); + assert_command_disposition(&runtime, disabled, CommandDisposition::Committed); - let error = map + let missing = map .set_geojson_source_synchronous_tiling("missing", true) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); + .unwrap(); + let (_, code, _) = assert_command_disposition(&runtime, missing, CommandDisposition::Failed); + assert_eq!(code, sys::MLN_STATUS_INVALID_ARGUMENT); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-105. -fn style_source_type_and_info_call_real_c_api() { +fn style_source_info_reports_type_and_found_flag() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let geojson_source = serde_json::to_vec(&json!({ @@ -599,15 +737,15 @@ fn style_source_type_and_info_call_real_c_api() { })) .unwrap(); - assert_eq!(map.style_source_type("missing-source").unwrap(), None); - assert_eq!(map.style_source_info("missing-source").unwrap(), None); - - map.add_style_source_json("empty", &geojson_source).unwrap(); assert_eq!( - map.style_source_type("empty").unwrap(), - Some(SourceType::GeoJson) + operation_result!(map.style_source_info("missing-source")).unwrap(), + None ); - let info = map.style_source_info("empty").unwrap().unwrap(); + + map.add_style_source_json("empty", &geojson_source).unwrap(); + let info = operation_result!(map.style_source_info("empty")) + .unwrap() + .unwrap(); assert_eq!(info.source_type, SourceType::GeoJson); assert_eq!(info.raw_source_type, sys::MLN_STYLE_SOURCE_TYPE_GEOJSON); assert!(!info.is_volatile); @@ -615,29 +753,30 @@ fn style_source_type_and_info_call_real_c_api() { map.add_style_source_json("vector-meta", &vector_source) .unwrap(); - assert_eq!( - map.style_source_type("vector-meta").unwrap(), - Some(SourceType::Vector) - ); - let info = map.style_source_info("vector-meta").unwrap().unwrap(); + let info = operation_result!(map.style_source_info("vector-meta")) + .unwrap() + .unwrap(); assert_eq!(info.source_type, SourceType::Vector); assert_eq!(info.raw_source_type, sys::MLN_STYLE_SOURCE_TYPE_VECTOR); assert_eq!(info.attribution.as_deref(), Some("Example attribution")); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-066, BND-109. fn style_source_info_copies_reconstructible_source_state() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); map.add_vector_source_url("remote", "https://example.com/source.json", None) .unwrap(); - let remote = map.style_source_info("remote").unwrap().unwrap(); + let remote = operation_result!(map.style_source_info("remote")) + .unwrap() + .unwrap(); assert_eq!( remote.url.as_deref(), Some("https://example.com/source.json") @@ -660,7 +799,9 @@ fn style_source_info_copies_reconstructible_source_state() { ]; map.add_vector_source_tiles("inline", &tiles, Some(&options)) .unwrap(); - let copied = map.style_source_info("inline").unwrap().unwrap(); + let copied = operation_result!(map.style_source_info("inline")) + .unwrap() + .unwrap(); assert_eq!(copied.url, None); assert_eq!(copied.attribution.as_deref(), Some("Example attribution")); assert_eq!(copied.tile_size, Some(512)); @@ -672,9 +813,10 @@ fn style_source_info_copies_reconstructible_source_state() { assert_eq!(tile_json.scheme, TileScheme::Tms); assert_eq!(tile_json.bounds, options.bounds); - assert!(map.remove_style_source("inline").unwrap()); - map.close().unwrap(); - runtime.close().unwrap(); + let removed = map.remove_style_source("inline").unwrap(); + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + map.close_and_wait(); + runtime.close_and_wait(); assert_eq!( copied.tile_json.unwrap().tiles, @@ -713,7 +855,8 @@ fn options_counting_releases(releases: &Arc) -> CustomGeometrySourc // Spec coverage: BND-124. fn custom_geometry_source_apis_call_real_c_api_and_style_replacement_releases_state() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let releases = Arc::new(AtomicUsize::new(0)); @@ -743,52 +886,55 @@ fn custom_geometry_source_apis_call_real_c_api_and_style_replacement_releases_st ) .unwrap(); assert_eq!(releases.load(Ordering::SeqCst), 0); + let removed = map.remove_style_source("custom").unwrap(); + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + await_release_count(&runtime, &releases, 1); + assert!(source_type_of(&map, "custom").is_none()); - // An explicit removal releases the callback state once. - assert!(map.remove_style_source("custom").unwrap()); - assert_eq!(releases.load(Ordering::SeqCst), 1); - assert!(!map.style_source_exists("custom").unwrap()); - - // A duplicate source ID is rejected, and a rejected add releases nothing - // this call handed over, so the state it built is freed here instead. - map.add_custom_geometry_source("custom", options_counting_releases(&releases)) + let first_add = map + .add_custom_geometry_source("custom", options_counting_releases(&releases)) .unwrap(); - let error = map + let duplicate_add = map .add_custom_geometry_source("custom", options_counting_releases(&releases)) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(releases.load(Ordering::SeqCst), 2); + .unwrap(); + assert_command_disposition(&runtime, first_add, CommandDisposition::Committed); + assert_command_disposition(&runtime, duplicate_add, CommandDisposition::Failed); + await_release_count(&runtime, &releases, 2); - // The inline style load replaces the style before it returns, so the source - // it dropped is released by then. map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); - assert_eq!(releases.load(Ordering::SeqCst), 3); + await_release_count(&runtime, &releases, 3); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-124. fn custom_geometry_source_state_is_released_on_map_close() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let releases = Arc::new(AtomicUsize::new(0)); map.add_custom_geometry_source("custom", options_counting_releases(&releases)) .unwrap(); - map.close().unwrap(); + map.close_and_wait(); + // The browser C ABI suite covers callback retirement at native completion. + // Its synchronous Rust harness keeps the graphics-owning pthread available + // and relies on process isolation for final teardown instead. + #[cfg(not(target_os = "emscripten"))] assert_eq!(releases.load(Ordering::SeqCst), 1); - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-124. fn custom_geometry_source_adds_to_current_style_after_url_style_request() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); map.set_style_url("unsupported://style.json").unwrap(); let releases = Arc::new(AtomicUsize::new(0)); @@ -796,10 +942,10 @@ fn custom_geometry_source_adds_to_current_style_after_url_style_request() { map.add_custom_geometry_source("custom", options_counting_releases(&releases)) .unwrap(); - assert!(map.style_source_exists("custom").unwrap()); + assert!(source_type_of(&map, "custom").is_some()); assert_eq!(releases.load(Ordering::SeqCst), 0); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -817,7 +963,8 @@ fn custom_geometry_source_state_releases_after_url_style_replacement() { ResourceProviderDecision::PassThrough }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); // The host wants no style-loaded events, and the release runs anyway. map.set_event_mask(RuntimeEventMask::ALL - RuntimeEventMask::MAP_STYLE_LOADED) .unwrap(); @@ -830,8 +977,8 @@ fn custom_geometry_source_state_releases_after_url_style_replacement() { map.set_style_url("custom://style.json").unwrap(); let mut style_loaded_events = 0; for _ in 0..1000 { - runtime.pump(Some(Duration::ZERO), None).unwrap(); - for event in runtime.drain_events(0).unwrap().iter() { + std::thread::sleep(std::time::Duration::from_millis(1)); + for event in runtime.drain_events().unwrap().iter() { if event.event_type() == RuntimeEventType::MapStyleLoaded { style_loaded_events += 1; } @@ -851,14 +998,9 @@ fn custom_geometry_source_state_releases_after_url_style_replacement() { style_loaded_events, 0, "the release must not need an event the host left unselected" ); - assert_eq!( - map.event_mask().unwrap(), - RuntimeEventMask::ALL - RuntimeEventMask::MAP_STYLE_LOADED, - "the mask should report what the host set" - ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } fn mvt_options_counting_releases(releases: &Arc) -> CustomMvtVectorSourceOptions { @@ -874,7 +1016,8 @@ fn mvt_options_counting_releases(releases: &Arc) -> CustomMvtVector // Spec coverage: BND-105 and BND-124. fn custom_mvt_vector_source_apis_call_real_c_api_and_style_replacement_releases_state() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let releases = Arc::new(AtomicUsize::new(0)); @@ -884,7 +1027,7 @@ fn custom_mvt_vector_source_apis_call_real_c_api_and_style_replacement_releases_ map.add_custom_mvt_vector_source("custom", custom_options) .unwrap(); assert_eq!( - map.style_source_type("custom").unwrap(), + source_type_of(&map, "custom"), Some(SourceType::CustomMvtVector) ); @@ -897,45 +1040,50 @@ fn custom_mvt_vector_source_apis_call_real_c_api_and_style_replacement_releases_ .unwrap(); assert_eq!(releases.load(Ordering::SeqCst), 0); - assert!(map.remove_style_source("custom").unwrap()); - assert_eq!(releases.load(Ordering::SeqCst), 1); - assert!(!map.style_source_exists("custom").unwrap()); + let removed = map.remove_style_source("custom").unwrap(); + assert_command_disposition(&runtime, removed, CommandDisposition::Committed); + await_release_count(&runtime, &releases, 1); + assert!(source_type_of(&map, "custom").is_none()); - map.add_custom_mvt_vector_source("custom", mvt_options_counting_releases(&releases)) + let first_add = map + .add_custom_mvt_vector_source("custom", mvt_options_counting_releases(&releases)) .unwrap(); - let error = map + let duplicate_add = map .add_custom_mvt_vector_source("custom", mvt_options_counting_releases(&releases)) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(releases.load(Ordering::SeqCst), 2); + .unwrap(); + assert_command_disposition(&runtime, first_add, CommandDisposition::Committed); + assert_command_disposition(&runtime, duplicate_add, CommandDisposition::Failed); + await_release_count(&runtime, &releases, 2); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); - assert_eq!(releases.load(Ordering::SeqCst), 3); + await_release_count(&runtime, &releases, 3); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-124. fn custom_mvt_vector_source_state_is_released_on_map_close() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); let releases = Arc::new(AtomicUsize::new(0)); map.add_custom_mvt_vector_source("custom", mvt_options_counting_releases(&releases)) .unwrap(); - map.close().unwrap(); + map.close_and_wait(); + #[cfg(not(target_os = "emscripten"))] assert_eq!(releases.load(Ordering::SeqCst), 1); - runtime.close().unwrap(); + runtime.close_and_wait(); } fn drain_runtime_events(runtime: &mut RuntimeHandle) { for _ in 0..20 { - runtime.pump(Some(Duration::ZERO), None).unwrap(); - let _ = runtime.drain_events(0).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); + let _ = runtime.drain_events().unwrap(); } } @@ -949,8 +1097,8 @@ fn collect_style_load_event_types( let mut types = Vec::new(); map.set_style_json(style_json.as_bytes()).unwrap(); for _ in 0..20 { - runtime.pump(Some(Duration::ZERO), None).unwrap(); - for event in runtime.drain_events(0).unwrap().iter() { + std::thread::sleep(std::time::Duration::from_millis(1)); + for event in runtime.drain_events().unwrap().iter() { if event.source() == RuntimeEventSource::Map(map.id()) { types.push(event.event_type()); } @@ -963,10 +1111,8 @@ fn collect_style_load_event_types( // Spec coverage: BND-091. fn a_narrowed_map_mask_delivers_the_kept_type_alone() { let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - - // The default options mask selects every type. - assert_eq!(map.event_mask().unwrap(), RuntimeEventMask::ALL); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_event_mask(RuntimeEventMask::ALL - RuntimeEventMask::MAP_LOADING_STARTED) .unwrap(); @@ -984,12 +1130,11 @@ fn a_narrowed_map_mask_delivers_the_kept_type_alone() { // An empty mask leaves the map with nothing to report. map.set_event_mask(RuntimeEventMask::NONE).unwrap(); - assert!(map.event_mask().unwrap().is_empty()); let types = collect_style_load_event_types(&mut runtime, &map, STYLE_WITH_IDS_JSON); assert!(types.is_empty(), "{types:?}"); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -998,12 +1143,8 @@ fn a_creation_mask_narrows_a_map_from_its_first_style_load() { let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); let mut options = MapOptions::default(); options.event_mask = RuntimeEventMask::ALL - RuntimeEventMask::MAP_LOADING_STARTED; - let map = MapHandle::with_options(&runtime, &options).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options(&runtime, &options)); - assert_eq!( - map.event_mask().unwrap(), - RuntimeEventMask::ALL - RuntimeEventMask::MAP_LOADING_STARTED - ); let types = collect_style_load_event_types(&mut runtime, &map, VALID_STYLE_JSON); assert!( @@ -1015,48 +1156,44 @@ fn a_creation_mask_narrows_a_map_from_its_first_style_load() { "{types:?}" ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-091. -fn a_map_mask_round_trips_and_rejects_undefined_bits() { +fn map_mask_commands_complete_and_reject_undefined_bits() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); - map.set_event_mask(RuntimeEventMask::ALL).unwrap(); - assert_eq!(map.event_mask().unwrap(), RuntimeEventMask::ALL); - - // Read, clear one bit, write back: every other bit survives. - let mut mask = map.event_mask().unwrap(); - mask.remove(RuntimeEventMask::MAP_TILE_ACTION); - map.set_event_mask(mask).unwrap(); - let read_back = map.event_mask().unwrap(); - assert!(!read_back.contains(RuntimeEventMask::MAP_TILE_ACTION)); - assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED)); - assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED)); + let first = map.set_event_mask(RuntimeEventMask::ALL).unwrap(); + let second = map + .set_event_mask(RuntimeEventMask::ALL - RuntimeEventMask::MAP_TILE_ACTION) + .unwrap(); + assert_command_disposition(&runtime, first, CommandDisposition::Committed); + assert_command_disposition(&runtime, second, CommandDisposition::Committed); let error = map .set_event_mask(RuntimeEventMask::from_bits_retain(1 << 63)) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::InvalidArgument); assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT)); - assert_eq!(map.event_mask().unwrap(), read_back); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-060, BND-061, BND-070, and BND-105. fn style_transition_options_round_trip_through_the_real_c_api() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); // MapLibre Native always holds a placement flag, so it reports even before // a style is loaded. - let empty = map.style_transition_options().unwrap(); + let empty = operation_result!(map.style_transition_options()).unwrap(); assert_eq!(empty.duration_ms, None); assert_eq!(empty.delay_ms, None); assert_eq!(empty.enable_placement_transitions, Some(true)); @@ -1064,7 +1201,7 @@ fn style_transition_options_round_trip_through_the_real_c_api() { // The style parser fills in its own 300ms duration when the style carries // no transition member. map.set_style_json(VALID_STYLE_JSON.as_bytes()).unwrap(); - let parsed = map.style_transition_options().unwrap(); + let parsed = operation_result!(map.style_transition_options()).unwrap(); assert_eq!(parsed.duration_ms, Some(300.0)); assert_eq!(parsed.delay_ms, None); @@ -1072,13 +1209,13 @@ fn style_transition_options_round_trip_through_the_real_c_api() { // transition replaces that default with no duration. map.set_style_json(STYLE_WITH_DELAY_ONLY_TRANSITION_JSON.as_bytes()) .unwrap(); - let delay_only = map.style_transition_options().unwrap(); + let delay_only = operation_result!(map.style_transition_options()).unwrap(); assert_eq!(delay_only.duration_ms, None); assert_eq!(delay_only.delay_ms, Some(100.0)); map.set_style_json(STYLE_WITH_TRANSITION_JSON.as_bytes()) .unwrap(); - let declared = map.style_transition_options().unwrap(); + let declared = operation_result!(map.style_transition_options()).unwrap(); assert_eq!(declared.duration_ms, Some(750.0)); assert_eq!(declared.delay_ms, Some(100.0)); assert_eq!(declared.enable_placement_transitions, Some(true)); @@ -1090,7 +1227,7 @@ fn style_transition_options_round_trip_through_the_real_c_api() { options.enable_placement_transitions = Some(false); map.set_style_transition_options(&options).unwrap(); - let applied = map.style_transition_options().unwrap(); + let applied = operation_result!(map.style_transition_options()).unwrap(); assert_eq!(applied, options); assert_eq!(applied.duration_ms, Some(0.0)); assert_eq!(applied.delay_ms, None); @@ -1101,7 +1238,7 @@ fn style_transition_options_round_trip_through_the_real_c_api() { duration_only.duration_ms = Some(250.0); map.set_style_transition_options(&duration_only).unwrap(); assert_eq!( - map.style_transition_options() + operation_result!(map.style_transition_options()) .unwrap() .enable_placement_transitions, Some(true) @@ -1110,23 +1247,28 @@ fn style_transition_options_round_trip_through_the_real_c_api() { // Loading a style replaces the override with what that style declares. map.set_style_json(STYLE_WITH_TRANSITION_JSON.as_bytes()) .unwrap(); - assert_eq!(map.style_transition_options().unwrap(), declared); + assert_eq!( + operation_result!(map.style_transition_options()).unwrap(), + declared + ); let mut negative = StyleTransitionOptions::default(); negative.delay_ms = Some(-1.0); - let error = map.set_style_transition_options(&negative).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert!(error.diagnostic().contains("delay_ms")); + let rejected_command = map.set_style_transition_options(&negative).unwrap(); + let (_, _, diagnostic) = + assert_command_disposition(&runtime, rejected_command, CommandDisposition::Failed); + assert!(diagnostic.unwrap().contains("delay_ms")); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-063, BND-064, and BND-105. fn style_json_buffers_copy_owned_rust_values() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_json(STYLE_WITH_IDS_JSON.as_bytes()).unwrap(); let layer = serde_json::to_vec(&json!({ @@ -1136,8 +1278,7 @@ fn style_json_buffers_copy_owned_rust_values() { })) .unwrap(); map.add_style_layer_json(&layer, None).unwrap(); - let copied_layer = map - .style_layer_json("owned-background") + let copied_layer = operation_result!(map.style_layer_json("owned-background")) .unwrap() .expect("added layer should have a JSON snapshot"); let copied_layer: JsonValue = serde_json::from_slice(&copied_layer).unwrap(); @@ -1155,347 +1296,158 @@ fn style_json_buffers_copy_owned_rust_values() { map.set_layer_property("owned-background", "background-opacity", br#"0.75"#) .unwrap(); assert_eq!( - map.layer_property("owned-background", "background-opacity") - .unwrap(), + operation_result!(map.layer_property("owned-background", "background-opacity")).unwrap(), Some(b"0.75".to_vec()) ); let filter = br#"["==",["get","kind"],"park"]"#; map.set_layer_filter("geo-fill", Some(filter)).unwrap(); - assert_eq!(map.layer_filter("geo-fill").unwrap(), Some(filter.to_vec())); - map.set_layer_filter("geo-fill", None).unwrap(); - assert_eq!(map.layer_filter("geo-fill").unwrap(), None); - - let error = map - .set_layer_filter("owned-background", Some(b"NaN")) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT)); - - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-102 and BND-103. -fn camera_jump_and_coordinate_conversions_round_trip() { - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let mut options = MapOptions::new(512, 512, 1.0); - options.mode = MapMode::Continuous; - let map = MapHandle::with_options(&runtime, &options).unwrap(); - let center = LatLng::new(45.0, -122.0); - let eased_center = LatLng::new(46.0, -123.0); - let flown_center = LatLng::new(47.0, -124.0); - - let mut jump_camera = CameraOptions::default(); - jump_camera.center = Some(center); - jump_camera.zoom = Some(4.0); - map.jump_to(&jump_camera).unwrap(); - let camera = map.camera().unwrap(); - assert_eq!(camera.center, Some(center)); - assert_eq!(camera.zoom, Some(4.0)); - - let mut immediate = AnimationOptions::default(); - immediate.duration_ms = Some(0.0); - let mut ease_camera = CameraOptions::default(); - ease_camera.center = Some(eased_center); - ease_camera.zoom = Some(5.0); - map.ease_to(&ease_camera, Some(&immediate)).unwrap(); - let camera = map.camera().unwrap(); - assert_lat_lng_close(camera.center.unwrap(), eased_center); - assert_eq!(camera.zoom, Some(5.0)); - - let mut fly_camera = CameraOptions::default(); - fly_camera.center = Some(flown_center); - fly_camera.zoom = Some(6.0); - map.fly_to(&fly_camera, Some(&immediate)).unwrap(); - let camera = map.camera().unwrap(); - assert_lat_lng_close(camera.center.unwrap(), flown_center); - assert_eq!(camera.zoom, Some(6.0)); - - let mut cancel_camera = CameraOptions::default(); - cancel_camera.center = Some(center); - let mut cancel_animation = AnimationOptions::default(); - cancel_animation.duration_ms = Some(1000.0); - map.ease_to(&cancel_camera, Some(&cancel_animation)) - .unwrap(); - map.cancel_transitions().unwrap(); - - assert!(!map.is_gesture_in_progress().unwrap()); - map.set_gesture_in_progress(true).unwrap(); - map.move_by(8.0, -4.0).unwrap(); - assert!(map.is_gesture_in_progress().unwrap()); - map.set_gesture_in_progress(false).unwrap(); - assert!(!map.is_gesture_in_progress().unwrap()); - - let point = map.pixel_for_lat_lng(center).unwrap(); - let round_tripped = map.lat_lng_for_pixel(point).unwrap(); - assert!((round_tripped.latitude - center.latitude).abs() < 1e-7); - assert!((round_tripped.longitude - center.longitude).abs() < 1e-7); - - let points = map.pixels_for_lat_lngs(&[center]).unwrap(); - let coordinates = map.lat_lngs_for_pixels(&points).unwrap(); - assert_eq!(points.len(), 1); - assert!((coordinates[0].latitude - center.latitude).abs() < 1e-7); - assert!((coordinates[0].longitude - center.longitude).abs() < 1e-7); - - map.close().unwrap(); - runtime.close().unwrap(); -} - -/// Camera events drained from one runtime queue, in arrival order. -#[derive(Default)] -struct CameraEventTally { - finished_transition_ids: Vec, - did_change_modes: Vec, - did_change_followed_finish: bool, -} - -/// Tallies the camera events among the queued runtime events. -/// -/// The transition-finished event is queued while the camera command that ends -/// the transition runs, so one drain observes it. -fn drain_camera_events(runtime: &mut RuntimeHandle) -> CameraEventTally { - let mut tally = CameraEventTally::default(); - for event in runtime.drain_events(0).unwrap().iter() { - match event.event_type() { - RuntimeEventType::MapCameraTransitionFinished => { - let RuntimeEventPayload::CameraTransitionFinished(payload) = event.payload() else { - panic!("transition-finished event should carry its typed payload"); - }; - tally.finished_transition_ids.push(payload.transition_id); - } - RuntimeEventType::MapCameraDidChange => { - tally - .did_change_modes - .push(CameraChangeMode::from_raw(event.code() as u32)); - tally.did_change_followed_finish |= !tally.finished_transition_ids.is_empty(); - } - _ => {} - } - } - tally -} - -fn identified_ease(transition_id: u64, duration_ms: f64) -> AnimationOptions { - let mut animation = AnimationOptions::default(); - animation.transition_id = Some(transition_id); - animation.duration_ms = Some(duration_ms); - animation -} - -#[test] -// Spec coverage: BND-061, BND-102, and BND-087. -fn identified_camera_transitions_report_each_terminal_outcome_once() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let mut options = MapOptions::new(512, 512, 1.0); - options.mode = MapMode::Continuous; - let map = MapHandle::with_options(&runtime, &options).unwrap(); - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(45.0, -122.0)); - camera.zoom = Some(4.0); - // Map construction queues its own camera events, so start from an empty - // queue. - let _ = drain_camera_events(&mut runtime); - - // A zero-duration ease resolves inside the call, so its end is reported - // ahead of the did-change event for the same jump. - map.ease_to(&camera, Some(&identified_ease(0, 0.0))) - .unwrap(); - let tally = drain_camera_events(&mut runtime); - assert_eq!(tally.finished_transition_ids, vec![0]); - assert!(tally.did_change_followed_finish); - assert_eq!(tally.did_change_modes, vec![CameraChangeMode::Immediate]); - - camera.zoom = Some(12.0); - map.ease_to(&camera, Some(&identified_ease(11, 5_000.0))) - .unwrap(); - let tally = drain_camera_events(&mut runtime); - assert!(tally.finished_transition_ids.is_empty()); - - // A later camera command supersedes the running transition. - camera.zoom = Some(13.0); - map.ease_to(&camera, Some(&identified_ease(12, 5_000.0))) - .unwrap(); - let tally = drain_camera_events(&mut runtime); - assert_eq!(tally.finished_transition_ids, vec![11]); - assert_eq!(tally.did_change_modes, vec![CameraChangeMode::Animated]); - - map.cancel_transitions().unwrap(); - let tally = drain_camera_events(&mut runtime); - assert_eq!(tally.finished_transition_ids, vec![12]); - - // An absent identity keeps the transition silent, so the present zero ID - // above stayed distinguishable from an omitted one. - camera.zoom = Some(14.0); - map.ease_to(&camera, Some(&AnimationOptions::default())) - .unwrap(); - let tally = drain_camera_events(&mut runtime); - assert!(tally.finished_transition_ids.is_empty()); - - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-104. -fn empty_coordinate_slice_is_rejected_before_calling_c() { - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - let mut fit = CameraFitOptions::default(); - fit.padding = Some(EdgeInsets::new(1.0, 1.0, 1.0, 1.0)); - - let error = map.camera_for_lat_lngs(&[], Some(&fit)).unwrap_err(); - - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(error.raw_status(), None); - assert!(error.diagnostic().contains("at least one coordinate")); - - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-102, BND-103. -fn unbounded_and_world_bounds_constrain_the_camera_differently() { - fn jumped_longitude(map: &MapHandle, longitude: f64) -> f64 { - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(0.0, longitude)); - camera.zoom = Some(2.0); - map.jump_to(&camera).unwrap(); - map.camera().unwrap().center.unwrap().longitude - } - - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - assert_eq!( - map.bounds().unwrap().bounds, - Some(BoundsConstraint::Unbounded) + operation_result!(map.layer_filter("geo-fill")).unwrap(), + Some(filter.to_vec()) ); - assert!((jumped_longitude(&map, 200.0) - -160.0).abs() < 1e-6); - - let world = LatLngBounds::new(LatLng::new(-90.0, -180.0), LatLng::new(90.0, 180.0)); - let mut options = BoundOptions::default(); - options.bounds = Some(BoundsConstraint::Bounded(world)); - map.set_bounds(&options).unwrap(); + map.set_layer_filter("geo-fill", None).unwrap(); assert_eq!( - map.bounds().unwrap().bounds, - Some(BoundsConstraint::Bounded(world)) + operation_result!(map.layer_filter("geo-fill")).unwrap(), + None ); - // World bounds clamp at the antimeridian instead of wrapping. - assert!((jumped_longitude(&map, 200.0) - 180.0).abs() < 1e-6); - let mut options = BoundOptions::default(); - options.bounds = Some(BoundsConstraint::Unbounded); - map.set_bounds(&options).unwrap(); + let rejected_command = map + .set_layer_filter("owned-background", Some(b"NaN")) + .unwrap(); + assert_command_disposition(&runtime, rejected_command, CommandDisposition::Failed); assert_eq!( - map.bounds().unwrap().bounds, - Some(BoundsConstraint::Unbounded) + operation_result!(map.layer_filter("owned-background")).unwrap(), + None ); - // Releasing the constraint restores antimeridian wrapping. - assert!((jumped_longitude(&map, 200.0) - -160.0).abs() < 1e-6); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-102. -fn projection_mode_round_trips_through_real_c_api() { +fn camera_commands_and_ordered_queries_return_typed_completions() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - - let mut projection_mode = ProjectionMode::default(); - projection_mode.axonometric = Some(false); - projection_mode.x_skew = Some(0.0); - projection_mode.y_skew = Some(0.0); - map.set_projection_mode(&projection_mode).unwrap(); - let copied_projection_mode = map.projection_mode().unwrap(); - - assert_eq!(copied_projection_mode.axonometric, Some(false)); - - map.close().unwrap(); - runtime.close().unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + let center = LatLng::new(45.0, -122.0); + let mut update = CameraUpdate::default(); + update.camera.center = Some(center); + update.camera.zoom = Some(4.0); + + let command = map.update_camera(&update).unwrap(); + + let query = map.camera_query().unwrap(); + assert!(query.wait(Duration::from_secs(5)).unwrap()); + let queried = query.take().unwrap(); + query.release(); + assert_command_disposition(&runtime, command, CommandDisposition::Committed); + assert_eq!(queried.camera.center, Some(center)); + assert_eq!(queried.camera.zoom, Some(4.0)); + + let published = map.camera_snapshot().unwrap(); + assert!(published.generation <= queried.generation); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-045. -fn a_released_map_id_replayed_after_a_new_map_reports_it_stale() { +fn map_accepts_concurrent_commands_and_cross_thread_snapshots() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - - let first = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - let released = first.inner.native().unwrap(); - first.close().unwrap(); - - // The next map takes the released slot, so the replayed id names a retired - // generation of a slot that is live again. - let second = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - - let mut width = 0; - let mut height = 0; - let mut scale = 0.0; - // SAFETY: the id is well-formed and the out-pointers are live locals; the - // C API resolves the id rather than dereferencing it. - let status = unsafe { sys::mln_map_get_size(released, &mut width, &mut height, &mut scale) }; - assert_eq!(status, sys::MLN_STATUS_INVALID_ARGUMENT); - assert!(maplibre_native_ffi_core::error::capture_thread_diagnostic().contains("stale")); - - assert_eq!(second.size().unwrap().0, MapOptions::default().width); - second.close().unwrap(); - runtime.close().unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + + std::thread::scope(|scope| { + let first = scope.spawn(|| map.request_repaint().unwrap()); + let second = scope.spawn(|| map.request_repaint().unwrap()); + let first = first.join().unwrap(); + let second = second.join().unwrap(); + assert_command_disposition(&runtime, first, CommandDisposition::Committed); + assert_command_disposition(&runtime, second, CommandDisposition::Committed); + + let snapshot = scope.spawn(|| map.snapshot().unwrap()).join().unwrap(); + assert_eq!(snapshot.logical_extent.width, MapOptions::default().width); + }); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-047. -fn a_map_id_passed_to_a_runtime_operation_reports_invalid_argument() { +// Spec coverage: BND-102. +fn a_snapshot_at_the_committed_generation_observes_the_commit() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - - // `mln_map` and `mln_runtime` are distinct newtypes, so this call has no - // expression in the safe API. - let wrong_kind = sys::mln_runtime(map.inner.native().unwrap().0); - // SAFETY: the value is well-formed; the C API rejects it on its kind tag. - let status = unsafe { sys::mln_runtime_pump(wrong_kind, 0, -1) }; - - assert_eq!(status, sys::MLN_STATUS_INVALID_ARGUMENT); - let message = maplibre_native_ffi_core::error::capture_thread_diagnostic(); - assert!(message.contains("map"), "{message}"); - assert!(message.contains("runtime"), "{message}"); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + assert_eq!( + map.snapshot().unwrap().debug_options, + MapDebugOptions::empty() + ); - map.close().unwrap(); - runtime.close().unwrap(); + let completion = map + .set_debug_options(MapDebugOptions::TILE_BORDERS) + .unwrap(); + let (finished, _, _) = + assert_command_disposition(&runtime, completion, CommandDisposition::Committed); + let finished = finished.unwrap(); + assert_ne!(finished.generation, 0); + + // The commit fence: a snapshot at or past the reported generation shows + // the committed value. + let snapshot = map.snapshot().unwrap(); + assert!(snapshot.generation >= finished.generation); + assert_eq!(snapshot.debug_options, MapDebugOptions::TILE_BORDERS); + + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-049. -fn a_live_map_id_called_from_another_thread_reports_wrong_thread() { +// Spec coverage: BND-102. +fn snapshot_fields_round_trip_through_their_set_commands() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - - let live = map.inner.native().unwrap(); - let (status, message) = std::thread::scope(|scope| { - scope - .spawn(|| { - let mut width = 0; - let mut height = 0; - let mut scale = 0.0; - // SAFETY: the id is live; only the calling thread is wrong. - let status = - unsafe { sys::mln_map_get_size(live, &mut width, &mut height, &mut scale) }; - ( - status, - maplibre_native_ffi_core::error::capture_thread_diagnostic(), - ) - }) - .join() - .unwrap() - }); - - // The id is live, so the owner-thread rule decides rather than identity. - assert_eq!(status, sys::MLN_STATUS_WRONG_THREAD); - assert!(!message.contains("stale"), "{message}"); - - map.close().unwrap(); - runtime.close().unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + + let mut tile = crate::MapTileOptions::default(); + tile.prefetch_zoom_delta = Some(3); + tile.lod_scale = Some(1.5); + map.set_tile_options(&tile).unwrap(); + + let mut bounds = BoundOptions::default(); + bounds.min_zoom = Some(2.0); + bounds.max_zoom = Some(15.0); + map.set_bounds(&bounds).unwrap(); + + let mut viewport = crate::MapViewportOptions::default(); + viewport.viewport_mode = Some(crate::ViewportMode::FlippedY); + map.set_viewport_options(&viewport).unwrap(); + + let mut free_camera = FreeCameraOptions::default(); + free_camera.position = Some(crate::Vec3::new(0.25, 0.25, 0.5)); + map.set_free_camera_options(&free_camera).unwrap(); + + let stats_command = map.set_rendering_stats_view_enabled(true).unwrap(); + let (finished, _, _) = + assert_command_disposition(&runtime, stats_command, CommandDisposition::Committed); + + let snapshot = map.snapshot().unwrap(); + assert!(snapshot.generation >= finished.unwrap().generation); + assert_eq!(snapshot.tile.prefetch_zoom_delta, Some(3)); + assert_eq!(snapshot.tile.lod_scale, Some(1.5)); + assert_eq!(snapshot.bounds.min_zoom, Some(2.0)); + assert_eq!(snapshot.bounds.max_zoom, Some(15.0)); + assert_eq!( + snapshot.viewport.viewport_mode, + Some(crate::ViewportMode::FlippedY) + ); + // Native renormalizes altitude, so assert the committed ground position. + let position = snapshot.free_camera.position.unwrap(); + assert!((position.x - 0.25).abs() < 1e-6); + assert!((position.y - 0.25).abs() < 1e-6); + assert!(position.z > 0.0); + assert!(snapshot.rendering_stats_view_enabled); + + map.close_and_wait(); + runtime.close_and_wait(); } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/projection.rs b/bindings/rust/crates/maplibre-native-ffi/src/projection.rs index 1bad7ad1a..1f9166a6f 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/projection.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/projection.rs @@ -1,15 +1,17 @@ use std::fmt; +use std::sync::Arc; use maplibre_native_ffi_core as maplibre_core; use maplibre_native_ffi_core::ptr::const_ptr_or_null; -use maplibre_native_ffi_core::values::{empty_lat_lng, empty_screen_point, lat_lngs_to_native}; +use maplibre_native_ffi_core::values::lat_lngs_to_native; use maplibre_native_ffi_sys as sys; use crate::camera::CameraOptionsNativeExt; -use crate::handle::{ConcurrentNativeHandle, closed_handle_error, out_handle}; +use crate::handle::{ConcurrentNativeHandle, closed_handle_error}; use crate::values::NativeValue; use crate::{ - CameraOptions, EdgeInsets, Error, HandleOperationError, LatLng, MapHandle, Result, ScreenPoint, + CameraOptions, EdgeInsets, Error, HandleOperationError, LatLng, MapHandle, NativeFuture, + Result, ScreenPoint, }; #[derive(Debug)] @@ -19,15 +21,9 @@ pub(crate) struct MapProjectionState { impl MapProjectionState { fn new(native: sys::mln_map_projection) -> Result { - // SAFETY: native came from successful mln_map_projection_create and is - // paired with the matching projection destroy function. - let handle = unsafe { - ConcurrentNativeHandle::from_handle( - native, - sys::mln_map_projection_destroy, - "mln_map_projection", - ) - }?; + // SAFETY: native came from the typed creation take and projection + // control state supports calls from any thread. + let handle = unsafe { ConcurrentNativeHandle::from_handle(native, "mln_map_projection") }?; Ok(Self { handle }) } @@ -42,16 +38,33 @@ impl MapProjectionState { } fn close(&self) -> Result<()> { - self.handle.close() + self.handle.close_with(|projection| { + // SAFETY: projection is live; the synchronous close waits for + // calls already running on other threads before it retires the + // handle. + maplibre_core::check(unsafe { sys::mln_map_projection_close(projection) }) + })?; + Ok(()) + } +} + +impl Drop for MapProjectionState { + fn drop(&mut self) { + if self.close().is_err() { + self.handle.leak_for_report(); + } } } -/// Standalone projection snapshot created from a map transform. +/// Any-thread standalone projection snapshot created from a map transform. /// -/// The projection does not retain the source map after creation. It remains -/// usable from any thread, and native calls serialize access to its transform. +/// Every call after creation is synchronous, runs on the calling thread, and +/// is internally serialized, so a projection is usable from any thread. A +/// projection copies the map transform once at creation and never observes +/// map changes made after it and remains usable after that map and its runtime +/// close. pub struct MapProjectionHandle { - inner: MapProjectionState, + inner: Arc, } impl fmt::Debug for MapProjectionHandle { @@ -63,114 +76,111 @@ impl fmt::Debug for MapProjectionHandle { } impl MapProjectionHandle { - pub(crate) fn new(map: &MapHandle) -> Result { + pub(crate) fn new(map: &MapHandle) -> Result> { let map_ptr = map.inner.native()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: map_ptr is a live map handle. out is a valid null-initialized - // out-pointer owned by this call. - maplibre_core::check(unsafe { sys::mln_map_projection_create(map_ptr, out.as_mut_ptr()) })?; - let ptr = out_handle(out, "mln_map_projection")?; - Ok(Self { - inner: MapProjectionState::new(ptr)?, - }) + crate::completion::submit( + move |completion| unsafe { sys::mln_map_projection_create(map_ptr, completion) }, + |result| { + let native = crate::completion::copy_value::(result)?; + Ok(Self { + inner: Arc::new(MapProjectionState::new(native)?), + }) + }, + ) } - /// Explicitly destroys the projection snapshot. + /// Closes the projection synchronously. pub fn close(self) -> std::result::Result<(), HandleOperationError> { self.inner .close() .map_err(|error| HandleOperationError::new(error, self)) } - /// Reads the projection helper's current camera snapshot. + /// Copies the projection camera, observing every earlier setter. pub fn camera(&self) -> Result { - let projection = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. + // SAFETY: the constructor initializes this ABI version's descriptor. let mut raw = unsafe { sys::mln_camera_options_default() }; - // SAFETY: projection is live and raw has a valid size field for C to fill. - maplibre_core::check(unsafe { sys::mln_map_projection_get_camera(projection, &mut raw) })?; + // SAFETY: projection is live and raw is size-tagged writable storage. + maplibre_core::check(unsafe { + sys::mln_map_projection_get_camera(self.inner.native()?, &mut raw) + })?; Ok(CameraOptions::from_native(raw)) } - /// Applies camera fields to this projection helper. + /// Applies a camera update before returning; the map camera is unaffected. pub fn set_camera(&self, camera: &CameraOptions) -> Result<()> { - let projection = self.inner.native()?; - let raw = camera.to_native(); - // SAFETY: projection is live and raw is a materialized descriptor valid - // for the duration of this call. - maplibre_core::check(unsafe { sys::mln_map_projection_set_camera(projection, &raw) }) + // SAFETY: projection is live and the native camera struct is borrowed + // for this call. + maplibre_core::check(unsafe { + sys::mln_map_projection_set_camera(self.inner.native()?, &camera.to_native()) + }) } - /// Updates the projection camera so coordinates are visible within padding. + /// Applies a camera fit for geographic coordinates before returning. pub fn set_visible_coordinates( &self, coordinates: &[LatLng], padding: EdgeInsets, ) -> Result<()> { - let projection = self.inner.native()?; if coordinates.is_empty() { return Err(Error::invalid_argument( "set_visible_coordinates requires at least one coordinate", )); } - let raw_coordinates = lat_lngs_to_native(coordinates); - // SAFETY: projection is live. coordinates points to coordinate_count - // non-empty entries. padding is passed by value. + let coordinates = lat_lngs_to_native(coordinates); + // SAFETY: projection is live and coordinates points to call-scoped + // native coordinate storage. maplibre_core::check(unsafe { sys::mln_map_projection_set_visible_coordinates( - projection, - const_ptr_or_null(&raw_coordinates), - raw_coordinates.len(), + self.inner.native()?, + const_ptr_or_null(&coordinates), + coordinates.len(), padding.to_native(), ) }) } - /// Updates the projection camera so geometry coordinates are visible. + /// Applies a camera fit for GeoJSON Geometry bytes before returning. pub fn set_visible_geometry(&self, geometry: &[u8], padding: EdgeInsets) -> Result<()> { - let projection = self.inner.native()?; - let native_geometry = maplibre_core::string::buffer_view(geometry); - // SAFETY: projection is live, native_geometry owns backing storage for - // the duration of this call, and padding is passed by value. + // SAFETY: projection is live and geometry is borrowed for this call. maplibre_core::check(unsafe { sys::mln_map_projection_set_visible_geometry( - projection, - native_geometry, + self.inner.native()?, + maplibre_core::string::buffer_view(geometry), padding.to_native(), ) }) } - /// Converts a geographic world coordinate to a screen point. + /// Converts a geographic coordinate to a screen point synchronously. pub fn pixel_for_lat_lng(&self, coordinate: LatLng) -> Result { - let projection = self.inner.native()?; - let mut raw_point = empty_screen_point(); - // SAFETY: projection is live, coordinate is passed by value, and - // raw_point is writable output storage. + let mut raw = sys::mln_screen_point { x: 0.0, y: 0.0 }; + // SAFETY: projection is live and raw is writable storage. maplibre_core::check(unsafe { sys::mln_map_projection_pixel_for_lat_lng( - projection, + self.inner.native()?, coordinate.to_native(), - &mut raw_point, + &mut raw, ) })?; - Ok(ScreenPoint::from_native(raw_point)) + Ok(ScreenPoint::from_native(raw)) } - /// Converts a screen point to a geographic world coordinate. + /// Converts a screen point to a geographic coordinate synchronously. pub fn lat_lng_for_pixel(&self, point: ScreenPoint) -> Result { - let projection = self.inner.native()?; - let mut raw_coordinate = empty_lat_lng(); - // SAFETY: projection is live, point is passed by value, and - // raw_coordinate is writable output storage. + let mut raw = sys::mln_lat_lng { + latitude: 0.0, + longitude: 0.0, + }; + // SAFETY: projection is live and raw is writable storage. maplibre_core::check(unsafe { sys::mln_map_projection_lat_lng_for_pixel( - projection, + self.inner.native()?, point.to_native(), - &mut raw_coordinate, + &mut raw, ) })?; - Ok(LatLng::from_native(raw_coordinate)) + Ok(LatLng::from_native(raw)) } } @@ -185,22 +195,31 @@ mod tests { #[test] // Spec coverage: BND-043 and BND-103. - fn projection_create_round_trip_close_and_stays_live_after_map_close() { + fn projection_observes_earlier_camera_commands_and_round_trips_synchronously() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(512, 512, 1.0)).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(512, 512, 1.0), + )); let center = LatLng::new(37.7749, -122.4194); let mut camera_options = CameraOptions::default(); camera_options.center = Some(center); camera_options.zoom = Some(5.0); - map.jump_to(&camera_options).unwrap(); + let mut update = crate::CameraUpdate::default(); + update.camera = camera_options.clone(); + map.update_camera(&update).unwrap(); - let projection = map.create_projection().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + // Creation is ordered after the accepted camera command, so the + // projection observes it: the committed center is the viewport center. + let projection = crate::completion::blocking(map.create_projection()); + let center_point = projection.pixel_for_lat_lng(center).unwrap(); + assert!((center_point.x - 256.0).abs() < 1e-6); + assert!((center_point.y - 256.0).abs() < 1e-6); + map.close_and_wait(); + runtime.close_and_wait(); std::thread::spawn(move || { - let point = projection.pixel_for_lat_lng(center).unwrap(); - let round_tripped = projection.lat_lng_for_pixel(point).unwrap(); + let round_tripped = projection.lat_lng_for_pixel(center_point).unwrap(); assert!((round_tripped.latitude - center.latitude).abs() < 1e-7); assert!((round_tripped.longitude - center.longitude).abs() < 1e-7); projection.close().unwrap(); @@ -214,35 +233,49 @@ mod tests { // attempt unsafe cleanup from an uncontrolled destructor path. fn projection_drops_without_explicit_close() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); { - let _projection = map.create_projection().unwrap(); + let _projection = crate::completion::blocking(map.create_projection()); } - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-103. - fn projection_camera_and_visible_region_helpers_call_c_api() { + fn projection_setters_change_later_conversions_synchronously() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - let projection = map.create_projection().unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(512, 512, 1.0), + )); + let projection = crate::completion::blocking(map.create_projection()); + let center = LatLng::new(10.0, 20.0); let mut camera_options = CameraOptions::default(); - camera_options.center = Some(LatLng::new(0.0, 0.0)); + camera_options.center = Some(center); camera_options.zoom = Some(2.0); projection.set_camera(&camera_options).unwrap(); let camera = projection.camera().unwrap(); - assert_eq!(camera.center, Some(LatLng::new(0.0, 0.0))); + let read_center = camera.center.unwrap(); + assert!((read_center.latitude - center.latitude).abs() < 1e-9); + assert!((read_center.longitude - center.longitude).abs() < 1e-9); assert_eq!(camera.zoom, Some(2.0)); + // The setter completed before returning, so the very next conversion + // maps the new center to the viewport center. + let center_point = projection.pixel_for_lat_lng(center).unwrap(); + assert!((center_point.x - 256.0).abs() < 1e-6); + assert!((center_point.y - 256.0).abs() < 1e-6); let padding = EdgeInsets::new(0.0, 0.0, 0.0, 0.0); projection .set_visible_coordinates(&[LatLng::new(0.0, 0.0), LatLng::new(1.0, 1.0)], padding) .unwrap(); + let fitted = projection.camera().unwrap(); + assert_ne!(fitted.center, Some(center)); let error = projection .set_visible_coordinates(&[], padding) .unwrap_err(); @@ -257,7 +290,44 @@ mod tests { .unwrap(); projection.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); + } + + #[test] + // Spec coverage: BND-049 and BND-190. + fn projection_calls_work_from_a_second_thread_and_never_observe_later_map_changes() { + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(512, 512, 1.0), + )); + let projection = crate::completion::blocking(map.create_projection()); + let creation_camera = projection.camera().unwrap(); + + // A later map camera command leaves the projection's snapshot alone. + let mut update = crate::CameraUpdate::default(); + update.camera.center = Some(LatLng::new(45.0, 45.0)); + update.camera.zoom = Some(9.0); + map.update_camera(&update).unwrap(); + let barrier = runtime.barrier().unwrap(); + assert!(barrier.wait(std::time::Duration::from_secs(5)).unwrap()); + barrier.finish().unwrap(); + barrier.release(); + assert_eq!(projection.camera().unwrap(), creation_camera); + + std::thread::scope(|scope| { + let worker = scope.spawn(|| { + let point = projection.pixel_for_lat_lng(LatLng::new(0.0, 0.0)).unwrap(); + projection.lat_lng_for_pixel(point).unwrap() + }); + let round_tripped = worker.join().unwrap(); + assert!(round_tripped.latitude.abs() < 1e-7); + assert!(round_tripped.longitude.abs() < 1e-7); + }); + + projection.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/render.rs b/bindings/rust/crates/maplibre-native-ffi/src/render.rs index a527c8ded..57f2b89e7 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/render.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/render.rs @@ -1,17 +1,18 @@ -use std::cell::Cell; use std::fmt; use std::marker::PhantomData; use std::mem; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; pub use maplibre_core::{PremultipliedRgba8Image, TextureImageInfo}; use maplibre_native_ffi_core as maplibre_core; -use maplibre_native_ffi_core::{OpenGLClientApi, OpenGLContextOwnership, RenderResult}; +use maplibre_native_ffi_core::{OpenGLClientApi, OpenGLContextOwnership}; use maplibre_native_ffi_sys as sys; -use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle}; -use crate::map::MapAttachRef; -use crate::{HandleOperationError, Result}; +use crate::handle::{ConcurrentNativeHandle, closed_handle_error, out_handle}; +use crate::map::MapHandle; +use crate::{HandleOperationError, NativeFuture, Result}; /// Borrowed opaque native address used for backend interop handles. It does not /// own, retain, dereference, or validate the pointed-to object, and passing it @@ -455,34 +456,51 @@ impl Default for EglContextDescriptor { } } -/// Browser WebGL context a session renders into. -/// -/// The host creates the context and keeps owning it; a session shares it rather -/// than holding it exclusively. `context` is an -/// `EMSCRIPTEN_WEBGL_CONTEXT_HANDLE`, which the native library requires to be -/// positive. +/// Browser WebGL context placement. #[derive(Debug, Clone, PartialEq)] -#[non_exhaustive] -pub struct WebGlContextDescriptor { - pub context: i32, +pub enum WebGlContextDescriptor { + /// A host-created WebGL context on the caller graphics thread. + Existing { context: i32 }, + /// A transferable canvas selector whose WebGL2 context is created on the + /// native core worker. + TransferredCanvas { canvas_selector: String }, } impl WebGlContextDescriptor { - pub fn new(context: i32) -> Self { - Self { context } + pub fn existing(context: i32) -> Self { + Self::Existing { context } + } + + pub fn transferred_canvas(canvas_selector: impl Into) -> Self { + Self::TransferredCanvas { + canvas_selector: canvas_selector.into(), + } } pub(crate) fn to_core(&self) -> maplibre_core::render::WebGlContextDescriptorFields { - maplibre_core::render::WebGlContextDescriptorFields { - context: self.context, + match self { + Self::Existing { context } => maplibre_core::render::WebGlContextDescriptorFields { + kind: sys::MLN_WEBGL_CONTEXT_EXISTING, + context: *context, + canvas_selector_data: std::ptr::null(), + canvas_selector_size: 0, + }, + Self::TransferredCanvas { canvas_selector } => { + maplibre_core::render::WebGlContextDescriptorFields { + kind: sys::MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS, + context: 0, + canvas_selector_data: canvas_selector.as_ptr(), + canvas_selector_size: canvas_selector.len(), + } + } } } } /// OpenGL platform context a render session draws through. /// -/// Each platform descriptor carries its own thread ownership. A browser session -/// renders through the host's own WebGL context, so it is shared only. +/// Each platform descriptor carries its own thread ownership. An existing +/// WebGL context is shared, while a transferred canvas is dedicated. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum OpenGLContextDescriptor { @@ -928,1101 +946,931 @@ impl OpenGLBorrowedTextureDescriptor { } } -#[derive(Debug)] -struct RenderSessionState { - handle: ThreadAffineNativeHandle, - detached: Cell, - frame_acquired: Cell, +/// Native execution placement for a render session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderDriverKind { + CoreWorker, + CallerGraphicsThread, + Unknown(u32), } -impl RenderSessionState { - fn new(native: sys::mln_render_session) -> Result { - // SAFETY: native came from a successful render-session attach call and is - // paired with the matching render-session destroy function. - let handle = unsafe { - ThreadAffineNativeHandle::from_handle( - native, - sys::mln_render_session_destroy, - "mln_render_session", - ) - }?; - Ok(Self { - handle, - detached: Cell::new(false), - frame_acquired: Cell::new(false), - }) +impl RenderDriverKind { + fn to_native(self) -> u32 { + match self { + Self::CoreWorker => sys::MLN_RENDER_DRIVER_CORE_WORKER, + Self::CallerGraphicsThread => sys::MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + Self::Unknown(value) => value, + } } - fn ensure_no_frame_acquired(&self) -> Result<()> { - if self.frame_acquired.get() { - Err(frame_acquired_error()) - } else { - Ok(()) + fn from_native(value: u32) -> Self { + match value { + sys::MLN_RENDER_DRIVER_CORE_WORKER => Self::CoreWorker, + sys::MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD => Self::CallerGraphicsThread, + value => Self::Unknown(value), } } +} - fn native(&self) -> Result { - self.handle - .live_handle() - .ok_or_else(|| closed_handle_error("RenderSessionHandle")) - } +/// Attachment policy for a render session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderSessionAttachOptions { + pub driver: RenderDriverKind, + pub requested_texture_ring_depth: u32, +} - fn close(&self) -> Result<()> { - self.handle.close() +impl RenderSessionAttachOptions { + pub fn core_worker(requested_texture_ring_depth: u32) -> Self { + Self { + driver: RenderDriverKind::CoreWorker, + requested_texture_ring_depth, + } } -} -/// Outcome of a successful [`RenderSessionHandle::render_update`] call. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub struct RenderUpdate { - /// Which outcome the call reached; see [`RenderResult`] for the wake each - /// variant names. - pub result: RenderResult, - /// Whether the map asked for another frame while it rendered this one, as - /// during an ongoing camera transition. Set only when `result` is - /// [`RenderResult::Rendered`]; false for every other outcome. This is the - /// same signal the render-frame-finished event carries, delivered here - /// without the event round trip, so a host can re-arm its frame loop - /// before it drains events. - pub needs_repaint: bool, -} + pub fn caller_graphics_thread(requested_texture_ring_depth: u32) -> Self { + Self { + driver: RenderDriverKind::CallerGraphicsThread, + requested_texture_ring_depth, + } + } -/// Render session handle bound to the thread that attached it. -/// -/// The session holds no Rust-level retention of its map. Native keeps the map -/// alive instead: destroying a map fails while a session is attached to it. -pub struct RenderSessionHandle { - inner: Rc, + fn to_native(self) -> sys::mln_render_session_attach_options { + let mut raw = unsafe { sys::mln_render_session_attach_options_default() }; + raw.driver = self.driver.to_native(); + raw.requested_texture_ring_depth = self.requested_texture_ring_depth; + raw + } } -impl fmt::Debug for RenderSessionHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RenderSessionHandle") - .field("closed", &self.inner.handle.is_closed()) - .field("detached", &self.inner.detached.get()) - .finish() +impl Default for RenderSessionAttachOptions { + fn default() -> Self { + Self::core_worker(1) } } -/// Render session after backend resources have been detached. -/// -/// A detached session holds no reference to its former map, so it stays -/// destroyable after that map closes. -pub struct DetachedRenderSessionHandle { - inner: Rc, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderSessionCapabilities { + pub driver: RenderDriverKind, + pub texture_ring_depth: u32, + pub frame_acquisition: bool, + pub readback: bool, + pub consumer_sync: bool, + pub presentation: bool, } -impl fmt::Debug for DetachedRenderSessionHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DetachedRenderSessionHandle") - .field("closed", &self.inner.handle.is_closed()) - .finish() - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderSessionLifecycle { + Attaching, + Attached, + Detaching, + Detached, + TargetLost, + Abandoned, + Unknown(u32), } -impl DetachedRenderSessionHandle { - /// Explicitly destroys the detached render session. - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - if let Err(error) = self.inner.close() { - return Err(HandleOperationError::new(error, self)); - } - Ok(()) +fn lifecycle_from_native(value: u32) -> RenderSessionLifecycle { + match value { + sys::MLN_RENDER_SESSION_STATE_ATTACHING => RenderSessionLifecycle::Attaching, + sys::MLN_RENDER_SESSION_STATE_ATTACHED => RenderSessionLifecycle::Attached, + sys::MLN_RENDER_SESSION_STATE_DETACHING => RenderSessionLifecycle::Detaching, + sys::MLN_RENDER_SESSION_STATE_DETACHED => RenderSessionLifecycle::Detached, + sys::MLN_RENDER_SESSION_STATE_TARGET_LOST => RenderSessionLifecycle::TargetLost, + sys::MLN_RENDER_SESSION_STATE_ABANDONED => RenderSessionLifecycle::Abandoned, + value => RenderSessionLifecycle::Unknown(value), } } -fn frame_acquired_error() -> crate::Error { - crate::Error::new( - crate::ErrorKind::InvalidState, - None, - "render session has an acquired texture frame", - ) -} - -/// Copied metadata for an acquired Metal session-owned texture frame. -/// -/// Backend pointers are exposed by [`MetalOwnedTextureFrameHandle`] so their -/// lifetime stays tied to the open frame handle. -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub struct MetalOwnedTextureFrame { - pub generation: u64, - pub width: u32, - pub height: u32, - pub scale_factor: f64, - pub frame_id: u64, - pub pixel_format: u64, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameDisposition { + Rendered, + NoUpdate, + SizePending, + TargetNotReady, + Superseded, + DeadlineMissed, + Unknown(u32), } -impl MetalOwnedTextureFrame { - fn from_native(raw: &sys::mln_metal_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - pixel_format: raw.pixel_format, - } +fn disposition_from_native(value: u32) -> FrameDisposition { + match value { + sys::MLN_RENDER_RESULT_RENDERED => FrameDisposition::Rendered, + sys::MLN_RENDER_RESULT_NO_UPDATE => FrameDisposition::NoUpdate, + sys::MLN_RENDER_RESULT_SIZE_PENDING => FrameDisposition::SizePending, + sys::MLN_RENDER_RESULT_TARGET_NOT_READY => FrameDisposition::TargetNotReady, + sys::MLN_RENDER_RESULT_SUPERSEDED => FrameDisposition::Superseded, + sys::MLN_RENDER_RESULT_DEADLINE_MISSED => FrameDisposition::DeadlineMissed, + value => FrameDisposition::Unknown(value), } } -/// Copied metadata for an acquired Vulkan session-owned texture frame. -/// -/// Backend pointers are exposed by [`VulkanOwnedTextureFrameHandle`] so their -/// lifetime stays tied to the open frame handle. -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub struct VulkanOwnedTextureFrame { - pub generation: u64, - pub width: u32, - pub height: u32, - pub scale_factor: f64, - pub frame_id: u64, - pub format: u32, - pub layout: u32, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FrameDemand { + pub if_needed: bool, + pub present: bool, + pub token: u64, + pub coalescing_boundary: u64, + pub timeout_ns: u64, } -impl VulkanOwnedTextureFrame { - fn from_native(raw: &sys::mln_vulkan_owned_texture_frame) -> Self { +impl Default for FrameDemand { + fn default() -> Self { Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - format: raw.format, - layout: raw.layout, + if_needed: true, + present: false, + token: 0, + coalescing_boundary: 0, + timeout_ns: 0, } } } -/// Copied metadata for an acquired WebGPU session-owned texture frame. -/// -/// Backend pointers are exposed by [`WebGpuOwnedTextureFrameHandle`] so their -/// lifetime stays tied to the open frame handle. -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub struct WebGpuOwnedTextureFrame { - pub generation: u64, - pub width: u32, - pub height: u32, - pub scale_factor: f64, - pub frame_id: u64, - pub format: u32, -} - -impl WebGpuOwnedTextureFrame { - fn from_native(raw: &sys::mln_webgpu_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - format: raw.format, +impl FrameDemand { + fn to_native(self) -> sys::mln_frame_demand { + let mut flags = 0; + if self.if_needed { + flags |= sys::MLN_FRAME_DEMAND_IF_NEEDED; + } + if self.present { + flags |= sys::MLN_FRAME_DEMAND_PRESENT; + } + sys::mln_frame_demand { + size: mem::size_of::() as u32, + flags, + token: self.token, + coalescing_boundary: self.coalescing_boundary, + timeout_ns: self.timeout_ns, } } } -/// Copied metadata for an acquired OpenGL session-owned texture frame. -/// -/// The texture object name is exposed by [`OpenGLOwnedTextureFrameHandle`] so -/// its lifetime stays tied to the open frame handle. -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub struct OpenGLOwnedTextureFrame { - pub generation: u64, - pub width: u32, - pub height: u32, - pub scale_factor: f64, - pub frame_id: u64, - pub target: u32, - pub internal_format: u32, - pub format: u32, - pub type_: u32, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderFrameResult { + pub disposition: FrameDisposition, + pub token: u64, + pub map_update_generation: u64, + pub extent_generation: u64, + pub frame_generation: u64, + /// Whether the map asked for another frame while it rendered this one, as + /// during an ongoing camera transition. Set only when `disposition` is + /// [`FrameDisposition::Rendered`]; false for every other outcome. This is + /// the same signal the render-frame-finished event carries, delivered with + /// the frame result so a host can re-arm its frame loop without the + /// runtime event round trip. + pub needs_repaint: bool, } -impl OpenGLOwnedTextureFrame { - fn from_native(raw: &sys::mln_opengl_owned_texture_frame) -> Self { - Self { - generation: raw.generation, - width: raw.width, - height: raw.height, - scale_factor: raw.scale_factor, - frame_id: raw.frame_id, - target: raw.target, - internal_format: raw.internal_format, - format: raw.format, - type_: raw.type_, - } +fn frame_result_from_native(raw: sys::mln_render_frame_result) -> RenderFrameResult { + RenderFrameResult { + disposition: disposition_from_native(raw.disposition), + token: raw.token, + map_update_generation: raw.map_update_generation, + extent_generation: raw.extent_generation, + frame_generation: raw.frame_generation, + needs_repaint: raw.needs_repaint, } } -/// RAII guard for an acquired Metal session-owned texture frame. -/// -/// Releasing the guard ends the borrow of the backend Metal texture and device. -pub struct MetalOwnedTextureFrameHandle { - session: Rc, - raw: sys::mln_metal_owned_texture_frame, - frame: MetalOwnedTextureFrame, - closed: Cell, - _thread_affine: PhantomData>, +#[derive(Debug, Clone, PartialEq)] +pub struct RenderSessionSnapshot { + pub state: RenderSessionLifecycle, + pub driver: RenderDriverKind, + pub latest_result: FrameDisposition, + pub extent: RenderTargetExtent, + pub generation: u64, + pub map_update_generation: u64, + pub rendered_update_generation: u64, + pub extent_generation: u64, + pub frame_generation: u64, + pub latest_demand_token: u64, + pub pending_demand_count: u32, + pub acquired_frame_count: u32, + pub target_ready: bool, + pub pending_changes: bool, } -impl fmt::Debug for MetalOwnedTextureFrameHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MetalOwnedTextureFrameHandle") - .field("closed", &self.closed.get()) - .field("frame", &self.frame) - .finish() - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RenderAbandonResult { + pub quarantined: bool, + pub quarantined_resource_count: u32, } -impl MetalOwnedTextureFrameHandle { - /// Returns copied metadata for this acquired frame. - pub fn frame(&self) -> Result<&MetalOwnedTextureFrame> { - if self.closed.get() { - Err(closed_handle_error("MetalOwnedTextureFrameHandle")) - } else { - Ok(&self.frame) - } - } - /// Returns the borrowed Metal texture pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer is valid only while this frame handle remains open. - /// The caller must not store or use it after frame release and must satisfy - /// Metal synchronization and thread-affinity requirements. - pub unsafe fn texture(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("MetalOwnedTextureFrameHandle")) - } else { - // SAFETY: The active native frame owns the validity contract for - // this borrowed backend handle until release. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture) }) - } - } +#[derive(Debug)] +struct RenderSessionState { + handle: ConcurrentNativeHandle, +} - /// Returns the borrowed Metal device pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer has the same lifetime and synchronization - /// requirements as [`MetalOwnedTextureFrameHandle::texture`]. - pub unsafe fn device(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("MetalOwnedTextureFrameHandle")) - } else { - // SAFETY: See texture above. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) }) - } +impl RenderSessionState { + fn new(native: sys::mln_render_session) -> Result { + let handle = unsafe { ConcurrentNativeHandle::from_handle(native, "mln_render_session") }?; + Ok(Self { handle }) } - /// Explicitly releases this frame. - #[allow(clippy::result_large_err)] - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - self.close_with_release(sys::mln_metal_owned_texture_release_frame) + fn native(&self) -> Result { + self.handle + .live_handle() + .ok_or_else(|| closed_handle_error("RenderSessionHandle")) } - #[allow(clippy::result_large_err)] - fn close_with_release( - self, - release: unsafe extern "C" fn( - sys::mln_render_session, - *const sys::mln_metal_owned_texture_frame, - ) -> sys::mln_status, - ) -> std::result::Result<(), HandleOperationError> { - if self.closed.get() { - return Ok(()); - } - let session = match self.session.native() { - Ok(session) => session, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - // SAFETY: session is live, and raw is the active frame returned by a - // successful acquire for this session until release succeeds. - if let Err(error) = maplibre_core::check(unsafe { release(session, &self.raw) }) { - return Err(HandleOperationError::new(error, self)); - } - self.closed.set(true); - self.session.frame_acquired.set(false); + fn destroy(&self) -> Result<()> { + self.handle.close_with(|session| { + maplibre_core::check(unsafe { sys::mln_render_session_destroy(session) }) + })?; Ok(()) } } -impl Drop for MetalOwnedTextureFrameHandle { +impl Drop for RenderSessionState { fn drop(&mut self) { - if self.closed.get() { - return; - } - if let Ok(session) = self.session.native() { - // SAFETY: Best-effort release of the active frame. Drop cannot - // report errors and never panics. - let status = unsafe { sys::mln_metal_owned_texture_release_frame(session, &self.raw) }; - if status == sys::MLN_STATUS_OK { - self.closed.set(true); - self.session.frame_acquired.set(false); - } + if self.destroy().is_err() { + self.handle.leak_for_report(); } } } -/// RAII guard for an acquired Vulkan session-owned texture frame. -/// -/// Releasing the guard ends the borrow of the backend Vulkan image, image view, -/// and device. -pub struct VulkanOwnedTextureFrameHandle { - session: Rc, - raw: sys::mln_vulkan_owned_texture_frame, - frame: VulkanOwnedTextureFrame, - closed: Cell, - _thread_affine: PhantomData>, +/// Send-safe render-session control handle. +#[derive(Clone)] +pub struct RenderSessionHandle { + inner: Arc, } -impl fmt::Debug for VulkanOwnedTextureFrameHandle { +impl fmt::Debug for RenderSessionHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("VulkanOwnedTextureFrameHandle") - .field("closed", &self.closed.get()) - .field("frame", &self.frame) + f.debug_struct("RenderSessionHandle") + .field("closed", &self.inner.handle.is_closed()) .finish() } } -impl VulkanOwnedTextureFrameHandle { - /// Returns copied metadata for this acquired frame. - pub fn frame(&self) -> Result<&VulkanOwnedTextureFrame> { - if self.closed.get() { - Err(closed_handle_error("VulkanOwnedTextureFrameHandle")) - } else { - Ok(&self.frame) - } - } - /// Returns the borrowed Vulkan image pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer is valid only while this frame handle remains open. - /// The caller must not store or use it after frame release and must satisfy - /// Vulkan synchronization and thread-affinity requirements. - pub unsafe fn image(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("VulkanOwnedTextureFrameHandle")) - } else { - // SAFETY: The active native frame owns the validity contract for - // this borrowed backend handle until release. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.image) }) - } - } +/// A session returned at submission and its asynchronous attachment completion. +#[derive(Debug)] +pub struct RenderSessionAttachment { + pub session: RenderSessionHandle, + pub completion: NativeFuture<()>, +} - /// Returns the borrowed Vulkan image view pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer has the same lifetime and synchronization - /// requirements as [`VulkanOwnedTextureFrameHandle::image`]. - pub unsafe fn image_view(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("VulkanOwnedTextureFrameHandle")) - } else { - // SAFETY: See image above. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.image_view) }) - } +impl RenderSessionHandle { + pub(crate) fn attach( + map: &MapHandle, + options: RenderSessionAttachOptions, + attach: F, + ) -> Result + where + F: FnOnce( + sys::mln_map, + *const sys::mln_render_session_attach_options, + *mut sys::mln_render_session, + *const sys::mln_completion, + ) -> sys::mln_status, + { + let map_native = map.inner.native()?; + let raw_options = options.to_native(); + let mut session = maplibre_core::ptr::OutHandle::::new(); + let completion = crate::completion::submit( + |completion| attach(map_native, &raw_options, session.as_mut_ptr(), completion), + crate::completion::unit, + )?; + let session = out_handle(session, "mln_render_session")?; + let inner = Arc::new(RenderSessionState::new(session)?); + completion.retain(Arc::clone(&inner)); + Ok(RenderSessionAttachment { + session: Self { inner }, + completion, + }) } - /// Returns the borrowed Vulkan device pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer has the same lifetime and synchronization - /// requirements as [`VulkanOwnedTextureFrameHandle::image`]. - pub unsafe fn device(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("VulkanOwnedTextureFrameHandle")) - } else { - // SAFETY: See image above. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) }) - } + fn start_unit( + &self, + start: impl FnOnce(sys::mln_render_session, *const sys::mln_completion) -> sys::mln_status, + ) -> Result> { + let session = self.inner.native()?; + crate::completion::submit( + |completion| start(session, completion), + crate::completion::unit, + ) } - /// Explicitly releases this frame. - #[allow(clippy::result_large_err)] - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - if self.closed.get() { - return Ok(()); - } - let session = match self.session.native() { - Ok(session) => session, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - // SAFETY: session is live, and raw is the active frame returned by a - // successful acquire for this session until release succeeds. - if let Err(error) = maplibre_core::check(unsafe { - sys::mln_vulkan_owned_texture_release_frame(session, &self.raw) - }) { - return Err(HandleOperationError::new(error, self)); - } - self.closed.set(true); - self.session.frame_acquired.set(false); - Ok(()) + pub fn capabilities(&self) -> Result { + let mut raw: sys::mln_render_session_capabilities = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_session_get_capabilities(self.inner.native()?, &mut raw) + })?; + Ok(RenderSessionCapabilities { + driver: RenderDriverKind::from_native(raw.driver), + texture_ring_depth: raw.texture_ring_depth, + frame_acquisition: raw.flags & sys::MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION + != 0, + readback: raw.flags & sys::MLN_RENDER_SESSION_CAPABILITY_READBACK != 0, + consumer_sync: raw.flags & sys::MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC != 0, + presentation: raw.flags & sys::MLN_RENDER_SESSION_CAPABILITY_PRESENTATION != 0, + }) } -} -impl Drop for VulkanOwnedTextureFrameHandle { - fn drop(&mut self) { - if self.closed.get() { - return; - } - if let Ok(session) = self.session.native() { - // SAFETY: Best-effort release of the active frame. Drop cannot - // report errors and never panics. - let status = unsafe { sys::mln_vulkan_owned_texture_release_frame(session, &self.raw) }; - if status == sys::MLN_STATUS_OK { - self.closed.set(true); - self.session.frame_acquired.set(false); - } - } + pub fn snapshot(&self) -> Result { + let mut raw: sys::mln_render_session_snapshot = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_session_get_snapshot(self.inner.native()?, &mut raw) + })?; + Ok(RenderSessionSnapshot { + state: lifecycle_from_native(raw.state), + driver: RenderDriverKind::from_native(raw.driver), + latest_result: disposition_from_native(raw.latest_result), + extent: RenderTargetExtent::new( + raw.extent.width, + raw.extent.height, + raw.extent.scale_factor, + ), + generation: raw.generation, + map_update_generation: raw.map_update_generation, + rendered_update_generation: raw.rendered_update_generation, + extent_generation: raw.extent_generation, + frame_generation: raw.frame_generation, + latest_demand_token: raw.latest_demand_token, + pending_demand_count: raw.pending_demand_count, + acquired_frame_count: raw.acquired_frame_count, + target_ready: raw.target_ready, + pending_changes: raw.pending_changes, + }) } -} - -/// RAII guard for an acquired WebGPU session-owned texture frame. -/// -/// Releasing the guard ends the borrow of the backend WebGPU texture, texture -/// view, and device. -pub struct WebGpuOwnedTextureFrameHandle { - session: Rc, - raw: sys::mln_webgpu_owned_texture_frame, - frame: WebGpuOwnedTextureFrame, - closed: Cell, - _thread_affine: PhantomData>, -} -impl fmt::Debug for WebGpuOwnedTextureFrameHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("WebGpuOwnedTextureFrameHandle") - .field("closed", &self.closed.get()) - .field("frame", &self.frame) - .finish() + pub fn request_frame(&self, demand: FrameDemand) -> Result<()> { + let raw = demand.to_native(); + maplibre_core::check(unsafe { + sys::mln_render_session_request_frame(self.inner.native()?, &raw) + }) } -} -impl WebGpuOwnedTextureFrameHandle { - /// Returns copied metadata for this acquired frame. - pub fn frame(&self) -> Result<&WebGpuOwnedTextureFrame> { - if self.closed.get() { - Err(closed_handle_error("WebGpuOwnedTextureFrameHandle")) - } else { - Ok(&self.frame) - } + pub fn drain_frame_results(&self) -> Result { + let mut batch = sys::mln_render_frame_batch(0); + maplibre_core::check(unsafe { + sys::mln_render_session_drain_frame_results(self.inner.native()?, &mut batch) + })?; + RenderFrameBatch::new(batch) } - /// Returns the borrowed WebGPU texture pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer is valid only while this frame handle remains open. - /// The caller must not store or use it after frame release and must satisfy - /// WebGPU synchronization and thread-affinity requirements. - pub unsafe fn texture(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("WebGpuOwnedTextureFrameHandle")) - } else { - // SAFETY: The active native frame owns the validity contract for - // this borrowed backend handle until release. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture) }) - } + pub fn acquire_frame(&self) -> Result { + let mut frame = sys::mln_acquired_frame(0); + maplibre_core::check(unsafe { + sys::mln_render_session_acquire_frame(self.inner.native()?, &mut frame) + })?; + AcquiredFrameHandle::new(frame) } - /// Returns the borrowed WebGPU texture view pointer for backend interop. - /// - /// # Safety + /// Resizes this attached render session. /// - /// The returned pointer has the same lifetime and synchronization - /// requirements as [`WebGpuOwnedTextureFrameHandle::texture`]. - pub unsafe fn texture_view(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("WebGpuOwnedTextureFrameHandle")) - } else { - // SAFETY: See texture above. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.texture_view) }) - } + /// Surface and owned-texture sessions resize in place, keeping the renderer + /// along with the tile pyramid, glyph and image atlases, and symbol + /// placement. A scale factor change retires the renderer instead, because + /// shaders are compiled for one pixel ratio. Map-owned feature state + /// survives either way. Borrowed texture targets report an + /// unsupported-feature error; hand over a new texture with the backend's + /// `set_*_borrowed_texture_target` method instead. + pub fn resize(&self, extent: &RenderTargetExtent) -> Result> { + let raw = maplibre_core::render::render_target_extent_to_native(extent.to_core()); + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_resize(session, &raw, completion) + }) } - /// Returns the borrowed WebGPU device pointer for backend interop. - /// - /// # Safety - /// - /// The returned pointer has the same lifetime and synchronization - /// requirements as [`WebGpuOwnedTextureFrameHandle::texture`]. - pub unsafe fn device(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("WebGpuOwnedTextureFrameHandle")) - } else { - // SAFETY: See texture above. - Ok(unsafe { FrameNativePointer::from_ptr(self.raw.device) }) - } + pub fn barrier(&self) -> Result> { + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_barrier(session, completion) + }) } - /// Explicitly releases this frame. - #[allow(clippy::result_large_err)] - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - if self.closed.get() { - return Ok(()); - } - let session = match self.session.native() { - Ok(session) => session, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - // SAFETY: session is live, and raw is the active frame returned by a - // successful acquire for this session until release succeeds. - if let Err(error) = maplibre_core::check(unsafe { - sys::mln_webgpu_owned_texture_release_frame(session, &self.raw) - }) { - return Err(HandleOperationError::new(error, self)); - } - self.closed.set(true); - self.session.frame_acquired.set(false); - Ok(()) + pub fn reduce_memory_use(&self) -> Result> { + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_reduce_memory_use(session, completion) + }) } -} -impl Drop for WebGpuOwnedTextureFrameHandle { - fn drop(&mut self) { - if self.closed.get() { - return; - } - if let Ok(session) = self.session.native() { - // SAFETY: Best-effort release of the active frame. Drop cannot - // report errors and never panics. - let status = unsafe { sys::mln_webgpu_owned_texture_release_frame(session, &self.raw) }; - if status == sys::MLN_STATUS_OK { - self.closed.set(true); - self.session.frame_acquired.set(false); - } - } + pub fn clear_data(&self) -> Result> { + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_clear_data(session, completion) + }) } -} -/// RAII guard for an acquired OpenGL session-owned texture frame. -/// -/// Releasing the guard ends the borrow of the backend OpenGL texture object. -pub struct OpenGLOwnedTextureFrameHandle { - session: Rc, - raw: sys::mln_opengl_owned_texture_frame, - frame: OpenGLOwnedTextureFrame, - closed: Cell, - _thread_affine: PhantomData>, -} - -impl fmt::Debug for OpenGLOwnedTextureFrameHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("OpenGLOwnedTextureFrameHandle") - .field("closed", &self.closed.get()) - .field("frame", &self.frame) - .finish() + pub fn dump_debug_logs(&self) -> Result> { + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_dump_debug_logs(session, completion) + }) } -} -impl OpenGLOwnedTextureFrameHandle { - /// Returns copied metadata for this acquired frame. - pub fn frame(&self) -> Result<&OpenGLOwnedTextureFrame> { - if self.closed.get() { - Err(closed_handle_error("OpenGLOwnedTextureFrameHandle")) - } else { - Ok(&self.frame) - } - } - /// Returns the borrowed OpenGL texture object name for backend interop. - pub fn texture(&self) -> Result> { - if self.closed.get() { - Err(closed_handle_error("OpenGLOwnedTextureFrameHandle")) - } else { - Ok(FrameOpenGLTextureName::new(self.raw.texture)) - } + pub fn read_premultiplied_rgba8(&self) -> Result> { + let session = self.inner.native()?; + crate::completion::submit( + |completion| unsafe { sys::mln_texture_read_premultiplied_rgba8(session, completion) }, + |result| { + let raw = + crate::completion::copy_value::(result)?; + Ok(PremultipliedRgba8Image::new( + maplibre_core::values::texture_image_info_from_native(&raw.info), + unsafe { maplibre_core::string::copy_string_view_bytes(raw.data) }?, + )) + }, + ) } - /// Explicitly releases this frame. - #[allow(clippy::result_large_err)] - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - if self.closed.get() { - return Ok(()); - } - let session = match self.session.native() { - Ok(session) => session, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - // SAFETY: session is live, and raw is the active frame returned by a - // successful acquire for this session until release succeeds. - if let Err(error) = maplibre_core::check(unsafe { - sys::mln_opengl_owned_texture_release_frame(session, &self.raw) - }) { - return Err(HandleOperationError::new(error, self)); - } - self.closed.set(true); - self.session.frame_acquired.set(false); - Ok(()) + /// Services typed native work on the caller driver's graphics thread. + /// + /// The first successful call fixes the graphics-thread identity. Later + /// calls from another thread return a wrong-thread error. + pub fn service_driver_work(&self, max_work: usize) -> Result { + let mut serviced = 0; + maplibre_core::check(unsafe { + sys::mln_render_session_service_driver_work( + self.inner.native()?, + max_work, + &mut serviced, + ) + })?; + Ok(serviced) } -} -impl Drop for OpenGLOwnedTextureFrameHandle { - fn drop(&mut self) { - if self.closed.get() { - return; - } - if let Ok(session) = self.session.native() { - // SAFETY: Best-effort release of the active frame. Drop cannot - // report errors and never panics. - let status = unsafe { sys::mln_opengl_owned_texture_release_frame(session, &self.raw) }; - if status == sys::MLN_STATUS_OK { - self.closed.set(true); - self.session.frame_acquired.set(false); - } - } + pub fn detach(&self) -> Result> { + self.start_unit(|session, completion| unsafe { + sys::mln_render_session_detach(session, completion) + }) } -} -impl RenderSessionHandle { - pub(crate) fn attach(map: &MapAttachRef, attach: F) -> Result - where - F: FnOnce(sys::mln_map, *mut sys::mln_render_session) -> sys::mln_status, - { - let mut out = maplibre_core::ptr::OutHandle::::new(); - // A close racing this attach makes the handle stale, which the C API - // rejects. - let status = attach(map.map(), out.as_mut_ptr()); - maplibre_core::check(status)?; - let ptr = out_handle(out, "mln_render_session")?; - Ok(Self { - inner: Rc::new(RenderSessionState::new(ptr)?), + /// Irreversibly closes control and mailboxes without making graphics + /// calls, for a host whose device or target is already gone. + /// + /// Before returning, the call waits for the map's in-flight tile work, + /// which can still reference quarantined renderer resources and through + /// them the host's graphics objects. After it returns, no library thread + /// touches the session's target or device, so the host may destroy them + /// immediately. Do not call it from a MapLibre worker callback. + pub fn abandon(&self) -> Result { + let mut raw: sys::mln_render_abandon_result = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_render_session_abandon(self.inner.native()?, &mut raw) + })?; + Ok(RenderAbandonResult { + quarantined: raw.disposition == sys::MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED, + quarantined_resource_count: raw.quarantined_resource_count, }) } - /// Explicitly destroys the render session. - /// - /// Native destruction errors are returned. When destruction fails, the - /// underlying native handle remains live so a later `close` can retry. - pub fn close(self) -> std::result::Result<(), HandleOperationError> { - if let Err(error) = self.inner.ensure_no_frame_acquired() { - return Err(HandleOperationError::new(error, self)); - } - if let Err(error) = self.inner.close() { + pub fn destroy(self) -> std::result::Result<(), HandleOperationError> { + if let Err(error) = self.inner.destroy() { return Err(HandleOperationError::new(error, self)); } Ok(()) } - /// Resizes this attached render session. - /// - /// Surface and owned-texture sessions resize in place, keeping the - /// renderer along with the tile pyramid, glyph and image atlases, and - /// symbol placement. A scale factor change retires the renderer instead, - /// because shaders are compiled for one pixel ratio. Map-owned feature - /// state survives either way. Borrowed texture targets report an - /// unsupported-feature error; hand over a new texture with the backend's - /// `set_*_borrowed_texture_target` method instead. - pub fn resize(&self, width: u32, height: u32, scale_factor: f64) -> Result<()> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: session is a live render session handle owned by this wrapper. - maplibre_core::check(unsafe { - sys::mln_render_session_resize(session, width, height, scale_factor) - }) + + fn set_target( + &self, + raw: &D, + start: unsafe extern "C" fn( + sys::mln_render_session, + *const D, + *const sys::mln_completion, + ) -> sys::mln_status, + ) -> Result> { + self.start_unit(|session, completion| unsafe { start(session, raw, completion) }) } - /// Presents this attached surface session through a new surface, keeping - /// this session's renderer and its state. - /// - /// The descriptor's extent applies as a resize does. A `context.device` - /// that is neither null nor this session's device reports an - /// invalid-argument error and leaves the current surface in place. - pub fn set_metal_surface_target(&self, descriptor: &MetalSurfaceDescriptor) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_metal_surface_set_target(session, raw) } - }) + pub fn set_metal_surface_target( + &self, + value: &MetalSurfaceDescriptor, + ) -> Result> { + self.set_target(&value.to_native(), sys::mln_metal_surface_set_target) } - /// Presents this attached surface session through a new surface. - /// - /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing - /// a surface preserves. The outgoing `VkSurfaceKHR` must still be valid: - /// this session holds a swapchain built from it, and Vulkan destroys every - /// swapchain before its surface. - pub fn set_vulkan_surface_target(&self, descriptor: &VulkanSurfaceDescriptor) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_vulkan_surface_set_target(session, raw) } - }) + pub fn set_vulkan_surface_target( + &self, + value: &VulkanSurfaceDescriptor, + ) -> Result> { + self.set_target(&value.to_native(), sys::mln_vulkan_surface_set_target) } - /// Presents this attached surface session through a new WebGPU surface. - /// - /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing - /// a surface preserves. The replacement names the same device and format as - /// the session attached with. - pub fn set_webgpu_surface_target(&self, descriptor: &WebGpuSurfaceDescriptor) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_webgpu_surface_set_target(session, raw) } - }) + pub fn set_webgpu_surface_target( + &self, + value: &WebGpuSurfaceDescriptor, + ) -> Result> { + self.set_target(&value.to_native(), sys::mln_webgpu_surface_set_target) } - /// Presents this attached surface session through a new surface. - /// - /// See [`RenderSessionHandle::set_metal_surface_target`] for what replacing - /// a surface preserves. The new surface is made current on the next render, - /// so a host may hand over a replacement for one it has already destroyed. - /// A surface accepted here can still prove unusable, which the next - /// `render_update` reports rather than this call. - pub fn set_opengl_surface_target(&self, descriptor: &OpenGLSurfaceDescriptor) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_opengl_surface_set_target(session, raw) } - }) + pub fn set_opengl_surface_target( + &self, + value: &OpenGLSurfaceDescriptor, + ) -> Result> { + self.set_target(&value.to_native(), sys::mln_opengl_surface_set_target) } - /// Renders this attached texture session into a new caller-owned texture, - /// keeping this session's renderer. A scale factor change starts a new - /// renderer, as [`RenderSessionHandle::resize`] does. - /// - /// The replacement must belong to the device this session attached with and - /// carry the pixel format it attached with; otherwise this reports an error - /// and leaves the current texture in place. The caller owns the replacement - /// and keeps it valid until the next replacement, detach, or close. The - /// outgoing texture is neither retained nor read here. pub fn set_metal_borrowed_texture_target( &self, - descriptor: &MetalBorrowedTextureDescriptor, - ) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_metal_borrowed_texture_set_target(session, raw) } - }) + value: &MetalBorrowedTextureDescriptor, + ) -> Result> { + self.set_target( + &value.to_native(), + sys::mln_metal_borrowed_texture_set_target, + ) } - /// Renders this attached texture session into a new caller-owned image. - /// - /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what - /// replacing a target preserves. The replacement carries the format and - /// both layouts this session attached with, since its render pass was built - /// around them. pub fn set_vulkan_borrowed_texture_target( &self, - descriptor: &VulkanBorrowedTextureDescriptor, - ) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_vulkan_borrowed_texture_set_target(session, raw) } - }) + value: &VulkanBorrowedTextureDescriptor, + ) -> Result> { + self.set_target( + &value.to_native(), + sys::mln_vulkan_borrowed_texture_set_target, + ) } - /// Renders this attached texture session into a new caller-owned texture. - /// - /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what - /// replacing a target preserves. The replacement carries the format this - /// session attached with, and belongs to the device it attached with. pub fn set_webgpu_borrowed_texture_target( &self, - descriptor: &WebGpuBorrowedTextureDescriptor, - ) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_webgpu_borrowed_texture_set_target(session, raw) } - }) + value: &WebGpuBorrowedTextureDescriptor, + ) -> Result> { + self.set_target( + &value.to_native(), + sys::mln_webgpu_borrowed_texture_set_target, + ) } - /// Renders this attached texture session into a new caller-owned texture. - /// - /// See [`RenderSessionHandle::set_metal_borrowed_texture_target`] for what - /// replacing a target preserves. The replacement belongs to the context - /// this session attached with, or one in its share group, and the host - /// context must be current on this thread. pub fn set_opengl_borrowed_texture_target( &self, - descriptor: &OpenGLBorrowedTextureDescriptor, - ) -> Result<()> { - self.set_target(descriptor.to_native(), |session, raw| { - // SAFETY: session is a live render session handle owned by this - // wrapper, and raw is a materialized descriptor valid for this call. - unsafe { sys::mln_opengl_borrowed_texture_set_target(session, raw) } - }) + value: &OpenGLBorrowedTextureDescriptor, + ) -> Result> { + self.set_target( + &value.to_native(), + sys::mln_opengl_borrowed_texture_set_target, + ) } +} - /// Shared body for the `set_*_target` methods. The caller-materialized - /// descriptor lives for the whole call, which is all the C API borrows it - /// for. - fn set_target( - &self, - raw: D, - set_target: impl FnOnce(sys::mln_render_session, &D) -> sys::mln_status, - ) -> Result<()> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - maplibre_core::check(set_target(session, &raw)) +pub struct RenderFrameBatch { + raw: sys::mln_render_frame_batch, +} + +impl RenderFrameBatch { + fn new(raw: sys::mln_render_frame_batch) -> Result { + if raw.0 == 0 { + return Err(crate::Error::invalid_argument( + "frame batch must not be zero", + )); + } + Ok(Self { raw }) } - /// Processes the latest map render update for this render target. - /// - /// The map retains its latest update, so repeated calls re-render it and - /// report [`RenderResult::Rendered`] again. Every other result names the - /// wake to wait for: [`RenderResult::NoUpdate`] and - /// [`RenderResult::SizePending`] resolve on a render-update-available - /// event, and [`RenderResult::TargetNotReady`] resolves when the host - /// changes the render target. - /// - /// The returned [`RenderUpdate::needs_repaint`] reports whether the map - /// asked for another frame while rendering this one, so a frame loop can - /// re-arm without draining events first. - pub fn render_update(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let mut result = sys::MLN_RENDER_RESULT_NO_UPDATE; - let mut needs_repaint = false; - // SAFETY: session is a live render session handle owned by this wrapper, - // and result and needs_repaint point to caller-owned output storage. + pub fn len(&self) -> Result { + let mut count = 0; + maplibre_core::check(unsafe { sys::mln_render_frame_batch_count(self.raw, &mut count) })?; + Ok(count) + } + + pub fn is_empty(&self) -> Result { + Ok(self.len()? == 0) + } + + pub fn get(&self, index: usize) -> Result { + let mut raw: sys::mln_render_frame_result = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_render_session_render_update(session, &raw mut result, &raw mut needs_repaint) + sys::mln_render_frame_batch_get(self.raw, index, &mut raw) })?; - Ok(RenderUpdate { - result: RenderResult::from_raw(result), - needs_repaint, - }) + Ok(frame_result_from_native(raw)) } - /// Detaches backend-bound render resources from the map, consuming this - /// handle and returning a close-only handle. - /// - /// A detached session no longer reaches its map, so the map is free to - /// close and the detached session stays destroyable afterwards. - pub fn detach( - self, - ) -> std::result::Result> { - if let Err(error) = self.inner.ensure_no_frame_acquired() { - return Err(HandleOperationError::new(error, self)); - } - let session = match self.inner.native() { - Ok(session) => session, - Err(error) => return Err(HandleOperationError::new(error, self)), + pub fn copy_results(&self) -> Result> { + (0..self.len()?).map(|index| self.get(index)).collect() + } +} + +impl Drop for RenderFrameBatch { + fn drop(&mut self) { + unsafe { sys::mln_render_frame_batch_release(self.raw) }; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GpuSyncKind { + CpuComplete, + MetalSharedEvent, + VulkanTimelineSemaphore, + OpenGlFence, + WebGpuToken, + Unknown(u32), +} + +/// Consumer completion passed when releasing an acquired frame. +/// +/// A non-CPU backend object remains caller-owned and must stay valid until the +/// next render-session barrier or detach completes. Constructing its +/// [`NativePointer`] is unsafe because Rust cannot verify that lifetime. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GpuSync { + pub kind: GpuSyncKind, + pub object: NativePointer, + pub value: u64, +} + +impl GpuSync { + pub const CPU_COMPLETE: Self = Self { + kind: GpuSyncKind::CpuComplete, + object: NativePointer::NULL, + value: 0, + }; +} + +/// Producer synchronization borrowed from an acquired frame. +/// +/// Backend objects remain valid only while the frame lease is live. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FrameGpuSync<'frame> { + pub kind: GpuSyncKind, + pub object: FrameNativePointer<'frame>, + pub value: u64, +} + +impl FrameGpuSync<'_> { + fn from_native(raw: sys::mln_gpu_sync) -> Self { + let kind = match raw.kind { + sys::MLN_GPU_SYNC_CPU_COMPLETE => GpuSyncKind::CpuComplete, + sys::MLN_GPU_SYNC_METAL_SHARED_EVENT => GpuSyncKind::MetalSharedEvent, + sys::MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE => GpuSyncKind::VulkanTimelineSemaphore, + sys::MLN_GPU_SYNC_OPENGL_FENCE => GpuSyncKind::OpenGlFence, + sys::MLN_GPU_SYNC_WEBGPU_TOKEN => GpuSyncKind::WebGpuToken, + value => GpuSyncKind::Unknown(value), }; - // SAFETY: session is a live render session handle owned by this wrapper. - if let Err(error) = maplibre_core::check(unsafe { sys::mln_render_session_detach(session) }) - { - return Err(HandleOperationError::new(error, self)); + Self { + kind, + object: unsafe { FrameNativePointer::from_ptr(raw.object) }, + value: raw.value, } - self.inner.detached.set(true); - Ok(DetachedRenderSessionHandle { - inner: Rc::clone(&self.inner), - }) } +} - /// Asks the session renderer to release cached resources where possible. - pub fn reduce_memory_use(&self) -> Result<()> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: session is a live render session handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_render_session_reduce_memory_use(session) }) +impl GpuSync { + fn to_native(self) -> sys::mln_gpu_sync { + let mut raw = unsafe { sys::mln_gpu_sync_default() }; + raw.kind = match self.kind { + GpuSyncKind::CpuComplete => sys::MLN_GPU_SYNC_CPU_COMPLETE, + GpuSyncKind::MetalSharedEvent => sys::MLN_GPU_SYNC_METAL_SHARED_EVENT, + GpuSyncKind::VulkanTimelineSemaphore => sys::MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE, + GpuSyncKind::OpenGlFence => sys::MLN_GPU_SYNC_OPENGL_FENCE, + GpuSyncKind::WebGpuToken => sys::MLN_GPU_SYNC_WEBGPU_TOKEN, + GpuSyncKind::Unknown(value) => value, + }; + raw.object = self.object.as_void_ptr(); + raw.value = self.value; + raw } +} - /// Clears renderer data for the session. - pub fn clear_data(&self) -> Result<()> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: session is a live render session handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_render_session_clear_data(session) }) +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MetalOwnedTextureFrame { + pub generation: u64, + pub width: u32, + pub height: u32, + pub scale_factor: f64, + pub frame_id: u64, + pub pixel_format: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct VulkanOwnedTextureFrame { + pub generation: u64, + pub width: u32, + pub height: u32, + pub scale_factor: f64, + pub frame_id: u64, + pub format: u32, + pub layout: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WebGpuOwnedTextureFrame { + pub generation: u64, + pub width: u32, + pub height: u32, + pub scale_factor: f64, + pub frame_id: u64, + pub format: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct OpenGLOwnedTextureFrame { + pub generation: u64, + pub width: u32, + pub height: u32, + pub scale_factor: f64, + pub frame_id: u64, + pub target: u32, + pub internal_format: u32, + pub format: u32, + pub type_: u32, +} + +/// Owned lease for one session-owned texture-ring slot. +/// +/// Call [`AcquiredFrameHandle::release`] with the completion of every consumer +/// GPU access. Dropping an untouched frame releases it as CPU-complete. +/// Dropping a frame after exposing backend handles leaks the lease rather than +/// allowing the session to reuse resources that the GPU may still access. +#[must_use = "an acquired frame must be released with the consumer's GPU completion"] +pub struct AcquiredFrameHandle { + raw: Mutex>, + backend_exposed: AtomicBool, +} + +impl AcquiredFrameHandle { + fn new(raw: sys::mln_acquired_frame) -> Result { + if raw.0 == 0 { + return Err(crate::Error::invalid_argument( + "acquired frame must not be zero", + )); + } + Ok(Self { + raw: Mutex::new(Some(raw)), + backend_exposed: AtomicBool::new(false), + }) } - /// Dumps renderer debug logs through MapLibre Native logging. - pub fn dump_debug_logs(&self) -> Result<()> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: session is a live render session handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_render_session_dump_debug_logs(session) }) + fn native(&self) -> Result { + self.raw + .lock() + .unwrap_or_else(|p| p.into_inner()) + .ok_or_else(|| closed_handle_error("AcquiredFrameHandle")) } - /// Returns CPU readback metadata for the most recently rendered texture frame. - pub fn texture_image_info(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut info = unsafe { sys::mln_texture_image_info_default() }; - // SAFETY: session is live. Passing a null buffer with zero capacity is - // the documented metadata probe path; out_info points to initialized storage. + pub fn result(&self) -> Result { + let mut raw: sys::mln_render_frame_result = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_texture_read_premultiplied_rgba8(session, std::ptr::null_mut(), 0, &mut info) + sys::mln_acquired_frame_get_result(self.native()?, &mut raw) })?; - Ok(maplibre_core::values::texture_image_info_from_native(&info)) + Ok(frame_result_from_native(raw)) } - /// Reads the most recently rendered texture frame as premultiplied RGBA8. - pub fn read_premultiplied_rgba8_into(&self, data: &mut [u8]) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - // SAFETY: Default constructor takes no arguments and initializes size. - let mut info = unsafe { sys::mln_texture_image_info_default() }; - let data_ptr = if data.is_empty() { - std::ptr::null_mut() - } else { - data.as_mut_ptr() - }; - // SAFETY: session is live, data_ptr either points to data's mutable - // storage for data.len() bytes or is null for an empty buffer, and info - // points to initialized writable storage. + pub fn producer_sync(&self) -> Result> { + let mut raw = unsafe { sys::mln_gpu_sync_default() }; maplibre_core::check(unsafe { - sys::mln_texture_read_premultiplied_rgba8(session, data_ptr, data.len(), &mut info) + sys::mln_acquired_frame_get_producer_sync(self.native()?, &mut raw) })?; - // An empty destination reaches native code as the size probe, which - // succeeds without copying, so report it as too small here. - if data.is_empty() && info.byte_length > 0 { - return Err(crate::Error::invalid_argument(format!( - "buffer length 0 is smaller than the required {} bytes", - info.byte_length - ))); + Ok(FrameGpuSync::from_native(raw)) + } + + pub fn release( + self, + consumer_completion: GpuSync, + ) -> std::result::Result<(), HandleOperationError> { + let mut guard = self.raw.lock().unwrap_or_else(|p| p.into_inner()); + let Some(mut frame) = *guard else { + drop(guard); + return Err(HandleOperationError::new( + closed_handle_error("AcquiredFrameHandle"), + self, + )); + }; + let sync = consumer_completion.to_native(); + if let Err(error) = + maplibre_core::check(unsafe { sys::mln_acquired_frame_release(&mut frame, &sync) }) + { + drop(guard); + return Err(HandleOperationError::new(error, self)); } - Ok(maplibre_core::values::texture_image_info_from_native(&info)) + *guard = None; + drop(guard); + Ok(()) } +} - /// Acquires a borrowed Metal frame from a session-owned texture target. - pub fn acquire_metal_owned_texture_frame(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let mut raw = empty_metal_owned_texture_frame(); - // SAFETY: session is live and raw points to initialized writable frame storage. - maplibre_core::check(unsafe { - sys::mln_metal_owned_texture_acquire_frame(session, &mut raw) - })?; - self.inner.frame_acquired.set(true); - Ok(MetalOwnedTextureFrameHandle { - session: Rc::clone(&self.inner), - frame: MetalOwnedTextureFrame::from_native(&raw), - raw, - closed: Cell::new(false), - _thread_affine: PhantomData, - }) +impl Drop for AcquiredFrameHandle { + fn drop(&mut self) { + let Some(mut frame) = self + .raw + .get_mut() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take() + else { + return; + }; + if self.backend_exposed.load(Ordering::Acquire) { + maplibre_core::handle::report_leak(maplibre_core::handle::NativeHandleLeak { + type_name: "mln_acquired_frame", + id: frame.0, + }); + return; + } + let sync = GpuSync::CPU_COMPLETE.to_native(); + unsafe { sys::mln_acquired_frame_release(&mut frame, &sync) }; } +} - /// Acquires a borrowed Vulkan frame from a session-owned texture target. - pub fn acquire_vulkan_owned_texture_frame(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let mut raw = empty_vulkan_owned_texture_frame(); - // SAFETY: session is live and raw points to initialized writable frame storage. +impl AcquiredFrameHandle { + pub fn metal_texture( + &self, + ) -> Result<( + MetalOwnedTextureFrame, + FrameNativePointer<'_>, + FrameNativePointer<'_>, + )> { + let mut raw: sys::mln_metal_owned_texture_frame = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_vulkan_owned_texture_acquire_frame(session, &mut raw) + sys::mln_acquired_frame_get_metal_texture(self.native()?, &mut raw) })?; - self.inner.frame_acquired.set(true); - Ok(VulkanOwnedTextureFrameHandle { - session: Rc::clone(&self.inner), - frame: VulkanOwnedTextureFrame::from_native(&raw), - raw, - closed: Cell::new(false), - _thread_affine: PhantomData, - }) + self.backend_exposed.store(true, Ordering::Release); + Ok(( + MetalOwnedTextureFrame { + generation: raw.generation, + width: raw.width, + height: raw.height, + scale_factor: raw.scale_factor, + frame_id: raw.frame_id, + pixel_format: raw.pixel_format, + }, + unsafe { FrameNativePointer::from_ptr(raw.texture) }, + unsafe { FrameNativePointer::from_ptr(raw.device) }, + )) } - /// Acquires a borrowed WebGPU frame from a session-owned texture target. - pub fn acquire_webgpu_owned_texture_frame(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let mut raw = empty_webgpu_owned_texture_frame(); - // SAFETY: session is live and raw points to initialized writable frame storage. + pub fn vulkan_texture( + &self, + ) -> Result<( + VulkanOwnedTextureFrame, + FrameNativePointer<'_>, + FrameNativePointer<'_>, + FrameNativePointer<'_>, + )> { + let mut raw: sys::mln_vulkan_owned_texture_frame = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_webgpu_owned_texture_acquire_frame(session, &mut raw) + sys::mln_acquired_frame_get_vulkan_texture(self.native()?, &mut raw) })?; - self.inner.frame_acquired.set(true); - Ok(WebGpuOwnedTextureFrameHandle { - session: Rc::clone(&self.inner), - frame: WebGpuOwnedTextureFrame::from_native(&raw), - raw, - closed: Cell::new(false), - _thread_affine: PhantomData, - }) + self.backend_exposed.store(true, Ordering::Release); + Ok(( + VulkanOwnedTextureFrame { + generation: raw.generation, + width: raw.width, + height: raw.height, + scale_factor: raw.scale_factor, + frame_id: raw.frame_id, + format: raw.format, + layout: raw.layout, + }, + unsafe { FrameNativePointer::from_ptr(raw.image) }, + unsafe { FrameNativePointer::from_ptr(raw.image_view) }, + unsafe { FrameNativePointer::from_ptr(raw.device) }, + )) } - /// Acquires a borrowed OpenGL frame from a session-owned texture target. - pub fn acquire_opengl_owned_texture_frame(&self) -> Result { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let mut raw = empty_opengl_owned_texture_frame(); - // SAFETY: session is live and raw points to initialized writable frame storage. + pub fn webgpu_texture( + &self, + ) -> Result<( + WebGpuOwnedTextureFrame, + FrameNativePointer<'_>, + FrameNativePointer<'_>, + FrameNativePointer<'_>, + )> { + let mut raw: sys::mln_webgpu_owned_texture_frame = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; maplibre_core::check(unsafe { - sys::mln_opengl_owned_texture_acquire_frame(session, &mut raw) + sys::mln_acquired_frame_get_webgpu_texture(self.native()?, &mut raw) })?; - self.inner.frame_acquired.set(true); - Ok(OpenGLOwnedTextureFrameHandle { - session: Rc::clone(&self.inner), - frame: OpenGLOwnedTextureFrame::from_native(&raw), - raw, - closed: Cell::new(false), - _thread_affine: PhantomData, - }) + self.backend_exposed.store(true, Ordering::Release); + Ok(( + WebGpuOwnedTextureFrame { + generation: raw.generation, + width: raw.width, + height: raw.height, + scale_factor: raw.scale_factor, + frame_id: raw.frame_id, + format: raw.format, + }, + unsafe { FrameNativePointer::from_ptr(raw.texture) }, + unsafe { FrameNativePointer::from_ptr(raw.texture_view) }, + unsafe { FrameNativePointer::from_ptr(raw.device) }, + )) } -} -fn empty_metal_owned_texture_frame() -> sys::mln_metal_owned_texture_frame { - sys::mln_metal_owned_texture_frame { - size: mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - texture: std::ptr::null_mut(), - device: std::ptr::null_mut(), - pixel_format: 0, - } -} - -fn empty_webgpu_owned_texture_frame() -> sys::mln_webgpu_owned_texture_frame { - sys::mln_webgpu_owned_texture_frame { - size: mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - texture: std::ptr::null_mut(), - texture_view: std::ptr::null_mut(), - device: std::ptr::null_mut(), - format: 0, - } -} - -fn empty_vulkan_owned_texture_frame() -> sys::mln_vulkan_owned_texture_frame { - sys::mln_vulkan_owned_texture_frame { - size: mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - image: std::ptr::null_mut(), - image_view: std::ptr::null_mut(), - device: std::ptr::null_mut(), - format: 0, - layout: 0, - } -} - -fn empty_opengl_owned_texture_frame() -> sys::mln_opengl_owned_texture_frame { - sys::mln_opengl_owned_texture_frame { - size: mem::size_of::() as u32, - generation: 0, - width: 0, - height: 0, - scale_factor: 0.0, - frame_id: 0, - texture: 0, - target: 0, - internal_format: 0, - format: 0, - type_: 0, + /// Copies OpenGL texture data on the fixed caller graphics thread. + pub fn opengl_texture(&self) -> Result<(OpenGLOwnedTextureFrame, FrameOpenGLTextureName<'_>)> { + let mut raw: sys::mln_opengl_owned_texture_frame = unsafe { mem::zeroed() }; + raw.size = mem::size_of::() as u32; + maplibre_core::check(unsafe { + sys::mln_acquired_frame_get_opengl_texture(self.native()?, &mut raw) + })?; + self.backend_exposed.store(true, Ordering::Release); + Ok(( + OpenGLOwnedTextureFrame { + generation: raw.generation, + width: raw.width, + height: raw.height, + scale_factor: raw.scale_factor, + frame_id: raw.frame_id, + target: raw.target, + internal_format: raw.internal_format, + format: raw.format, + type_: raw.type_, + }, + FrameOpenGLTextureName::new(raw.texture), + )) } } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/render/query.rs b/bindings/rust/crates/maplibre-native-ffi/src/render/query.rs index 8921c6c0a..36bff9c4c 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/render/query.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/render/query.rs @@ -3,7 +3,7 @@ use std::ptr; use maplibre_native_ffi_core as maplibre_core; use maplibre_native_ffi_sys as sys; -use crate::Result; +use crate::{NativeFuture, Result}; pub use maplibre_core::query::{ FeatureStateSelector, QueriedFeature, RenderedFeatureQueryOptions, RenderedQueryGeometry, @@ -16,73 +16,60 @@ pub(crate) use maplibre_core::query::{ }; impl super::RenderSessionHandle { - /// Copies rendered features from the latest render session state. + /// Starts a rendered-feature query whose result is a typed feature list. pub fn query_rendered_features( &self, geometry: &RenderedQueryGeometry, options: Option<&RenderedFeatureQueryOptions>, - ) -> Result> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; + ) -> Result>> { let geometry = geometry.to_native(); let options = options .map(RenderedFeatureQueryOptions::to_native) .transpose()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: All descriptors and out storage remain valid for the call. - maplibre_core::check(unsafe { - sys::mln_render_session_query_rendered_features( - session, - geometry.as_ptr(), - options - .as_ref() - .map_or(ptr::null(), NativeRenderedFeatureQueryOptions::as_ptr), - out.as_mut_ptr(), - ) - })?; - // SAFETY: On success, the C API returns an owned queried-feature list - // handle; core copies and releases it. - unsafe { - maplibre_core::query::copy_queried_feature_list( - out.into_live("mln_queried_feature_list")?, - ) - } + let session = self.inner.native()?; + crate::completion::submit( + |completion| unsafe { + sys::mln_render_session_query_rendered_features( + session, + geometry.as_ptr(), + options + .as_ref() + .map_or(ptr::null(), NativeRenderedFeatureQueryOptions::as_ptr), + completion, + ) + }, + copy_queried_features, + ) } - /// Copies source features from the latest render session state. + /// Starts a source-feature query whose result is a typed feature list. pub fn query_source_features( &self, source_id: &str, options: Option<&SourceFeatureQueryOptions>, - ) -> Result> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let source_id = maplibre_core::string::string_view(source_id); + ) -> Result>> { + let source_id = maplibre_core::string::buffer_view(source_id.as_bytes()); let options = options .map(SourceFeatureQueryOptions::to_native) .transpose()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: All descriptors and out storage remain valid for the call. - maplibre_core::check(unsafe { - sys::mln_render_session_query_source_features( - session, - source_id.raw(), - options - .as_ref() - .map_or(ptr::null(), NativeSourceFeatureQueryOptions::as_ptr), - out.as_mut_ptr(), - ) - })?; - // SAFETY: On success, the C API returns an owned queried-feature list - // handle; core copies and releases it. - unsafe { - maplibre_core::query::copy_queried_feature_list( - out.into_live("mln_queried_feature_list")?, - ) - } + let session = self.inner.native()?; + crate::completion::submit( + |completion| unsafe { + sys::mln_render_session_query_source_features( + session, + source_id, + options + .as_ref() + .map_or(ptr::null(), NativeSourceFeatureQueryOptions::as_ptr), + completion, + ) + }, + copy_queried_features, + ) } - /// Queries a feature extension as UTF-8 JSON or GeoJSON bytes. + /// Starts a feature-extension query whose result is UTF-8 JSON or GeoJSON + /// bytes. pub fn query_feature_extension( &self, source_id: &str, @@ -90,28 +77,32 @@ impl super::RenderSessionHandle { extension: &str, extension_field: &str, arguments: Option<&[u8]>, - ) -> Result> { - self.inner.ensure_no_frame_acquired()?; - let session = self.inner.native()?; - let source_id = maplibre_core::string::string_view(source_id); + ) -> Result>> { + let source_id = maplibre_core::string::buffer_view(source_id.as_bytes()); let feature = maplibre_core::string::buffer_view(feature); - let extension = maplibre_core::string::string_view(extension); - let extension_field = maplibre_core::string::string_view(extension_field); + let extension = maplibre_core::string::buffer_view(extension.as_bytes()); + let extension_field = maplibre_core::string::buffer_view(extension_field.as_bytes()); let arguments = arguments.map(maplibre_core::string::buffer_view); - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: All views and out storage remain valid for the call. - maplibre_core::check(unsafe { - sys::mln_render_session_query_feature_extensions( - session, - source_id.raw(), - feature, - extension.raw(), - extension_field.raw(), - arguments.as_ref().map_or(ptr::null(), ptr::from_ref), - out.as_mut_ptr(), - ) - })?; - // SAFETY: Success transfers the owned buffer to this call. - unsafe { maplibre_core::string::copy_owned_buffer(out.get()) } + let session = self.inner.native()?; + crate::completion::submit( + |completion| unsafe { + sys::mln_render_session_query_feature_extensions( + session, + source_id, + feature, + extension, + extension_field, + arguments.as_ref().map_or(ptr::null(), ptr::from_ref), + completion, + ) + }, + crate::completion::buffer, + ) } } + +fn copy_queried_features(result: &sys::mln_completion_result) -> Result> { + let hits = crate::completion::copy_slice::(result)?; + // SAFETY: completion keeps every nested feature view alive for this callback. + unsafe { maplibre_core::query::copy_queried_features(&hits) } +} diff --git a/bindings/rust/crates/maplibre-native-ffi/src/render/tests.rs b/bindings/rust/crates/maplibre-native-ffi/src/render/tests.rs index 0616ac355..3204bdee1 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/render/tests.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/render/tests.rs @@ -1,15 +1,17 @@ -use std::cell::Cell; use std::error::Error as StdError; #[cfg(not(target_os = "emscripten"))] use std::ffi::CStr; use std::ffi::CString; #[cfg(target_os = "windows")] use std::ffi::c_char; -#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "windows" +))] use std::ffi::c_void; -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -29,72 +31,284 @@ mod webgl_gl; use glutin_egl_sys::egl; #[cfg(any(target_os = "linux", target_os = "android"))] use glutin_egl_sys::egl::types::{EGLConfig, EGLContext, EGLDisplay, EGLSurface, EGLint}; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] use libloading::Library; -use static_assertions::assert_not_impl_any; +#[cfg(target_os = "macos")] +use macos_egl as egl; +#[cfg(target_os = "macos")] +use macos_egl::types::{EGLConfig, EGLContext, EGLDisplay, EGLSurface, EGLint}; +use static_assertions::{assert_impl_all, assert_not_impl_any}; #[cfg(target_os = "emscripten")] use webgl_gl as gl_api; +#[cfg(target_os = "macos")] +#[allow(non_snake_case)] +mod macos_egl { + use std::ffi::{c_char, c_void}; + + pub mod types { + use std::ffi::c_void; + + pub type EGLConfig = *const c_void; + pub type EGLContext = *const c_void; + pub type EGLDisplay = *const c_void; + pub type EGLSurface = *const c_void; + pub type EGLint = i32; + } + + use types::{EGLConfig, EGLContext, EGLDisplay, EGLSurface, EGLint}; + + pub const FALSE: u32 = 0; + pub const NONE: u32 = 0x3038; + pub const WIDTH: u32 = 0x3057; + pub const HEIGHT: u32 = 0x3056; + pub const SURFACE_TYPE: u32 = 0x3033; + pub const PBUFFER_BIT: u32 = 0x0001; + pub const RENDERABLE_TYPE: u32 = 0x3040; + pub const OPENGL_ES3_BIT: u32 = 0x0040; + pub const RED_SIZE: u32 = 0x3024; + pub const GREEN_SIZE: u32 = 0x3023; + pub const BLUE_SIZE: u32 = 0x3022; + pub const ALPHA_SIZE: u32 = 0x3021; + pub const DEPTH_SIZE: u32 = 0x3025; + pub const STENCIL_SIZE: u32 = 0x3026; + pub const CONTEXT_CLIENT_VERSION: u32 = 0x3098; + pub const OPENGL_ES_API: u32 = 0x30A0; + pub const DEFAULT_DISPLAY: usize = 0; + pub const NO_CONTEXT: EGLContext = std::ptr::null(); + pub const NO_DISPLAY: EGLDisplay = std::ptr::null(); + pub const NO_SURFACE: EGLSurface = std::ptr::null(); + + type GetDisplay = unsafe extern "system" fn(*mut c_void) -> EGLDisplay; + type Initialize = unsafe extern "system" fn(EGLDisplay, *mut EGLint, *mut EGLint) -> u32; + type BindApi = unsafe extern "system" fn(u32) -> u32; + type ChooseConfig = unsafe extern "system" fn( + EGLDisplay, + *const EGLint, + *mut EGLConfig, + EGLint, + *mut EGLint, + ) -> u32; + type CreateContext = + unsafe extern "system" fn(EGLDisplay, EGLConfig, EGLContext, *const EGLint) -> EGLContext; + type CreatePbufferSurface = + unsafe extern "system" fn(EGLDisplay, EGLConfig, *const EGLint) -> EGLSurface; + type MakeCurrent = + unsafe extern "system" fn(EGLDisplay, EGLSurface, EGLSurface, EGLContext) -> u32; + type GetError = unsafe extern "system" fn() -> u32; + type DestroySurface = unsafe extern "system" fn(EGLDisplay, EGLSurface) -> u32; + type DestroyContext = unsafe extern "system" fn(EGLDisplay, EGLContext) -> u32; + type Terminate = unsafe extern "system" fn(EGLDisplay) -> u32; + type GetCurrentContext = unsafe extern "system" fn() -> EGLContext; + type GetProcAddress = unsafe extern "system" fn(*const c_char) -> *const c_void; + + pub struct Egl { + GetDisplay: GetDisplay, + Initialize: Initialize, + BindAPI: BindApi, + ChooseConfig: ChooseConfig, + CreateContext: CreateContext, + CreatePbufferSurface: CreatePbufferSurface, + MakeCurrent: MakeCurrent, + GetError: GetError, + DestroySurface: DestroySurface, + DestroyContext: DestroyContext, + Terminate: Terminate, + GetCurrentContext: GetCurrentContext, + GetProcAddress: GetProcAddress, + } + + impl Egl { + pub unsafe fn load_with(mut load: impl FnMut(&str) -> *const c_void) -> Self { + unsafe fn symbol(pointer: *const c_void, name: &str) -> T { + assert!(!pointer.is_null(), "EGL loader did not provide {name}"); + // SAFETY: the loader returns the function named by `name`, and + // every field below declares that function's EGL signature. + unsafe { std::mem::transmute_copy(&pointer) } + } + + macro_rules! load { + ($name:literal) => { + unsafe { symbol(load($name), $name) } + }; + } + Self { + GetDisplay: load!("eglGetDisplay"), + Initialize: load!("eglInitialize"), + BindAPI: load!("eglBindAPI"), + ChooseConfig: load!("eglChooseConfig"), + CreateContext: load!("eglCreateContext"), + CreatePbufferSurface: load!("eglCreatePbufferSurface"), + MakeCurrent: load!("eglMakeCurrent"), + GetError: load!("eglGetError"), + DestroySurface: load!("eglDestroySurface"), + DestroyContext: load!("eglDestroyContext"), + Terminate: load!("eglTerminate"), + GetCurrentContext: load!("eglGetCurrentContext"), + GetProcAddress: load!("eglGetProcAddress"), + } + } + + pub unsafe fn GetDisplay(&self, display: *mut c_void) -> EGLDisplay { + unsafe { (self.GetDisplay)(display) } + } + pub unsafe fn Initialize( + &self, + display: EGLDisplay, + major: *mut EGLint, + minor: *mut EGLint, + ) -> u32 { + unsafe { (self.Initialize)(display, major, minor) } + } + pub unsafe fn BindAPI(&self, api: u32) -> u32 { + unsafe { (self.BindAPI)(api) } + } + pub unsafe fn ChooseConfig( + &self, + display: EGLDisplay, + attributes: *const EGLint, + config: *mut EGLConfig, + config_size: EGLint, + count: *mut EGLint, + ) -> u32 { + unsafe { (self.ChooseConfig)(display, attributes, config, config_size, count) } + } + pub unsafe fn CreateContext( + &self, + display: EGLDisplay, + config: EGLConfig, + share: EGLContext, + attributes: *const EGLint, + ) -> EGLContext { + unsafe { (self.CreateContext)(display, config, share, attributes) } + } + pub unsafe fn CreatePbufferSurface( + &self, + display: EGLDisplay, + config: EGLConfig, + attributes: *const EGLint, + ) -> EGLSurface { + unsafe { (self.CreatePbufferSurface)(display, config, attributes) } + } + pub unsafe fn MakeCurrent( + &self, + display: EGLDisplay, + draw: EGLSurface, + read: EGLSurface, + context: EGLContext, + ) -> u32 { + unsafe { (self.MakeCurrent)(display, draw, read, context) } + } + pub unsafe fn GetError(&self) -> u32 { + unsafe { (self.GetError)() } + } + pub unsafe fn DestroySurface(&self, display: EGLDisplay, surface: EGLSurface) -> u32 { + unsafe { (self.DestroySurface)(display, surface) } + } + pub unsafe fn DestroyContext(&self, display: EGLDisplay, context: EGLContext) -> u32 { + unsafe { (self.DestroyContext)(display, context) } + } + pub unsafe fn Terminate(&self, display: EGLDisplay) -> u32 { + unsafe { (self.Terminate)(display) } + } + pub unsafe fn GetCurrentContext(&self) -> EGLContext { + unsafe { (self.GetCurrentContext)() } + } + pub unsafe fn GetProcAddress(&self, name: *const c_char) -> *const c_void { + unsafe { (self.GetProcAddress)(name) } + } + } +} + use super::*; -use crate::logging::test_support::LoggingTestGuard; use crate::{ - AnimationOptions, CameraChangeMode, CameraOptions, ErrorKind, GeoJsonSourceOptions, LatLng, - LogSeverity, LogSeverityMask, MapAttachRef, MapHandle, MapMode, MapOptions, - OpenGLContextProviderMask, RenderBackendMask, RuntimeEventPayload, RuntimeEventType, - RuntimeHandle, ScreenBox, ScreenPoint, + ErrorKind, GeoJsonSourceOptions, MapHandle, MapOptions, OpenGLContextProviderMask, + RenderBackendMask, RuntimeHandle, ScreenBox, ScreenPoint, }; assert_not_impl_any!(NativePointer: Send, Sync); assert_not_impl_any!(FrameNativePointer<'static>: Send, Sync); assert_not_impl_any!(FrameOpenGLTextureName<'static>: Send, Sync); -assert_not_impl_any!(RenderSessionHandle: Send, Sync); -assert_not_impl_any!(MetalOwnedTextureFrameHandle: Send, Sync); -assert_not_impl_any!(VulkanOwnedTextureFrameHandle: Send, Sync); -assert_not_impl_any!(OpenGLOwnedTextureFrameHandle: Send, Sync); +assert_impl_all!(RenderSessionHandle: Send, Sync); +assert_impl_all!(AcquiredFrameHandle: Send, Sync); const FEATURE_STATE_STYLE_JSON: &str = r#"{"version":8,"sources":{"point":{"type":"geojson","data":{"type":"FeatureCollection","features":[{"type":"Feature","id":"feature-1","properties":{},"geometry":{"type":"Point","coordinates":[0,0]}}]}}},"layers":[{"id":"circle","type":"circle","source":"point","paint":{"circle-radius":["case",["boolean",["feature-state","hover"],false],10,5]}}]}"#; const QUERY_STYLE_JSON: &str = r##"{"version":8,"sources":{"point":{"type":"geojson","data":{"type":"FeatureCollection","features":[{"type":"Feature","id":"feature-1","geometry":{"type":"Point","coordinates":[-122.4194,37.7749]},"properties":{"kind":"capital","visible":true}}]}}},"layers":[{"id":"background","type":"background","paint":{"background-color":"#d8f1ff"}},{"id":"point-circle","type":"circle","source":"point","paint":{"circle-color":"#f97316","circle-radius":12}}]}"##; -// QUERY_STYLE_JSON's background color. +#[cfg(mln_webgpu_backend)] const QUERY_STYLE_BACKGROUND_RGBA: [u8; 4] = [0xd8, 0xf1, 0xff, 0xff]; - const CLUSTER_BASE_STYLE_JSON: &str = r##"{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#ffffff"}}]}"##; +fn wait_until_completed(session: &RenderSessionHandle, operation: &NativeFuture) { + let deadline = Instant::now() + Duration::from_secs(5); + while !operation.is_completed().unwrap() { + let _ = session.service_driver_work(64); + assert!(Instant::now() < deadline, "render operation timed out"); + std::thread::yield_now(); + } + maplibre_core::check(operation.terminal_status().unwrap()).unwrap(); +} + +fn wait_for_operation(session: &RenderSessionHandle, operation: &NativeFuture<()>) { + wait_until_completed(session, operation); + operation.finish().unwrap(); +} + +fn finish_attachment(attachment: RenderSessionAttachment) -> Result { + wait_for_operation(&attachment.session, &attachment.completion); + Ok(attachment.session) +} + +fn finish_unit(session: &RenderSessionHandle, operation: NativeFuture<()>) { + wait_for_operation(session, &operation); +} + +fn release_frame(frame: AcquiredFrameHandle) { + match frame.release(GpuSync::CPU_COMPLETE) { + Ok(()) => {} + Err(error) => panic!("failed to release acquired frame: {}", error.error()), + } +} + +fn caller_attach_options() -> RenderSessionAttachOptions { + RenderSessionAttachOptions::caller_graphics_thread(2) +} + fn create_owned_texture_session( - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> std::result::Result<(OwnedTextureTestContext, RenderSessionHandle), Box> { let backends = crate::supported_render_backends(); if backends.contains(RenderBackendMask::METAL) { let context = MetalTestContext::new()?; - let session = map.attach_metal_owned_texture(&MetalOwnedTextureDescriptor::new( - extent, - context.descriptor(), - ))?; + let session = finish_attachment(map.attach_metal_owned_texture( + &MetalOwnedTextureDescriptor::new(extent, context.descriptor()), + RenderSessionAttachOptions::core_worker(2), + )?)?; return Ok((OwnedTextureTestContext::Metal(context), session)); } #[cfg(mln_webgpu_backend)] if backends.contains(RenderBackendMask::WEBGPU) { let context = WebGpuTestContext::new()?; - let session = map.attach_webgpu_owned_texture(&WebGpuOwnedTextureDescriptor::new( - extent, - context.descriptor(), - ))?; + let session = finish_attachment(map.attach_webgpu_owned_texture( + &WebGpuOwnedTextureDescriptor::new(extent, context.descriptor()), + caller_attach_options(), + )?)?; return Ok((OwnedTextureTestContext::WebGpu(context), session)); } #[cfg(not(target_os = "emscripten"))] if backends.contains(RenderBackendMask::VULKAN) { let context = VulkanTestContext::new()?; - let session = map.attach_vulkan_owned_texture(&VulkanOwnedTextureDescriptor::new( - extent, - context.descriptor(), - ))?; + let session = finish_attachment(map.attach_vulkan_owned_texture( + &VulkanOwnedTextureDescriptor::new(extent, context.descriptor()), + RenderSessionAttachOptions::core_worker(2), + )?)?; return Ok((OwnedTextureTestContext::Vulkan(Box::new(context)), session)); } if has_opengl_test_context_backend() { let context = OpenGLTestContext::new(extent.width, extent.height)?; - let session = map.attach_opengl_owned_texture(&OpenGLOwnedTextureDescriptor::new( - extent, - context.descriptor(), - ))?; + let session = finish_attachment(map.attach_opengl_owned_texture( + &OpenGLOwnedTextureDescriptor::new(extent, context.descriptor()), + caller_attach_options(), + )?)?; return Ok((OwnedTextureTestContext::OpenGL(Box::new(context)), session)); } Err("no configured render backend offers an owned texture test session".into()) @@ -119,7 +333,7 @@ fn has_opengl_test_context_backend() -> bool { return false; } - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] { crate::supported_opengl_context_providers().contains(OpenGLContextProviderMask::EGL) } @@ -134,17 +348,19 @@ fn has_opengl_test_context_backend() -> bool { #[cfg(not(any( target_os = "linux", target_os = "android", + target_os = "macos", target_os = "windows", target_os = "emscripten" )))] { - // The Rust test helper implements Linux EGL, Windows WGL, and browser WebGL. + // The Rust test helper implements desktop and Android EGL, Windows WGL, + // and browser WebGL. false } } fn create_opengl_owned_texture_session( - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> std::result::Result<(OpenGLTestContext, RenderSessionHandle), Box> { let backends = crate::supported_render_backends(); @@ -152,15 +368,15 @@ fn create_opengl_owned_texture_session( return Err("native library does not support OpenGL owned texture sessions".into()); } let context = OpenGLTestContext::new(extent.width, extent.height)?; - let session = map.attach_opengl_owned_texture(&OpenGLOwnedTextureDescriptor::new( - extent, - context.descriptor(), - ))?; + let session = finish_attachment(map.attach_opengl_owned_texture( + &OpenGLOwnedTextureDescriptor::new(extent, context.descriptor()), + caller_attach_options(), + )?)?; Ok((context, session)) } fn create_opengl_surface_session( - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> std::result::Result<(OpenGLTestContext, RenderSessionHandle), Box> { let backends = crate::supported_render_backends(); @@ -168,16 +384,15 @@ fn create_opengl_surface_session( return Err("native library does not support OpenGL surface sessions".into()); } let context = OpenGLTestContext::new(extent.width, extent.height)?; - let session = map.attach_opengl_surface(&OpenGLSurfaceDescriptor::new( - extent, - context.descriptor(), - context.surface(), - ))?; + let session = finish_attachment(map.attach_opengl_surface( + &OpenGLSurfaceDescriptor::new(extent, context.descriptor(), context.surface()), + caller_attach_options(), + )?)?; Ok((context, session)) } fn create_opengl_borrowed_texture_session( - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> std::result::Result<(OpenGLBorrowedTexture, RenderSessionHandle), Box> { let backends = crate::supported_render_backends(); @@ -186,14 +401,17 @@ fn create_opengl_borrowed_texture_session( } let (physical_width, physical_height) = extent.physical_size()?; let texture = OpenGLBorrowedTexture::new(physical_width, physical_height)?; - let session = map.attach_opengl_borrowed_texture(&OpenGLBorrowedTextureDescriptor::new( - extent, - physical_width, - physical_height, - texture.descriptor(), - texture.name(), - gl_api::TEXTURE_2D, - ))?; + let session = finish_attachment(map.attach_opengl_borrowed_texture( + &OpenGLBorrowedTextureDescriptor::new( + extent, + physical_width, + physical_height, + texture.descriptor(), + texture.name(), + gl_api::TEXTURE_2D, + ), + caller_attach_options(), + )?)?; Ok((texture, session)) } @@ -201,7 +419,7 @@ fn create_opengl_borrowed_texture_session( /// allocates its own render target does. #[cfg(mln_webgpu_backend)] fn create_webgpu_borrowed_texture_session( - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> std::result::Result< ( @@ -214,7 +432,10 @@ fn create_webgpu_borrowed_texture_session( let context = WebGpuTestContext::new()?; let (physical_width, physical_height) = extent.physical_size()?; let texture = WebGpuBorrowedTexture::new(&context, physical_width, physical_height)?; - let session = map.attach_webgpu_borrowed_texture(&texture.descriptor(extent, &context))?; + let session = finish_attachment(map.attach_webgpu_borrowed_texture( + &texture.descriptor(extent, &context), + caller_attach_options(), + )?)?; Ok((context, texture, session)) } @@ -233,25 +454,30 @@ enum OwnedTextureTestContext { impl OwnedTextureTestContext { fn attach_owned_texture( &self, - map: &MapAttachRef, + map: &MapHandle, extent: RenderTargetExtent, ) -> Result { - match self { + let attachment = match self { Self::Metal(context) => map.attach_metal_owned_texture( &MetalOwnedTextureDescriptor::new(extent, context.descriptor()), + RenderSessionAttachOptions::core_worker(2), ), #[cfg(not(target_os = "emscripten"))] Self::Vulkan(context) => map.attach_vulkan_owned_texture( &VulkanOwnedTextureDescriptor::new(extent, context.descriptor()), + RenderSessionAttachOptions::core_worker(2), ), #[cfg(mln_webgpu_backend)] Self::WebGpu(context) => map.attach_webgpu_owned_texture( &WebGpuOwnedTextureDescriptor::new(extent, context.descriptor()), + caller_attach_options(), ), Self::OpenGL(context) => map.attach_opengl_owned_texture( &OpenGLOwnedTextureDescriptor::new(extent, context.descriptor()), + caller_attach_options(), ), - } + }?; + finish_attachment(attachment) } /// Hands the session a placeholder target of this context's own backend. @@ -262,7 +488,7 @@ impl OwnedTextureTestContext { &self, session: &RenderSessionHandle, extent: &RenderTargetExtent, - ) -> Result<()> { + ) -> Result> { // SAFETY: Test passes an opaque non-null address that the rejected call // never dereferences. let placeholder = unsafe { NativePointer::from_address(0x1) }; @@ -319,49 +545,57 @@ impl OwnedTextureTestContext { ) -> bool { match self { Self::Metal(_) => { - let Ok(frame) = session.acquire_metal_owned_texture_frame() else { + let Ok(frame) = session.acquire_frame() else { + return false; + }; + let Ok((metadata, _, _)) = frame.metal_texture() else { return false; }; - let metadata = frame.frame().unwrap(); let matches = (metadata.width, metadata.height) == (expected.width, expected.height) && metadata.scale_factor == expected.scale_factor; - frame.close().unwrap(); + release_frame(frame); matches } #[cfg(mln_webgpu_backend)] Self::WebGpu(_) => { - let Ok(frame) = session.acquire_webgpu_owned_texture_frame() else { + let Ok(frame) = session.acquire_frame() else { + return false; + }; + let Ok((metadata, _, _, _)) = frame.webgpu_texture() else { return false; }; - let metadata = frame.frame().unwrap(); let matches = metadata.width == expected.width && metadata.height == expected.height && metadata.scale_factor == expected.scale_factor; - frame.close().unwrap(); + release_frame(frame); matches } #[cfg(not(target_os = "emscripten"))] Self::Vulkan(_) => { - let Ok(frame) = session.acquire_vulkan_owned_texture_frame() else { + let Ok(frame) = session.acquire_frame() else { + return false; + }; + let Ok((metadata, _, _, _)) = frame.vulkan_texture() else { return false; }; - let metadata = frame.frame().unwrap(); let matches = (metadata.width, metadata.height) == (expected.width, expected.height) && metadata.scale_factor == expected.scale_factor; - frame.close().unwrap(); + release_frame(frame); matches } Self::OpenGL(_) => { - let Ok(frame) = session.acquire_opengl_owned_texture_frame() else { + let Ok(frame) = session.acquire_frame() else { + return false; + }; + let Ok((metadata, _)) = frame.opengl_texture() else { return false; }; - let metadata = frame.frame().unwrap(); let matches = (metadata.width, metadata.height) == (expected.width, expected.height) && metadata.scale_factor == expected.scale_factor; - frame.close().unwrap(); + release_frame(frame); matches } } @@ -409,7 +643,7 @@ struct OpenGLTestContext { descriptor: OpenGLContextDescriptor, surface_handle: NativePointer, gl: gl_api::Context, - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] platform: EglTestContext, #[cfg(target_os = "windows")] platform: WglTestContext, @@ -417,7 +651,7 @@ struct OpenGLTestContext { platform: WebGlTestContext, } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] struct EglTestContext { egl: egl::Egl, _lib: Library, @@ -427,9 +661,9 @@ struct EglTestContext { context: EGLContext, } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] impl EglTestContext { - fn new() -> std::result::Result> { + fn new(width: u32, height: u32) -> std::result::Result> { let lib = load_egl_library()?; let egl = load_egl_bindings(&lib)?; let display = open_egl_display(&egl)?; @@ -473,9 +707,9 @@ impl EglTestContext { let surface_attributes = [ egl::WIDTH as EGLint, - 8, + width as EGLint, egl::HEIGHT as EGLint, - 8, + height as EGLint, egl::NONE as EGLint, ]; let surface = @@ -541,7 +775,7 @@ impl EglTestContext { } } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] impl Drop for EglTestContext { fn drop(&mut self) { unsafe { @@ -563,7 +797,7 @@ impl Drop for EglTestContext { /// This fixture creates no context and makes none current: naming dedicated /// ownership is what asks the session to create its own context and keep it /// current between renders. -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] struct DedicatedEglTestSurface { egl: egl::Egl, _lib: Library, @@ -572,7 +806,7 @@ struct DedicatedEglTestSurface { surface: EGLSurface, } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] impl DedicatedEglTestSurface { fn new(width: u32, height: u32) -> std::result::Result> { let lib = load_egl_library()?; @@ -631,7 +865,7 @@ impl DedicatedEglTestSurface { } } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] impl Drop for DedicatedEglTestSurface { fn drop(&mut self) { unsafe { @@ -642,16 +876,15 @@ impl Drop for DedicatedEglTestSurface { } /// Opens and initializes the EGL display the OpenGL fixtures render through. -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] fn open_egl_display(egl: &egl::Egl) -> std::result::Result> { // A desktop Linux run without a display server needs Mesa's surfaceless - // platform. Device EGL implementations ship no such platform, so they take - // the default display. - #[cfg(any(target_env = "ohos", target_os = "android"))] + // platform. Android and macOS ANGLE take their default displays. + #[cfg(any(target_env = "ohos", target_os = "android", target_os = "macos"))] let display = unsafe { egl.GetDisplay(egl::DEFAULT_DISPLAY as *mut c_void) }; - #[cfg(any(target_env = "ohos", target_os = "android"))] + #[cfg(any(target_env = "ohos", target_os = "android", target_os = "macos"))] let display_operation = "eglGetDisplay"; - #[cfg(not(any(target_env = "ohos", target_os = "android")))] + #[cfg(not(any(target_env = "ohos", target_os = "android", target_os = "macos")))] let display = { const EGL_PLATFORM_SURFACELESS_MESA: u32 = 0x31DD; if !egl.GetPlatformDisplayEXT.is_loaded() { @@ -665,7 +898,7 @@ fn open_egl_display(egl: &egl::Egl) -> std::result::Result std::result::Result std::result::Result> { - unsafe { Library::new("libEGL.so.1") } + #[cfg(target_os = "macos")] + let library = unsafe { Library::new("libEGL.dylib") }; + #[cfg(not(target_os = "macos"))] + let library = unsafe { Library::new("libEGL.so.1") } .or_else(|_| unsafe { Library::new("libEGL.so") }) - .map_err(|error| format!("failed to load libEGL: {error}").into()) + .map_err(|error| format!("failed to load libEGL: {error}")); + library.map_err(|error| format!("failed to load libEGL: {error}").into()) } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] fn load_egl_bindings(lib: &Library) -> std::result::Result> { type EglGetProcAddress = unsafe extern "system" fn(*const c_void) -> *const c_void; @@ -1475,7 +1712,7 @@ impl WebGlTestContext { } fn descriptor(&self) -> OpenGLContextDescriptor { - OpenGLContextDescriptor::WebGl(WebGlContextDescriptor::new(self.context)) + OpenGLContextDescriptor::WebGl(WebGlContextDescriptor::existing(self.context)) } // A browser session presents to the canvas this context was created @@ -1816,9 +2053,9 @@ impl OpenGLTestContext { Self::new_platform(width, height) } - #[cfg(any(target_os = "linux", target_os = "android"))] - fn new_platform(_width: u32, _height: u32) -> std::result::Result> { - let platform = EglTestContext::new()?; + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + fn new_platform(width: u32, height: u32) -> std::result::Result> { + let platform = EglTestContext::new(width, height)?; let gl = unsafe { gl_api::Context::from_loader_function(|symbol| { let symbol = CString::new(symbol).expect("GL symbol names do not contain NULs"); @@ -1881,6 +2118,7 @@ impl OpenGLTestContext { #[cfg(not(any( target_os = "linux", target_os = "android", + target_os = "macos", target_os = "windows", target_os = "emscripten" )))] @@ -1897,7 +2135,7 @@ impl OpenGLTestContext { } fn make_current(&self) -> std::result::Result<(), Box> { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] { self.platform.make_current()?; Ok(()) @@ -1915,6 +2153,7 @@ impl OpenGLTestContext { #[cfg(not(any( target_os = "linux", target_os = "android", + target_os = "macos", target_os = "windows", target_os = "emscripten" )))] @@ -2319,219 +2558,85 @@ fn pick_vulkan_physical_device( Err("no Vulkan physical device with a graphics queue was found".into()) } -fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool { - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - let _ = runtime.pump(Some(Duration::ZERO), None); - let found = runtime - .drain_events(0) - .map(|batch| batch.iter().any(|event| event.event_type() == event_type)) - .unwrap_or(false); - if found { - return true; - } - std::thread::sleep(Duration::from_millis(10)); - } - false -} - -fn static_map_options(width: u32, height: u32, scale_factor: f64) -> MapOptions { - let mut options = MapOptions::new(width, height, scale_factor); - options.mode = MapMode::Static; - options -} - -fn load_query_style(runtime: &mut RuntimeHandle, map: &MapHandle, session: &RenderSessionHandle) { - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(37.7749, -122.4194)); - camera.zoom = Some(10.0); - map.jump_to(&camera).unwrap(); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - render_available_updates(runtime, session, 5); -} - -fn cluster_point(latitude: f64, longitude: f64, name: &str, weight: u64) -> JsonValue { - json!({ - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [longitude, latitude]}, - "properties": {"name": name, "weight": weight}, - }) -} - -/// Builds the clustered source through the GeoJSON data adder so the render -/// tests exercise `GeoJsonSourceOptions`. -fn load_cluster_style(runtime: &mut RuntimeHandle, map: &MapHandle, session: &RenderSessionHandle) { - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(0.0, 0.0)); - camera.zoom = Some(0.0); - map.jump_to(&camera).unwrap(); - map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) - .unwrap(); - - let data = serde_json::to_vec(&json!({ - "type": "FeatureCollection", - "features": [ - cluster_point(0.0, 0.0, "one", 1), - cluster_point(0.001, 0.001, "two", 2), - cluster_point(0.002, 0.002, "three", 3), - ], - })) - .unwrap(); - let mut options = GeoJsonSourceOptions::default(); - options.cluster = Some(true); - options.cluster_radius = Some(60); - options.cluster_min_points = Some(2); - options.cluster_max_zoom = Some(17.0); - options.cluster_properties = - Some(serde_json::to_vec(&json!({"weight_sum": ["+", ["get", "weight"]]})).unwrap()); - let data = crate::GeoJsonSourceDataHandle::new(&data, Some(&options)).unwrap(); - map.add_geojson_source_data("cluster-source", &data) - .unwrap(); - - let layer = serde_json::to_vec(&json!({ - "id": "cluster-circle", - "type": "circle", - "source": "cluster-source", - "filter": ["has", "point_count"], - "paint": {"circle-color": "#2563eb", "circle-radius": 20.0}, - })) - .unwrap(); - map.add_style_layer_json(&layer, None).unwrap(); - - render_available_updates(runtime, session, 5); -} - -fn numeric_member(value: &JsonValue, key: &str) -> Option { - json_member(value, key)?.as_f64() -} - -fn render_available_updates( - runtime: &mut RuntimeHandle, - session: &RenderSessionHandle, - count: usize, -) { - for _ in 0..count { - if wait_for_runtime_event(runtime, RuntimeEventType::MapRenderUpdateAvailable) { - let _ = session.render_update(); - } - } +fn await_runtime_barrier(runtime: &RuntimeHandle) { + let operation = runtime.barrier().unwrap(); + assert!(operation.wait(Duration::from_secs(5)).unwrap()); + maplibre_core::check(operation.terminal_status().unwrap()).unwrap(); + operation.finish().unwrap(); } -fn render_pending_updates(runtime: &mut RuntimeHandle, session: &RenderSessionHandle) { - let _ = runtime.pump(Some(Duration::ZERO), None); - let updates = runtime - .drain_events(0) - .map(|batch| { - batch - .iter() - .filter(|event| event.event_type() == RuntimeEventType::MapRenderUpdateAvailable) - .count() +fn render_frame(session: &RenderSessionHandle, if_needed: bool) -> RenderFrameResult { + static NEXT_TOKEN: AtomicUsize = AtomicUsize::new(1); + let token = NEXT_TOKEN.fetch_add(1, Ordering::Relaxed) as u64; + session + .request_frame(FrameDemand { + if_needed, + present: session.capabilities().unwrap().presentation, + token, + ..FrameDemand::default() }) - .unwrap_or(0); - for _ in 0..updates { - let _ = session.render_update(); - } -} - -fn wait_for_rendered_feature( - runtime: &mut RuntimeHandle, - session: &RenderSessionHandle, - geometry: &RenderedQueryGeometry, - options: &RenderedFeatureQueryOptions, - description: &str, -) -> QueriedFeature { - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - let features = session - .query_rendered_features(geometry, Some(options)) - .unwrap(); - if features.len() == 1 { - return features.into_iter().next().unwrap(); - } - render_pending_updates(runtime, session); - std::thread::sleep(Duration::from_millis(1)); - } - panic!("timed out waiting for {description}"); -} + .unwrap(); -fn wait_for_source_feature( - runtime: &mut RuntimeHandle, - session: &RenderSessionHandle, - source_id: &str, - options: &SourceFeatureQueryOptions, - description: &str, -) -> QueriedFeature { let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - let features = session - .query_source_features(source_id, Some(options)) - .unwrap(); - if features.len() == 1 { - return features.into_iter().next().unwrap(); + loop { + let _ = session.service_driver_work(64); + let batch = match session.drain_frame_results() { + Ok(batch) => batch, + Err(error) if error.kind() == ErrorKind::NotReady => { + assert!(Instant::now() < deadline, "frame demand timed out"); + std::thread::yield_now(); + continue; + } + Err(error) => panic!("failed to drain frame results: {error}"), + }; + if let Some(result) = batch + .copy_results() + .unwrap() + .into_iter() + .find(|result| result.token == token) + { + return result; } - render_pending_updates(runtime, session); - std::thread::sleep(Duration::from_millis(1)); + assert!(Instant::now() < deadline, "frame demand timed out"); + std::thread::yield_now(); } - panic!("timed out waiting for {description}"); -} - -fn single_cluster_leaf(session: &RenderSessionHandle, feature: &[u8], offset: u64) -> JsonValue { - let arguments = serde_json::to_vec(&json!({"limit": 1, "offset": offset})).unwrap(); - let result = session - .query_feature_extension( - "cluster-source", - feature, - "supercluster", - "leaves", - Some(&arguments), - ) - .unwrap(); - let result: JsonValue = serde_json::from_slice(&result).unwrap(); - let leaves = result["features"] - .as_array() - .expect("expected leaves feature collection"); - assert_eq!(leaves.len(), 1); - leaves[0].clone() } -fn feature_member<'a>(feature: &'a JsonValue, key: &str) -> Option<&'a JsonValue> { - feature.get("properties")?.get(key) +fn close_session(session: RenderSessionHandle) { + finish_unit(&session, session.detach().unwrap()); + session.destroy().unwrap(); } -fn json_member<'a>(value: &'a JsonValue, key: &str) -> Option<&'a JsonValue> { - value.as_object()?.get(key) +fn take_json(session: &RenderSessionHandle, operation: NativeFuture>) -> JsonValue { + wait_until_completed(session, &operation); + serde_json::from_slice(&operation.take().unwrap()).unwrap() } -fn assert_json_member(value: &JsonValue, key: &str, expected: &JsonValue) { - assert_eq!(json_member(value, key), Some(expected)); +fn take_features( + session: &RenderSessionHandle, + operation: NativeFuture>, +) -> Vec { + wait_until_completed(session, &operation); + operation.take().unwrap() } -fn assert_point_geometry_close(geometry: &JsonValue, expected: LatLng) { - assert_eq!(geometry["type"], json!("Point")); - let coordinates = geometry["coordinates"] - .as_array() - .expect("expected point coordinates"); - assert!((coordinates[1].as_f64().unwrap() - expected.latitude).abs() < 0.0001); - assert!((coordinates[0].as_f64().unwrap() - expected.longitude).abs() < 0.0001); +fn feature_json(feature: &QueriedFeature) -> JsonValue { + serde_json::from_slice(&feature.feature).unwrap() } #[test] -// Spec coverage: BND-161. fn native_pointer_round_trips_address() { - // SAFETY: Test uses a dummy opaque address and does not dereference it. + // SAFETY: The test reconstructs but never dereferences this dummy address. let pointer = unsafe { NativePointer::from_address(0x1234) }; assert_eq!(pointer.address(), 0x1234); - // SAFETY: Test only verifies address reconstruction; it does not dereference. assert_eq!(unsafe { pointer.as_ptr::() } as usize, 0x1234); assert!(NativePointer::NULL.is_null()); } #[test] -// Spec coverage: BND-161. -fn opengl_context_provider_mask_is_exposed_semantically() { +fn opengl_context_provider_mask_matches_backend_availability() { let providers = crate::supported_opengl_context_providers(); - let backends = crate::supported_render_backends(); - if backends.contains(RenderBackendMask::OPENGL) { + if has_opengl_backend() { assert!(providers.intersects( OpenGLContextProviderMask::WGL | OpenGLContextProviderMask::EGL @@ -2543,1868 +2648,701 @@ fn opengl_context_provider_mask_is_exposed_semantically() { } #[test] -// Spec coverage: BND-162. -fn opengl_owned_texture_session_attaches_with_platform_context() { - if !has_opengl_test_context_backend() { +fn owned_texture_session_renders_acquires_resizes_and_reads_back() { + if !has_test_owned_texture_session_backend() { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_opengl_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("OpenGL owned texture test session should attach when OpenGL is supported"); - let error = session.acquire_opengl_owned_texture_frame().unwrap_err(); - assert!(matches!( - error.kind(), - ErrorKind::InvalidState | ErrorKind::Unsupported + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 16, 1.0), )); + let initial_extent = RenderTargetExtent::new(32, 16, 1.0); + let (context, session) = create_owned_texture_session(&map, initial_extent.clone()).unwrap(); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable - )); + let capabilities = session.capabilities().unwrap(); + assert!(capabilities.frame_acquisition); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + session.snapshot().unwrap().state, + RenderSessionLifecycle::Attached ); - let frame = session.acquire_opengl_owned_texture_frame().unwrap(); - assert_eq!(frame.frame().unwrap().width, 32); - assert_eq!(frame.frame().unwrap().height, 16); - assert_eq!(frame.frame().unwrap().target, gl_api::TEXTURE_2D); - assert_eq!(frame.frame().unwrap().internal_format, gl_api::RGBA8); - assert_eq!(frame.frame().unwrap().format, gl_api::RGBA); - assert_eq!(frame.frame().unwrap().type_, gl_api::UNSIGNED_BYTE); - assert!(!frame.texture().unwrap().is_zero()); - frame.close().unwrap(); + map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); + await_runtime_barrier(&runtime); + let mut rendered = render_frame(&session, false); + assert_eq!(rendered.disposition, FrameDisposition::Rendered); + // The frame result carries the map's follow-up request; once placement + // settles with no transition in flight, the map stops asking to repaint. + let repaint_deadline = Instant::now() + Duration::from_secs(5); + while rendered.needs_repaint { + assert!(Instant::now() < repaint_deadline, "map never settled"); + rendered = render_frame(&session, false); + assert_eq!(rendered.disposition, FrameDisposition::Rendered); + } + assert!(context.try_acquire_frame_extent(&session, &initial_extent)); + + if capabilities.readback { + let operation = session.read_premultiplied_rgba8().unwrap(); + wait_until_completed(&session, &operation); + let image = operation.take().unwrap(); + assert_eq!((image.info.width, image.info.height), (32, 16)); + assert_eq!(image.data.len(), image.info.byte_length); + assert!(image.data.iter().any(|byte| *byte != 0)); + } - #[cfg(target_os = "windows")] - { - let info = session.texture_image_info().unwrap(); - assert_eq!((info.width, info.height), (32, 16)); - assert!(info.stride >= info.width * 4); - assert!(info.byte_length >= info.stride as usize * info.height as usize); - - let mut undersized = vec![0x7f; info.byte_length - 1]; - let error = session - .read_premultiplied_rgba8_into(&mut undersized) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT)); - assert!(undersized.iter().all(|byte| *byte == 0x7f)); - - let mut pixels = vec![0; info.byte_length]; - let copied_info = session.read_premultiplied_rgba8_into(&mut pixels).unwrap(); - assert_eq!(copied_info, info); - assert!(pixels.iter().any(|byte| *byte != 0)); + let resized_extent = RenderTargetExtent::new(48, 24, 1.0); + finish_unit(&session, session.resize(&resized_extent).unwrap()); + drop( + map.resize(crate::LogicalExtent { + width: 48, + height: 24, + scale_factor: 1.0, + }) + .unwrap(), + ); + await_runtime_barrier(&runtime); + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let result = render_frame(&session, false); + if result.disposition == FrameDisposition::Rendered { + break; + } + assert_eq!(result.disposition, FrameDisposition::SizePending); + // The repaint flag is meaningful on rendered frames alone. + assert!(!result.needs_repaint); + assert!( + Instant::now() < deadline, + "resized frame did not become renderable" + ); + await_runtime_barrier(&runtime); } + assert!(context.try_acquire_frame_extent(&session, &resized_extent)); + assert_eq!(session.snapshot().unwrap().extent, resized_extent); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} + match context.set_placeholder_borrowed_target(&session, &resized_extent) { + Ok(operation) => { + let deadline = Instant::now() + Duration::from_secs(5); + while !operation.is_completed().unwrap() { + let _ = session.service_driver_work(64); + assert!(Instant::now() < deadline, "target replacement timed out"); + } + assert_ne!(operation.terminal_status().unwrap(), sys::MLN_STATUS_OK); + } + Err(error) => assert_ne!(error.raw_status(), Some(sys::MLN_STATUS_OK)), + } -/// Whether this platform has an OpenGL surface for a session to render into. -/// -/// A browser has none: a WebGL context presents through its canvas, and a -/// browser host renders into a texture instead, so `mln_opengl_surface_attach` -/// has nothing to be handed. Every other OpenGL provider supplies one. -fn has_opengl_surface_test_backend() -> bool { - has_opengl_test_context_backend() + close_session(session); + let session = context + .attach_owned_texture(&map, RenderTargetExtent::new(24, 12, 1.0)) + .unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-162. -fn opengl_surface_session_renders_with_platform_context() { - if !has_opengl_surface_test_backend() { +fn opengl_owned_texture_exposes_backend_metadata() { + if !has_opengl_test_context_backend() { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - - let (_context, session) = create_opengl_surface_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("OpenGL surface test session should attach when OpenGL is supported"); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 16, 1.0), )); + let (_context, session) = + create_opengl_owned_texture_session(&map, RenderTargetExtent::new(32, 16, 1.0)).unwrap(); + map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); - #[cfg(any(target_os = "windows", target_os = "emscripten"))] - { - let pixels = _context.read_surface_rgba(32, 16).unwrap(); - assert!(pixels.iter().any(|byte| *byte != 0)); - } + let frame = session.acquire_frame().unwrap(); + let (metadata, texture) = frame.opengl_texture().unwrap(); + assert_eq!((metadata.width, metadata.height), (32, 16)); + assert_eq!(metadata.target, gl_api::TEXTURE_2D); + assert_eq!(metadata.internal_format, gl_api::RGBA8); + assert!(!texture.is_zero()); + release_frame(frame); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } -/// A dedicated session owns its thread's OpenGL context: it creates a context -/// that joins no share group, keeps it current between renders, and gives the -/// thread back when it closes. -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] #[test] -// Spec coverage: BND-162. -fn dedicated_opengl_surface_session_renders_and_keeps_its_context_current() { - if !has_opengl_test_context_backend() { +fn dedicated_opengl_surface_keeps_its_context_on_the_graphics_thread() { + if !has_opengl_backend() { return; } + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let surface = DedicatedEglTestSurface::new(64, 64) - .expect("a dedicated EGL fixture reaches a display and a pbuffer surface"); - // The fixture makes no context current, so the session has nothing to - // inherit and nothing to restore. + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 16, 1.0), + )); + let surface = DedicatedEglTestSurface::new(32, 16).unwrap(); assert!(!surface.has_current_context()); - - let session = map - .attach_ref() - .unwrap() - .attach_opengl_surface(&OpenGLSurfaceDescriptor::new( - RenderTargetExtent::new(64, 64, 1.0), - surface.descriptor(), - surface.surface(), - )) - .expect("a dedicated OpenGL surface session attaches when EGL is supported"); - + let session = finish_attachment( + map.attach_opengl_surface( + &OpenGLSurfaceDescriptor::new( + RenderTargetExtent::new(32, 16, 1.0), + surface.descriptor(), + surface.surface(), + ), + caller_attach_options(), + ) + .unwrap(), + ) + .unwrap(); + assert!(surface.has_current_context()); map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - // The session owns this thread, which is also the map's, so the map only - // reaches the style and the extent when this loop pumps the runtime. - let deadline = Instant::now() + Duration::from_secs(5); - let mut result = RenderResult::NoUpdate; - while result != RenderResult::Rendered && Instant::now() < deadline { - let _ = runtime.pump(Some(Duration::ZERO), None); - result = session.render_update().unwrap().result; - if result != RenderResult::Rendered { - std::thread::sleep(Duration::from_millis(10)); - } - } - assert_eq!(result, RenderResult::Rendered); + await_runtime_barrier(&runtime); + assert_eq!( + render_frame(&session, false).disposition, + FrameDisposition::Rendered + ); assert!(surface.has_current_context()); - session.close().unwrap(); - // Closing the session releases the thread it had taken over. - assert!(!surface.has_current_context()); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } -#[cfg(mln_webgpu_backend)] #[test] -// Spec coverage: BND-162. -fn webgpu_surface_session_renders_and_presents_through_a_canvas() { - if !crate::supported_render_backends().contains(RenderBackendMask::WEBGPU) { +fn opengl_surface_session_renders_into_the_platform_surface() { + if !has_opengl_test_context_backend() || cfg!(target_os = "emscripten") { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(128, 128, 1.0)).unwrap(); - let context = WebGpuTestContext::new().expect("a browser build reaches a WebGPU device"); - let surface = WebGpuTestSurface::new(&context, 128, 128) - .expect("a WebGPU instance creates a surface over a canvas"); - let extent = RenderTargetExtent::new(128, 128, 1.0); - let session = map - .attach_ref() - .unwrap() - .attach_webgpu_surface(&surface.descriptor(extent.clone(), &context)) - .expect("WebGPU surface session should attach when WebGPU is supported"); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 16, 1.0), )); - // A frame reported rendered means the surface gave up its texture and took - // it back at present, which is the whole of the acquire and present cycle. + let (_context, session) = + create_opengl_surface_session(&map, RenderTargetExtent::new(32, 16, 1.0)).unwrap(); + map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); + await_runtime_barrier(&runtime); + assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); + #[cfg(any(target_os = "windows", target_os = "emscripten"))] + { + let pixels = _context.read_surface_rgba(32, 16).unwrap(); + assert!(pixels.iter().any(|byte| *byte != 0)); + } - // The frame reached the canvas, not just the render pass: the style paints - // its background over every pixel. - // - // A browser shows a WebGPU frame once the task that drew it yields, which - // is what expires the texture the surface handed out, so this yields before - // reading rather than snapshotting the canvas mid-task. - // SAFETY: yielding is legal on this thread, which entered through main. - unsafe { emscripten_sleep(1) }; - let pixels = surface.read_rgba(128, 128).unwrap(); - assert!( - pixels - .chunks_exact(4) - .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA), - "the session should present its frame to the canvas" - ); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); +} - // A surface hands out one texture per frame, so a second frame proves the - // first was released rather than held. - map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) - .unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable +#[test] +fn opengl_borrowed_texture_session_replaces_its_target() { + if !has_opengl_test_context_backend() { + return; + } + + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 16, 1.0), )); + let initial_extent = RenderTargetExtent::new(32, 16, 1.0); + let (mut texture, session) = + create_opengl_borrowed_texture_session(&map, initial_extent).unwrap(); + map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); + assert!(texture.read_rgba().unwrap().iter().any(|byte| *byte != 0)); - // A session-owned frame belongs to the texture session kind, so this - // reports the mismatch rather than handing back a texture it does not own. - assert_eq!( - session - .acquire_webgpu_owned_texture_frame() - .unwrap_err() - .kind(), - ErrorKind::Unsupported + let replacement = texture.allocate_replacement(48, 24).unwrap(); + let replacement_name = replacement.0.get(); + let replacement_extent = RenderTargetExtent::new(48, 24, 1.0); + let operation = session + .set_opengl_borrowed_texture_target(&OpenGLBorrowedTextureDescriptor::new( + replacement_extent.clone(), + 48, + 24, + texture.descriptor(), + replacement_name, + gl_api::TEXTURE_2D, + )) + .unwrap(); + finish_unit(&session, operation); + texture.adopt(replacement, 48, 24).unwrap(); + drop( + map.resize(crate::LogicalExtent { + width: 48, + height: 24, + scale_factor: 1.0, + }) + .unwrap(), ); - - let replacement = WebGpuTestSurface::new(&context, 128, 128) - .expect("a WebGPU instance creates a second surface over a canvas"); - session - .set_webgpu_surface_target(&replacement.descriptor(extent, &context)) - .expect("a replacement naming the same device and format is accepted"); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable - )); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); + assert!(texture.read_rgba().unwrap().iter().any(|byte| *byte != 0)); + assert_eq!(session.snapshot().unwrap().extent, replacement_extent); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[cfg(mln_webgpu_backend)] #[test] -// Spec coverage: BND-162 and BND-171. -fn webgpu_borrowed_texture_session_renders_into_a_host_texture() { +fn webgpu_surface_session_renders_into_the_browser_canvas() { if !crate::supported_render_backends().contains(RenderBackendMask::WEBGPU) { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(128, 128, 1.0)).unwrap(); - let (context, texture, session) = create_webgpu_borrowed_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(128, 128, 1.0), + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(64, 64, 1.0), + )); + let context = WebGpuTestContext::new().unwrap(); + let surface = WebGpuTestSurface::new(&context, 64, 64).unwrap(); + let session = finish_attachment( + map.attach_webgpu_surface( + &surface.descriptor(RenderTargetExtent::new(64, 64, 1.0), &context), + caller_attach_options(), + ) + .unwrap(), ) - .expect("WebGPU borrowed texture session should attach when WebGPU is supported"); - + .unwrap(); map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable - )); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); - - // The style paints its background over every pixel, so finding it in the - // host's texture is what says this session rendered into the one it was - // handed rather than only reporting that it did. - let pixels = texture.read_rgba(&context).unwrap(); assert!( - pixels + surface + .read_rgba(64, 64) + .unwrap() .chunks_exact(4) - .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA), - "the session should render into the host's texture" - ); - - // A session-owned frame belongs to the other target kind, so this reports - // the mismatch rather than handing back a texture it does not own. - assert_eq!( - session - .acquire_webgpu_owned_texture_frame() - .unwrap_err() - .kind(), - ErrorKind::Unsupported + .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA) ); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } +#[cfg(mln_webgpu_backend)] #[test] -// Spec coverage: BND-162 and BND-171. -fn opengl_borrowed_texture_session_renders_with_platform_context() { - if !has_opengl_test_context_backend() { +fn webgpu_borrowed_texture_session_renders_into_a_host_texture() { + if !crate::supported_render_backends().contains(RenderBackendMask::WEBGPU) { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(128, 128, 1.0)).unwrap(); - - let (texture, session) = create_opengl_borrowed_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(128, 128, 1.0), - ) - .expect("OpenGL borrowed texture test session should attach when OpenGL is supported"); - map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(64, 64, 1.0), )); - assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered - ); - - let error = session.acquire_opengl_owned_texture_frame().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - - let pixels = texture.read_rgba().unwrap(); - assert!(pixels.iter().any(|byte| *byte != 0)); - - session.close().unwrap(); - let pixels_after_close = texture.read_rgba().unwrap(); - assert!(pixels_after_close.iter().any(|byte| *byte != 0)); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-175. -fn set_target_hands_a_live_session_a_new_borrowed_texture() { - if !has_opengl_test_context_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(128, 128, 1.0)).unwrap(); - - let initial_extent = RenderTargetExtent::new(128, 128, 1.0); - let (mut texture, session) = - create_opengl_borrowed_texture_session(&map.attach_ref().unwrap(), initial_extent) - .expect("OpenGL borrowed texture test session should attach when OpenGL is supported"); - + let (context, texture, session) = + create_webgpu_borrowed_texture_session(&map, RenderTargetExtent::new(64, 64, 1.0)).unwrap(); map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable - )); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered - ); - - // A caller-owned texture is sized by its owner, so resize is unsupported. - let resized_extent = RenderTargetExtent::new(96, 64, 1.0); - let resize_error = session - .resize( - resized_extent.width, - resized_extent.height, - resized_extent.scale_factor, - ) - .unwrap_err(); - assert_eq!(resize_error.kind(), ErrorKind::Unsupported); - - let (physical_width, physical_height) = resized_extent.physical_size().unwrap(); - let replacement = texture - .allocate_replacement(physical_width, physical_height) - .unwrap(); - session - .set_opengl_borrowed_texture_target(&OpenGLBorrowedTextureDescriptor::new( - resized_extent.clone(), - physical_width, - physical_height, - texture.descriptor(), - replacement.0.get(), - gl_api::TEXTURE_2D, - )) - .unwrap(); - texture - .adopt(replacement, physical_width, physical_height) - .unwrap(); - - // QUERY_STYLE_JSON paints this background over every pixel it covers, so - // finding it in a texture that started zeroed means the session rendered - // into the replacement. - const QUERY_STYLE_BACKGROUND_RGBA: [u8; 4] = [0xd8, 0xf1, 0xff, 0xff]; - assert!( - texture.read_rgba().unwrap().iter().all(|byte| *byte == 0), - "a freshly allocated replacement should start blank" + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); - let deadline = Instant::now() + Duration::from_secs(5); - let mut rendered_into_replacement = false; - while Instant::now() < deadline && !rendered_into_replacement { - let _ = runtime.pump(Some(Duration::ZERO), None); - let _ = runtime.drain_events(0); - if session.render_update().unwrap().result == RenderResult::Rendered { - rendered_into_replacement = texture - .read_rgba() - .unwrap() - .chunks_exact(4) - .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA); - } - std::thread::sleep(Duration::from_millis(1)); - } assert!( - rendered_into_replacement, - "the session should render into the texture it was handed" - ); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-176. -fn set_target_reports_unsupported_for_a_session_owned_texture() { - if !has_test_owned_texture_session_backend() { - return; - } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let extent = RenderTargetExtent::new(64, 64, 1.0); - let (context, session) = - create_owned_texture_session(&map.attach_ref().unwrap(), extent.clone()) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - // The setter must be this build's own backend, or "not supported by this - // build" answers in place of the target-kind rejection under test. - let error = context - .set_placeholder_borrowed_target(&session, &extent) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - - assert!(session.render_update().is_ok()); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-176. -fn set_target_reports_unsupported_for_a_session_owned_opengl_texture() { - // The owned texture fixture above covers Metal and Vulkan only. OpenGL has - // its own fixture, so an OpenGL-only build covers the same rejection here - // rather than skipping it. - if !has_opengl_test_context_backend() { - return; - } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let extent = RenderTargetExtent::new(64, 64, 1.0); - let (context, session) = - create_opengl_owned_texture_session(&map.attach_ref().unwrap(), extent.clone()) - .expect("OpenGL owned texture test session should attach when OpenGL is supported"); - - let error = session - .set_opengl_borrowed_texture_target(&OpenGLBorrowedTextureDescriptor::new( - extent, - 64, - 64, - context.descriptor(), - 1, - gl_api::TEXTURE_2D, - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - - assert!(session.render_update().is_ok()); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-176. -fn set_target_reports_unsupported_for_a_target_kind_the_session_does_not_have() { - if !has_opengl_test_context_backend() { - return; - } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - - let extent = RenderTargetExtent::new(64, 64, 1.0); - let (texture, session) = - create_opengl_borrowed_texture_session(&map.attach_ref().unwrap(), extent.clone()) - .expect("OpenGL borrowed texture test session should attach when OpenGL is supported"); - - // A surface descriptor names a target this session does not have. - let error = session - .set_opengl_surface_target(&OpenGLSurfaceDescriptor::new( - extent.clone(), - texture.descriptor(), - // SAFETY: Test passes an opaque non-null address that the rejected - // call never dereferences. - unsafe { NativePointer::from_address(0x1) }, - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - - assert!(session.render_update().is_ok()); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-167, BND-168, and BND-173. -fn frame_native_pointer_round_trips_address_without_plain_native_pointer() { - // A backend frame pointer is tied to the borrowed frame, so a released - // handle cannot expose one through a safe API. - // SAFETY: Test uses a dummy opaque address and does not dereference it. - let pointer = unsafe { FrameNativePointer::<'_>::from_ptr(0x4321usize as *mut u8) }; - // SAFETY: Test only verifies address reconstruction while the typed frame borrow is live. - assert_eq!(unsafe { pointer.address() }, 0x4321); - // SAFETY: Test only verifies raw pointer reconstruction; it does not dereference. - assert_eq!(unsafe { pointer.as_ptr::() } as usize, 0x4321); - assert!(!pointer.is_null()); -} - -#[test] -// Spec coverage: BND-167. -fn frame_metadata_copies_values_without_exposing_backend_pointers() { - let mut metal = empty_metal_owned_texture_frame(); - metal.generation = 1; - metal.width = 64; - metal.height = 32; - metal.scale_factor = 2.0; - metal.frame_id = 9; - metal.texture = 0x1000usize as *mut _; - metal.device = 0x2000usize as *mut _; - metal.pixel_format = 80; - let copied = MetalOwnedTextureFrame::from_native(&metal); - assert_eq!(copied.generation, 1); - assert_eq!( - (copied.width, copied.height, copied.scale_factor), - (64, 32, 2.0) - ); - assert_eq!(copied.frame_id, 9); - assert_eq!(copied.pixel_format, 80); - - let mut vulkan = empty_vulkan_owned_texture_frame(); - vulkan.generation = 3; - vulkan.width = 128; - vulkan.height = 96; - vulkan.scale_factor = 1.5; - vulkan.frame_id = 11; - vulkan.image = 0x3000usize as *mut _; - vulkan.image_view = 0x4000usize as *mut _; - vulkan.device = 0x5000usize as *mut _; - vulkan.format = 44; - vulkan.layout = 55; - let copied = VulkanOwnedTextureFrame::from_native(&vulkan); - assert_eq!(copied.generation, 3); - assert_eq!( - (copied.width, copied.height, copied.scale_factor), - (128, 96, 1.5) - ); - assert_eq!(copied.frame_id, 11); - assert_eq!((copied.format, copied.layout), (44, 55)); - - let mut opengl = empty_opengl_owned_texture_frame(); - opengl.generation = 5; - opengl.width = 256; - opengl.height = 128; - opengl.scale_factor = 2.0; - opengl.frame_id = 13; - opengl.texture = 23; - opengl.target = 0x0de1; - opengl.internal_format = 0x8058; - opengl.format = 0x1908; - opengl.type_ = 0x1401; - let copied = OpenGLOwnedTextureFrame::from_native(&opengl); - assert_eq!(copied.generation, 5); - assert_eq!( - (copied.width, copied.height, copied.scale_factor), - (256, 128, 2.0) - ); - assert_eq!(copied.frame_id, 13); - assert_eq!(copied.target, 0x0de1); - assert_eq!(copied.internal_format, 0x8058); - assert_eq!(copied.format, 0x1908); - assert_eq!(copied.type_, 0x1401); -} - -static FRAME_RELEASE_STATUS: AtomicI32 = AtomicI32::new(sys::MLN_STATUS_OK); -static FRAME_RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0); - -unsafe extern "C" fn fake_session_destroy(_session: sys::mln_render_session) -> sys::mln_status { - sys::MLN_STATUS_OK -} - -unsafe extern "C" fn fake_metal_frame_release( - _session: sys::mln_render_session, - _frame: *const sys::mln_metal_owned_texture_frame, -) -> sys::mln_status { - FRAME_RELEASE_COUNT.fetch_add(1, Ordering::SeqCst); - FRAME_RELEASE_STATUS.load(Ordering::SeqCst) -} - -#[test] -// Spec coverage: BND-169. -fn failed_frame_release_leaves_frame_live_for_later_release() { - FRAME_RELEASE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst); - FRAME_RELEASE_COUNT.store(0, Ordering::SeqCst); - let session = Rc::new(RenderSessionState { - // SAFETY: The fake handle is never dereferenced, and the fake destroy - // only reports success, so dropping the state is harmless. - handle: unsafe { - ThreadAffineNativeHandle::from_handle( - sys::mln_render_session(0x0400_0000_0000_002a), - fake_session_destroy, - "mln_render_session", - ) + texture + .read_rgba(&context) .unwrap() - }, - detached: Cell::new(false), - frame_acquired: Cell::new(true), - }); - let raw = empty_metal_owned_texture_frame(); - let frame = MetalOwnedTextureFrameHandle { - session: Rc::clone(&session), - frame: MetalOwnedTextureFrame::from_native(&raw), - raw, - closed: Cell::new(false), - _thread_affine: PhantomData, - }; - - let error = frame - .close_with_release(fake_metal_frame_release) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_STATE)); - assert!(session.frame_acquired.get()); - let frame = error.into_handle(); - assert!(frame.frame().is_ok()); - - FRAME_RELEASE_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst); - frame.close_with_release(fake_metal_frame_release).unwrap(); + .chunks_exact(4) + .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA) + ); - assert_eq!(FRAME_RELEASE_COUNT.load(Ordering::SeqCst), 2); - assert!(!session.frame_acquired.get()); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// The map scale factor is fixed at creation, so rendering a target at another -// scale warns instead of failing. -fn scale_factor_warnings_compare_the_preserved_creation_double() { +fn feature_state_and_rendered_queries_copy_native_results() { if !has_test_owned_texture_session_backend() { return; } - let _logging = LoggingTestGuard::new(); - // Warnings dispatch asynchronously by default, which would race this test. - crate::set_async_log_severity_mask(LogSeverityMask::empty()).unwrap(); - let warnings = Arc::new(Mutex::new(Vec::new())); - let captured = warnings.clone(); - crate::set_log_callback(move |record| { - if record.message.contains("scale_factor") { - captured - .lock() - .unwrap() - .push((record.severity, record.message)); - } - false - }) - .unwrap(); let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - // 32.1 differs from its native float representation by more than the - // warning tolerance, so a comparison against anything but the preserved - // creation double would warn here. - let map = MapHandle::with_options(&runtime, &MapOptions::new(1, 1, 32.1)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(1, 1, 32.1), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - assert!(warnings.lock().unwrap().is_empty()); - - session.resize(1, 1, 2.0).unwrap(); - let mismatch_warnings = warnings.lock().unwrap().clone(); - assert_eq!(mismatch_warnings.len(), 1); - assert_eq!(mismatch_warnings[0].0, LogSeverity::Warning); - assert!(mismatch_warnings[0].1.contains('2')); - - session.resize(1, 1, 32.1).unwrap(); - assert_eq!(warnings.lock().unwrap().len(), 1); - - session.resize(1, 1, 2.0).unwrap(); - assert_eq!(warnings.lock().unwrap().len(), 2); -} - -#[test] -// Spec coverage: BND-105 and BND-106. -fn feature_state_set_get_and_remove_copy_snapshots() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - let selector = FeatureStateSelector::new("point").with_feature_id("feature-1"); - let state = br#"{"hover":true,"radius":20}"#; - - map.set_feature_state(&selector, state).unwrap(); - let queued: JsonValue = - serde_json::from_slice(&map.get_feature_state(&selector).unwrap()).unwrap(); - assert_json_member(&queued, "hover", &json!(true)); - assert_json_member(&queued, "radius", &json!(20)); - - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(64, 64, 1.0), )); - let _ = session.render_update(); - let before_style: JsonValue = - serde_json::from_slice(&map.get_feature_state(&selector).unwrap()).unwrap(); - assert_json_member(&before_style, "hover", &json!(true)); - assert_json_member(&before_style, "radius", &json!(20)); - + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(64, 64, 1.0)).unwrap(); map.set_style_json(FEATURE_STATE_STYLE_JSON.as_bytes()) .unwrap(); - assert!(wait_for_runtime_event( - &mut runtime, - RuntimeEventType::MapRenderUpdateAvailable - )); + await_runtime_barrier(&runtime); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); - let copied: JsonValue = - serde_json::from_slice(&map.get_feature_state(&selector).unwrap()).unwrap(); - assert_json_member(&copied, "hover", &json!(true)); - assert_json_member(&copied, "radius", &json!(20)); - - let hover_selector = FeatureStateSelector::new("point") - .with_feature_id("feature-1") - .with_state_key("hover") - .unwrap(); - map.remove_feature_state(&hover_selector).unwrap(); - let _ = wait_for_runtime_event(&mut runtime, RuntimeEventType::MapRenderUpdateAvailable); - let _ = session.render_update(); - - let after_remove: JsonValue = - serde_json::from_slice(&map.get_feature_state(&selector).unwrap()).unwrap(); - assert_json_member(&after_remove, "radius", &json!(20)); - assert!(json_member(&after_remove, "hover").is_none()); - - session.resize(64, 64, 2.0).unwrap(); - match session.render_update().unwrap().result { - RenderResult::Rendered => {} - RenderResult::SizePending => { - runtime.pump(Some(Duration::ZERO), None).unwrap(); - assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered - ); - } - other => panic!("unexpected render result after scale-factor resize: {other:?}"), - } - let after_scale: JsonValue = - serde_json::from_slice(&map.get_feature_state(&selector).unwrap()).unwrap(); - assert_json_member(&after_scale, "radius", &json!(20)); - assert!(json_member(&after_scale, "hover").is_none()); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} + let selector = FeatureStateSelector::new("point").with_feature_id("feature-1"); + crate::completion::blocking(map.set_feature_state(&selector, br#"{"hover":true,"count":3}"#)); + let state: JsonValue = serde_json::from_slice(&crate::completion::blocking( + map.get_feature_state(&selector), + )) + .unwrap(); + assert_eq!(state["hover"], true); + assert_eq!(state["count"], 3); -#[test] -// Spec coverage: BND-106. -fn rendered_and_source_queries_copy_results() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); + let deadline = Instant::now() + Duration::from_secs(5); + let features = loop { + let query = session + .query_rendered_features( + &RenderedQueryGeometry::box_(ScreenBox::new( + ScreenPoint::new(0.0, 0.0), + ScreenPoint::new(64.0, 64.0), + )), + None, + ) + .unwrap(); + wait_until_completed(&session, &query); + let features = query.take().unwrap(); + if !features.is_empty() { + break features; + } + assert!( + Instant::now() < deadline, + "rendered feature did not become queryable" + ); + map.request_repaint().unwrap(); + await_runtime_barrier(&runtime); + let _ = render_frame(&session, false); + }; + assert_eq!(features[0].source_id.as_deref(), Some("point")); + assert_eq!(features[0].source_layer_id, None); + assert_eq!(feature_json(&features[0])["id"], "feature-1"); + let rendered_state: JsonValue = + serde_json::from_slice(features[0].state.as_deref().unwrap()).unwrap(); + assert_eq!(rendered_state, json!({"hover": true, "count": 3})); - let error = session - .query_rendered_features( - &RenderedQueryGeometry::point(ScreenPoint::new(32.0, 32.0)), - None, - ) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - - load_query_style(&mut runtime, &map, &session); - let state_selector = FeatureStateSelector::new("point").with_feature_id("feature-1"); - let query_state = br#"{"selected":true}"#; - map.set_feature_state(&state_selector, query_state).unwrap(); - let _ = wait_for_runtime_event(&mut runtime, RuntimeEventType::MapRenderUpdateAvailable); - let _ = session.render_update(); - - let query_point = map - .pixel_for_lat_lng(LatLng::new(37.7749, -122.4194)) - .unwrap(); - let geometry = RenderedQueryGeometry::box_(ScreenBox::new( - ScreenPoint::new(query_point.x - 20.0, query_point.y - 20.0), - ScreenPoint::new(query_point.x + 20.0, query_point.y + 20.0), - )); - let filter = br#"["==",["get","kind"],"capital"]"#; - let mut rendered_options = RenderedFeatureQueryOptions::default(); - rendered_options.layer_ids = Some(vec!["point-circle".into()]); - rendered_options.filter = Some(filter.to_vec()); - let rendered = wait_for_rendered_feature( - &mut runtime, + let source_features = take_features( &session, - &geometry, - &rendered_options, - "rendered point feature", - ); - let rendered_feature: JsonValue = serde_json::from_slice(&rendered.feature).unwrap(); - let rendered_state: JsonValue = - serde_json::from_slice(rendered.state.as_deref().unwrap()).unwrap(); - assert_eq!(rendered.source_id.as_deref(), Some("point")); - assert_eq!(rendered.source_layer_id, None); - assert_eq!(rendered_feature["id"], json!("feature-1")); - assert_point_geometry_close( - &rendered_feature["geometry"], - LatLng::new(37.7749, -122.4194), - ); - assert_eq!( - feature_member(&rendered_feature, "kind"), - Some(&json!("capital")) + session.query_source_features("point", None).unwrap(), ); - assert_eq!(rendered_state, json!({"selected": true})); + assert_eq!(source_features.len(), 1); + assert_eq!(source_features[0].source_id.as_deref(), Some("point")); + assert_eq!(feature_json(&source_features[0])["id"], "feature-1"); - let mut source_options = SourceFeatureQueryOptions::default(); - source_options.filter = Some(filter.to_vec()); - let source = wait_for_source_feature( - &mut runtime, + let oversized = take_features( &session, - "point", - &source_options, - "source point feature", - ); - let source_feature: JsonValue = serde_json::from_slice(&source.feature).unwrap(); - assert_eq!(source.source_id.as_deref(), Some("point")); - assert_eq!(source.source_layer_id, None); - assert_eq!(source_feature["id"], json!("feature-1")); - assert_point_geometry_close(&source_feature["geometry"], LatLng::new(37.7749, -122.4194)); - assert_eq!( - feature_member(&source_feature, "kind"), - Some(&json!("capital")) + session + .query_rendered_features( + &RenderedQueryGeometry::box_(ScreenBox::new( + ScreenPoint::new(-4096.0, -4096.0), + ScreenPoint::new(4096.0, 4096.0), + )), + None, + ) + .unwrap(), ); + assert_eq!(oversized.len(), 1); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-106. -fn rendered_box_queries_clip_to_the_viewport() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - load_query_style(&mut runtime, &map, &session); - let mut options = RenderedFeatureQueryOptions::default(); - options.layer_ids = Some(vec!["point-circle".into()]); - - // A box that over-covers the viewport answers like the viewport itself. - let oversized = RenderedQueryGeometry::box_(ScreenBox::new( - ScreenPoint::new(-4096.0, -4096.0), - ScreenPoint::new(4096.0, 4096.0), - )); - let rendered = wait_for_rendered_feature( - &mut runtime, + let inverted = take_features( &session, - &oversized, - &options, - "over-covering box query", + session + .query_rendered_features( + &RenderedQueryGeometry::box_(ScreenBox::new( + ScreenPoint::new(4096.0, 4096.0), + ScreenPoint::new(-4096.0, -4096.0), + )), + None, + ) + .unwrap(), ); - let rendered_feature: JsonValue = serde_json::from_slice(&rendered.feature).unwrap(); - assert_eq!(rendered_feature["id"], json!("feature-1")); + assert_eq!(inverted.len(), 1); - // Corners in either order describe the same box. - let inverted = RenderedQueryGeometry::box_(ScreenBox::new( - ScreenPoint::new(4096.0, 4096.0), - ScreenPoint::new(-4096.0, -4096.0), - )); - let inverted_features = session - .query_rendered_features(&inverted, Some(&options)) - .unwrap(); - assert_eq!(inverted_features.len(), 1); + let offscreen = take_features( + &session, + session + .query_rendered_features( + &RenderedQueryGeometry::box_(ScreenBox::new( + ScreenPoint::new(512.0, 512.0), + ScreenPoint::new(1024.0, 1024.0), + )), + None, + ) + .unwrap(), + ); + assert!(offscreen.is_empty()); + + // Feature state belongs to the map, so a style replacement copies it into + // the reloaded style instead of dropping it. + crate::completion::blocking(map.set_style_json(FEATURE_STATE_STYLE_JSON.as_bytes())); + let copied: JsonValue = serde_json::from_slice(&crate::completion::blocking( + map.get_feature_state(&selector), + )) + .unwrap(); + assert_eq!(copied["hover"], true); + assert_eq!(copied["count"], 3); - // Clipping keeps a fully off-screen box empty instead of collapsing it onto - // a viewport edge. - let offscreen = RenderedQueryGeometry::box_(ScreenBox::new( - ScreenPoint::new(512.0, 512.0), - ScreenPoint::new(1024.0, 1024.0), - )); - let offscreen_features = session - .query_rendered_features(&offscreen, Some(&options)) + // A selector with a state key removes only that key. + let hover_selector = FeatureStateSelector::new("point") + .with_feature_id("feature-1") + .with_state_key("hover") .unwrap(); - assert!(offscreen_features.is_empty()); + crate::completion::blocking(map.remove_feature_state(&hover_selector)); + let after_remove: JsonValue = serde_json::from_slice(&crate::completion::blocking( + map.get_feature_state(&selector), + )) + .unwrap(); + assert_eq!(after_remove["count"], 3); + assert!(after_remove.get("hover").is_none()); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + crate::completion::blocking(map.remove_feature_state(&selector)); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-106 and BND-107. -fn feature_extension_queries_copy_value_and_feature_collection_results() { +fn cluster_feature_extensions_copy_values_and_feature_collections() { if !has_test_owned_texture_session_backend() { return; } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - load_cluster_style(&mut runtime, &map, &session); - let query_point = map.pixel_for_lat_lng(LatLng::new(0.0, 0.0)).unwrap(); - let geometry = RenderedQueryGeometry::box_(ScreenBox::new( - ScreenPoint::new(query_point.x - 30.0, query_point.y - 30.0), - ScreenPoint::new(query_point.x + 30.0, query_point.y + 30.0), - )); - let mut options = RenderedFeatureQueryOptions::default(); - options.layer_ids = Some(vec!["cluster-circle".into()]); - let cluster = wait_for_rendered_feature( - &mut runtime, - &session, - &geometry, - &options, - "rendered cluster", - ); - - let cluster_feature: JsonValue = serde_json::from_slice(&cluster.feature).unwrap(); - - // Native matches cluster_id by exact JSON value type, so the copied feature - // keeps the unsigned alternative to resolve on the way back in. - assert!(matches!( - feature_member(&cluster_feature, "cluster_id"), - Some(JsonValue::Number(_)) + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(64, 64, 1.0), )); - - // weight_sum comes from the cluster_properties aggregation lowered through - // GeoJsonSourceOptions. - assert_eq!( - numeric_member(&cluster_feature["properties"], "point_count"), - Some(3.0) - ); - assert_eq!( - numeric_member(&cluster_feature["properties"], "weight_sum"), - Some(6.0) - ); - - let children = session - .query_feature_extension( - "cluster-source", - &cluster.feature, - "supercluster", - "children", - None, - ) + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(64, 64, 1.0)).unwrap(); + map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) .unwrap(); - let children: JsonValue = serde_json::from_slice(&children).unwrap(); - assert!(!children["features"].as_array().unwrap().is_empty()); - - // Native ignores arguments of another type and falls back to ten leaves at - // offset zero, so both unsigned bounds must move the observed result. - let first = single_cluster_leaf(&session, &cluster.feature, 0); - let second = single_cluster_leaf(&session, &cluster.feature, 1); - assert_ne!( - feature_member(&first, "name"), - feature_member(&second, "name") - ); - let expansion_zoom = session - .query_feature_extension( - "cluster-source", - &cluster.feature, - "supercluster", - "expansion-zoom", - None, - ) + let data = serde_json::to_vec(&json!({ + "type": "FeatureCollection", + "features": [ + {"type":"Feature","geometry":{"type":"Point","coordinates":[0.0,0.0]},"properties":{"name":"one","weight":1}}, + {"type":"Feature","geometry":{"type":"Point","coordinates":[0.001,0.001]},"properties":{"name":"two","weight":2}}, + {"type":"Feature","geometry":{"type":"Point","coordinates":[0.002,0.002]},"properties":{"name":"three","weight":3}} + ] + })) + .unwrap(); + let mut options = GeoJsonSourceOptions::default(); + options.cluster = Some(true); + options.cluster_radius = Some(60); + options.cluster_min_points = Some(2); + options.cluster_max_zoom = Some(17.0); + options.cluster_properties = + Some(serde_json::to_vec(&json!({"weight_sum":["+",["get","weight"]]})).unwrap()); + let data = crate::GeoJsonSourceDataHandle::new(&data, Some(&options)).unwrap(); + map.add_geojson_source_data("cluster-source", &data) .unwrap(); - let expansion_zoom: JsonValue = serde_json::from_slice(&expansion_zoom).unwrap(); - assert!(expansion_zoom.is_u64()); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -/// Metal blocks a queue that reaches its in-flight command buffer limit, and on -/// Apple targets each frame takes objects an autorelease pool owns until it -/// drains, so a library that leaves draining to the host wedges partway through -/// a long loop. -#[test] -fn sustained_render_loop_outlasts_the_graphics_queue_depth() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), + map.add_style_layer_json( + br##"{"id":"cluster-circle","type":"circle","source":"cluster-source","filter":["has","point_count"],"paint":{"circle-color":"#2563eb","circle-radius":20}}"##, + None, ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - // A background-only style keeps each frame to the passes that take command - // buffers, without the tile work that would make the loop slow. - map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) - .unwrap(); - render_available_updates(&mut runtime, &session, 3); - - // Metal allows 64 command buffers in flight per queue and a frame takes - // several, so this many frames clears that limit many times over. - const TARGET_FRAMES: u32 = 256; - let mut rendered_frames = 0; - let mut step = 0; - while rendered_frames < TARGET_FRAMES && step < 200 { - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(37.0, -122.0)); - camera.zoom = Some(10.0 + f64::from(step % 8) * 0.25); - map.jump_to(&camera).unwrap(); - let _ = runtime.pump(Some(Duration::ZERO), None); - let updates = runtime - .drain_events(0) - .map(|batch| { - batch - .iter() - .filter(|event| { - event.event_type() == RuntimeEventType::MapRenderUpdateAvailable - }) - .count() - }) - .unwrap_or(0); - for _ in 0..updates { - if session.render_update().unwrap().result == RenderResult::Rendered { - rendered_frames += 1; - } - } - step += 1; - } - - assert!( - rendered_frames >= TARGET_FRAMES, - "the loop rendered {rendered_frames} frames before running out of steps" + .unwrap(); + await_runtime_barrier(&runtime); + assert_eq!( + render_frame(&session, false).disposition, + FrameDisposition::Rendered ); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-163 and BND-174. -fn owned_texture_session_enforces_single_session_and_blocks_map_close() { - if !has_test_owned_texture_session_backend() { - return; - } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - let error = context - .attach_owned_texture( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - - let error = map.close().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - assert!(error.diagnostic().contains("render session")); - let map = error.into_handle(); - - drop(runtime); - - let detached = session.detach().unwrap(); - detached.close().unwrap(); - map.close().unwrap(); -} - -#[test] -// Spec coverage: BND-042. The binding's parent retention ends at detach rather -// than at close. -fn detached_session_releases_the_parent_map_retention() { - if !has_test_owned_texture_session_backend() { - return; - } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - let error = map.close().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - let map = error.into_handle(); - - let detached = session.detach().unwrap(); - map.close().unwrap(); - detached.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-165. -fn resize_updates_owned_texture_frame_extent() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let initial_extent = RenderTargetExtent::new(32, 16, 1.0); - let resized_extent = RenderTargetExtent::new(48, 24, 1.0); - let (context, session) = - create_owned_texture_session(&map.attach_ref().unwrap(), initial_extent) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - load_query_style(&mut runtime, &map, &session); - session - .resize( - resized_extent.width, - resized_extent.height, - resized_extent.scale_factor, - ) - .unwrap(); - // A static map renders only on request, and the map applies the new logical - // size on its next pump. Requesting the still image first spends it on an - // update the session's size gate discards, so pump the resize through. - runtime.pump(Some(Duration::ZERO), None).unwrap(); - map.request_still_image().unwrap(); let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - render_pending_updates(&mut runtime, &session); - if context.try_acquire_frame_extent(&session, &resized_extent) { - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); - return; + let cluster = loop { + let queried = take_features( + &session, + session + .query_rendered_features( + &RenderedQueryGeometry::box_(ScreenBox::new( + ScreenPoint::new(0.0, 0.0), + ScreenPoint::new(64.0, 64.0), + )), + None, + ) + .unwrap(), + ); + if let Some(cluster) = queried.into_iter().next() { + break cluster; } - std::thread::sleep(Duration::from_millis(1)); - } - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); - panic!("timed out waiting for resized owned texture frame"); -} - -#[test] -// A resize leaves the map holding an update built for the previous extent, so -// the next render reports a pending size until the map catches up. -fn render_update_reports_size_pending_until_the_map_applies_a_resize() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - load_query_style(&mut runtime, &map, &session); + assert!( + Instant::now() < deadline, + "cluster did not become queryable" + ); + map.request_repaint().unwrap(); + await_runtime_barrier(&runtime); + let _ = render_frame(&session, false); + }; + let feature_value = feature_json(&cluster); + assert_eq!(feature_value["properties"]["point_count"], 3); assert_eq!( - session.render_update().unwrap().result, - RenderResult::Rendered + feature_value["properties"]["weight_sum"].as_f64(), + Some(6.0) ); + let feature = cluster.feature; - session.resize(96, 48, 1.0).unwrap(); - // A pending size is not a rendered frame, so it never asks for a repaint. - let update = session.render_update().unwrap(); - assert_eq!(update.result, RenderResult::SizePending); - assert!(!update.needs_repaint); + let children = take_json( + &session, + session + .query_feature_extension("cluster-source", &feature, "supercluster", "children", None) + .unwrap(), + ); + assert!(!children["features"].as_array().unwrap().is_empty()); - // The map publishes an update for the new size on its own thread, so the - // state resolves without another host request. - let deadline = Instant::now() + Duration::from_secs(5); - let mut update = session.render_update().unwrap(); - while Instant::now() < deadline && update.result != RenderResult::Rendered { - let _ = runtime.pump(Some(Duration::ZERO), None); - let _ = runtime.drain_events(0); - update = session.render_update().unwrap(); - if update.result != RenderResult::Rendered { - std::thread::sleep(Duration::from_millis(2)); - } - } - assert_eq!(update.result, RenderResult::Rendered); + let expansion_zoom = take_json( + &session, + session + .query_feature_extension( + "cluster-source", + &feature, + "supercluster", + "expansion-zoom", + None, + ) + .unwrap(), + ); + assert!(expansion_zoom.is_u64()); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// The map size follows the attached target, while the map pixel ratio stays at -// the creation value even when the target renders at a different scale factor. -fn map_size_follows_attach_and_resize_and_keeps_the_creation_scale_factor() { +fn live_session_blocks_map_close_and_drop_reports_the_leaked_map() { if !has_test_owned_texture_session_backend() { return; } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 32, 2.0)).unwrap(); - assert_eq!(map.size().unwrap(), (64, 32, 2.0)); - - // A session enqueues the map size for the map's owner thread, so the map - // keeps its previous size until the runtime is pumped. - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - assert_eq!(map.size().unwrap(), (64, 32, 2.0)); - runtime.pump(Some(Duration::ZERO), None).unwrap(); - assert_eq!(map.size().unwrap(), (32, 16, 2.0)); - - session.resize(48, 24, 1.0).unwrap(); - runtime.pump(Some(Duration::ZERO), None).unwrap(); - assert_eq!(map.size().unwrap(), (48, 24, 2.0)); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} -#[test] -// Spec coverage: BND-048. -// -// Nothing stops a host from dropping the map while a session is live. The C API -// rejects that destroy, and an infallible `Drop` cannot return the error, so it -// reports the handle instead of discarding it. -fn dropping_a_map_with_an_attached_session_reports_a_leak() { - if !has_test_owned_texture_session_backend() { - return; - } - let leaks: Arc>> = Arc::new(Mutex::new(Vec::new())); + let leaks = Arc::new(Mutex::new(Vec::new())); let sink = Arc::clone(&leaks); - crate::set_leak_reporter(Some(Box::new(move |leak: crate::NativeHandleLeak| { + assert!(!crate::set_leak_reporter(Some(Box::new(move |leak| { sink.lock().unwrap().push(leak); - }))); + })))); let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("owned texture test session should attach when supported"); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 32, 1.0), + )); + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(32, 32, 1.0)).unwrap(); - // The wrong order on purpose: the map goes first, while the session is live. + let close_error = map.close().unwrap_err(); + assert_eq!(close_error.kind(), ErrorKind::InvalidState); + let map = close_error.into_handle(); drop(map); crate::set_leak_reporter(None); let reported = leaks.lock().unwrap().clone(); - assert_eq!(reported.len(), 1, "expected exactly one reported leak"); + assert_eq!(reported.len(), 1); assert_eq!(reported[0].type_name, "mln_map"); assert_ne!(reported[0].id, 0); - // The reported handle makes the leak recoverable: closing the session - // unblocks the destroy that Drop could not complete, and without it the - // runtime below would stay pinned forever. - session.close().unwrap(); - // SAFETY: the handle came from a map whose Rust wrapper was dropped without - // destroying it, and its session is now closed, so this is the one - // outstanding destroy for that map. - let status = unsafe { sys::mln_map_destroy(sys::mln_map(reported[0].id)) }; - assert_eq!(status, sys::MLN_STATUS_OK); - - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-196. Dropping a map destroys it, so the reference names a -// released handle that the C API rejects. -fn dropping_a_map_makes_its_attach_refs_stale() { - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let attach_ref = map.attach_ref().unwrap(); - - drop(map); - - // The replacement reuses the slot the drop freed, so this proves the - // generation check rather than an empty slot. - let replacement = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - - let error = attach_ref - .attach_metal_owned_texture(&MetalOwnedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - MetalContextDescriptor::new(NativePointer::NULL), - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert!(error.to_string().contains("stale")); - - replacement.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-196. A `MapAttachRef` can outlive its `MapHandle`, and the -// generational handle keeps a stale reference from attaching to a later map. -fn a_stale_attach_ref_reports_a_stale_map() { - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let attach_ref = map.attach_ref().unwrap(); - map.close().unwrap(); - - // Resolving the map fails before the descriptor is ever inspected, so this - // needs no render backend. - let error = attach_ref - .attach_metal_owned_texture(&MetalOwnedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - MetalContextDescriptor::new(NativePointer::NULL), - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert!(error.to_string().contains("stale")); - - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-193 and BND-195. -fn a_second_thread_attaches_a_session_and_renders() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - // Continuous mode, so the map publishes render updates without a - // still-image request driving them. - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) - .unwrap(); - let attach_ref = map.attach_ref().unwrap(); - - std::thread::scope(|scope| { - let worker = scope.spawn(move || { - let (_context, session) = - create_owned_texture_session(&attach_ref, RenderTargetExtent::new(64, 64, 1.0)) - .expect("owned texture test session should attach on this thread"); - // The map applies its logical size on its own thread, so the first - // renders report no frame until the other thread has pumped. - let deadline = Instant::now() + Duration::from_secs(5); - let mut rendered = false; - while Instant::now() < deadline && !rendered { - rendered = session.render_update().unwrap().result == RenderResult::Rendered; - if !rendered { - std::thread::sleep(Duration::from_millis(2)); - } - } - assert!(rendered, "worker thread should render the map"); - // The session must be destroyed on the thread that attached it, - // which is what frees the map to close below. - session.close().unwrap(); - }); - let deadline = Instant::now() + Duration::from_secs(10); - while !worker.is_finished() && Instant::now() < deadline { - // A short park rather than zero: spinning would burn the deadline - // before the worker made progress. - runtime.pump(Some(Duration::from_millis(2)), None).unwrap(); - let _ = runtime.drain_events(0).unwrap(); - } - worker.join().expect("worker thread should not panic"); - }); - - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + crate::completion::blocking(crate::completion::submit( + |completion| unsafe { sys::mln_map_release(sys::mln_map(reported[0].id), completion) }, + crate::completion::unit, + )); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-194. -fn session_calls_are_rejected_on_a_foreign_thread() { +fn sustained_frame_demands_outlast_the_texture_ring_depth() { if !has_test_owned_texture_session_backend() { return; } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::new(64, 64, 1.0)).unwrap(); - map.set_style_json(CLUSTER_BASE_STYLE_JSON.as_bytes()) - .unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("owned texture test session should attach when supported"); - - // The handle is `!Send`, so a foreign thread can only reach the session - // through the raw handle the binding wraps. - let native = session.inner.native().unwrap(); - let kind = std::thread::scope(|scope| { - scope - .spawn(move || { - let mut result = maplibre_native_ffi_sys::MLN_RENDER_RESULT_NO_UPDATE; - let mut needs_repaint = false; - // SAFETY: the session is live for the duration of this scope, - // and the call is expected to be rejected before it is used. - let status = unsafe { - maplibre_native_ffi_sys::mln_render_session_render_update( - native, - &mut result, - &mut needs_repaint, - ) - }; - maplibre_core::check(status).unwrap_err().kind() - }) - .join() - .expect("worker thread should not panic") - }); - assert_eq!(kind, crate::ErrorKind::WrongThread); - - // Still bound and usable on the attaching thread. - let _ = session.render_update().unwrap(); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} -#[test] -// Spec coverage: BND-170. -fn acquired_frame_state_rejects_reentrant_session_operations_before_native_calls() { - if !has_test_owned_texture_session_backend() { - return; - } let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - session.inner.frame_acquired.set(true); - - let detach_error = session.detach().unwrap_err(); - assert_eq!(detach_error.kind(), ErrorKind::InvalidState); - assert!(detach_error.diagnostic().contains("acquired texture frame")); - let session = detach_error.into_handle(); - - for error in [ - session.resize(32, 16, 1.0).unwrap_err(), - session.render_update().unwrap_err(), - session - .query_rendered_features( - &RenderedQueryGeometry::point(ScreenPoint::new(0.0, 0.0)), - None, - ) - .unwrap_err(), - session.query_source_features("point", None).unwrap_err(), - session - .query_feature_extension( - "point", - br#"{"type":"Feature","geometry":null,"properties":{}}"#, - "x", - "y", - None, - ) - .unwrap_err(), - session.read_premultiplied_rgba8_into(&mut []).unwrap_err(), - session.acquire_metal_owned_texture_frame().unwrap_err(), - session.acquire_vulkan_owned_texture_frame().unwrap_err(), - session.acquire_opengl_owned_texture_frame().unwrap_err(), - ] { - assert_eq!(error.kind(), ErrorKind::InvalidState); - assert!(error.diagnostic().contains("acquired texture frame")); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 32, 1.0), + )); + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(32, 32, 1.0)).unwrap(); + map.set_style_json(QUERY_STYLE_JSON.as_bytes()).unwrap(); + await_runtime_barrier(&runtime); + + for _ in 0..64 { + map.request_repaint().unwrap(); + await_runtime_barrier(&runtime); + assert_eq!( + render_frame(&session, false).disposition, + FrameDisposition::Rendered + ); } + assert!(session.snapshot().unwrap().frame_generation >= 64); - let error = session.close().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - assert!(error.diagnostic().contains("acquired texture frame")); - let session = error.into_handle(); - - session.inner.frame_acquired.set(false); - session.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-164. -fn render_update_without_pending_update_reports_no_update_and_keeps_session_live() { +fn cloned_session_controls_can_be_used_from_another_thread() { if !has_test_owned_texture_session_backend() { return; } - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - assert_eq!( - session.render_update().unwrap().result, - RenderResult::NoUpdate - ); - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Rust regression: a readback that fails before native has produced a readable -// frame must leave the caller's buffer bytes untouched. -fn texture_readback_without_rendered_frame_maps_native_invalid_state() { - if !has_test_owned_texture_session_backend() { - return; - } let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - let _ = session.render_update(); - let mut undersized = [0x7f]; - let error = session - .read_premultiplied_rgba8_into(&mut undersized) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidState); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_STATE)); - assert_eq!(undersized, [0x7f]); - - session.close().unwrap(); -} - -#[test] -// Spec coverage: BND-166. -fn texture_readback_copies_metadata_and_fills_reusable_buffers_when_supported() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(32, 16, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - - load_query_style(&mut runtime, &map, &session); - map.request_still_image().unwrap(); - let mut info = None; - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - render_pending_updates(&mut runtime, &session); - match session.texture_image_info() { - Ok(copied) => { - info = Some(copied); - break; - } - Err(error) if error.kind() == ErrorKind::InvalidState => {} - Err(error) => panic!("unexpected readback metadata error: {error:?}"), - } - std::thread::sleep(Duration::from_millis(1)); - } - let info = info.expect("timed out waiting for readback metadata"); - assert_eq!((info.width, info.height), (32, 16)); - assert!(info.byte_length > 0); - - let mut undersized = vec![0x7f; info.byte_length - 1]; - let error = session - .read_premultiplied_rgba8_into(&mut undersized) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::InvalidArgument); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT)); - assert!(undersized.iter().all(|byte| *byte == 0x7f)); - - // An empty destination is a caller error here, not the C size probe. - let empty_error = session.read_premultiplied_rgba8_into(&mut []).unwrap_err(); - assert_eq!(empty_error.kind(), ErrorKind::InvalidArgument); - - let mut reusable = vec![0; info.byte_length]; - let copied_info = session - .read_premultiplied_rgba8_into(&mut reusable) - .unwrap(); - assert_eq!(copied_info, info); - assert_eq!(reusable.len(), info.byte_length); - - // What comes back is the frame that was drawn, not a buffer of the right - // shape. This renders on rather than reading once: a generation counts as - // rendered from the first frame that completes for it, which can be before - // the style has anything to paint, so the first readable frame is blank on - // every backend. - let content_deadline = Instant::now() + Duration::from_secs(5); - let mut rendered_the_style = false; - while Instant::now() < content_deadline && !rendered_the_style { - let _ = runtime.pump(Some(Duration::ZERO), None); - let _ = runtime.drain_events(0); - let _ = session.render_update(); - session - .read_premultiplied_rgba8_into(&mut reusable) - .unwrap(); - rendered_the_style = reusable - .chunks_exact(4) - .any(|pixel| pixel == QUERY_STYLE_BACKGROUND_RGBA); - std::thread::sleep(Duration::from_millis(5)); - } - assert!( - rendered_the_style, - "readback should return the frame this session rendered" - ); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-160 and BND-162. -fn backend_specific_attach_calls_report_native_statuses() { - let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - - let metal_error = map - .attach_ref() - .unwrap() - .attach_metal_owned_texture(&MetalOwnedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - MetalContextDescriptor::new(NativePointer::NULL), - )) - .unwrap_err(); - assert!(matches!( - metal_error.kind(), - ErrorKind::InvalidArgument | ErrorKind::Unsupported - )); - - let vulkan_error = map - .attach_ref() - .unwrap() - .attach_vulkan_surface(&VulkanSurfaceDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - VulkanContextDescriptor::new( - NativePointer::NULL, - NativePointer::NULL, - NativePointer::NULL, - NativePointer::NULL, - 0, - ), - NativePointer::NULL, - )) - .unwrap_err(); - assert!(matches!( - vulkan_error.kind(), - ErrorKind::InvalidArgument | ErrorKind::Unsupported - )); - - let opengl_context = OpenGLContextDescriptor::Wgl(WglContextDescriptor::new( - NativePointer::NULL, - NativePointer::NULL, - )); - let opengl_error = map - .attach_ref() - .unwrap() - .attach_opengl_surface(&OpenGLSurfaceDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - opengl_context.clone(), - NativePointer::NULL, - )) - .unwrap_err(); - assert!(matches!( - opengl_error.kind(), - ErrorKind::InvalidArgument | ErrorKind::Unsupported - )); - - let opengl_error = map - .attach_ref() - .unwrap() - .attach_opengl_owned_texture(&OpenGLOwnedTextureDescriptor::new( - RenderTargetExtent::new(0, 16, 1.0), - opengl_context.clone(), - )) - .unwrap_err(); - assert_eq!(opengl_error.kind(), ErrorKind::InvalidArgument); - - let opengl_error = map - .attach_ref() - .unwrap() - .attach_opengl_borrowed_texture(&OpenGLBorrowedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - 32, - 16, - opengl_context, - 0, - 0x0de1, - )) - .unwrap_err(); - assert!(matches!( - opengl_error.kind(), - ErrorKind::InvalidArgument | ErrorKind::Unsupported + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(32, 32, 1.0), )); + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(32, 32, 1.0)).unwrap(); + let clone = session.clone(); + let snapshot = std::thread::spawn(move || clone.snapshot().unwrap()) + .join() + .unwrap(); + assert_eq!(snapshot.state, RenderSessionLifecycle::Attached); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] -// Spec coverage: BND-160. -fn opengl_attach_calls_report_unsupported_when_backend_unavailable() { - if crate::supported_render_backends().contains(RenderBackendMask::OPENGL) { +fn texture_readback_before_a_frame_reports_invalid_state() { + if !has_test_owned_texture_session_backend() { return; } let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &static_map_options(64, 64, 1.0)).unwrap(); - // SAFETY: Test uses dummy opaque addresses and never dereferences them. - let fake = unsafe { NativePointer::from_address(1) }; - let opengl_context = OpenGLContextDescriptor::Wgl(WglContextDescriptor::new(fake, fake)); - - let error = map - .attach_ref() - .unwrap() - .attach_opengl_owned_texture(&OpenGLOwnedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - opengl_context.clone(), - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_UNSUPPORTED)); - - let error = map - .attach_ref() - .unwrap() - .attach_opengl_borrowed_texture(&OpenGLBorrowedTextureDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - 32, - 16, - opengl_context.clone(), - 1, - gl_api::TEXTURE_2D, - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_UNSUPPORTED)); - - let error = map - .attach_ref() - .unwrap() - .attach_opengl_surface(&OpenGLSurfaceDescriptor::new( - RenderTargetExtent::new(32, 16, 1.0), - opengl_context, - fake, - )) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_UNSUPPORTED)); - - map.close().unwrap(); - runtime.close().unwrap(); -} - -#[test] -// Spec coverage: BND-102. Rendered frames advance a camera transition, so only -// a session-backed map can run an ease to completion. -fn identified_camera_transition_reports_its_end_once_when_it_runs_to_completion() { - if !has_test_owned_texture_session_backend() { - return; - } - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let mut options = MapOptions::new(64, 64, 1.0); - options.mode = MapMode::Continuous; - let map = MapHandle::with_options(&runtime, &options).unwrap(); - let (_context, session) = create_owned_texture_session( - &map.attach_ref().unwrap(), - RenderTargetExtent::new(64, 64, 1.0), - ) - .expect("Metal or Vulkan owned texture test session should attach when supported"); - load_query_style(&mut runtime, &map, &session); - - let mut animation = AnimationOptions::default(); - animation.transition_id = Some(31); - animation.duration_ms = Some(200.0); - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(37.0, -122.0)); - camera.zoom = Some(12.0); - map.ease_to(&camera, Some(&animation)).unwrap(); - - // Keep rendering for a while after the transition ends so a repeated - // report would show up in the tally. - let mut finished_transition_ids = Vec::new(); - let mut saw_animated_did_change = false; - let mut finished_at: Option = None; - let deadline = Instant::now() + Duration::from_secs(10); - while Instant::now() < deadline { - let _ = runtime.pump(Some(Duration::ZERO), None); - let mut updates = 0; - for event in runtime.drain_events(0).unwrap().iter() { - match event.event_type() { - RuntimeEventType::MapCameraTransitionFinished => { - let RuntimeEventPayload::CameraTransitionFinished(payload) = event.payload() - else { - panic!("transition-finished event should carry its typed payload"); - }; - finished_transition_ids.push(payload.transition_id); - finished_at.get_or_insert_with(Instant::now); - } - RuntimeEventType::MapCameraDidChange => { - saw_animated_did_change |= CameraChangeMode::from_raw(event.code() as u32) - == CameraChangeMode::Animated; - } - RuntimeEventType::MapRenderUpdateAvailable => updates += 1, - _ => {} - } - } - for _ in 0..updates { - let _ = session.render_update(); - } - if finished_at.is_some_and(|at| at.elapsed() >= Duration::from_millis(500)) { - break; - } - std::thread::sleep(Duration::from_millis(5)); + let map = crate::completion::blocking(MapHandle::with_options( + &runtime, + &MapOptions::new(16, 16, 1.0), + )); + let (_context, session) = + create_owned_texture_session(&map, RenderTargetExtent::new(16, 16, 1.0)).unwrap(); + if session.capabilities().unwrap().readback { + let operation = session.read_premultiplied_rgba8().unwrap(); + while !operation.is_completed().unwrap() { + let _ = session.service_driver_work(64); + std::thread::yield_now(); + } + assert_eq!( + operation.terminal_status().unwrap(), + sys::MLN_STATUS_INVALID_STATE + ); } - assert_eq!(finished_transition_ids, vec![31]); - assert!(saw_animated_did_change); - // Reaching the requested camera shows the transition ran to completion - // rather than being superseded or cancelled. - let settled = map.camera().unwrap(); - assert!((settled.zoom.unwrap() - 12.0).abs() < 1e-6); - - session.close().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + close_session(session); + map.close_and_wait(); + runtime.close_and_wait(); } diff --git a/bindings/rust/crates/maplibre-native-ffi/src/render/tests/webgl_gl.rs b/bindings/rust/crates/maplibre-native-ffi/src/render/tests/webgl_gl.rs index 0f52a1b16..613334602 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/render/tests/webgl_gl.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/render/tests/webgl_gl.rs @@ -2,9 +2,9 @@ //! //! glow supports Emscripten in its source, but its manifest declares js-sys and //! wasm-bindgen for every `wasm32` target, and those do not build there. The -//! fixtures need ten entry points, which Emscripten links into the module -//! directly, so this binds them under the names and shapes glow uses and the -//! call sites stay identical on every target. The surface readback the Windows +//! fixtures need a small set of entry points, which Emscripten links into the +//! module directly. This module preserves the names and shapes that the shared +//! fixture call sites use on every target. The surface readback the Windows //! fixture performs has no browser counterpart and is absent here. #![allow(non_snake_case)] diff --git a/bindings/rust/crates/maplibre-native-ffi/src/resource.rs b/bindings/rust/crates/maplibre-native-ffi/src/resource.rs index b33b3d7c5..9366ee9fe 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/resource.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/resource.rs @@ -114,10 +114,12 @@ impl ResourceProviderState { } pub(crate) fn descriptor(&self) -> sys::mln_resource_provider { - maplibre_core::resource::resource_provider_descriptor( + let mut descriptor = maplibre_core::resource::resource_provider_descriptor( Some(resource_provider_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_resource_provider_state); + descriptor } fn invoke( @@ -148,6 +150,13 @@ impl ResourceProviderState { } } +unsafe extern "C" fn release_resource_provider_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn resource_provider_trampoline( user_data: *mut c_void, request: *const sys::mln_resource_request, @@ -187,10 +196,12 @@ impl ResourceTransformState { } pub(crate) fn descriptor(&self) -> sys::mln_resource_transform { - maplibre_core::resource::resource_transform_descriptor( + let mut descriptor = maplibre_core::resource::resource_transform_descriptor( Some(resource_transform_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_resource_transform_state); + descriptor } fn invoke( @@ -240,6 +251,13 @@ impl ResourceTransformState { } } +unsafe extern "C" fn release_resource_transform_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn resource_transform_trampoline( user_data: *mut c_void, kind: u32, @@ -280,10 +298,12 @@ impl HttpHeaderTransformState { } pub(crate) fn descriptor(&self) -> sys::mln_http_header_transform { - maplibre_core::resource::http_header_transform_descriptor( + let mut descriptor = maplibre_core::resource::http_header_transform_descriptor( Some(http_header_transform_trampoline), ptr::from_ref(self).cast_mut().cast::(), - ) + ); + descriptor.release_user_data = Some(release_http_header_transform_state); + descriptor } fn invoke( @@ -338,6 +358,13 @@ impl HttpHeaderTransformState { } } +unsafe extern "C" fn release_http_header_transform_state(user_data: *mut c_void) { + if !user_data.is_null() { + // SAFETY: successful registration transfers this Box to native exactly once. + drop(unsafe { Box::from_raw(user_data.cast::()) }); + } +} + unsafe extern "C" fn http_header_transform_trampoline( user_data: *mut c_void, kind: u32, diff --git a/bindings/rust/crates/maplibre-native-ffi/src/runtime.rs b/bindings/rust/crates/maplibre-native-ffi/src/runtime.rs index 0fbf7190a..7565619fc 100644 --- a/bindings/rust/crates/maplibre-native-ffi/src/runtime.rs +++ b/bindings/rust/crates/maplibre-native-ffi/src/runtime.rs @@ -1,20 +1,15 @@ -use std::cell::{Cell, RefCell}; use std::fmt; -use std::marker::PhantomData; -use std::rc::Rc; -use std::time::Duration; +use std::sync::Arc; use maplibre_core::AmbientCacheOperation; use maplibre_native_ffi_core as maplibre_core; use maplibre_native_ffi_sys as sys; +use crate::completion::{self, CommandCompletion, NativeFuture}; use crate::events::{OfflineRegionDownloadState, OfflineRegionStatus, RuntimeEventBatch}; -use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle}; +use crate::handle::{ConcurrentNativeHandle, closed_handle_error, out_handle}; use crate::resource::{HttpHeaderTransformState, ResourceProviderState, ResourceTransformState}; -use crate::{ - Error, ErrorKind, HandleOperationError, OfflineOperationTakeError, ResourceProviderDecision, - Result, RuntimeEventMask, -}; +use crate::{HandleOperationError, ResourceProviderDecision, Result, RuntimeEventMask}; #[cfg(test)] use crate::{LatLngBounds, MapHandle, MapOptions}; @@ -25,25 +20,14 @@ pub(crate) use maplibre_core::runtime::{ #[derive(Debug)] pub(crate) struct RuntimeState { - handle: ThreadAffineNativeHandle, - resource_transform: RefCell>>, - http_header_transform: RefCell>>, - resource_provider: RefCell>>, + handle: ConcurrentNativeHandle, } - impl RuntimeState { fn new(native: sys::mln_runtime) -> Result { - // SAFETY: native came from successful mln_runtime_create and is paired - // with the matching runtime destroy function. - let handle = unsafe { - ThreadAffineNativeHandle::from_handle(native, sys::mln_runtime_destroy, "mln_runtime") - }?; - Ok(Self { - handle, - resource_transform: RefCell::new(None), - http_header_transform: RefCell::new(None), - resource_provider: RefCell::new(None), - }) + // SAFETY: native came from a successful typed creation take and its + // registry/control state supports calls from any thread. + let handle = unsafe { ConcurrentNativeHandle::from_handle(native, "mln_runtime") }?; + Ok(Self { handle }) } pub(crate) fn native(&self) -> Result { @@ -56,15 +40,21 @@ impl RuntimeState { self.handle.is_closed() } - fn close(&self) -> Result<()> { - self.handle.close()?; - self.resource_transform.borrow_mut().take(); - self.http_header_transform.borrow_mut().take(); - self.resource_provider.borrow_mut().take(); - Ok(()) + fn close(&self) -> Result> { + self.handle + .close_with(|runtime| { + // SAFETY: runtime is live and native consumes it only on + // success. A rejected release leaves the completion state + // owned by `submit`. + completion::submit( + |completion| unsafe { sys::mln_runtime_release(runtime, completion) }, + completion::unit, + ) + }) + .map(|teardown| teardown.unwrap_or_else(|| completion::ready(()))) } - fn set_resource_provider(&self, callback: F) -> Result<()> + fn set_resource_provider(&self, callback: F) -> Result> where F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision + Send @@ -82,7 +72,7 @@ impl RuntimeState { fn set_resource_provider_with_rejected_descriptor_for_testing( &self, callback: F, - ) -> Result<()> + ) -> Result> where F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision + Send @@ -99,84 +89,105 @@ impl RuntimeState { &self, replacement: Box, descriptor: sys::mln_resource_provider, - ) -> Result<()> { + ) -> Result> { let runtime = self.native()?; - - // SAFETY: runtime is live and descriptor's user_data points to the boxed - // replacement, kept alive on success. Native retires the previous - // provider before returning, so the state dropped below is unreachable. - maplibre_core::check(unsafe { - sys::mln_runtime_set_resource_provider(runtime, &descriptor) - })?; - self.resource_provider.borrow_mut().replace(replacement); - Ok(()) + let replacement = Box::into_raw(replacement); + // SAFETY: descriptor points to replacement, transferred before native can accept and + // release it. A rejected registration leaves ownership with this binding. + let result = completion::submit( + |completion| unsafe { + sys::mln_runtime_set_resource_provider(runtime, &descriptor, completion) + }, + completion::command, + ); + if result.is_err() { + // SAFETY: native retains no callback state after rejected submission. + drop(unsafe { Box::from_raw(replacement) }); + } + result } - fn clear_resource_provider(&self) -> Result<()> { + fn clear_resource_provider(&self) -> Result> { let runtime = self.native()?; - - // SAFETY: runtime is live. Native clear waits for in-flight provider - // callbacks before returning, so dropping Rust callback state below is safe. - maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_provider(runtime) })?; - self.resource_provider.borrow_mut().take(); - Ok(()) + completion::submit( + |completion| unsafe { sys::mln_runtime_clear_resource_provider(runtime, completion) }, + completion::command, + ) } - fn set_resource_transform(&self, callback: F) -> Result<()> + fn set_resource_transform(&self, callback: F) -> Result> where F: Fn(crate::ResourceTransformRequest) -> Option + Send + Sync + 'static, { let runtime = self.native()?; let replacement = ResourceTransformState::new(callback); let descriptor = replacement.descriptor(); - - // SAFETY: runtime is live and descriptor's user_data points to - // replacement, kept alive on success. On failure native preserves the - // previous transform and replacement is dropped below. - maplibre_core::check(unsafe { - sys::mln_runtime_set_resource_transform(runtime, &descriptor) - })?; - self.resource_transform.borrow_mut().replace(replacement); - Ok(()) + let replacement = Box::into_raw(replacement); + let result = completion::submit( + |completion| unsafe { + sys::mln_runtime_set_resource_transform(runtime, &descriptor, completion) + }, + completion::command, + ); + if result.is_err() { + drop(unsafe { Box::from_raw(replacement) }); + } + result } - fn clear_resource_transform(&self) -> Result<()> { + fn clear_resource_transform(&self) -> Result> { let runtime = self.native()?; - - // SAFETY: runtime is live. Native clear waits for in-flight transform - // callbacks before returning, so dropping Rust callback state below is safe. - maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_transform(runtime) })?; - self.resource_transform.borrow_mut().take(); - Ok(()) + completion::submit( + |completion| unsafe { sys::mln_runtime_clear_resource_transform(runtime, completion) }, + completion::command, + ) } - fn set_http_header_transform(&self, callback: F) -> Result<()> + fn set_http_header_transform(&self, callback: F) -> Result> where F: Fn(crate::HttpHeaderTransformRequest) -> Vec + Send + Sync + 'static, { let runtime = self.native()?; let replacement = HttpHeaderTransformState::new(callback); let descriptor = replacement.descriptor(); - // SAFETY: descriptor retains replacement through native registration. - maplibre_core::check(unsafe { - sys::mln_runtime_set_http_header_transform(runtime, &descriptor) - })?; - self.http_header_transform.borrow_mut().replace(replacement); - Ok(()) + let replacement = Box::into_raw(replacement); + let result = completion::submit( + |completion| unsafe { + sys::mln_runtime_set_http_header_transform(runtime, &descriptor, completion) + }, + completion::command, + ); + if result.is_err() { + drop(unsafe { Box::from_raw(replacement) }); + } + result } - fn clear_http_header_transform(&self) -> Result<()> { + fn clear_http_header_transform(&self) -> Result> { let runtime = self.native()?; - // SAFETY: native waits for in-flight callbacks before returning. - maplibre_core::check(unsafe { sys::mln_runtime_clear_http_header_transform(runtime) })?; - self.http_header_transform.borrow_mut().take(); - Ok(()) + completion::submit( + |completion| unsafe { + sys::mln_runtime_clear_http_header_transform(runtime, completion) + }, + completion::command, + ) + } +} + +impl Drop for RuntimeState { + fn drop(&mut self) { + // Dropping the teardown future discards its completion rather than + // blocking here. Native teardown still runs to the end; a host that + // must observe it calls RuntimeHandle::close and awaits the future. + if self.close().is_err() { + self.handle.leak_for_report(); + } } } -/// Owner-thread runtime handle for MapLibre Native work and event draining. +/// Any-thread runtime handle backed by a core-owned worker. pub struct RuntimeHandle { - pub(crate) inner: Rc, + pub(crate) inner: Arc, } impl fmt::Debug for RuntimeHandle { @@ -187,251 +198,6 @@ impl fmt::Debug for RuntimeHandle { } } -/// Owner-thread offline database operation token that must be taken or discarded. -pub struct OfflineOperationHandle { - runtime: Rc, - operation_id: sys::mln_offline_operation_id, - operation_kind: maplibre_core::OfflineOperationKind, - result_kind: maplibre_core::OfflineOperationResultKind, - live: Cell, - _result: PhantomData T>, - _thread_affine: PhantomData>, -} - -impl fmt::Debug for OfflineOperationHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("OfflineOperationHandle") - .field("operation_id", &self.operation_id) - .field("operation_kind", &self.operation_kind) - .field("result_kind", &self.result_kind) - .field("live", &self.live.get()) - .finish() - } -} - -impl OfflineOperationHandle { - fn new( - runtime: Rc, - operation_id: sys::mln_offline_operation_id, - operation_kind: maplibre_core::OfflineOperationKind, - result_kind: maplibre_core::OfflineOperationResultKind, - ) -> Result { - if operation_id == 0 { - return Err(Error::invalid_argument( - "offline operation id must not be zero", - )); - } - Ok(Self { - runtime, - operation_id, - operation_kind, - result_kind, - live: Cell::new(true), - _result: PhantomData, - _thread_affine: PhantomData, - }) - } - - fn runtime_ptr(&self) -> Result { - if !self.live.get() { - return Err(closed_handle_error("OfflineOperationHandle")); - } - self.runtime.native() - } - - /// Retires this handle after native took or discarded the operation, which - /// also drops the operation's undrained completion event from the native - /// queue. - fn mark_consumed(&self) { - self.live.set(false); - } - - /// Discards runtime-owned state for this offline operation. - #[allow(clippy::result_large_err)] - pub fn discard(self) -> std::result::Result<(), HandleOperationError> { - if !self.live.get() { - return Ok(()); - } - let runtime = match self.runtime_ptr() { - Ok(runtime) => runtime, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - let status = - unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) }; - if let Err(error) = maplibre_core::check(status) { - return Err(HandleOperationError::new(error, self)); - } - self.mark_consumed(); - Ok(()) - } -} - -impl Drop for OfflineOperationHandle { - fn drop(&mut self) { - if !self.live.get() { - return; - } - if let Ok(runtime) = self.runtime.native() { - // SAFETY: Safe Rust keeps this !Send/!Sync handle on the runtime owner thread. - let status = - unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) }; - if status == sys::MLN_STATUS_OK { - self.mark_consumed(); - } - } - } -} - -impl OfflineOperationHandle { - /// Takes a completed create/update operation result as copied region info. - #[allow(clippy::result_large_err)] - pub fn take(self) -> std::result::Result> { - let runtime = match self.runtime_ptr() { - Ok(runtime) => runtime, - Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)), - }; - let mut out = maplibre_core::ptr::OutHandle::::new(); - let status = match self.operation_kind { - maplibre_core::OfflineOperationKind::RegionCreate => unsafe { - sys::mln_runtime_offline_region_create_take_result( - runtime, - self.operation_id, - out.as_mut_ptr(), - ) - }, - maplibre_core::OfflineOperationKind::RegionUpdateMetadata => unsafe { - sys::mln_runtime_offline_region_update_metadata_take_result( - runtime, - self.operation_id, - out.as_mut_ptr(), - ) - }, - _ => sys::MLN_STATUS_INVALID_STATE, - }; - if let Err(error) = maplibre_core::check(status) { - return Err(OfflineOperationTakeError::retryable(error, self)); - } - self.mark_consumed(); - let snapshot = match out.into_live("mln_offline_region_snapshot") { - Ok(snapshot) => snapshot, - Err(error) => return Err(OfflineOperationTakeError::consumed(error)), - }; - // SAFETY: On success, the C API returns an owned snapshot handle; - // core copies and releases it. - unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) } - .map_err(OfflineOperationTakeError::consumed) - } -} - -impl OfflineOperationHandle> { - /// Takes a completed get operation result as optional copied region info. - #[allow(clippy::result_large_err)] - pub fn take( - self, - ) -> std::result::Result, OfflineOperationTakeError> { - let runtime = match self.runtime_ptr() { - Ok(runtime) => runtime, - Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)), - }; - let mut out = maplibre_core::ptr::OutHandle::::new(); - let mut found = false; - let status = unsafe { - sys::mln_runtime_offline_region_get_take_result( - runtime, - self.operation_id, - out.as_mut_ptr(), - &mut found, - ) - }; - if let Err(error) = maplibre_core::check(status) { - return Err(OfflineOperationTakeError::retryable(error, self)); - } - self.mark_consumed(); - if !found { - return Ok(None); - } - let snapshot = match out.into_live("mln_offline_region_snapshot") { - Ok(snapshot) => snapshot, - Err(error) => return Err(OfflineOperationTakeError::consumed(error)), - }; - // SAFETY: When found is true, the C API returns an owned snapshot - // handle; core copies and releases it. - Ok(Some( - unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) } - .map_err(OfflineOperationTakeError::consumed)?, - )) - } -} - -impl OfflineOperationHandle> { - /// Takes a completed list/merge operation result as copied region info. - #[allow(clippy::result_large_err)] - pub fn take( - self, - ) -> std::result::Result, OfflineOperationTakeError> { - let runtime = match self.runtime_ptr() { - Ok(runtime) => runtime, - Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)), - }; - let mut out = maplibre_core::ptr::OutHandle::::new(); - let status = match self.operation_kind { - maplibre_core::OfflineOperationKind::RegionsList => unsafe { - sys::mln_runtime_offline_regions_list_take_result( - runtime, - self.operation_id, - out.as_mut_ptr(), - ) - }, - maplibre_core::OfflineOperationKind::RegionsMergeDatabase => unsafe { - sys::mln_runtime_offline_regions_merge_database_take_result( - runtime, - self.operation_id, - out.as_mut_ptr(), - ) - }, - _ => sys::MLN_STATUS_INVALID_STATE, - }; - if let Err(error) = maplibre_core::check(status) { - return Err(OfflineOperationTakeError::retryable(error, self)); - } - self.mark_consumed(); - let list = match out.into_live("mln_offline_region_list") { - Ok(list) => list, - Err(error) => return Err(OfflineOperationTakeError::consumed(error)), - }; - // SAFETY: On success, the C API returns an owned list handle; core - // copies and releases it. - unsafe { maplibre_core::runtime::copy_offline_region_list(list) } - .map_err(OfflineOperationTakeError::consumed) - } -} - -impl OfflineOperationHandle { - /// Takes a completed status operation result as copied status data. - #[allow(clippy::result_large_err)] - pub fn take(self) -> std::result::Result> { - let runtime = match self.runtime_ptr() { - Ok(runtime) => runtime, - Err(error) => return Err(HandleOperationError::new(error, self)), - }; - let mut raw = maplibre_core::events::empty_offline_region_status_native(); - let status = unsafe { - sys::mln_runtime_offline_region_get_status_take_result( - runtime, - self.operation_id, - &mut raw, - ) - }; - if let Err(error) = maplibre_core::check(status) { - return Err(HandleOperationError::new(error, self)); - } - self.mark_consumed(); - Ok(maplibre_core::events::offline_region_status_from_native( - raw, - )) - } -} - impl RuntimeHandle { /// Creates a runtime on the current thread using explicit options. pub fn with_options(options: &RuntimeOptions) -> Result { @@ -453,16 +219,20 @@ impl RuntimeHandle { fn create_with_native_options_after_abi_validation( options: *const sys::mln_runtime_options, ) -> Result { + // SAFETY: A nonnull options pointer comes from the binding's + // materialized native options and remains readable for this call. + let raw_options = if options.is_null() { + unsafe { sys::mln_runtime_options_default() } + } else { + unsafe { *options } + }; let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: options is either null to request native defaults or points to - // a materialized mln_runtime_options value whose backing strings live - // for this call. out is a valid null-initialized out-pointer owned by - // this call. - maplibre_core::check(unsafe { sys::mln_runtime_create(options, out.as_mut_ptr()) })?; - let ptr = out_handle(out, "mln_runtime")?; - - Ok(Self { - inner: Rc::new(RuntimeState::new(ptr)?), + // SAFETY: raw_options remains readable and out is null writable storage. + maplibre_core::check(unsafe { sys::mln_runtime_create(&raw_options, out.as_mut_ptr()) })?; + out_handle(out, "mln_runtime").and_then(|ptr| { + Ok(Self { + inner: Arc::new(RuntimeState::new(ptr)?), + }) }) } @@ -473,10 +243,11 @@ impl RuntimeHandle { /// let native networking handle the request, or `Handle` to complete or /// release the provided `ResourceRequestHandle` inline or later. /// - /// A successful replacement retires the previous provider before returning - /// and then releases its Rust state. Handles the previous provider already - /// took stay valid; complete and release each one as usual. - pub fn set_resource_provider(&self, callback: F) -> Result<()> + /// A committed replacement retires the previous provider after its + /// in-flight callbacks return and then releases its Rust state. Handles the + /// previous provider already took stay valid; complete and release each one + /// as usual. + pub fn set_resource_provider(&self, callback: F) -> Result> where F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision + Send @@ -487,10 +258,10 @@ impl RuntimeHandle { } /// Clears the runtime-scoped network resource provider, sending later - /// requests to MapLibre's online file source. The clear waits for in-flight - /// provider callbacks before returning and then releases the Rust callback - /// state. Handles the provider already took stay valid. - pub fn clear_resource_provider(&self) -> Result<()> { + /// requests to MapLibre's online file source. The command commits after + /// in-flight provider callbacks return and releases the Rust callback state. + /// Handles the provider already took stay valid. + pub fn clear_resource_provider(&self) -> Result> { self.inner.clear_resource_provider() } @@ -499,17 +270,17 @@ impl RuntimeHandle { /// Native may invoke the closure from worker or network threads, so keep it /// quick and call no MapLibre Native APIs from it. `Some(url)` replaces the /// request URL; `None`, an empty string, or a panic keeps the original. - pub fn set_resource_transform(&self, callback: F) -> Result<()> + pub fn set_resource_transform(&self, callback: F) -> Result> where F: Fn(crate::ResourceTransformRequest) -> Option + Send + Sync + 'static, { self.inner.set_resource_transform(callback) } - /// Clears the runtime-scoped network URL transform. The clear waits for - /// in-flight transform callbacks before returning and then releases the - /// Rust callback state. - pub fn clear_resource_transform(&self) -> Result<()> { + /// Clears the runtime-scoped network URL transform. The command commits + /// after in-flight transform callbacks return and releases the Rust callback + /// state. + pub fn clear_resource_transform(&self) -> Result> { self.inner.clear_resource_transform() } @@ -519,7 +290,10 @@ impl RuntimeHandle { /// after URL transformation. Returned headers are copied before the closure /// returns. Panics, duplicate names, and invalid headers leave the request /// unchanged. - pub fn set_http_header_transform(&self, callback: F) -> Result<()> + pub fn set_http_header_transform( + &self, + callback: F, + ) -> Result> where F: Fn(crate::HttpHeaderTransformRequest) -> Vec + Send + Sync + 'static, { @@ -527,42 +301,25 @@ impl RuntimeHandle { } /// Clears the runtime-scoped outgoing HTTP header transform. - pub fn clear_http_header_transform(&self) -> Result<()> { + pub fn clear_http_header_transform(&self) -> Result> { self.inner.clear_http_header_transform() } - fn start_operation( - &self, - operation_id: sys::mln_offline_operation_id, - operation_kind: maplibre_core::OfflineOperationKind, - result_kind: maplibre_core::OfflineOperationResultKind, - ) -> Result> { - OfflineOperationHandle::new( - Rc::clone(&self.inner), - operation_id, - operation_kind, - result_kind, - ) - } - /// Starts an ambient cache maintenance operation for this runtime. - pub fn start_ambient_cache_operation( + pub fn ambient_cache_operation( &self, - operation: AmbientCacheOperation, - ) -> Result> { + ambient_operation: AmbientCacheOperation, + ) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_run_ambient_cache_operation_start( - runtime, - operation.to_native(), - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::AmbientCache, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_run_ambient_cache_operation( + runtime, + ambient_operation.to_native(), + completion, + ) + }, + completion::unit, ) } @@ -570,323 +327,208 @@ impl RuntimeHandle { /// /// MapLibre evicts ambient resources to fit the new budget, so lowering it /// discards cached resources. Offline regions are unaffected. - pub fn start_set_maximum_ambient_cache_size( - &self, - size: u64, - ) -> Result> { + pub fn set_maximum_ambient_cache_size(&self, size: u64) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_set_maximum_ambient_cache_size_start(runtime, size, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::SetMaximumAmbientCacheSize, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_set_maximum_ambient_cache_size(runtime, size, completion) + }, + completion::unit, ) } /// Starts creating an offline region. - pub fn start_create_offline_region( + pub fn create_offline_region( &self, definition: &OfflineRegionDefinition, metadata: &[u8], - ) -> Result> { + ) -> Result> { let runtime = self.inner.native()?; let definition = definition.to_native()?; let raw_definition = definition.to_raw(); - let mut operation_id: sys::mln_offline_operation_id = 0; // SAFETY: runtime is live. raw_definition points into definition-owned // string and geometry storage, metadata storage is valid for this call. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_create_start( - runtime, - &raw_definition, - maplibre_core::runtime::metadata_ptr(metadata), - metadata.len(), - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionCreate, - maplibre_core::OfflineOperationResultKind::Region, + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_create( + runtime, + &raw_definition, + maplibre_core::runtime::metadata_ptr(metadata), + metadata.len(), + completion, + ) + }, + copy_offline_region, ) } /// Starts getting an offline region snapshot by ID. - pub fn start_offline_region( + pub fn offline_region( &self, region_id: i64, - ) -> Result>> { + ) -> Result>> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - // SAFETY: runtime is live and operation_id points to writable storage. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_get_start(runtime, region_id, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionGet, - maplibre_core::OfflineOperationResultKind::OptionalRegion, + // SAFETY: runtime is live and operation points to writable storage. + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_get(runtime, region_id, completion) + }, + |result| { + if result.value_count == 0 { + return Ok(None); + } + copy_offline_region(result).map(Some) + }, ) } /// Starts listing offline regions in this runtime's database. - pub fn start_offline_regions(&self) -> Result>> { + pub fn offline_regions(&self) -> Result>> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - // SAFETY: runtime is live and operation_id points to writable storage. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_regions_list_start(runtime, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionsList, - maplibre_core::OfflineOperationResultKind::RegionList, + // SAFETY: runtime is live and operation points to writable storage. + completion::submit( + |completion| unsafe { sys::mln_runtime_offline_regions_list(runtime, completion) }, + copy_offline_regions, ) } /// Starts merging offline regions from another database path. - pub fn start_merge_offline_regions_database( + pub fn merge_offline_regions_database( &self, path: &str, - ) -> Result>> { + ) -> Result>> { let runtime = self.inner.native()?; let path = maplibre_core::string::c_string(path)?; - let mut operation_id: sys::mln_offline_operation_id = 0; // SAFETY: runtime is live, path is NUL-terminated and valid for this - // call, and operation_id points to writable storage. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_regions_merge_database_start( - runtime, - path.as_ptr(), - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionsMergeDatabase, - maplibre_core::OfflineOperationResultKind::RegionList, + // call, and operation points to writable storage. + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_regions_merge_database(runtime, path.as_ptr(), completion) + }, + copy_offline_regions, ) } /// Starts updating opaque metadata for an offline region. - pub fn start_update_offline_region_metadata( + pub fn update_offline_region_metadata( &self, region_id: i64, metadata: &[u8], - ) -> Result> { + ) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; // SAFETY: runtime is live, metadata storage is valid for this call, and - // operation_id points to writable storage. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_update_metadata_start( - runtime, - region_id, - maplibre_core::runtime::metadata_ptr(metadata), - metadata.len(), - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionUpdateMetadata, - maplibre_core::OfflineOperationResultKind::Region, + // operation points to writable storage. + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_update_metadata( + runtime, + region_id, + maplibre_core::runtime::metadata_ptr(metadata), + metadata.len(), + completion, + ) + }, + copy_offline_region, ) } /// Starts getting the current completed/download status for an offline region. - pub fn start_offline_region_status( + pub fn offline_region_status( &self, region_id: i64, - ) -> Result> { + ) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - // SAFETY: runtime is live and operation_id points to writable storage. - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_get_status_start(runtime, region_id, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionGetStatus, - maplibre_core::OfflineOperationResultKind::RegionStatus, + // SAFETY: runtime is live and operation points to writable storage. + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_get_status(runtime, region_id, completion) + }, + |result| { + Ok(maplibre_core::events::offline_region_status_from_native( + completion::copy_value(result)?, + )) + }, ) } /// Starts enabling or disabling runtime events for an offline region. - pub fn start_set_offline_region_observed( + pub fn set_offline_region_observed( &self, region_id: i64, observed: bool, - ) -> Result> { + ) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_set_observed_start( - runtime, - region_id, - observed, - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionSetObserved, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_set_observed( + runtime, region_id, observed, completion, + ) + }, + completion::unit, ) } /// Starts setting an offline region's native download state. - pub fn start_set_offline_region_download_state( + pub fn set_offline_region_download_state( &self, region_id: i64, state: OfflineRegionDownloadState, - ) -> Result> { + ) -> Result> { let runtime = self.inner.native()?; let state = state.raw_for_set()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_set_download_state_start( - runtime, - region_id, - state, - &mut operation_id, - ) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionSetDownloadState, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_set_download_state( + runtime, region_id, state, completion, + ) + }, + completion::unit, ) } /// Starts invalidating cached resources for an offline region. - pub fn start_invalidate_offline_region( - &self, - region_id: i64, - ) -> Result> { + pub fn invalidate_offline_region(&self, region_id: i64) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_invalidate_start(runtime, region_id, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionInvalidate, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_invalidate(runtime, region_id, completion) + }, + completion::unit, ) } /// Starts deleting an offline region. - pub fn start_delete_offline_region( - &self, - region_id: i64, - ) -> Result> { + pub fn delete_offline_region(&self, region_id: i64) -> Result> { let runtime = self.inner.native()?; - let mut operation_id: sys::mln_offline_operation_id = 0; - maplibre_core::check(unsafe { - sys::mln_runtime_offline_region_delete_start(runtime, region_id, &mut operation_id) - })?; - self.start_operation( - operation_id, - maplibre_core::OfflineOperationKind::RegionDelete, - maplibre_core::OfflineOperationResultKind::None, + completion::submit( + |completion| unsafe { + sys::mln_runtime_offline_region_delete(runtime, region_id, completion) + }, + completion::unit, ) } - /// Advances this runtime: parks the owner thread when `timeout` allows it, - /// then drains the owner-thread task queues. Drain the queued runtime - /// events with [`Self::drain_events`] afterwards. - /// - /// `Some(Duration::ZERO)` drains and returns, a longer `Some` parks for up - /// to that long, and `None` parks until a wake arrives. The drain runs - /// every task queued when it begins plus every task those enqueue, so a - /// single call can span a full style parse. - /// - /// `budget` bounds the drain. `None` drains without a bound; a `Some` - /// value stops the drain at the first task boundary after that long, - /// measured from the start of the drain. The first queued task always - /// runs, so a bounded pump always makes progress, and tasks left behind - /// set the wake flag so the next pump returns without parking and - /// continues them. The budget bounds the task queues alone: expired timers - /// and ready file descriptors are serviced regardless, and a single task - /// runs to completion once started, so one long task can overrun the - /// budget. - /// - /// A parking call returns as soon as the runtime's wake flag is set, and - /// returns without parking while unread runtime events are queued. Timers - /// and ready file descriptors set the flag only when they queue - /// owner-thread work, so pass a bounded timeout to cap how long a call - /// waits. - /// - /// A non-zero timeout blocks the calling thread. Call it outside any lock - /// that a thread signalling a [`WakeSource`] takes, and outside native - /// callbacks. - pub fn pump(&self, timeout: Option, budget: Option) -> Result<()> { + /// Starts an ordered barrier that completes after all previously accepted + /// runtime work reaches a terminal disposition. + pub fn barrier(&self) -> Result> { let runtime = self.inner.native()?; - let timeout_ms = timeout.map_or(-1, |timeout| { - i64::try_from(timeout.as_millis()).unwrap_or(i64::MAX) - }); - let budget_ms = budget.map_or(-1, |budget| { - i64::try_from(budget.as_millis()).unwrap_or(i64::MAX) - }); - // SAFETY: runtime is a live runtime handle owned by this wrapper. - maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime, timeout_ms, budget_ms) }) - } - - /// Acquires a [`WakeSource`] that releases this runtime's parked owner - /// thread. The returned source is usable from any thread. - pub fn wake_source(&self) -> Result { - let runtime = self.inner.native()?; - let mut out = maplibre_core::ptr::OutHandle::::new(); - // SAFETY: runtime is a live runtime handle owned by this wrapper, and - // out is a valid null-initialized out-pointer owned by this call. - maplibre_core::check(unsafe { - sys::mln_runtime_wake_source_acquire(runtime, out.as_mut_ptr()) - })?; - Ok(WakeSource { - handle: out_handle(out, "mln_wake_source")?, - }) + completion::submit( + |completion| unsafe { sys::mln_runtime_barrier(runtime, completion) }, + completion::unit, + ) } - /// Drains this runtime's queued events into one batch borrowed from - /// runtime-owned storage. + /// Drains an owned batch of queued runtime events. /// - /// `max_events` bounds the drain. Zero drains every queued event, and a - /// positive value drains at most that many and reports the rest through - /// [`RuntimeEventBatch::remaining`]. - /// - /// The returned batch borrows this handle, so the next drain waits until - /// the batch is dropped, and an event read out of the batch borrows the - /// batch. Take [`crate::RuntimeEventRef::to_owned`] for a value that - /// outlives either. - /// - /// A drain invalidates the batch before it, which the mutable borrow turns - /// into a compile error: - /// - /// ```compile_fail,E0499 - /// # use maplibre_native_ffi::{RuntimeHandle, RuntimeOptions}; - /// let mut runtime = RuntimeHandle::with_options(&RuntimeOptions::default()).unwrap(); - /// let batch = runtime.drain_events(0).unwrap(); - /// let next = runtime.drain_events(0).unwrap(); - /// let _ = batch.len(); - /// ``` - pub fn drain_events(&mut self, max_events: usize) -> Result> { + /// The returned batch contains the whole queue, remains stable across later + /// drains, and releases its native storage when dropped. + pub fn drain_events(&self) -> Result { let runtime = self.inner.native()?; - // SAFETY: The default constructor takes no arguments and fills the size - // field for this C ABI version. - let mut raw = unsafe { sys::mln_runtime_event_batch_default() }; - // SAFETY: runtime is live, and raw points to writable batch storage - // whose size field this call reads. - maplibre_core::check(unsafe { - sys::mln_runtime_drain_events(runtime, max_events, &mut raw) - })?; - // SAFETY: raw came from a successful drain on this runtime, so its - // storage stays readable until the next drain, which the returned - // batch's borrow of this handle keeps out of the way. - Ok(unsafe { RuntimeEventBatch::new(raw) }) + let mut batch = sys::mln_event_batch(0); + // SAFETY: runtime is live and batch points to a null writable handle. + maplibre_core::check(unsafe { sys::mln_runtime_drain_events(runtime, &mut batch) })?; + // SAFETY: A successful drain returns one owned native batch. + unsafe { RuntimeEventBatch::new(batch) } } /// Selects which runtime-originated event types this runtime queues. @@ -912,57 +554,51 @@ impl RuntimeHandle { Ok(RuntimeEventMask::from_bits_retain(raw)) } - /// Explicitly destroys the runtime. A failed destroy leaves the native - /// handle live so child handles can still close safely. - pub fn close(self) -> std::result::Result<(), HandleOperationError> { + /// Explicitly releases the runtime's public handle and reports when native + /// teardown finishes. + /// + /// The returned future completes after every earlier accepted submission, + /// including released maps' teardown, has finished and the runtime's + /// threads and resources are gone. No library thread touches library state + /// afterward, so a host that awaits it may exit the process without racing + /// native teardown. Dropping the handle, or the future, starts the same + /// teardown and discards its completion. + pub fn close(self) -> std::result::Result, HandleOperationError> { if self.inner.is_closed() { - return Ok(()); - } - if Rc::strong_count(&self.inner) > 1 { - return Err(HandleOperationError::new( - Error::new( - ErrorKind::InvalidState, - None, - "RuntimeHandle cannot close while child handles are live", - ), - self, - )); + return Ok(completion::ready(())); } self.inner .close() .map_err(|error| HandleOperationError::new(error, self)) } -} -/// Releases a runtime owner thread parked in [`RuntimeHandle::pump`]. -/// -/// Signalling and destruction are callable from any thread. Each source holds -/// its own reference to the runtime's wake state, so it stays usable after the -/// runtime closes, where signalling does nothing. -#[derive(Debug)] -pub struct WakeSource { - handle: sys::mln_wake_source, + /// Closes this runtime and waits for teardown when the test target permits + /// a synchronous wait. + #[cfg(test)] + pub(crate) fn close_and_wait(self) { + let completion = self + .close() + .map_err(HandleOperationError::into_error) + .expect("native close submission failed"); + #[cfg(not(target_os = "emscripten"))] + completion::blocking(Ok(completion)); + // Each browser integration test has a dedicated process, so returning + // keeps the runtime owner thread available until process teardown. + #[cfg(target_os = "emscripten")] + drop(completion); + } } -impl WakeSource { - /// Sets the runtime's wake flag and releases the parked owner thread. - /// - /// A signal raised while the owner thread is running sets the wake flag, - /// so the next [`RuntimeHandle::pump`] returns without parking. Signalling - /// after the runtime closes succeeds and does nothing. - pub fn signal(&self) -> Result<()> { - // SAFETY: handle is a live wake source owned by this wrapper, and - // native accepts signals from any thread. - maplibre_core::check(unsafe { sys::mln_wake_source_signal(self.handle) }) - } +fn copy_offline_region(result: &sys::mln_completion_result) -> Result { + let raw = completion::copy_value::(result)?; + maplibre_core::runtime::copy_offline_region_info(&raw) } -impl Drop for WakeSource { - fn drop(&mut self) { - // SAFETY: handle is a live wake source this wrapper owns and destroys - // exactly once, and native accepts destruction from any thread. - unsafe { sys::mln_wake_source_destroy(self.handle) }; - } +fn copy_offline_regions(result: &sys::mln_completion_result) -> Result> { + completion::copy_slice::(result)? + .iter() + .map(maplibre_core::runtime::copy_offline_region_info) + .collect() } #[cfg(test)] @@ -979,11 +615,9 @@ mod tests { use super::*; use crate::{ - ErrorKind, OfflineOperationCompletedEvent, ResourceErrorReason, ResourceKind, - ResourceProviderDecision, ResourceResponse, RuntimeEvent, RuntimeEventPayload, - RuntimeEventSource, RuntimeEventType, + Error, ErrorKind, ResourceErrorReason, ResourceKind, ResourceProviderDecision, + ResourceResponse, RuntimeEvent, RuntimeEventSource, RuntimeEventType, }; - use maplibre_core::{OfflineOperationKind as Op, OfflineOperationResultKind as OpResult}; const PROVIDER_STYLE_JSON: &str = r#"{"version":8,"sources":{},"layers":[]}"#; @@ -1158,48 +792,28 @@ mod tests { } fn wait_for_operation( - runtime: &mut RuntimeHandle, - operation: &OfflineOperationHandle, - operation_kind: Op, - result_kind: OpResult, - ) -> Result { + _runtime: &mut RuntimeHandle, + operation: &NativeFuture, + ) -> Result<()> { let deadline = Instant::now() + Duration::from_secs(30); loop { if Instant::now() >= deadline { return Err(Error::new( ErrorKind::InvalidState, None, - format!( - "timed out waiting for offline operation {:?}/{:?} with id {}", - operation_kind, result_kind, operation.operation_id - ), + "timed out waiting for native completion", )); } - runtime.pump(Some(Duration::ZERO), None)?; - let mut outcome = None; - for event in runtime.drain_events(0)?.iter() { - let RuntimeEventPayload::OfflineOperationCompleted(completed) = event.payload() - else { - continue; - }; - if completed.operation_id != operation.operation_id { - continue; - } - assert_eq!(completed.operation_kind, operation_kind); - assert_eq!(completed.raw_operation_kind, operation_kind.raw_value()); - assert_eq!(completed.result_kind, result_kind); - assert_eq!(completed.raw_result_kind, result_kind.raw_value()); - outcome = Some((completed, event.message()?.unwrap_or_default().to_owned())); - break; - } - if let Some((completed, message)) = outcome { - if completed.result_status != sys::MLN_STATUS_OK { + std::thread::sleep(std::time::Duration::from_millis(1)); + if operation.is_completed()? { + let status = operation.terminal_status()?; + if status != sys::MLN_STATUS_OK { return Err(Error::from_status_and_diagnostic( - completed.result_status, - message, + status, + operation.diagnostic()?, )); } - return Ok(completed); + return Ok(()); } std::thread::sleep(Duration::from_millis(1)); } @@ -1221,15 +835,13 @@ mod tests { AmbientCacheOperation::Clear, AmbientCacheOperation::ResetDatabase, ] { - let operation = runtime.start_ambient_cache_operation(operation).unwrap(); - let completed = - wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None) - .unwrap(); - assert_eq!(completed.operation_id, operation.operation_id); - operation.discard().unwrap(); + let operation = runtime.ambient_cache_operation(operation).unwrap(); + wait_for_operation(&mut runtime, &operation).unwrap(); + operation.finish().unwrap(); + operation.release(); } - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] @@ -1242,47 +854,30 @@ mod tests { options.cache_path = Some(cache.to_string_lossy().into_owned()); let mut runtime = RuntimeHandle::with_options(&options).unwrap(); - // Raising then lowering the budget both report through the same event. + // Raising then lowering the budget exercises the same operation API. for size in [8 * 1024 * 1024, 0] { - let operation = runtime.start_set_maximum_ambient_cache_size(size).unwrap(); - let completed = wait_for_operation( - &mut runtime, - &operation, - Op::SetMaximumAmbientCacheSize, - OpResult::None, - ) - .unwrap(); - assert_eq!(completed.operation_id, operation.operation_id); - operation.discard().unwrap(); + let operation = runtime.set_maximum_ambient_cache_size(size).unwrap(); + wait_for_operation(&mut runtime, &operation).unwrap(); + operation.finish().unwrap(); + operation.release(); } - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-084. - fn offline_take_result_failure_returns_live_handle() { + fn operation_remains_usable_after_runtime_close() { let mut options = RuntimeOptions::default(); options.cache_path = Some(":memory:".into()); let runtime = RuntimeHandle::with_options(&options).unwrap(); - let ambient = runtime - .start_ambient_cache_operation(AmbientCacheOperation::Clear) - .unwrap(); - let region_result = runtime - .start_operation::( - ambient.operation_id, - maplibre_core::OfflineOperationKind::RegionCreate, - maplibre_core::OfflineOperationResultKind::Region, - ) + let operation = runtime + .ambient_cache_operation(AmbientCacheOperation::Clear) .unwrap(); - let error = region_result.take().unwrap_err(); - - assert_eq!(error.kind(), ErrorKind::InvalidState); - let region_result = error.into_retryable().unwrap().into_handle(); - region_result.discard().unwrap(); - drop(ambient); - runtime.close().unwrap(); + runtime.close_and_wait(); + assert!(operation.wait(Duration::from_secs(10)).unwrap()); + operation.finish().unwrap(); } #[test] @@ -1293,11 +888,10 @@ mod tests { let mut runtime = RuntimeHandle::with_options(&options).unwrap(); let definition = test_offline_region_definition("custom://offline-style.json"); - let create = runtime - .start_create_offline_region(&definition, b"abc") - .unwrap(); - wait_for_operation(&mut runtime, &create, Op::RegionCreate, OpResult::Region).unwrap(); + let create = runtime.create_offline_region(&definition, b"abc").unwrap(); + wait_for_operation(&mut runtime, &create).unwrap(); let created = create.take().unwrap(); + create.release(); assert_eq!(created.definition, definition); assert_eq!(created.metadata, b"abc"); @@ -1310,145 +904,90 @@ mod tests { include_ideographs: false, }; let create_geometry = runtime - .start_create_offline_region(&geometry_definition, b"geo") + .create_offline_region(&geometry_definition, b"geo") .unwrap(); - wait_for_operation( - &mut runtime, - &create_geometry, - Op::RegionCreate, - OpResult::Region, - ) - .unwrap(); + wait_for_operation(&mut runtime, &create_geometry).unwrap(); let geometry_region = create_geometry.take().unwrap(); + create_geometry.release(); assert_eq!(geometry_region.definition, geometry_definition); assert_eq!(geometry_region.metadata, b"geo"); - let get = runtime.start_offline_region(created.id).unwrap(); - wait_for_operation(&mut runtime, &get, Op::RegionGet, OpResult::OptionalRegion).unwrap(); + let get = runtime.offline_region(created.id).unwrap(); + wait_for_operation(&mut runtime, &get).unwrap(); let fetched = get.take().unwrap().unwrap(); + get.release(); assert_eq!(fetched, created); - let list = runtime.start_offline_regions().unwrap(); - wait_for_operation(&mut runtime, &list, Op::RegionsList, OpResult::RegionList).unwrap(); + let list = runtime.offline_regions().unwrap(); + wait_for_operation(&mut runtime, &list).unwrap(); let listed = list.take().unwrap(); + list.release(); assert!(listed.iter().any(|region| region.id == created.id)); let update = runtime - .start_update_offline_region_metadata(created.id, b"") + .update_offline_region_metadata(created.id, b"") .unwrap(); - wait_for_operation( - &mut runtime, - &update, - Op::RegionUpdateMetadata, - OpResult::Region, - ) - .unwrap(); + wait_for_operation(&mut runtime, &update).unwrap(); let updated = update.take().unwrap(); + update.release(); assert_eq!(updated.id, created.id); assert!(updated.metadata.is_empty()); - let status_operation = runtime.start_offline_region_status(created.id).unwrap(); - wait_for_operation( - &mut runtime, - &status_operation, - Op::RegionGetStatus, - OpResult::RegionStatus, - ) - .unwrap(); + let status_operation = runtime.offline_region_status(created.id).unwrap(); + wait_for_operation(&mut runtime, &status_operation).unwrap(); let status = status_operation.take().unwrap(); + status_operation.release(); assert!(matches!( status.download_state, OfflineRegionDownloadState::Inactive | OfflineRegionDownloadState::Active )); let set_inactive = runtime - .start_set_offline_region_download_state( - created.id, - OfflineRegionDownloadState::Inactive, - ) + .set_offline_region_download_state(created.id, OfflineRegionDownloadState::Inactive) .unwrap(); - wait_for_operation( - &mut runtime, - &set_inactive, - Op::RegionSetDownloadState, - OpResult::None, - ) - .unwrap(); - set_inactive.discard().unwrap(); + wait_for_operation(&mut runtime, &set_inactive).unwrap(); + set_inactive.finish().unwrap(); + set_inactive.release(); let error = runtime - .start_set_offline_region_download_state( - created.id, - OfflineRegionDownloadState::Unknown(99), - ) + .set_offline_region_download_state(created.id, OfflineRegionDownloadState::Unknown(99)) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::InvalidArgument); let observe = runtime - .start_set_offline_region_observed(created.id, true) + .set_offline_region_observed(created.id, true) .unwrap(); - wait_for_operation( - &mut runtime, - &observe, - Op::RegionSetObserved, - OpResult::None, - ) - .unwrap(); - observe.discard().unwrap(); + wait_for_operation(&mut runtime, &observe).unwrap(); + observe.finish().unwrap(); + observe.release(); let unobserve = runtime - .start_set_offline_region_observed(created.id, false) + .set_offline_region_observed(created.id, false) .unwrap(); - wait_for_operation( - &mut runtime, - &unobserve, - Op::RegionSetObserved, - OpResult::None, - ) - .unwrap(); - unobserve.discard().unwrap(); - let invalidate = runtime.start_invalidate_offline_region(created.id).unwrap(); - wait_for_operation( - &mut runtime, - &invalidate, - Op::RegionInvalidate, - OpResult::None, - ) - .unwrap(); - invalidate.discard().unwrap(); - let delete = runtime.start_delete_offline_region(created.id).unwrap(); - wait_for_operation(&mut runtime, &delete, Op::RegionDelete, OpResult::None).unwrap(); - delete.discard().unwrap(); - let delete_geometry = runtime - .start_delete_offline_region(geometry_region.id) - .unwrap(); - wait_for_operation( - &mut runtime, - &delete_geometry, - Op::RegionDelete, - OpResult::None, - ) - .unwrap(); - delete_geometry.discard().unwrap(); - - let missing_created = runtime.start_offline_region(created.id).unwrap(); - wait_for_operation( - &mut runtime, - &missing_created, - Op::RegionGet, - OpResult::OptionalRegion, - ) - .unwrap(); + wait_for_operation(&mut runtime, &unobserve).unwrap(); + unobserve.finish().unwrap(); + unobserve.release(); + let invalidate = runtime.invalidate_offline_region(created.id).unwrap(); + wait_for_operation(&mut runtime, &invalidate).unwrap(); + invalidate.finish().unwrap(); + invalidate.release(); + let delete = runtime.delete_offline_region(created.id).unwrap(); + wait_for_operation(&mut runtime, &delete).unwrap(); + delete.finish().unwrap(); + delete.release(); + let delete_geometry = runtime.delete_offline_region(geometry_region.id).unwrap(); + wait_for_operation(&mut runtime, &delete_geometry).unwrap(); + delete_geometry.finish().unwrap(); + delete_geometry.release(); + + let missing_created = runtime.offline_region(created.id).unwrap(); + wait_for_operation(&mut runtime, &missing_created).unwrap(); assert!(missing_created.take().unwrap().is_none()); - let missing_geometry = runtime.start_offline_region(geometry_region.id).unwrap(); - wait_for_operation( - &mut runtime, - &missing_geometry, - Op::RegionGet, - OpResult::OptionalRegion, - ) - .unwrap(); + missing_created.release(); + let missing_geometry = runtime.offline_region(geometry_region.id).unwrap(); + wait_for_operation(&mut runtime, &missing_geometry).unwrap(); assert!(missing_geometry.take().unwrap().is_none()); + missing_geometry.release(); - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] @@ -1464,37 +1003,27 @@ mod tests { side_options.cache_path = Some(side_cache.to_string_lossy().into_owned()); let mut side_runtime = RuntimeHandle::with_options(&side_options).unwrap(); let create = side_runtime - .start_create_offline_region(&definition, b"merge") + .create_offline_region(&definition, b"merge") .unwrap(); - wait_for_operation( - &mut side_runtime, - &create, - Op::RegionCreate, - OpResult::Region, - ) - .unwrap(); + wait_for_operation(&mut side_runtime, &create).unwrap(); create.take().unwrap(); - side_runtime.close().unwrap(); + create.release(); + side_runtime.close_and_wait(); } let mut main_options = RuntimeOptions::default(); main_options.cache_path = Some(main_cache.to_string_lossy().into_owned()); let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap(); let merge = main_runtime - .start_merge_offline_regions_database(&side_cache.to_string_lossy()) + .merge_offline_regions_database(&side_cache.to_string_lossy()) .unwrap(); - wait_for_operation( - &mut main_runtime, - &merge, - Op::RegionsMergeDatabase, - OpResult::RegionList, - ) - .unwrap(); + wait_for_operation(&mut main_runtime, &merge).unwrap(); let merged = merge.take().unwrap(); + merge.release(); assert_eq!(merged.len(), 1); assert_eq!(merged[0].definition, definition); assert_eq!(merged[0].metadata, b"merge"); - main_runtime.close().unwrap(); + main_runtime.close_and_wait(); } fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition { @@ -1546,8 +1075,8 @@ mod tests { options.cache_path = Some(String::new()); let runtime = RuntimeHandle::with_options(&options).unwrap(); - runtime.pump(Some(Duration::ZERO), None).unwrap(); - runtime.close().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); + runtime.close_and_wait(); } #[test] @@ -1568,9 +1097,9 @@ mod tests { ); } - fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool { + fn drain_holds_event_type(runtime: &RuntimeHandle, event_type: RuntimeEventType) -> bool { runtime - .drain_events(0) + .drain_events() .unwrap() .iter() .any(|event| event.event_type() == event_type) @@ -1578,7 +1107,7 @@ mod tests { fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool { for _ in 0..100 { - let _ = runtime.pump(Some(Duration::ZERO), None); + std::thread::sleep(std::time::Duration::from_millis(1)); if drain_holds_event_type(runtime, event_type) { return true; } @@ -1587,11 +1116,19 @@ mod tests { false } + fn wait_for_arc_release(value: &Arc<()>) { + let deadline = std::time::Instant::now() + Duration::from_secs(5); + while Arc::strong_count(value) != 1 && std::time::Instant::now() < deadline { + std::thread::yield_now(); + } + assert_eq!(Arc::strong_count(value), 1); + } + fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent { for _ in 0..100 { - runtime.pump(Some(Duration::ZERO), None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); let mut failure = None; - for event in runtime.drain_events(0).unwrap().iter() { + for event in runtime.drain_events().unwrap().iter() { if event.event_type() == RuntimeEventType::MapLoadingFailed { failure = Some(event.to_owned().unwrap()); break; @@ -1608,173 +1145,69 @@ mod tests { #[test] // Spec coverage: BND-080. fn runtime_create_run_drain_and_close() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - runtime.pump(Some(Duration::ZERO), None).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); // A fresh runtime with no map queues nothing. - let batch = runtime.drain_events(0).unwrap(); + let batch = runtime.drain_events().unwrap(); assert!(batch.is_empty()); assert_eq!(batch.len(), 0); - assert_eq!(batch.remaining(), 0); assert_eq!(batch.iter().count(), 0); // A second drain of an empty queue reports the same empty batch. - assert!(runtime.drain_events(0).unwrap().is_empty()); - runtime.close().unwrap(); + assert!(runtime.drain_events().unwrap().is_empty()); + runtime.close_and_wait(); } - // Pumps until the runtime is idle, so a park that follows is released by - // the signal the test raises. - fn quiesce(runtime: &mut RuntimeHandle) { - for _ in 0..100 { - runtime.pump(Some(Duration::ZERO), None).unwrap(); - if runtime.drain_events(0).unwrap().is_empty() { - return; - } - } - panic!("the runtime kept producing events while idle"); - } - - // Drives the runtime the way a parked host does, so a missing wake shows up - // as an expired timeout. - fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool { - let started = Instant::now(); - for _ in 0..20 { - runtime.pump(Some(Duration::from_secs(10)), None).unwrap(); - assert!( - started.elapsed() < Duration::from_secs(5), - "parks sat out their timeouts instead of taking wakes" - ); + fn wait_for_event(runtime: &RuntimeHandle, event_type: RuntimeEventType) -> bool { + for _ in 0..500 { if drain_holds_event_type(runtime, event_type) { return true; } + std::thread::sleep(Duration::from_millis(1)); } false } #[test] - // Spec coverage: BND-088. - fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - runtime - .set_resource_provider(move |request, handle| { - if request.requested_url != "custom://style.json" { - return ResourceProviderDecision::PassThrough; - } - handle - .complete(ResourceResponse::ok( - PROVIDER_STYLE_JSON.as_bytes().to_vec(), - )) - .unwrap(); - ResourceProviderDecision::PassThrough - }) - .unwrap(); - - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - map.set_style_url("custom://style.json").unwrap(); - assert!(park_for_runtime_event( - &mut runtime, - RuntimeEventType::MapStyleLoaded - )); - - // A source moved to another thread matches a host's submission path, - // and the park it releases has no other work to end it. - let source = runtime.wake_source().unwrap(); - quiesce(&mut runtime); - let signaller = std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(20)); - source.signal().unwrap(); - source - }); - let started = Instant::now(); - runtime.pump(Some(Duration::from_secs(10)), None).unwrap(); - assert!( - started.elapsed() < Duration::from_secs(5), - "the parked owner thread timed out instead of taking the signal" - ); - let source = signaller.join().unwrap(); - - // A wake source stays usable after its runtime closes, so hosts tear - // the two down in either order. - map.close().unwrap(); - runtime.close().unwrap(); - source.signal().unwrap(); - } - - #[test] - // Spec coverage: BND-089. - fn a_pump_clears_the_wake_flag_it_returns_on() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let source = runtime.wake_source().unwrap(); - quiesce(&mut runtime); - - source.signal().unwrap(); - let started = Instant::now(); - runtime.pump(Some(Duration::from_secs(10)), None).unwrap(); - assert!( - started.elapsed() < Duration::from_secs(5), - "a pump waited even though the wake flag was set" - ); + fn native_worker_makes_progress_without_host_driving() { + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap(); - // The pump above cleared the wake flag, so this one waits its full timeout. - let started = Instant::now(); - runtime - .pump(Some(Duration::from_millis(200)), None) - .unwrap(); - assert!( - started.elapsed() >= Duration::from_millis(100), - "the first pump left the wake flag set" - ); + assert!(wait_for_event(&runtime, RuntimeEventType::MapStyleLoaded)); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] - // Spec coverage: BND-090. - fn a_bounded_drain_reports_the_events_it_left_queued() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap(); - runtime.pump(Some(Duration::ZERO), None).unwrap(); - - let bounded = runtime.drain_events(1).unwrap(); - assert_eq!(bounded.len(), 1); - assert!( - bounded.remaining() > 0, - "a style load should queue more than one event" - ); - let rest = runtime.drain_events(0).unwrap(); - assert!(rest.len() > 1, "one drain should report the whole queue"); - assert_eq!(rest.remaining(), 0); - let first = rest.iter().next().unwrap(); - assert_eq!(first.source(), RuntimeEventSource::Map(map.id())); - - map.close().unwrap(); - runtime.close().unwrap(); + fn runtime_accepts_concurrent_barrier_submissions() { + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + std::thread::scope(|scope| { + let first = scope.spawn(|| runtime.barrier().unwrap()); + let second = scope.spawn(|| runtime.barrier().unwrap()); + for operation in [first.join().unwrap(), second.join().unwrap()] { + assert!(operation.wait(Duration::from_secs(5)).unwrap()); + assert_eq!(operation.terminal_status().unwrap(), sys::MLN_STATUS_OK); + operation.finish().unwrap(); + } + }); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-060 and BND-091. - fn a_creation_mask_narrows_a_runtime_before_its_first_operation() { + fn a_creation_mask_is_applied_when_the_runtime_is_created() { let mut options = crate::RuntimeOptions::default(); - options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED; - let mut runtime = RuntimeHandle::with_options(&options).unwrap(); + options.event_mask = RuntimeEventMask::MAP_STYLE_LOADED; + let runtime = RuntimeHandle::with_options(&options).unwrap(); assert_eq!( runtime.event_mask().unwrap(), - RuntimeEventMask::OFFLINE_OPERATION_COMPLETED + RuntimeEventMask::MAP_STYLE_LOADED ); - - // The narrowed runtime still reports the completion of the operation - // this test starts, and the ambient cache operation needs no database. - let operation = runtime - .start_ambient_cache_operation(AmbientCacheOperation::Clear) - .unwrap(); - let completed = - wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap(); - assert_eq!(completed.operation_id, operation.operation_id); - operation.discard().unwrap(); - - runtime.close().unwrap(); + runtime.close_and_wait(); // A creation mask carrying an undefined bit fails before the runtime // exists. @@ -1800,7 +1233,7 @@ mod tests { runtime.set_event_mask(mask).unwrap(); let read_back = runtime.event_mask().unwrap(); assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED)); - assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED)); + assert!(read_back.contains(RuntimeEventMask::MAP_IDLE)); assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED)); let undefined = RuntimeEventMask::from_bits_retain(1 << 63); @@ -1809,28 +1242,20 @@ mod tests { assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT)); assert_eq!(runtime.event_mask().unwrap(), read_back); - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] - // Spec coverage: BND-020, BND-022, BND-190, and BND-191. - fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() { + fn runtime_state_is_readable_from_another_thread() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let runtime_handle = runtime.inner.native().unwrap(); - - let error = std::thread::spawn(move || { - // SAFETY: This intentionally exercises the C API's owner-thread - // validation path with a live runtime handle from another thread. - maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) }) - .unwrap_err() - }) - .join() - .unwrap(); - - assert_eq!(error.kind(), ErrorKind::WrongThread); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD)); - assert!(!error.diagnostic().is_empty()); - runtime.close().unwrap(); + std::thread::scope(|scope| { + let mask = scope + .spawn(|| runtime.event_mask().unwrap()) + .join() + .unwrap(); + assert_eq!(mask, RuntimeEventMask::ALL); + }); + runtime.close_and_wait(); } #[test] @@ -1840,40 +1265,34 @@ mod tests { let first = Arc::new(()); let first_callback = Arc::clone(&first); - runtime - .set_resource_provider(move |_, _| { - let _ = &first_callback; - crate::ResourceProviderDecision::PassThrough - }) - .unwrap(); + completion::blocking(runtime.set_resource_provider(move |_, _| { + let _ = &first_callback; + crate::ResourceProviderDecision::PassThrough + })); assert_eq!(Arc::strong_count(&first), 2); let second = Arc::new(()); let second_callback = Arc::clone(&second); - runtime - .set_resource_provider(move |_, _| { - let _ = &second_callback; - crate::ResourceProviderDecision::PassThrough - }) - .unwrap(); - assert_eq!(Arc::strong_count(&first), 1); + completion::blocking(runtime.set_resource_provider(move |_, _| { + let _ = &second_callback; + crate::ResourceProviderDecision::PassThrough + })); + wait_for_arc_release(&first); assert_eq!(Arc::strong_count(&second), 2); - runtime.clear_resource_provider().unwrap(); - assert_eq!(Arc::strong_count(&second), 1); + completion::blocking(runtime.clear_resource_provider()); + wait_for_arc_release(&second); let third = Arc::new(()); let third_callback = Arc::clone(&third); - runtime - .set_resource_provider(move |_, _| { - let _ = &third_callback; - crate::ResourceProviderDecision::PassThrough - }) - .unwrap(); + completion::blocking(runtime.set_resource_provider(move |_, _| { + let _ = &third_callback; + crate::ResourceProviderDecision::PassThrough + })); assert_eq!(Arc::strong_count(&third), 2); - runtime.close().unwrap(); - assert_eq!(Arc::strong_count(&third), 1); + runtime.close_and_wait(); + wait_for_arc_release(&third); } #[test] @@ -1904,8 +1323,8 @@ mod tests { assert_eq!(Arc::strong_count(&first), 2); assert_eq!(Arc::strong_count(&second), 1); - runtime.close().unwrap(); - assert_eq!(Arc::strong_count(&first), 1); + runtime.close_and_wait(); + wait_for_arc_release(&first); } // Requests a style no file source serves, so the loading-failure event that @@ -1926,7 +1345,8 @@ mod tests { // Spec coverage: BND-142 and BND-154. fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() { let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); let first_calls = Arc::new(AtomicUsize::new(0)); let first_callback_calls = Arc::clone(&first_calls); @@ -1970,8 +1390,8 @@ mod tests { // Clearing an already cleared provider stays a successful no-op. runtime.clear_resource_provider().unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -1996,7 +1416,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url("custom://style.json").unwrap(); assert!(wait_for_runtime_event( @@ -2004,8 +1425,8 @@ mod tests { RuntimeEventType::MapStyleLoaded )); assert_eq!(calls.load(Ordering::SeqCst), 1); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -2029,7 +1450,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url("maplibre://maps/style").unwrap(); assert!(wait_for_runtime_event( @@ -2040,8 +1462,8 @@ mod tests { resolved.lock().unwrap().as_deref(), Some("https://demotiles.maplibre.org/style.json") ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -2060,7 +1482,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url("custom://async-style.json").unwrap(); let handle = receiver .recv_timeout(Duration::from_secs(5)) @@ -2080,8 +1503,8 @@ mod tests { &mut runtime, RuntimeEventType::MapStyleLoaded )); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -2104,14 +1527,15 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); let map_id = map.id(); map.set_style_url("custom://broken-style.json").unwrap(); let event = wait_for_map_loading_failure(&mut runtime); let copied_message = event.message.clone(); // The copy stays intact after the drain that ends the batch's window. - let _ = runtime.drain_events(0).unwrap(); + let _ = runtime.drain_events().unwrap(); assert_eq!(event.source, RuntimeEventSource::Map(map_id)); assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed); @@ -2123,8 +1547,8 @@ mod tests { .is_some_and(|message| message.contains("provider failed")) ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -2134,32 +1558,28 @@ mod tests { let first = Arc::new(()); let first_callback = Arc::clone(&first); - runtime - .set_resource_transform(move |request| { - let _ = &first_callback; - assert!(matches!( - request.kind, - ResourceKind::Style | ResourceKind::UnknownRaw(_) - )); - None - }) - .unwrap(); + completion::blocking(runtime.set_resource_transform(move |request| { + let _ = &first_callback; + assert!(matches!( + request.kind, + ResourceKind::Style | ResourceKind::UnknownRaw(_) + )); + None + })); assert_eq!(Arc::strong_count(&first), 2); let second = Arc::new(()); let second_callback = Arc::clone(&second); - runtime - .set_resource_transform(move |_| { - let _ = &second_callback; - Some("https://example.test/replacement".to_owned()) - }) - .unwrap(); - assert_eq!(Arc::strong_count(&first), 1); + completion::blocking(runtime.set_resource_transform(move |_| { + let _ = &second_callback; + Some("https://example.test/replacement".to_owned()) + })); + wait_for_arc_release(&first); assert_eq!(Arc::strong_count(&second), 2); - runtime.clear_resource_transform().unwrap(); - assert_eq!(Arc::strong_count(&second), 1); - runtime.close().unwrap(); + completion::blocking(runtime.clear_resource_transform()); + wait_for_arc_release(&second); + runtime.close_and_wait(); } /// The browser has no in-process TCP server, so the runner serves the two @@ -2187,7 +1607,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url(&format!("{origin}/__fixture/original-style.json")) .unwrap(); assert!(wait_for_runtime_event( @@ -2196,12 +1617,10 @@ mod tests { )); // Contains rather than equals: MapLibre adds its own annotation layer to // every style it loads. - assert!( - map.style_layer_ids() - .unwrap() - .iter() - .any(|id| id == "rewritten") - ); + let layer_ids = map.style_layer_ids().unwrap(); + assert!(layer_ids.wait(Duration::from_secs(5)).unwrap()); + assert!(layer_ids.take().unwrap().iter().any(|id| id == "rewritten")); + layer_ids.release(); runtime.clear_resource_transform().unwrap(); map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json")) @@ -2210,15 +1629,19 @@ mod tests { &mut runtime, RuntimeEventType::MapStyleLoaded )); + let layer_ids = map.style_layer_ids().unwrap(); + assert!(layer_ids.wait(Duration::from_secs(5)).unwrap()); assert!( - map.style_layer_ids() + layer_ids + .take() .unwrap() .iter() .any(|id| id == "original-after-clear") ); + layer_ids.release(); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[cfg(not(target_os = "emscripten"))] @@ -2244,7 +1667,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url(&format!("{base_url}/original-style.json")) .unwrap(); assert!(wait_for_runtime_event( @@ -2268,8 +1692,8 @@ mod tests { "/original-after-clear.json" ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); server.join().unwrap(); } @@ -2286,7 +1710,8 @@ mod tests { }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url(&format!("{base_url}/with-header.json")) .unwrap(); assert!(wait_for_runtime_event( @@ -2314,8 +1739,8 @@ mod tests { .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:")) ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); server.join().unwrap(); } @@ -2333,14 +1758,15 @@ mod tests { Vec::new() }) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url("jar:file:/packaged/style.json").unwrap(); let _ = wait_for_map_loading_failure(&mut runtime); assert_eq!(calls.load(Ordering::SeqCst), 0); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))] @@ -2352,7 +1778,8 @@ mod tests { runtime .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")]) .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); map.set_style_url(&format!("{origin_url}/same-start.json")) .unwrap(); @@ -2384,8 +1811,8 @@ mod tests { ("/cross-final.json".to_owned(), false) ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); for server in servers { server.join().unwrap(); } @@ -2403,7 +1830,7 @@ mod tests { .set_http_header_transform(|_| Vec::new()) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported); - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] @@ -2412,29 +1839,26 @@ mod tests { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); let first = Arc::new(()); let first_callback = Arc::clone(&first); - runtime - .set_resource_transform(move |_| { - let _ = &first_callback; - None - }) - .unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + completion::blocking(runtime.set_resource_transform(move |_| { + let _ = &first_callback; + None + })); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); let second = Arc::new(()); let second_callback = Arc::clone(&second); - runtime - .set_resource_transform(move |_| { - let _ = &second_callback; - None - }) - .unwrap(); + completion::blocking(runtime.set_resource_transform(move |_| { + let _ = &second_callback; + None + })); - assert_eq!(Arc::strong_count(&first), 1); + wait_for_arc_release(&first); assert_eq!(Arc::strong_count(&second), 2); - map.close().unwrap(); - runtime.close().unwrap(); - assert_eq!(Arc::strong_count(&second), 1); + map.close_and_wait(); + runtime.close_and_wait(); + wait_for_arc_release(&second); } #[test] @@ -2443,17 +1867,15 @@ mod tests { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); let token = Arc::new(()); let callback_token = Arc::clone(&token); - runtime - .set_resource_transform(move |_| { - let _ = &callback_token; - None - }) - .unwrap(); + completion::blocking(runtime.set_resource_transform(move |_| { + let _ = &callback_token; + None + })); assert_eq!(Arc::strong_count(&token), 2); - runtime.close().unwrap(); + runtime.close_and_wait(); - assert_eq!(Arc::strong_count(&token), 1); + wait_for_arc_release(&token); } #[test] @@ -2461,12 +1883,13 @@ mod tests { // guard after map creation. fn resource_transform_installs_after_map_creation() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); runtime.set_resource_transform(|_| None).unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] @@ -2483,35 +1906,39 @@ mod tests { .unwrap(); assert_eq!(Arc::strong_count(&token), 2); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); - map.close().unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); + map.close_and_wait(); - runtime.clear_resource_transform().unwrap(); + completion::blocking(runtime.clear_resource_transform()); + wait_for_arc_release(&token); - assert_eq!(Arc::strong_count(&token), 1); - - runtime.close().unwrap(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-081, BND-090, and BND-092. fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() { - let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); let map_id = map.id(); - let error = map.set_style_json(b"{").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::NativeError); - assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR)); + let command = map.set_style_json(b"{").unwrap(); + assert!(command.wait(Duration::from_secs(5)).unwrap()); + let completion = command.take().unwrap(); + assert_eq!(completion.disposition, crate::CommandDisposition::Failed); + assert_eq!(completion.raw_status, sys::MLN_STATUS_NATIVE_ERROR); + assert!(!completion.diagnostic.is_empty()); - let batch = runtime.drain_events(0).unwrap(); + let batch = runtime.drain_events().unwrap(); let types = batch .iter() .map(|event| event.event_type()) .collect::>(); assert!( - types.len() > 1, - "a failed style load should queue more than one event, got {types:?}" + !types.is_empty(), + "a failed style load should queue an event, got {types:?}" ); let loading_failed = batch .iter() @@ -2526,9 +1953,10 @@ mod tests { ); let owned = loading_failed.to_owned().unwrap(); - // The next drain ends the previous batch's window; the copy is - // untouched. - assert!(runtime.drain_events(0).unwrap().is_empty()); + // A later drain leaves the owned batch readable. + assert!(runtime.drain_events().unwrap().is_empty()); + assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id)); + drop(batch); assert_eq!(owned.source, RuntimeEventSource::Map(map_id)); assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed); assert!( @@ -2538,23 +1966,24 @@ mod tests { .is_some_and(|message| !message.is_empty()) ); - map.close().unwrap(); - runtime.close().unwrap(); + map.close_and_wait(); + runtime.close_and_wait(); } #[test] // Spec coverage: BND-042. fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() { let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap(); - let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap(); + let map = + crate::completion::blocking(MapHandle::with_options(&runtime, &MapOptions::default())); let error = runtime.close().unwrap_err(); assert_eq!(error.kind(), ErrorKind::InvalidState); - assert_eq!(error.raw_status(), None); + assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_STATE)); let runtime = error.into_handle(); - runtime.pump(Some(Duration::ZERO), None).unwrap(); - map.close().unwrap(); - runtime.close().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); + map.close_and_wait(); + runtime.close_and_wait(); } } diff --git a/bindings/rust/mise.toml b/bindings/rust/mise.toml index a696069d0..2bf9d02de 100644 --- a/bindings/rust/mise.toml +++ b/bindings/rust/mise.toml @@ -24,9 +24,9 @@ exec cargo build \ [tasks.test] extends = "ffi:preset" dir = "../.." -# The preset selects the runner: Android and OpenHarmony x64 presets push to an -# emulator, Emscripten presets run as a page in headless Chromium, and host -# presets run in process. +# The preset selects the runner: Android presets and OpenHarmony x64 presets +# push to an emulator, Emscripten presets run as a page in headless Chromium, +# and host presets run in process. run = ''' case "$usage_preset" in android-* | ohos-*) @@ -36,7 +36,7 @@ case "$usage_preset" in exec "$MISE_MONOREPO_ROOT/scripts/test-rust-browser.sh" "$usage_preset" ;; ios-*) - echo "The Rust binding has no iOS target; test a host, Android x64, OpenHarmony x64, or Emscripten preset." >&2 + echo "The Rust binding has no iOS target; test a host, Android, OpenHarmony x64, or Emscripten preset." >&2 exit 2 ;; esac diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Camera.swift b/bindings/swift/Sources/MaplibreNativeFFI/Camera.swift index a8d797118..2d0edd728 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Camera.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Camera.swift @@ -1,3 +1,4 @@ +internal import CMaplibreNativeC public struct CameraOptions: Equatable, Sendable { public var center: LatLng? @@ -6,9 +7,8 @@ public struct CameraOptions: Equatable, Sendable { public var pitch: Double? public var centerAltitude: Double? public var padding: EdgeInsets? - /// Input-only screen point the camera pivots around. `jumpTo`, `easeTo`, and - /// `flyTo` honor it; MapLibre leaves it `nil` on every read path, including - /// camera snapshots and the camera-for-bounds helpers. + /// Input-only screen point the camera pivots around. Camera updates honor it; + /// MapLibre leaves it `nil` on camera reads. public var anchor: ScreenPoint? public var roll: Double? public var fieldOfView: Double? @@ -118,3 +118,103 @@ public struct AnimationOptions: Equatable, Sendable { ) } } + +public enum CameraUpdateMode: UInt32, Sendable, Hashable { + case jump = 0 + case ease = 1 + case fly = 2 +} + +public enum GesturePhase: UInt32, Sendable, Hashable { + case none = 0 + case begin = 1 + case update = 2 + case end = 3 + case cancel = 4 +} + +/// One camera command copied atomically by the runtime worker. +public struct CameraUpdate: Equatable, Sendable { + public var mode: CameraUpdateMode + public var camera: CameraOptions + public var animation: AnimationOptions + public var gesturePhase: GesturePhase + + public init( + mode: CameraUpdateMode = .jump, + camera: CameraOptions, + animation: AnimationOptions = AnimationOptions(), + gesturePhase: GesturePhase = .none + ) { + self.mode = mode + self.camera = camera + self.animation = animation + self.gesturePhase = gesturePhase + } + + func withNativeUpdate( + _ body: (UnsafePointer) throws -> Result + ) throws -> Result { + try camera.nativeInput.withNativeOptions { camera in + try animation.nativeInput.withOptionalNativeOptions { animation in + var update = mln_camera_update_default() + update.mode = mode.rawValue + update.camera = camera.pointee + if let animation { + update.animation = animation.pointee + } + update.gesture_phase = gesturePhase.rawValue + return try withUnsafePointer(to: &update, body) + } + } + } +} + +public enum CameraDeltaKind: UInt32, Sendable, Hashable { + case move = 0 + case scale = 1 + case bearing = 2 + case pitch = 3 +} + +/// One relative camera operation. +public struct CameraDelta: Equatable, Sendable { + public var kind: CameraDeltaKind + public var offset: ScreenPoint + public var amount: Double + public var anchor: ScreenPoint? + public var animation: AnimationOptions + + public init( + kind: CameraDeltaKind = .move, + offset: ScreenPoint = ScreenPoint(x: 0, y: 0), + amount: Double = 0, + anchor: ScreenPoint? = nil, + animation: AnimationOptions = AnimationOptions() + ) { + self.kind = kind + self.offset = offset + self.amount = amount + self.anchor = anchor + self.animation = animation + } + + func withNativeDelta( + _ body: (UnsafePointer) throws -> Result + ) throws -> Result { + try animation.nativeInput.withOptionalNativeOptions { animation in + var delta = mln_camera_delta_default() + delta.kind = kind.rawValue + delta.offset = offset.nativeInput.native + delta.amount = amount + if let anchor { + delta.has_anchor = true + delta.anchor = anchor.nativeInput.native + } + if let animation { + delta.animation = animation.pointee + } + return try withUnsafePointer(to: &delta, body) + } + } +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/CameraAdvanced.swift b/bindings/swift/Sources/MaplibreNativeFFI/CameraAdvanced.swift index 2742e63b2..3b6de0694 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/CameraAdvanced.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/CameraAdvanced.swift @@ -314,310 +314,208 @@ public struct MapTileOptions: Equatable, Sendable { } public extension MapHandle { - func setDebugOptions(_ options: MapDebugOptions) throws { - try mapNativeFailure { try checkStatus(mln_map_set_debug_options( - requireLiveHandle().raw, - options.rawValue - )) } - } - - func debugOptions() throws -> MapDebugOptions { - try mapNativeFailure { - try MapDebugOptions(rawValue: NativeMap - .debugOptions(requireLiveHandle())) - } - } - - func setRenderingStatsViewEnabled(_ enabled: Bool) throws { - try mapNativeFailure { - try checkStatus(mln_map_set_rendering_stats_view_enabled( - requireLiveHandle().raw, - enabled - )) + @discardableResult + func setDebugOptions( + _ options: MapDebugOptions + ) async throws -> CommandCompletion { + try await submitCommand { + mln_map_set_debug_options($0, options.rawValue, $1) } } - func renderingStatsViewEnabled() throws -> Bool { - try mapNativeFailure { - try NativeMap.renderingStatsViewEnabled(requireLiveHandle()) + @discardableResult + func setRenderingStatsViewEnabled( + _ enabled: Bool + ) async throws -> CommandCompletion { + try await submitCommand { + mln_map_set_rendering_stats_view_enabled($0, enabled, $1) } } - func isFullyLoaded() throws -> Bool { - try mapNativeFailure { try NativeMap.isFullyLoaded(requireLiveHandle()) } + @discardableResult + func dumpDebugLogs() async throws -> CommandCompletion { + try await submitCommand(mln_map_dump_debug_logs) } - func dumpDebugLogs() throws { - try mapNativeFailure { - try checkStatus(mln_map_dump_debug_logs(requireLiveHandle().raw)) - } - } - - /// Returns the map's logical viewport size in UI pixels and its scale factor, - /// which is fixed for the lifetime of the map and independent of any render - /// target's. + /// Returns the map's latest logical viewport extent. func size() throws -> (width: UInt32, height: UInt32, scaleFactor: Double) { - try mapNativeFailure { try NativeMap.size(requireLiveHandle()) } - } - - func viewportOptions() throws -> MapViewportOptions { - try mapNativeFailure { - try MapViewportOptions(native: NativeMapViewportOptionsInput(NativeMap - .viewportOptions(requireLiveHandle()))) - } - } - - func setViewportOptions(_ options: MapViewportOptions) throws { - try mapNativeFailure { - try options.nativeInput - .withNativeOptions { try checkStatus(mln_map_set_viewport_options( - requireLiveHandle().raw, - $0 - )) } - } - } - - func tileOptions() throws -> MapTileOptions { - try mapNativeFailure { - try MapTileOptions(native: NativeMapTileOptionsInput(NativeMap - .tileOptions(requireLiveHandle()))) - } - } - - func setTileOptions(_ options: MapTileOptions) throws { - try mapNativeFailure { - try options.nativeInput - .withNativeOptions { try checkStatus(mln_map_set_tile_options( - requireLiveHandle().raw, - $0 - )) } - } + let extent = try snapshot().logicalExtent + return (extent.width, extent.height, extent.scaleFactor) } - func fly(to camera: CameraOptions, - animation: AnimationOptions? = nil) throws - { - try mapNativeFailure { - try camera.nativeInput.withNativeOptions { nativeCamera in - try (animation?.nativeInput ?? NativeAnimationOptionsInput()) - .withOptionalNativeOptions { nativeAnimation in - try checkStatus(mln_map_fly_to( - requireLiveHandle().raw, - nativeCamera, - nativeAnimation - )) - } - } - } - } - - func rotateBy(first: ScreenPoint, second: ScreenPoint) throws { - try mapNativeFailure { try checkStatus(mln_map_rotate_by( - requireLiveHandle().raw, - first.nativeInput.native, - second.nativeInput.native - )) } - } - - func rotateBy( - first: ScreenPoint, - second: ScreenPoint, - animation: AnimationOptions - ) throws { - try mapNativeFailure { - try animation.nativeInput.withOptionalNativeOptions { animation in - try checkStatus(mln_map_rotate_by_animated( - requireLiveHandle().raw, - first.nativeInput.native, - second.nativeInput.native, - animation - )) + @discardableResult + func setViewportOptions( + _ options: MapViewportOptions + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try options.nativeInput.withNativeOptions { native in + try startCommand { mln_map_set_viewport_options($0, native, $1) } } } + return try await mapNativeFailure { try await future.value() } } - func pitchBy(_ pitch: Double) throws { - try mapNativeFailure { try checkStatus(mln_map_pitch_by( - requireLiveHandle().raw, - pitch - )) } - } - - func pitchBy(_ pitch: Double, animation: AnimationOptions) throws { - try mapNativeFailure { - try animation.nativeInput.withOptionalNativeOptions { animation in - try checkStatus(mln_map_pitch_by_animated( - requireLiveHandle().raw, - pitch, - animation - )) + @discardableResult + func setTileOptions( + _ options: MapTileOptions + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try options.nativeInput.withNativeOptions { native in + try startCommand { mln_map_set_tile_options($0, native, $1) } } } + return try await mapNativeFailure { try await future.value() } } func cameraForLatLngBounds( _ bounds: LatLngBounds, fitOptions: CameraFitOptions? = nil - ) throws -> CameraOptions { - try mapNativeFailure { + ) async throws -> CameraOptions { + let future = try mapNativeFailure { try (fitOptions?.nativeInput ?? NativeCameraFitOptionsInput()) - .withOptionalNativeOptions { fitOptions in - try CameraOptions(native: NativeCameraOptionsInput(NativeMap - .cameraForLatLngBounds( - requireLiveHandle(), - bounds: bounds.nativeInput, - fitOptions: fitOptions - ))) + .withOptionalNativeOptions { + try NativeMap.cameraForLatLngBounds( + requireLiveHandle(), + bounds: bounds.nativeInput, + fitOptions: $0 + ) } } + return try await mapNativeFailure { try await future.value() } } func cameraForLatLngs( _ coordinates: [LatLng], fitOptions: CameraFitOptions? = nil - ) throws -> CameraOptions { - try mapNativeFailure { + ) async throws -> CameraOptions { + let future = try mapNativeFailure { let native = coordinates.map { $0.nativeInput.native } return try native.withUnsafeBufferPointer { coordinates in try (fitOptions?.nativeInput ?? NativeCameraFitOptionsInput()) - .withOptionalNativeOptions { fitOptions in - try CameraOptions(native: NativeCameraOptionsInput(NativeMap - .cameraForLatLngs( - requireLiveHandle(), - coordinates: coordinates.baseAddress, - count: coordinates.count, - fitOptions: fitOptions - ))) + .withOptionalNativeOptions { + try NativeMap.cameraForLatLngs( + requireLiveHandle(), + coordinates: coordinates.baseAddress, + count: coordinates.count, + fitOptions: $0 + ) } } } + return try await mapNativeFailure { try await future.value() } } func cameraForGeometry( _ geometry: Data, fitOptions: CameraFitOptions? = nil - ) throws -> CameraOptions { - try mapNativeFailure { + ) async throws -> CameraOptions { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } return try (fitOptions?.nativeInput ?? NativeCameraFitOptionsInput()) - .withOptionalNativeOptions { fitOptions in - try CameraOptions(native: NativeCameraOptionsInput(NativeMap - .cameraForGeometry( - requireLiveHandle(), - geometry: arena.view(geometry), - fitOptions: fitOptions - ))) + .withOptionalNativeOptions { + try NativeMap.cameraForGeometry( + requireLiveHandle(), + geometry: arena.view(geometry), + fitOptions: $0 + ) } } + return try await mapNativeFailure { try await future.value() } } - /// Returns geographic bounds for a camera from two viewport corners. - /// - /// The box is the hull of the top-left and bottom-right screen corners for - /// that camera in the current viewport. When bearing and pitch are zero, the - /// box equals the visible area. Those corners are the northwest and southeast - /// of the viewport. Longitudes stay in -180 to 180. - /// - /// Pass `unwrapped: true` for the axis-aligned hull of all four screen - /// corners - /// and the center. That hull encompasses the projected viewport. Longitudes - /// unwrap onto the shortest path through the center. A viewport that crosses - /// the antimeridian reports values outside -180 to 180. - func latLngBounds(for camera: CameraOptions, - unwrapped: Bool = false) throws -> LatLngBounds - { - try mapNativeFailure { - try camera.nativeInput.withNativeOptions { camera in - try LatLngBounds(native: NativeMap.latLngBoundsForCamera( + /// Returns geographic bounds for a camera from the viewport corners. + func latLngBounds( + for camera: CameraOptions, + unwrapped: Bool = false + ) async throws -> LatLngBounds { + let future = try mapNativeFailure { + try camera.nativeInput.withNativeOptions { + try NativeMap.latLngBoundsForCamera( requireLiveHandle(), - camera: camera, + camera: $0, unwrapped: unwrapped - )) + ) } } + return try await mapNativeFailure { try await future.value() } } - func bounds() throws -> BoundOptions { - try mapNativeFailure { - try BoundOptions(native: NativeBoundOptionsInput(NativeMap - .bounds(requireLiveHandle()))) + @discardableResult + func setBounds(_ bounds: BoundOptions) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try bounds.nativeInput.withNativeOptions { native in + try startCommand { mln_map_set_bounds($0, native, $1) } + } } + return try await mapNativeFailure { try await future.value() } } - func setBounds(_ bounds: BoundOptions) throws { - try mapNativeFailure { - try bounds.nativeInput - .withNativeOptions { try checkStatus(mln_map_set_bounds( - requireLiveHandle().raw, - $0 - )) } + @discardableResult + func setFreeCameraOptions( + _ options: FreeCameraOptions + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try options.nativeInput.withNativeOptions { native in + try startCommand { mln_map_set_free_camera_options($0, native, $1) } + } } + return try await mapNativeFailure { try await future.value() } } - func freeCameraOptions() throws -> FreeCameraOptions { - try mapNativeFailure { - try FreeCameraOptions(native: NativeFreeCameraOptionsInput(NativeMap - .freeCameraOptions(requireLiveHandle()))) - } + func projectionMode() throws -> ProjectionMode { + try snapshot().projectionMode } - func setFreeCameraOptions(_ options: FreeCameraOptions) throws { - try mapNativeFailure { - try options.nativeInput - .withNativeOptions { try checkStatus(mln_map_set_free_camera_options( - requireLiveHandle().raw, - $0 - )) } + @discardableResult + func setProjectionMode( + _ mode: ProjectionMode + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try mode.nativeInput.withNativeMode { native in + try startCommand { mln_map_set_projection_mode($0, native, $1) } + } } + return try await mapNativeFailure { try await future.value() } } - func projectionMode() throws -> ProjectionMode { - try mapNativeFailure { - try ProjectionMode(native: NativeProjectionModeInput(NativeMap - .projectionMode(requireLiveHandle()))) + func pixel(for coordinate: LatLng) async throws -> ScreenPoint { + let future = try mapNativeFailure { + try NativeMap.pixelForLatLng( + requireLiveHandle(), + coordinate: coordinate.nativeInput + ) } + return try await mapNativeFailure { try await future.value() } } - func setProjectionMode(_ mode: ProjectionMode) throws { - try mapNativeFailure { - try mode.nativeInput - .withNativeMode { try checkStatus(mln_map_set_projection_mode( - requireLiveHandle().raw, - $0 - )) } + func latLng(for point: ScreenPoint) async throws -> LatLng { + let future = try mapNativeFailure { + try NativeMap.latLngForPixel( + requireLiveHandle(), + point: point.nativeInput + ) } + return try await mapNativeFailure { try await future.value() } } - func pixel(for coordinate: LatLng) throws -> ScreenPoint { - try mapNativeFailure { try ScreenPoint(native: NativeMap.pixelForLatLng( - requireLiveHandle(), - coordinate: coordinate.nativeInput - )) } - } - - func latLng(for point: ScreenPoint) throws -> LatLng { - try mapNativeFailure { try LatLng(native: NativeMap.latLngForPixel( - requireLiveHandle(), - point: point.nativeInput - )) } - } - - func pixels(for coordinates: [LatLng]) throws -> [ScreenPoint] { - try mapNativeFailure { + func pixels(for coordinates: [LatLng]) async throws -> [ScreenPoint] { + let future = try mapNativeFailure { try NativeMap.pixelsForLatLngs( requireLiveHandle(), coordinates: coordinates.map(\.nativeInput) - ).map(ScreenPoint.init(native:)) + ) } + return try await mapNativeFailure { try await future.value() } } - func latLngs(for points: [ScreenPoint]) throws -> [LatLng] { - try mapNativeFailure { + func latLngs(for points: [ScreenPoint]) async throws -> [LatLng] { + let future = try mapNativeFailure { try NativeMap.latLngsForPixels( requireLiveHandle(), points: points.map(\.nativeInput) - ).map(LatLng.init(native:)) + ) } + return try await mapNativeFailure { try await future.value() } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Handles.swift b/bindings/swift/Sources/MaplibreNativeFFI/Handles.swift index 54add9d9c..8113e06a6 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Handles.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Handles.swift @@ -1,7 +1,7 @@ /// Its only stored property is the lock-guarded `NativeHandleState`, so the box -/// itself is safe to share. The public handles that hold a box stay -/// non-`Sendable`. +/// itself is safe to share. Each public handle chooses whether its API contract +/// permits sharing. class NativeHandleBox: @unchecked Sendable { private let state: NativeHandleState @@ -17,7 +17,7 @@ class NativeHandleBox: @unchecked Sendable { state.isClosed } - /// Runs `use` with release held off. See `NativeHandleState.withLive`. + /// Runs `use` after checking that this wrapper still owns the handle. /// Only the box's own liveness failure translates to an invalid-state /// error; a native status thrown inside `use` keeps its own status. func withLive(_ use: (Handle) throws -> T) throws -> T { diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Map.swift b/bindings/swift/Sources/MaplibreNativeFFI/Map.swift index 24e78fc11..c13fd014a 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Map.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Map.swift @@ -8,15 +8,12 @@ public enum MapMode: UInt32, Sendable, Hashable { } public struct MapOptions: Equatable, Sendable { - /// Initial logical width in UI pixels, replaced by the extent of the first - /// attached render session. + /// Initial logical width in UI pixels. public var width: UInt32 - /// Initial logical height in UI pixels, replaced by the extent of the first - /// attached render session. + /// Initial logical height in UI pixels. public var height: UInt32 /// UI-to-device pixel scale, fixed for the lifetime of the map. It selects - /// sprites, glyphs, and raster tiles; a render session attached or resized - /// with a different scale factor logs a warning. + /// sprites, glyphs, and raster tiles. public var scaleFactor: Double public var mode: MapMode /// Decodes MapLibre Tile (MLT) tiles whose integer streams use FastPFOR @@ -55,12 +52,90 @@ public struct MapOptions: Equatable, Sendable { } } -public final class MapHandle { +public struct MapLogicalExtent: Equatable, Sendable { + public var width: UInt32 + public var height: UInt32 + public var scaleFactor: Double + + public init(width: UInt32, height: UInt32, scaleFactor: Double) { + self.width = width + self.height = height + self.scaleFactor = scaleFactor + } + + var native: mln_logical_extent { + mln_logical_extent( + width: width, + height: height, + scale_factor: scaleFactor + ) + } + + init(native: mln_logical_extent) { + width = native.width + height = native.height + scaleFactor = native.scale_factor + } +} + +/// The latest immutable map state, published by every committed map command. +/// +/// A command completion reports the generation its commit published, so a +/// snapshot whose ``generation`` is at or past that value observes the commit. +public struct MapSnapshot: Equatable, Sendable { + public let generation: UInt64 + /// Debug overlays currently drawn over the map. + public let debugOptions: MapDebugOptions + public let camera: CameraOptions + public let logicalExtent: MapLogicalExtent + public let projectionMode: ProjectionMode + public let viewportOptions: MapViewportOptions + /// True once every requested style and tile resource finished loading. + public let isFullyLoaded: Bool + public let renderingStatsViewEnabled: Bool + public let needsRepaint: Bool + public let eventMask: RuntimeEventMask + public let latestRenderUpdateGeneration: UInt64 + public let tileOptions: MapTileOptions + public let bounds: BoundOptions + public let freeCameraOptions: FreeCameraOptions + + init(native: mln_map_snapshot) { + generation = native.generation + debugOptions = MapDebugOptions(rawValue: native.debug_options) + camera = CameraOptions(native: NativeCameraOptionsInput(native.camera)) + logicalExtent = MapLogicalExtent(native: native.logical_extent) + projectionMode = ProjectionMode( + native: NativeProjectionModeInput(native.projection_mode) + ) + viewportOptions = MapViewportOptions( + native: NativeMapViewportOptionsInput(native.viewport) + ) + isFullyLoaded = native.fully_loaded + renderingStatsViewEnabled = native.rendering_stats_view_enabled + needsRepaint = native.repaint_demand + eventMask = RuntimeEventMask(rawValue: native.event_mask) + latestRenderUpdateGeneration = native.latest_render_update_generation + tileOptions = MapTileOptions(native: NativeMapTileOptionsInput(native.tile)) + bounds = BoundOptions(native: NativeBoundOptionsInput(native.bounds)) + freeCameraOptions = FreeCameraOptions( + native: NativeFreeCameraOptionsInput(native.free_camera) + ) + } +} + +public struct CameraSnapshot: Equatable, Sendable { + public let generation: UInt64 + public let camera: CameraOptions +} + +public final class MapHandle: @unchecked Sendable { private let handle: NativeHandleBox + private let runtime: RuntimeHandle private let mapId: MapId - public init(runtime: RuntimeHandle, options: MapOptions) throws { - let native = try mapNativeFailure { + public init(runtime: RuntimeHandle, options: MapOptions) async throws { + let future = try mapNativeFailure { try options.nativeInput.withNativeOptions { nativeOptions in try NativeMap.create( runtime: runtime.requireLiveHandle(), @@ -68,6 +143,8 @@ public final class MapHandle { ) } } + let native = try await mapNativeFailure { try await future.value() } + self.runtime = runtime mapId = MapId(value: native.raw) handle = try NativeHandleBox(typeName: "MapHandle", handle: native) } @@ -87,12 +164,22 @@ public final class MapHandle { try handle.requireLive() } - /// Produces a `Sendable` reference to this map for attaching a render - /// session from a thread other than the map's owner thread. - public func attachRef() throws -> MapAttachRef { - // Resolve once so a closed map fails here rather than at the first attach. - _ = try requireLiveHandle() - return MapAttachRef(handle: handle) + var runtimeForOperations: RuntimeHandle { + runtime + } + + func startCommand( + _ submit: (mln_map, UnsafePointer) -> mln_status + ) throws -> NativeFuture { + let map = try requireLiveHandle() + return try NativeCompletion.startCommand { submit(map.raw, $0) } + } + + func submitCommand( + _ submit: (mln_map, UnsafePointer) -> mln_status + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { try startCommand(submit) } + return try await mapNativeFailure { try await future.value() } } /// Selects which map-originated event types this map queues. @@ -106,31 +193,37 @@ public final class MapHandle { /// only invalidation report, the two still-image types are the only reports /// that a still-image request finished, and map-loading-failed and /// map-render-error carry native failure text. - public func setEventMask(_ mask: RuntimeEventMask) throws { - try mapNativeFailure { - try NativeMap.setEventMask(handle.requireLive(), mask: mask.rawValue) - } + @discardableResult + public func setEventMask( + _ mask: RuntimeEventMask + ) async throws -> CommandCompletion { + try await submitCommand { mln_map_set_event_mask($0, mask.rawValue, $1) } } - /// The mask last set. A map that has not been narrowed reports - /// ``RuntimeEventMask/all``. Read it, change one bit, and write it back to - /// leave the other bits alone. + /// The mask in the latest immutable map snapshot. public var eventMask: RuntimeEventMask { get throws { - try mapNativeFailure { - try RuntimeEventMask( - rawValue: NativeMap.eventMask(handle.requireLive()) - ) - } + try snapshot().eventMask } } - /// Destroys this map, discarding its undrained events the way the C API - /// discards the ones it still holds. - public func close() throws { - try handle.closeOnce { handle in - try checkStatus(mln_map_destroy(handle.raw)) + /// Releases this map's public native handle. + public func close() async throws { + guard let teardown = try startClose() else { return } + try await mapNativeFailure { try await teardown.value() } + } + + private func startClose() throws -> NativeFuture? { + var teardown: NativeFuture? + try mapNativeFailure { + try handle.closeOnce { teardown = try NativeMap.release($0) } } + return teardown + } + + func closeBlockingForTests() throws { + guard let teardown = try startClose() else { return } + try mapNativeFailure { try teardown.valueBlocking() } } /// Loads a style URL. @@ -139,12 +232,14 @@ public final class MapHandle { /// returns normally here and reports through a map-loading-failed runtime /// event. A style MapLibre rejects semantically, such as one with an unknown /// `version`, produces neither an error nor an event. - public func setStyleURL(_ url: String) throws { - try mapNativeFailure { + @discardableResult + public func setStyleURL(_ url: String) async throws -> CommandCompletion { + let future = try mapNativeFailure { try NativeString.withCString(url) { url in - try checkStatus(mln_map_set_style_url(handle.requireLive().raw, url)) + try startCommand { mln_map_set_style_url($0, url, $1) } } } + return try await mapNativeFailure { try await future.value() } } /// Loads inline style JSON. @@ -153,213 +248,167 @@ public final class MapHandle { /// the same message arrives as a map-loading-failed runtime event. A style /// MapLibre rejects semantically, such as one with an unknown `version`, /// produces neither an error nor an event. - public func setStyleJSON(_ json: Data) throws { - try mapNativeFailure { + @discardableResult + public func setStyleJSON(_ json: Data) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_style_json( - handle.requireLive().raw, - arena.view(json) - )) + return try startCommand { + mln_map_set_style_json($0, arena.view(json), $1) + } } + return try await mapNativeFailure { try await future.value() } } + /// Submits a per-feature-state update. The state must contain one UTF-8 + /// JSON object. + @discardableResult public func setFeatureState( selector: FeatureStateSelector, state: Data - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try selector.nativeSelector.withNativeSelector { selector in - try checkStatus(mln_map_set_feature_state( - handle.requireLive().raw, - selector, - arena.view(state) - )) + return try selector.nativeSelector.withNativeSelector { selector in + try startCommand { + mln_map_set_feature_state($0, selector, arena.view(state), $1) + } } } + return try await mapNativeFailure { try await future.value() } } - public func featureState(selector: FeatureStateSelector) throws -> Data { - try mapNativeFailure { + /// Reads per-feature state from the map store, observing every earlier + /// accepted command. Missing state is an empty JSON object. + public func featureState( + selector: FeatureStateSelector + ) async throws -> Data { + let map = try requireLiveHandle() + let future = try mapNativeFailure { try selector.nativeSelector.withNativeSelector { selector in - try NativeQuery.featureState(handle.requireLive(), selector: selector) + try NativeCompletion.start( + { mln_map_get_feature_state(map.raw, selector, $0) }, + convert: NativeCompletion.data + ) } } + return try await mapNativeFailure { try await future.value() } } - public func removeFeatureState(selector: FeatureStateSelector) throws { - try mapNativeFailure { + /// Submits a per-feature-state removal scoped by the selector: one key, one + /// feature, or every feature in the source. + @discardableResult + public func removeFeatureState( + selector: FeatureStateSelector + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { try selector.nativeSelector.withNativeSelector { selector in - try checkStatus(mln_map_remove_feature_state( - handle.requireLive().raw, - selector - )) + try startCommand { mln_map_remove_feature_state($0, selector, $1) } } } + return try await mapNativeFailure { try await future.value() } } /// Copies the style document this map's style was last parsed from, byte for /// byte, rather than a serialization of the live style. Runtime mutations do /// not change it, and a failed parse leaves the previous document in place. /// The result is empty when no document has been parsed. - public func loadedStyleJSON() throws -> Data { - try mapNativeFailure { - try NativeStyle.copyMapData(handle.requireLive()) { - mln_map_copy_loaded_style_json($0, $1, $2, $3) - } + public func loadedStyleJSON() async throws -> Data { + let map = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_map_loaded_style_json(map.raw, $0) }, + convert: NativeCompletion.data + ) } + return try await mapNativeFailure { try await future.value() } } - /// Copies the URL this map's style was last requested from. - /// - /// ``setStyleURL(_:)`` records the URL when the request is made, before the - /// document parses, so this can disagree with ``loadedStyleJSON()`` while a - /// load is in flight or after one fails. The result is empty for inline JSON, - /// for a map that has loaded no style, and for an empty URL alike. - public func styleURL() throws -> String { - try mapNativeFailure { - try NativeStyle - .copyMapText(handle.requireLive()) { map, text, capacity, size in - mln_map_copy_style_url(map, text, capacity, size) - } + public func styleURL() async throws -> String { + let map = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_map_style_url(map.raw, $0) }, + convert: NativeCompletion.string + ) } + return try await mapNativeFailure { try await future.value() } } - public func requestRepaint() throws { - try mapNativeFailure { - try checkStatus(mln_map_request_repaint(handle.requireLive().raw)) - } - } - - public func requestStillImage() throws { - try mapNativeFailure { - try checkStatus(mln_map_request_still_image(handle.requireLive().raw)) - } + /// Requests a repaint. + @discardableResult + public func requestRepaint() async throws -> CommandCompletion { + try await submitCommand(mln_map_request_repaint) } - public func camera() throws -> CameraOptions { - try mapNativeFailure { - try CameraOptions(native: NativeCameraOptionsInput(NativeMap - .camera(handle.requireLive()))) + /// Requests and awaits one noncoalescing still-image operation. + public func requestStillImage() async throws { + let future = try mapNativeFailure { + try NativeMap.requestStillImage(handle.requireLive()) } + try await mapNativeFailure { try await future.value() } } - public func jump(to camera: CameraOptions) throws { + /// Copies the latest immutable map state. + public func snapshot() throws -> MapSnapshot { try mapNativeFailure { - try camera.nativeInput.withNativeOptions { nativeCamera in - try checkStatus(mln_map_jump_to(handle.requireLive().raw, nativeCamera)) - } + try MapSnapshot(native: NativeMap.snapshot(handle.requireLive())) } } - public func ease( - to camera: CameraOptions, - animation: AnimationOptions? = nil - ) throws { + /// Copies the camera from the latest immutable map snapshot. + public func cameraSnapshot() throws -> CameraSnapshot { try mapNativeFailure { - try camera.nativeInput.withNativeOptions { nativeCamera in - try (animation?.nativeInput ?? NativeAnimationOptionsInput()) - .withOptionalNativeOptions { nativeAnimation in - try checkStatus(mln_map_ease_to( - handle.requireLive().raw, - nativeCamera, - nativeAnimation - )) - } - } - } - } - - public func moveBy(deltaX: Double, deltaY: Double) throws { - try mapNativeFailure { - try checkStatus(mln_map_move_by(handle.requireLive().raw, deltaX, deltaY)) + let snapshot = try NativeMap.cameraSnapshot(handle.requireLive()) + return CameraSnapshot( + generation: snapshot.generation, + camera: CameraOptions( + native: NativeCameraOptionsInput(snapshot.camera) + ) + ) } } - public func moveBy( - deltaX: Double, - deltaY: Double, - animation: AnimationOptions - ) throws { - try mapNativeFailure { - try animation.nativeInput.withOptionalNativeOptions { nativeAnimation in - try checkStatus(mln_map_move_by_animated( - handle.requireLive().raw, - deltaX, - deltaY, - nativeAnimation - )) - } + /// Reads the camera after every command accepted before this call. + public func queryCamera() async throws -> CameraSnapshot { + let future = try mapNativeFailure { + try NativeMap.cameraQuery(handle.requireLive()) } + return try await mapNativeFailure { try await future.value() } } - public func scaleBy(_ scale: Double, anchor: ScreenPoint? = nil) throws { - try mapNativeFailure { - if var nativeAnchor = anchor?.nativeInput.native { - try withUnsafePointer(to: &nativeAnchor) { anchor in - try checkStatus(mln_map_scale_by( - handle.requireLive().raw, - scale, - anchor - )) - } - } else { - try checkStatus(mln_map_scale_by(handle.requireLive().raw, scale, nil)) + /// Submits one atomic camera command. + @discardableResult + public func updateCamera( + _ update: CameraUpdate + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try update.withNativeUpdate { update in + try startCommand { mln_map_update_camera($0, update, $1) } } } + return try await mapNativeFailure { try await future.value() } } - public func scaleBy( - _ scale: Double, - anchor: ScreenPoint? = nil, - animation: AnimationOptions - ) throws { - try mapNativeFailure { - try animation.nativeInput.withOptionalNativeOptions { nativeAnimation in - if var nativeAnchor = anchor?.nativeInput.native { - try withUnsafePointer(to: &nativeAnchor) { anchor in - try checkStatus(mln_map_scale_by_animated( - handle.requireLive().raw, - scale, - anchor, - nativeAnimation - )) - } - } else { - try checkStatus(mln_map_scale_by_animated( - handle.requireLive().raw, - scale, - nil, - nativeAnimation - )) - } + /// Submits one relative camera operation. + @discardableResult + public func applyCameraDelta( + _ delta: CameraDelta + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try delta.withNativeDelta { delta in + try startCommand { mln_map_apply_camera_delta($0, delta, $1) } } } + return try await mapNativeFailure { try await future.value() } } - public func cancelTransitions() throws { - try mapNativeFailure { - try checkStatus(mln_map_cancel_transitions(handle.requireLive().raw)) - } - } - - /// Marks whether a host-driven gesture is in progress. The flag stays set - /// until the host clears it, so pair every `true` with a `false`. - public func setGestureInProgress(_ inProgress: Bool) throws { - try mapNativeFailure { - try checkStatus(mln_map_set_gesture_in_progress( - handle.requireLive().raw, - inProgress - )) - } - } - - /// Returns whether a host-driven gesture is currently in progress. - public func isGestureInProgress() throws -> Bool { - try mapNativeFailure { - try NativeMap.isGestureInProgress(handle.requireLive()) - } + /// Resizes the logical map viewport. + @discardableResult + public func resize( + to extent: MapLogicalExtent + ) async throws -> CommandCompletion { + try await submitCommand { mln_map_resize($0, extent.native, $1) } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/MaplibreError.swift b/bindings/swift/Sources/MaplibreNativeFFI/MaplibreError.swift index 7b40e40a4..aada99e14 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/MaplibreError.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/MaplibreError.swift @@ -5,6 +5,8 @@ public enum MaplibreErrorKind: Sendable, Equatable { case wrongThread case unsupported case nativeError + /// No style object has the requested ID. + case notFound case unknownStatus } @@ -54,6 +56,7 @@ public struct MaplibreError: Error, Sendable, Equatable, case -3: .wrongThread case -4: .unsupported case -5: .nativeError + case -10: .notFound default: .unknownStatus } } @@ -68,3 +71,13 @@ func mapNativeFailure(_ body: () throws -> T) throws -> T { throw MaplibreError.invalidArgument(error.message) } } + +func mapNativeFailure(_ body: () async throws -> T) async throws -> T { + do { + return try await body() + } catch let failure as NativeStatusFailure { + throw MaplibreError.fromNativeFailure(failure) + } catch let error as NativeStringError { + throw MaplibreError.invalidArgument(error.message) + } +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Offline.swift b/bindings/swift/Sources/MaplibreNativeFFI/Offline.swift index b9a0c4ba5..63a1f3c5e 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Offline.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Offline.swift @@ -119,200 +119,211 @@ public struct OfflineRegionInfo: Equatable, Sendable { } public extension RuntimeHandle { - func runAmbientCacheOperationStart(_ operation: AmbientCacheOperation) throws - -> UInt64 - { - try mapNativeFailure { - try NativeOffline.runAmbientCacheOperationStart( - requireLiveHandle(), - operation: operation.rawValue - ) - } - } - - /// Starts a change to this runtime's maximum ambient cache size. MapLibre - /// evicts ambient resources to fit the new budget, so lowering it discards - /// cached resources. Offline regions are unaffected. - func setMaximumAmbientCacheSizeStart(_ size: UInt64) throws -> UInt64 { - try mapNativeFailure { - try NativeOffline.setMaximumAmbientCacheSizeStart( - requireLiveHandle(), - size: size - ) + func runAmbientCacheOperation( + _ operation: AmbientCacheOperation + ) async throws { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.startUnit { completion in + mln_runtime_run_ambient_cache_operation( + runtime.raw, + operation.rawValue, + completion + ) + } } + try await mapNativeFailure { try await future.value() } } - func discardOfflineOperation(_ operationId: UInt64) throws { - try mapNativeFailure { - try checkStatus(mln_runtime_offline_operation_discard( - requireLiveHandle().raw, - operationId - )) + /// Changes this runtime's maximum ambient cache size. MapLibre evicts + /// ambient resources to fit the new budget; offline regions are unaffected. + func setMaximumAmbientCacheSize(_ size: UInt64) async throws { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.startUnit { completion in + mln_runtime_set_maximum_ambient_cache_size( + runtime.raw, + size, + completion + ) + } } + try await mapNativeFailure { try await future.value() } } - func offlineRegionCreateStart( + func createOfflineRegion( definition: OfflineRegionDefinition, metadata: Data = Data() - ) throws -> UInt64 { - try mapNativeFailure { + ) async throws -> OfflineRegionInfo { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { try definition.nativeDefinition.withNativeDefinition { definition in - try NativeOffline.regionCreateStart( - requireLiveHandle(), - definition: definition, - metadata: metadata - ) + try metadata.withUnsafeBytes { bytes in + try NativeCompletion.start( + { completion in + mln_runtime_offline_region_create( + runtime.raw, + definition, + bytes.bindMemory(to: UInt8.self).baseAddress, + bytes.count, + completion + ) + }, + convert: Self.copyOfflineRegion + ) + } } } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionGetStart(regionId: Int64) throws -> UInt64 { - try mapNativeFailure { try NativeOffline.regionGetStart( - requireLiveHandle(), - regionId: regionId - ) } + func offlineRegion(id: Int64) async throws -> OfflineRegionInfo? { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_runtime_offline_region_get(runtime.raw, id, $0) }, + convert: Self.copyOptionalOfflineRegion + ) + } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionsListStart() throws -> UInt64 { - try mapNativeFailure { - try NativeOffline.regionsListStart(requireLiveHandle()) + func offlineRegions() async throws -> [OfflineRegionInfo] { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_runtime_offline_regions_list(runtime.raw, $0) }, + convert: Self.copyOfflineRegions + ) } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionsMergeDatabaseStart(sideDatabasePath: String) throws - -> UInt64 - { - try mapNativeFailure { try NativeOffline.regionsMergeDatabaseStart( - requireLiveHandle(), - sideDatabasePath: sideDatabasePath - ) } + func mergeOfflineRegionsDatabase( + at sideDatabasePath: String + ) async throws -> [OfflineRegionInfo] { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeString.withCString(sideDatabasePath) { path in + try NativeCompletion.start( + { + mln_runtime_offline_regions_merge_database( + runtime.raw, + path, + $0 + ) + }, + convert: Self.copyOfflineRegions + ) + } + } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionUpdateMetadataStart(regionId: Int64, - metadata: Data) throws -> UInt64 - { - try mapNativeFailure { try NativeOffline.regionUpdateMetadataStart( - requireLiveHandle(), - regionId: regionId, - metadata: metadata - ) } + func updateOfflineRegionMetadata( + id: Int64, + metadata: Data + ) async throws -> OfflineRegionInfo { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try metadata.withUnsafeBytes { bytes in + try NativeCompletion.start( + { completion in + mln_runtime_offline_region_update_metadata( + runtime.raw, + id, + bytes.bindMemory(to: UInt8.self).baseAddress, + bytes.count, + completion + ) + }, + convert: Self.copyOfflineRegion + ) + } + } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionGetStatusStart(regionId: Int64) throws -> UInt64 { - try mapNativeFailure { try NativeOffline.regionGetStatusStart( - requireLiveHandle(), - regionId: regionId - ) } + func offlineRegionStatus(id: Int64) async throws -> OfflineRegionStatus { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_runtime_offline_region_get_status(runtime.raw, id, $0) } + ) { result in + try OfflineRegionStatus( + native: NativeOfflineRegionStatus( + NativeCompletion.value(result) + ) + ) + } + } + return try await mapNativeFailure { try await future.value() } } - func offlineRegionSetObservedStart(regionId: Int64, - observed: Bool) throws -> UInt64 - { - try mapNativeFailure { - try NativeOffline.regionSetObservedStart( - requireLiveHandle(), - regionId: regionId, - observed: observed - ) + func setOfflineRegionObserved( + id: Int64, + observed: Bool + ) async throws { + try await offlineUnit { + mln_runtime_offline_region_set_observed($0, id, observed, $1) } } - func offlineRegionSetDownloadStateStart( - regionId: Int64, + func setOfflineRegionDownloadState( + id: Int64, state: OfflineRegionDownloadState - ) throws -> UInt64 { - try mapNativeFailure { try NativeOffline.regionSetDownloadStateStart( - requireLiveHandle(), - regionId: regionId, - state: state.rawValue - ) } - } - - func offlineRegionInvalidateStart(regionId: Int64) throws -> UInt64 { - try mapNativeFailure { try NativeOffline.regionInvalidateStart( - requireLiveHandle(), - regionId: regionId - ) } - } - - func offlineRegionDeleteStart(regionId: Int64) throws -> UInt64 { - try mapNativeFailure { - try NativeOffline.regionDeleteStart( - requireLiveHandle(), - regionId: regionId + ) async throws { + try await offlineUnit { + mln_runtime_offline_region_set_download_state( + $0, + id, + state.rawValue, + $1 ) } } - func offlineRegionCreateTakeResult(operationId: UInt64) throws - -> OfflineRegionInfo - { - try mapNativeFailure { - let info = try NativeOffline.regionCreateTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return OfflineRegionInfo(native: info) + func invalidateOfflineRegion(id: Int64) async throws { + try await offlineUnit { + mln_runtime_offline_region_invalidate($0, id, $1) } } - func offlineRegionGetTakeResult(operationId: UInt64) throws - -> OfflineRegionInfo? - { - try mapNativeFailure { - let info = try NativeOffline.regionGetTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return info.map(OfflineRegionInfo.init(native:)) + func deleteOfflineRegion(id: Int64) async throws { + try await offlineUnit { + mln_runtime_offline_region_delete($0, id, $1) } } - func offlineRegionsListTakeResult(operationId: UInt64) throws - -> [OfflineRegionInfo] - { - try mapNativeFailure { - let regions = try NativeOffline.regionsListTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return regions.map(OfflineRegionInfo.init(native:)) + private func offlineUnit( + _ call: (mln_runtime, UnsafePointer) -> mln_status + ) async throws { + let runtime = try requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.startUnit { call(runtime.raw, $0) } } + try await mapNativeFailure { try await future.value() } } - func offlineRegionsMergeDatabaseTakeResult(operationId: UInt64) throws - -> [OfflineRegionInfo] - { - try mapNativeFailure { - let regions = try NativeOffline.regionsMergeDatabaseTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return regions.map(OfflineRegionInfo.init(native:)) - } + private static func copyOfflineRegion( + _ result: UnsafePointer + ) throws -> OfflineRegionInfo { + let raw: mln_offline_region_info = try NativeCompletion.value(result) + return try OfflineRegionInfo(native: NativeOfflineRegionInfo(copying: raw)) } - func offlineRegionUpdateMetadataTakeResult(operationId: UInt64) throws - -> OfflineRegionInfo - { - try mapNativeFailure { - let info = try NativeOffline.regionUpdateMetadataTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return OfflineRegionInfo(native: info) - } + private static func copyOptionalOfflineRegion( + _ result: UnsafePointer + ) throws -> OfflineRegionInfo? { + guard result.pointee.value_count > 0 else { return nil } + return try copyOfflineRegion(result) } - func offlineRegionGetStatusTakeResult(operationId: UInt64) throws - -> OfflineRegionStatus - { - try mapNativeFailure { - let status = try NativeOffline.regionGetStatusTakeResult( - requireLiveHandle(), - operationId: operationId - ) - return OfflineRegionStatus(native: status) + private static func copyOfflineRegions( + _ result: UnsafePointer + ) throws -> [OfflineRegionInfo] { + try NativeCompletion.values(result, as: mln_offline_region_info.self).map { + try OfflineRegionInfo(native: NativeOfflineRegionInfo(copying: $0)) } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Projection.swift b/bindings/swift/Sources/MaplibreNativeFFI/Projection.swift index f49fecb00..93f62654d 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Projection.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Projection.swift @@ -12,14 +12,27 @@ public struct ProjectedMeters: Equatable, Sendable { } } -/// An any-thread projection snapshot whose native calls are serialized. +/// A standalone projection copied from a map transform at creation. +/// +/// Every call after creation, including close, is synchronous, runs on the +/// calling thread, and is serialized by a native lock, so a projection is +/// usable from any thread. A projection never observes map changes made after +/// its creation and remains usable after its source map and runtime close. public final class MapProjectionHandle: @unchecked Sendable { private let handle: NativeHandleBox - public init(map: MapHandle) throws { - let projection = try mapNativeFailure { - try NativeProjection.create(map.requireLiveHandle()) + public init(map: MapHandle) async throws { + let nativeMap = try map.requireLiveHandle() + let future = try mapNativeFailure { + try NativeCompletion.start( + { mln_map_projection_create(nativeMap.raw, $0) } + ) { result in + try NativeMapProjectionHandle( + raw: NativeCompletion.value(result, as: mln_map_projection.self) + ) + } } + let projection = try await mapNativeFailure { try await future.value() } handle = try NativeHandleBox( typeName: "MapProjectionHandle", handle: projection @@ -30,28 +43,39 @@ public final class MapProjectionHandle: @unchecked Sendable { handle.isClosed } + /// Closes this projection. The native call waits for calls already running + /// on other threads before it retires the handle. public func close() throws { - try handle.closeOnce { projection in - try checkStatus(mln_map_projection_destroy(projection.raw)) + try mapNativeFailure { + try handle.closeOnce { projection in + try checkStatus(mln_map_projection_close(projection.raw)) + } } } + /// Copies the projection camera, observing every earlier setter. public func camera() throws -> CameraOptions { try mapNativeFailure { - try handle.withLive { projection in - try CameraOptions(native: NativeCameraOptionsInput(NativeProjection - .camera(projection))) + let native = try handle.withLive { projection in + try NativeMemory + .withTemporary(mln_camera_options_default()) { camera in + try checkStatus(mln_map_projection_get_camera( + projection.raw, camera + )) + }.value } + return CameraOptions(native: NativeCameraOptionsInput(native)) } } + /// Applies a camera update before returning. The source map's camera is + /// unaffected. public func setCamera(_ camera: CameraOptions) throws { try mapNativeFailure { try camera.nativeInput.withNativeOptions { nativeCamera in try handle.withLive { projection in try checkStatus(mln_map_projection_set_camera( - projection.raw, - nativeCamera + projection.raw, nativeCamera )) } } @@ -62,21 +86,16 @@ public final class MapProjectionHandle: @unchecked Sendable { _ coordinates: [LatLng], padding: EdgeInsets = EdgeInsets(top: 0, left: 0, bottom: 0, right: 0) ) throws { + guard !coordinates.isEmpty else { + throw MaplibreError.invalidArgument("visible coordinates cannot be empty") + } try mapNativeFailure { - guard !coordinates.isEmpty else { - throw MaplibreError - .invalidArgument("visible coordinates cannot be empty") - } let nativeCoordinates = coordinates.map(\.nativeInput.native) try nativeCoordinates.withUnsafeBufferPointer { buffer in - guard let baseAddress = buffer.baseAddress else { - throw MaplibreError - .invalidArgument("visible coordinates cannot be empty") - } try handle.withLive { projection in try checkStatus(mln_map_projection_set_visible_coordinates( projection.raw, - baseAddress, + buffer.baseAddress, buffer.count, padding.nativeInput.native )) @@ -104,24 +123,29 @@ public final class MapProjectionHandle: @unchecked Sendable { public func pixel(for coordinate: LatLng) throws -> ScreenPoint { try mapNativeFailure { - try handle.withLive { projection in - try ScreenPoint(native: NativeScreenPoint(NativeProjection - .pixelForLatLng( - projection, - coordinate: coordinate.nativeInput.native - ))) + let point = try handle.withLive { projection in + try NativeMemory + .withTemporary(mln_screen_point()) { point in + try checkStatus(mln_map_projection_pixel_for_lat_lng( + projection.raw, coordinate.nativeInput.native, point + )) + }.value } + return ScreenPoint(native: NativeScreenPoint(point)) } } public func latLng(for point: ScreenPoint) throws -> LatLng { try mapNativeFailure { - try handle.withLive { projection in - try LatLng(native: NativeLatLng(NativeProjection.latLngForPixel( - projection, - point: point.nativeInput.native - ))) + let coordinate = try handle.withLive { projection in + try NativeMemory + .withTemporary(mln_lat_lng()) { coordinate in + try checkStatus(mln_map_projection_lat_lng_for_pixel( + projection.raw, point.nativeInput.native, coordinate + )) + }.value } + return LatLng(native: NativeLatLng(coordinate)) } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Query.swift b/bindings/swift/Sources/MaplibreNativeFFI/Query.swift index 177d81626..fc1ad4b99 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Query.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Query.swift @@ -99,35 +99,45 @@ public extension RenderSessionHandle { func queryRenderedFeatures( geometry: RenderedQueryGeometry, options: RenderedFeatureQueryOptions = RenderedFeatureQueryOptions() - ) throws -> [QueriedFeature] { - try mapNativeFailure { - try geometry.nativeGeometry.withNativeGeometry { nativeGeometry in - try options.nativeOptions.withNativeOptions { nativeOptions in - try NativeQuery.renderedFeatures( - session: requireLiveHandle(), - geometry: nativeGeometry, - options: nativeOptions + ) async throws -> [QueriedFeature] { + let future = try mapNativeFailure { + let session = try requireLiveHandle() + return try geometry.nativeGeometry.withNativeGeometry { geometry in + try options.nativeOptions.withNativeOptions { options in + try NativeCompletion.start( + { + mln_render_session_query_rendered_features( + session.raw, geometry, options, $0 + ) + }, + convert: NativeQuery.copyQueriedFeatures ) } } } + return try await mapNativeFailure { try await future.value() } } func querySourceFeatures( sourceId: String, options: SourceFeatureQueryOptions = SourceFeatureQueryOptions() - ) throws -> [QueriedFeature] { - try mapNativeFailure { + ) async throws -> [QueriedFeature] { + let future = try mapNativeFailure { + let session = try requireLiveHandle() let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - return try options.nativeOptions.withNativeOptions { nativeOptions in - try NativeQuery.sourceFeatures( - session: self.requireLiveHandle(), - sourceId: arena.view(sourceId), - options: nativeOptions + return try options.nativeOptions.withNativeOptions { options in + try NativeCompletion.start( + { + mln_render_session_query_source_features( + session.raw, arena.view(sourceId), options, $0 + ) + }, + convert: NativeQuery.copyQueriedFeatures ) } } + return try await mapNativeFailure { try await future.value() } } func queryFeatureExtension( @@ -136,23 +146,33 @@ public extension RenderSessionHandle { extensionName: String, extensionField: String, arguments: Data? = nil - ) throws -> Data { - try mapNativeFailure { + ) async throws -> Data { + let future = try mapNativeFailure { + let session = try requireLiveHandle() let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - let call = { (arguments: UnsafePointer?) throws in - try NativeQuery.featureExtensions( - session: self.requireLiveHandle(), - sourceId: arena.view(sourceId), - feature: arena.view(feature), - extensionName: arena.view(extensionName), - extensionField: arena.view(extensionField), - arguments: arguments + let start = { (arguments: UnsafePointer?) throws in + try NativeCompletion.start( + { + mln_render_session_query_feature_extensions( + session.raw, + arena.view(sourceId), + arena.view(feature), + arena.view(extensionName), + arena.view(extensionField), + arguments, + $0 + ) + }, + convert: NativeCompletion.data ) } - guard let arguments else { return try call(nil) } - var argumentsView = arena.view(arguments) - return try withUnsafePointer(to: &argumentsView, call) + if let arguments { + var view = arena.view(arguments) + return try withUnsafePointer(to: &view, start) + } + return try start(nil) } + return try await mapNativeFailure { try await future.value() } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Render.swift b/bindings/swift/Sources/MaplibreNativeFFI/Render.swift index b6ece5b6f..78efead43 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Render.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Render.swift @@ -1,7 +1,7 @@ internal import CMaplibreNativeC import Foundation -public struct RenderTargetExtent: Equatable, Sendable { +public struct RenderTargetExtent: Hashable, Sendable { public var width: UInt32 public var height: UInt32 public var scaleFactor: Double @@ -91,17 +91,18 @@ public struct VulkanContextDescriptor: Equatable, Sendable { } } -/// How a session's OpenGL context relates to the thread that attached it. +/// How a session's OpenGL context relates to its driver and host graphics +/// state. /// /// A shared session leaves the thread as it found it: every render makes the /// session context current and restores whatever was current before. The /// session context joins the host share group named by the descriptor, so a /// host may hand the session a texture and sample it from its own context. /// -/// A dedicated session owns the thread. It makes its context current once and -/// keeps it current between renders, and it joins no share group. Use this when -/// a thread exists to drive one render session and runs no other graphics work, -/// such as an Android host that renders into a SurfaceView. +/// A dedicated session owns its driver thread's context. It keeps the context +/// current between renders and joins no share group. The driver may be a native +/// core worker or a dedicated host thread, such as an Android host that renders +/// into a SurfaceView. public enum OpenGLContextOwnership: Sendable, Hashable { /// The session shares its thread with host graphics work. case shared @@ -221,17 +222,36 @@ public struct EglContextDescriptor: Equatable, Sendable { } } +public enum WebGLContextDescriptor: Equatable, Sendable { + case existing(context: Int32) + case transferredCanvas(selector: String) + + var nativeInput: NativeWebGLContextDescriptor { + switch self { + case let .existing(context): + NativeWebGLContextDescriptor( + kind: .existing, + context: context, + canvasSelector: "" + ) + case let .transferredCanvas(selector): + NativeWebGLContextDescriptor( + kind: .transferredCanvas, + context: 0, + canvasSelector: selector + ) + } + } +} + public struct OpenGLContextDescriptor: Equatable, Sendable { - /// The context provider that supplies the session context. public enum Platform: Equatable, Sendable { case wgl(WglContextDescriptor) case egl(EglContextDescriptor) + case webGL(WebGLContextDescriptor) } public var platform: Platform - /// Whether the session shares its thread with host graphics work. WGL and - /// EGL surface sessions support both. A texture session hands its texture to - /// the host, so it is shared only. public var ownership: OpenGLContextOwnership public init( @@ -256,6 +276,13 @@ public struct OpenGLContextDescriptor: Equatable, Sendable { Self(platform: .egl(context), ownership: ownership) } + public static func webGL( + _ context: WebGLContextDescriptor, + ownership: OpenGLContextOwnership = .shared + ) -> Self { + Self(platform: .webGL(context), ownership: ownership) + } + var nativeInput: NativeOpenGLContextDescriptor { switch platform { case let .wgl(descriptor): @@ -268,6 +295,11 @@ public struct OpenGLContextDescriptor: Equatable, Sendable { platform: .egl(descriptor.nativeInput), ownershipRawValue: ownership.rawValue ) + case let .webGL(descriptor): + NativeOpenGLContextDescriptor( + platform: .webGL(descriptor.nativeInput), + ownershipRawValue: ownership.rawValue + ) } } } @@ -532,817 +564,1131 @@ public struct OpenGLBorrowedTextureDescriptor: Equatable, Sendable { } } -/// The outcome of a ``RenderSessionHandle/renderUpdate()`` call. +public struct WebGPUContextDescriptor: Equatable, Sendable { + public var instance: NativePointer + public var device: NativePointer + public var queue: NativePointer + + public init( + instance: NativePointer = .null, + device: NativePointer, + queue: NativePointer = .null + ) { + self.instance = instance + self.device = device + self.queue = queue + } + + var nativeInput: NativeWebGPUContextDescriptor { + NativeWebGPUContextDescriptor( + instanceAddress: instance.addressBitPattern, + deviceAddress: device.addressBitPattern, + queueAddress: queue.addressBitPattern + ) + } +} + +public struct WebGPUSurfaceDescriptor: Equatable, Sendable { + public var extent: RenderTargetExtent + public var context: WebGPUContextDescriptor + public var surface: NativePointer + public var format: UInt32 + + public init( + extent: RenderTargetExtent, + context: WebGPUContextDescriptor, + surface: NativePointer, + format: UInt32 + ) { + self.extent = extent + self.context = context + self.surface = surface + self.format = format + } + + var nativeInput: NativeWebGPUSurfaceDescriptorInput { + .init( + extent: extent.nativeInput, + context: context.nativeInput, + surfaceAddress: surface.addressBitPattern, + format: format + ) + } +} + +public struct WebGPUOwnedTextureDescriptor: Equatable, Sendable { + public var extent: RenderTargetExtent + public var context: WebGPUContextDescriptor + + public init(extent: RenderTargetExtent, context: WebGPUContextDescriptor) { + self.extent = extent + self.context = context + } + + var nativeInput: NativeWebGPUOwnedTextureDescriptorInput { + .init(extent: extent.nativeInput, context: context.nativeInput) + } +} + +public struct WebGPUBorrowedTextureDescriptor: Equatable, Sendable { + public var extent: RenderTargetExtent + public var physicalWidth: UInt32 + public var physicalHeight: UInt32 + public var context: WebGPUContextDescriptor + public var texture: NativePointer + public var textureView: NativePointer + public var format: UInt32 + + public init( + extent: RenderTargetExtent, + physicalWidth: UInt32, + physicalHeight: UInt32, + context: WebGPUContextDescriptor, + texture: NativePointer, + textureView: NativePointer, + format: UInt32 + ) { + self.extent = extent + self.physicalWidth = physicalWidth + self.physicalHeight = physicalHeight + self.context = context + self.texture = texture + self.textureView = textureView + self.format = format + } + + var nativeInput: NativeWebGPUBorrowedTextureDescriptorInput { + .init( + extent: extent.nativeInput, + physicalWidth: physicalWidth, + physicalHeight: physicalHeight, + context: context.nativeInput, + textureAddress: texture.addressBitPattern, + textureViewAddress: textureView.addressBitPattern, + format: format + ) + } +} + +public enum RenderDriver: UInt32, Sendable, Hashable { + case coreWorker = 1 + case callerGraphicsThread = 2 +} + +public struct RenderSessionAttachOptions: Sendable, Hashable { + public var driver: RenderDriver + public var requestedTextureRingDepth: UInt32 + + public init( + driver: RenderDriver, + requestedTextureRingDepth: UInt32 = 0 + ) { + self.driver = driver + self.requestedTextureRingDepth = requestedTextureRingDepth + } + + func withNative( + frameWake: mln_wake, + driverWorkWake: mln_wake, + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + var value = mln_render_session_attach_options_default() + value.driver = driver.rawValue + value.requested_texture_ring_depth = requestedTextureRingDepth + value.frame_wake = frameWake + value.driver_work_wake = driverWorkWake + return try withUnsafePointer(to: &value, body) + } +} + +/// A render session that native code is still attaching and the task that +/// reports the attachment result. +/// +/// A caller-graphics-thread host services driver work through ``session`` +/// while ``completion`` is pending. +public struct RenderSessionAttachment: Sendable { + public let session: RenderSessionHandle + public let completion: Task + + fileprivate init( + session: RenderSessionHandle, + completion: Task + ) { + self.session = session + self.completion = completion + } +} + +public struct RenderSessionCapabilities: Sendable, Hashable { + public struct Features: OptionSet, Sendable, Hashable { + public let rawValue: UInt32 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let frameAcquisition = Self(rawValue: 1 << 0) + public static let readback = Self(rawValue: 1 << 1) + public static let consumerSync = Self(rawValue: 1 << 2) + public static let presentation = Self(rawValue: 1 << 3) + } + + public let driver: RenderDriver + public let textureRingDepth: UInt32 + public let features: Features +} + public enum RenderResult: Sendable, Hashable { - /// The call rendered a frame into the render target. case rendered - /// The call produced no frame. case noUpdate - /// The map has not applied the session's current size yet. case sizePending - /// The render target had no frame to draw into. case targetNotReady + case superseded + case deadlineMissed case unknown(UInt32) - public static func fromNative(_ rawValue: UInt32) -> Self { - switch rawValue { + public static func fromNative(_ value: UInt32) -> Self { + switch value { case 0: .rendered case 1: .noUpdate case 2: .sizePending case 3: .targetNotReady - default: .unknown(rawValue) + case 4: .superseded + case 5: .deadlineMissed + default: .unknown(value) } } } -/// The outcome of a ``RenderSessionHandle/renderUpdate()`` call, with the -/// repaint signal the render carried. -public struct RenderUpdate: Equatable, Sendable { - /// Which outcome the call reached. +public enum RenderSessionState: UInt32, Sendable, Hashable { + case attaching = 1 + case attached = 2 + case detaching = 3 + case detached = 4 + case targetLost = 5 + case abandoned = 6 +} + +public struct FrameDemand: Sendable, Hashable { + public struct Options: OptionSet, Sendable, Hashable { + public let rawValue: UInt32 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let ifNeeded = Self(rawValue: 1 << 0) + public static let present = Self(rawValue: 1 << 1) + } + + public var options: Options + public var token: UInt64 + public var coalescingBoundary: UInt64 + public var timeoutNanoseconds: UInt64 + + public init( + options: Options = .ifNeeded, + token: UInt64 = 0, + coalescingBoundary: UInt64 = 0, + timeoutNanoseconds: UInt64 = 0 + ) { + self.options = options + self.token = token + self.coalescingBoundary = coalescingBoundary + self.timeoutNanoseconds = timeoutNanoseconds + } + + var native: mln_frame_demand { + var value = mln_frame_demand_default() + value.flags = options.rawValue + value.token = token + value.coalescing_boundary = coalescingBoundary + value.timeout_ns = timeoutNanoseconds + return value + } +} + +public struct RenderFrameResult: Sendable, Hashable { public let result: RenderResult + public let token: UInt64 + public let mapUpdateGeneration: UInt64 + public let extentGeneration: UInt64 + public let frameGeneration: UInt64 /// Whether the map asked for another frame while it rendered this one, as - /// during an ongoing camera transition. True only when ``result`` is - /// ``RenderResult/rendered``; the same signal the map - /// render-frame-finished event carries in ``RenderFrameEvent/needsRepaint``, - /// delivered here without the event round trip. + /// during an ongoing camera transition. Set only when ``result`` is + /// ``RenderResult/rendered``, and false for every other outcome. This is + /// the same signal the map render-frame-finished event carries in + /// ``RenderFrameEvent/needsRepaint``, delivered with the frame result so a + /// host can re-arm its frame loop without the runtime event round trip. public let needsRepaint: Bool + + fileprivate init(_ value: mln_render_frame_result) { + result = .fromNative(value.disposition) + token = value.token + mapUpdateGeneration = value.map_update_generation + extentGeneration = value.extent_generation + frameGeneration = value.frame_generation + needsRepaint = value.needs_repaint + } } -/// A render session, affine to the thread that attached it. -/// -/// The session holds no Swift-level retention of its map. Native keeps the map -/// alive instead: destroying a map fails while a session is attached to it. -public final class RenderSessionHandle { - private let handle: NativeHandleBox +public struct RenderSessionSnapshot: Sendable, Hashable { + public let state: RenderSessionState? + public let driver: RenderDriver? + public let latestResult: RenderResult + public let extent: RenderTargetExtent + public let generation: UInt64 + public let mapUpdateGeneration: UInt64 + public let renderedUpdateGeneration: UInt64 + public let extentGeneration: UInt64 + public let frameGeneration: UInt64 + public let latestDemandToken: UInt64 + public let pendingDemandCount: UInt32 + public let acquiredFrameCount: UInt32 + public let targetReady: Bool + public let pendingChanges: Bool +} - init(handle session: NativeRenderSessionHandle) throws { - handle = try NativeHandleBox( - typeName: "RenderSessionHandle", - handle: session - ) - } +public enum GPUSynchronization: Sendable, Hashable { + case cpuComplete + case metalSharedEvent(NativePointer, value: UInt64) + case vulkanTimelineSemaphore(NativePointer, value: UInt64) + case openGLFence(NativePointer) + case webGPUToken(NativePointer, value: UInt64) - public var isClosed: Bool { - handle.isClosed + var native: mln_gpu_sync { + var value = mln_gpu_sync_default() + switch self { + case .cpuComplete: + break + case let .metalSharedEvent(object, signal): + value.kind = MLN_GPU_SYNC_METAL_SHARED_EVENT.rawValue + value.object = object.unsafeMutableRawPointer + value.value = signal + case let .vulkanTimelineSemaphore(object, signal): + value.kind = MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE.rawValue + value.object = object.unsafeMutableRawPointer + value.value = signal + case let .openGLFence(object): + value.kind = MLN_GPU_SYNC_OPENGL_FENCE.rawValue + value.object = object.unsafeMutableRawPointer + case let .webGPUToken(object, signal): + value.kind = MLN_GPU_SYNC_WEBGPU_TOKEN.rawValue + value.object = object.unsafeMutableRawPointer + value.value = signal + } + return value } +} - func requireLiveHandle() throws -> NativeRenderSessionHandle { - try handle.requireLive() +public struct RenderAbandonResult: Sendable, Hashable { + public enum Disposition: UInt32, Sendable, Hashable { + case clean = 0 + case quarantined = 1 } - public func close() throws { - try handle.closeOnce { session in - try checkStatus(mln_render_session_destroy(session.raw)) - } + public let disposition: Disposition + public let quarantinedResourceCount: UInt32 +} + +public struct MetalOwnedTextureFrame: Sendable, Hashable { + public let generation: UInt64 + public let width: UInt32 + public let height: UInt32 + public let scaleFactor: Double + public let frameID: UInt64 + public let texture: NativePointer + public let device: NativePointer + public let pixelFormat: UInt64 +} + +public struct VulkanOwnedTextureFrame: Sendable, Hashable { + public let generation: UInt64 + public let width: UInt32 + public let height: UInt32 + public let scaleFactor: Double + public let frameID: UInt64 + public let image: NativePointer + public let imageView: NativePointer + public let device: NativePointer + public let format: UInt32 + public let layout: UInt32 +} + +public struct OpenGLOwnedTextureFrame: Sendable, Hashable { + public let generation: UInt64 + public let width: UInt32 + public let height: UInt32 + public let scaleFactor: Double + public let frameID: UInt64 + public let texture: UInt32 + public let target: UInt32 + public let internalFormat: UInt32 + public let format: UInt32 + public let type: UInt32 +} + +public struct WebGPUOwnedTextureFrame: Sendable, Hashable { + public let generation: UInt64 + public let width: UInt32 + public let height: UInt32 + public let scaleFactor: Double + public let frameID: UInt64 + public let texture: NativePointer + public let textureView: NativePointer + public let device: NativePointer + public let format: UInt32 +} + +public final class AcquiredFrameHandle: @unchecked Sendable { + private let lock = NSLock() + private var handle: mln_acquired_frame + + fileprivate init(handle: mln_acquired_frame) { + self.handle = handle } - /// Resizes this attached render session. - /// - /// Surface and owned-texture sessions resize in place. Borrowed texture - /// targets throw an unsupported-feature error; hand over a new texture with - /// the backend's borrowed-texture target setter instead. - /// - /// The session keeps its renderer, along with the tile pyramid, glyph and - /// image atlases, and symbol placement. A scale factor change retires the - /// renderer instead, because shaders are compiled for one pixel ratio. - /// Map-owned feature state survives either way. - public func resize(width: UInt32, height: UInt32, - scaleFactor: Double) throws - { - try mapNativeFailure { - try checkStatus(mln_render_session_resize( - handle.requireLive().raw, - width, - height, - scaleFactor + deinit { + let leaked = lock.withLock { handle } + if leaked != 0 { + NativeHandleLeakReporter.report(NativeHandleLeak( + typeName: "AcquiredFrameHandle", + handle: leaked, + detail: "release(consumerCompletion:) was not called" )) } } - /// Presents this attached surface session through a new surface, keeping the - /// session's renderer and its cached state. - /// - /// The descriptor's extent applies as a resize does. A `context.device` that - /// is neither nil nor this session's device throws an invalid-argument error - /// and leaves this session rendering into the surface it has. - public func setMetalSurfaceTarget( - _ descriptor: MetalSurfaceDescriptor - ) throws { + public var isReleased: Bool { + lock.withLock { handle == 0 } + } + + public func result() throws -> RenderFrameResult { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_metal_surface_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_render_frame_result() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_acquired_frame_get_result( + requireLiveLocked(), + &value )) + return RenderFrameResult(value) } } } - /// Presents this attached surface session through a new surface. - /// - /// The outgoing `VkSurfaceKHR` must still be valid: this session holds a - /// swapchain built from it, and Vulkan destroys every swapchain before its - /// surface. - public func setVulkanSurfaceTarget( - _ descriptor: VulkanSurfaceDescriptor - ) throws { + public func producerSynchronization() throws -> GPUSynchronization { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_vulkan_surface_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_gpu_sync_default() + try checkStatus(mln_acquired_frame_get_producer_sync( + requireLiveLocked(), &value )) + let object = NativePointer(bitPattern: UInt(bitPattern: value.object)) + switch value.kind { + case MLN_GPU_SYNC_CPU_COMPLETE.rawValue: + return .cpuComplete + case MLN_GPU_SYNC_METAL_SHARED_EVENT.rawValue: + return .metalSharedEvent(object, value: value.value) + case MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE.rawValue: + return .vulkanTimelineSemaphore(object, value: value.value) + case MLN_GPU_SYNC_OPENGL_FENCE.rawValue: + return .openGLFence(object) + case MLN_GPU_SYNC_WEBGPU_TOKEN.rawValue: + return .webGPUToken(object, value: value.value) + default: + throw NativeStatusFailure.swiftNativeError( + "Unknown producer synchronization kind" + ) + } } } } - /// Presents this attached surface session through a new surface. - /// - /// The new surface is made current on the next render, so a host may hand - /// over a replacement for one it has already destroyed. The next - /// ``renderUpdate()`` reports an unusable surface, not this call. - public func setOpenGLSurfaceTarget( - _ descriptor: OpenGLSurfaceDescriptor - ) throws { + public func withMetalTexture( + _ body: (MetalOwnedTextureFrame) throws -> Result + ) throws -> Result { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_opengl_surface_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_metal_owned_texture_frame() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_acquired_frame_get_metal_texture( + requireLiveLocked(), &value + )) + return try body(MetalOwnedTextureFrame( + generation: value.generation, + width: value.width, + height: value.height, + scaleFactor: value.scale_factor, + frameID: value.frame_id, + texture: NativePointer(bitPattern: UInt(bitPattern: value.texture)), + device: NativePointer(bitPattern: UInt(bitPattern: value.device)), + pixelFormat: value.pixel_format )) } } } - /// Renders this attached texture session into a new caller-owned texture, - /// keeping the session's renderer. A scale factor change starts a new one. - /// - /// The replacement must belong to the device and carry the pixel format this - /// session attached with; a mismatch throws and leaves this session rendering - /// into the texture it has. The caller keeps the replacement valid until the - /// next replacement, detach, or close. The outgoing texture is neither read - /// nor released here. - public func setMetalBorrowedTextureTarget( - _ descriptor: MetalBorrowedTextureDescriptor - ) throws { + public func withVulkanTexture( + _ body: (VulkanOwnedTextureFrame) throws -> Result + ) throws -> Result { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_metal_borrowed_texture_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_vulkan_owned_texture_frame() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_acquired_frame_get_vulkan_texture( + requireLiveLocked(), &value + )) + return try body(VulkanOwnedTextureFrame( + generation: value.generation, + width: value.width, + height: value.height, + scaleFactor: value.scale_factor, + frameID: value.frame_id, + image: NativePointer(bitPattern: UInt(bitPattern: value.image)), + imageView: NativePointer(bitPattern: UInt(bitPattern: value + .image_view)), + device: NativePointer(bitPattern: UInt(bitPattern: value.device)), + format: value.format, + layout: value.layout )) } } } - /// Renders this attached texture session into a new caller-owned image. - /// - /// The replacement must carry the format and both layouts this session - /// attached with, since its render pass was built around them. - public func setVulkanBorrowedTextureTarget( - _ descriptor: VulkanBorrowedTextureDescriptor - ) throws { + public func withOpenGLTexture( + _ body: (OpenGLOwnedTextureFrame) throws -> Result + ) throws -> Result { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_vulkan_borrowed_texture_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_opengl_owned_texture_frame() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_acquired_frame_get_opengl_texture( + requireLiveLocked(), &value + )) + return try body(OpenGLOwnedTextureFrame( + generation: value.generation, + width: value.width, + height: value.height, + scaleFactor: value.scale_factor, + frameID: value.frame_id, + texture: value.texture, + target: value.target, + internalFormat: value.internal_format, + format: value.format, + type: value.type )) } } } - /// Renders this attached texture session into a new caller-owned texture. - /// - /// The replacement must belong to the context this session attached with, or - /// one in its share group, and the host context must be current on this - /// thread. - public func setOpenGLBorrowedTextureTarget( - _ descriptor: OpenGLBorrowedTextureDescriptor - ) throws { + public func withWebGPUTexture( + _ body: (WebGPUOwnedTextureFrame) throws -> Result + ) throws -> Result { try mapNativeFailure { - try descriptor.nativeInput.withNativeDescriptor { nativeDescriptor in - try checkStatus(mln_opengl_borrowed_texture_set_target( - handle.requireLive().raw, - nativeDescriptor + try lock.withLock { + var value = mln_webgpu_owned_texture_frame() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_acquired_frame_get_webgpu_texture( + requireLiveLocked(), &value + )) + return try body(WebGPUOwnedTextureFrame( + generation: value.generation, + width: value.width, + height: value.height, + scaleFactor: value.scale_factor, + frameID: value.frame_id, + texture: NativePointer(bitPattern: UInt(bitPattern: value.texture)), + textureView: NativePointer( + bitPattern: UInt(bitPattern: value.texture_view) + ), + device: NativePointer(bitPattern: UInt(bitPattern: value.device)), + format: value.format )) } } } - /// Renders the latest available map render update, reporting the outcome. - /// - /// Each result names the wake that a host waits for before it calls again: - /// - /// - ``RenderResult/rendered``: the target holds a new frame. The map retains - /// its latest update, so a host redraws on demand after a resize or a - /// surface expose, and paces a frame loop on the map - /// render-update-available event. - /// - ``RenderResult/noUpdate``: the call produced no frame. The map either - /// has no update yet, or the Metal backend has not created an owned texture - /// because content is not ready. Wait for the map render-update-available - /// event. - /// - ``RenderResult/sizePending``: the session resized and the map, which - /// applies its size on its own thread, is still behind. The map publishes - /// an update for the new size on its own, so wait for the next map - /// render-update-available event. - /// - ``RenderResult/targetNotReady``: the render target had no frame - /// available, such as a Metal surface whose next drawable is nil. No map - /// update resolves this, so wait for a host event that changes the target, - /// or retry after a delay. - /// - /// The returned ``RenderUpdate/needsRepaint`` reports whether the map asked - /// for another frame while it rendered this one, so a host can re-arm its - /// frame loop before it drains events. - @discardableResult - public func renderUpdate() throws -> RenderUpdate { + public func release( + consumerCompletion: GPUSynchronization = .cpuComplete + ) throws { try mapNativeFailure { - var result = MLN_RENDER_RESULT_RENDERED - var needsRepaint = false - try checkStatus(mln_render_session_render_update( - handle.requireLive().raw, - &result, - &needsRepaint - )) - return RenderUpdate( - result: RenderResult.fromNative(result.rawValue), - needsRepaint: needsRepaint - ) + try lock.withLock { + guard handle != 0 else { return } + var completion = consumerCompletion.native + try checkStatus(mln_acquired_frame_release(&handle, &completion)) + } } } - public func detach() throws { - try mapNativeFailure { - try checkStatus(mln_render_session_detach(handle.requireLive().raw)) + private func requireLiveLocked() throws -> mln_acquired_frame { + guard handle != 0 else { + throw NativeStatusFailure.swiftNativeError("Acquired frame is released") } + return handle } +} - public func reduceMemoryUse() throws { - try mapNativeFailure { - try checkStatus(mln_render_session_reduce_memory_use(handle - .requireLive().raw)) - } - } +public final class RenderSessionHandle: @unchecked Sendable { + private let handle: NativeHandleBox + private let frameWake: NativeWakeState + private let driverWorkWake: NativeWakeState - public func clearData() throws { - try mapNativeFailure { - try checkStatus(mln_render_session_clear_data(handle.requireLive().raw)) - } + fileprivate init( + session: NativeRenderSessionHandle, + frameWake: NativeWakeState, + driverWorkWake: NativeWakeState + ) throws { + self.frameWake = frameWake + self.driverWorkWake = driverWorkWake + handle = try NativeHandleBox( + typeName: "RenderSessionHandle", + handle: session + ) } - public func dumpDebugLogs() throws { - try mapNativeFailure { - try checkStatus(mln_render_session_dump_debug_logs(handle - .requireLive().raw)) - } + public var isClosed: Bool { + handle.isClosed } - public func readPremultipliedRGBA8(into data: inout [UInt8]) throws - -> TextureImageInfo - { - try mapNativeFailure { - try data.withUnsafeMutableBufferPointer { buffer in - let rawInfo = try NativeRender.textureReadPremultipliedRGBA8( - session: handle.requireLive(), - data: buffer.baseAddress, - capacity: buffer.count - ) - // An empty destination reaches native code as the null pointer and - // zero capacity of a size probe, which succeeds without copying. - if buffer.isEmpty, rawInfo.byte_length > 0 { - throw MaplibreError.invalidArgument( - "buffer length 0 is smaller than the required \(rawInfo.byte_length) bytes" - ) - } - return TextureImageInfo(native: NativeTextureImageInfo(rawInfo)) - } - } + func requireLiveHandle() throws -> NativeRenderSessionHandle { + try handle.requireLive() } - public func acquireMetalOwnedTextureFrame() throws - -> MetalOwnedTextureFrameHandle - { - try mapNativeFailure { - try MetalOwnedTextureFrameHandle( - session: self, - frame: NativeMetalOwnedTextureFrame(NativeRender - .metalOwnedTextureAcquireFrame(handle.requireLive())) - ) - } + public func setFrameReadyHandler(_ handler: (@Sendable () -> Void)?) throws { + try handle.withLive { _ in frameWake.setHandler(handler) } } - public func acquireVulkanOwnedTextureFrame() throws - -> VulkanOwnedTextureFrameHandle - { - try mapNativeFailure { - try VulkanOwnedTextureFrameHandle( - session: self, - frame: NativeVulkanOwnedTextureFrame(NativeRender - .vulkanOwnedTextureAcquireFrame(handle.requireLive())) - ) - } + public func setDriverWorkReadyHandler( + _ handler: (@Sendable () -> Void)? + ) throws { + try handle.withLive { _ in driverWorkWake.setHandler(handler) } } - public func acquireOpenGLOwnedTextureFrame() throws - -> OpenGLOwnedTextureFrameHandle - { + public func capabilities() throws -> RenderSessionCapabilities { try mapNativeFailure { - try OpenGLOwnedTextureFrameHandle( - session: self, - frame: NativeOpenGLOwnedTextureFrame(NativeRender - .openGLOwnedTextureAcquireFrame(handle.requireLive())) - ) - } - } -} - -public final class MetalOwnedTextureFrameView { - private let texturePointer: FrameNativePointer - private let devicePointer: FrameNativePointer - - fileprivate init(texture: FrameNativePointer, device: FrameNativePointer) { - texturePointer = texture - devicePointer = device - } - - public var texture: FrameNativePointer { - get throws { - _ = try texturePointer.addressBitPattern - return texturePointer + try handle.withLive { session in + var value = mln_render_session_capabilities() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_render_session_get_capabilities( + session.raw, &value + )) + guard let driver = RenderDriver(rawValue: value.driver) else { + throw NativeStatusFailure.swiftNativeError("Unknown render driver") + } + return RenderSessionCapabilities( + driver: driver, + textureRingDepth: value.texture_ring_depth, + features: .init(rawValue: value.flags) + ) + } } } - public var device: FrameNativePointer { - get throws { - _ = try devicePointer.addressBitPattern - return devicePointer + public func snapshot() throws -> RenderSessionSnapshot { + try mapNativeFailure { + try handle.withLive { session in + var value = mln_render_session_snapshot() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_render_session_get_snapshot(session.raw, &value)) + return RenderSessionSnapshot( + state: RenderSessionState(rawValue: value.state), + driver: RenderDriver(rawValue: value.driver), + latestResult: .fromNative(value.latest_result), + extent: RenderTargetExtent( + width: value.extent.width, + height: value.extent.height, + scaleFactor: value.extent.scale_factor + ), + generation: value.generation, + mapUpdateGeneration: value.map_update_generation, + renderedUpdateGeneration: value.rendered_update_generation, + extentGeneration: value.extent_generation, + frameGeneration: value.frame_generation, + latestDemandToken: value.latest_demand_token, + pendingDemandCount: value.pending_demand_count, + acquiredFrameCount: value.acquired_frame_count, + targetReady: value.target_ready, + pendingChanges: value.pending_changes + ) + } } } -} - -public final class VulkanOwnedTextureFrameView { - private let imagePointer: FrameNativePointer - private let imageViewPointer: FrameNativePointer - fileprivate init(image: FrameNativePointer, imageView: FrameNativePointer) { - imagePointer = image - imageViewPointer = imageView - } - - public var image: FrameNativePointer { - get throws { - _ = try imagePointer.addressBitPattern - return imagePointer + public func requestFrame(_ demand: FrameDemand = FrameDemand()) throws { + try mapNativeFailure { + try handle.withLive { session in + var value = demand.native + try checkStatus(mln_render_session_request_frame(session.raw, &value)) + } } } - public var imageView: FrameNativePointer { - get throws { - _ = try imageViewPointer.addressBitPattern - return imageViewPointer + public func drainFrameResults() throws -> [RenderFrameResult] { + return try mapNativeFailure { + try handle.withLive { session in + var batch: mln_render_frame_batch = 0 + try checkStatus(mln_render_session_drain_frame_results( + session.raw, &batch + )) + guard batch != 0 else { return [] } + defer { mln_render_frame_batch_release(batch) } + var count = 0 + try checkStatus(mln_render_frame_batch_count(batch, &count)) + return try (0 ..< count).map { index in + var value = mln_render_frame_result() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_render_frame_batch_get(batch, index, &value)) + return RenderFrameResult(value) + } + } } } -} - -public final class OpenGLOwnedTextureFrameView { - private let textureName: FrameOpenGLTextureName - private let textureTarget: UInt32 - private let scope: NativeFrameScope - fileprivate init( - texture: FrameOpenGLTextureName, - target: UInt32, - scope: NativeFrameScope - ) { - textureName = texture - textureTarget = target - self.scope = scope - } - - public var texture: FrameOpenGLTextureName { - get throws { - _ = try textureName.value - return textureName + public func acquireFrame() throws -> AcquiredFrameHandle? { + try mapNativeFailure { + try handle.withLive { session in + var frame: mln_acquired_frame = 0 + let status = mln_render_session_acquire_frame(session.raw, &frame) + if status == MLN_STATUS_NOT_READY { return nil } + try checkStatus(status) + return AcquiredFrameHandle(handle: frame) + } } } - public var target: UInt32 { - get throws { - try scope.requireActive("OpenGL texture") - return textureTarget + @discardableResult + public func serviceDriverWork(maxWork: Int = 64) throws -> Int { + guard maxWork >= 0 else { + throw NativeStatusFailure.swiftInvalidArgument( + "maxWork cannot be negative" + ) } - } -} - -public final class MetalOwnedTextureFrameHandle { - private let releaseFrame: (inout NativeMetalOwnedTextureFrame) throws -> Void - private var frame: NativeMetalOwnedTextureFrame? - - init(session: RenderSessionHandle, frame: NativeMetalOwnedTextureFrame) { - releaseFrame = { frame in - try withUnsafePointer(to: &frame.raw) { rawFrame in - try checkStatus(mln_metal_owned_texture_release_frame( - session.requireLiveHandle().raw, - rawFrame + return try mapNativeFailure { + try handle.withLive { session in + var serviced = 0 + try checkStatus(mln_render_session_service_driver_work( + session.raw, maxWork, &serviced )) + return serviced } } - self.frame = frame } - init( - frame: NativeMetalOwnedTextureFrame, - releaseFrame: @escaping (inout NativeMetalOwnedTextureFrame) throws -> Void - ) { - self.releaseFrame = releaseFrame - self.frame = frame - } - - deinit { - if let frame { - NativeHandleLeakReporter.report( - NativeHandleLeak( - typeName: "MetalOwnedTextureFrameHandle", - handle: 0, - detail: "texture 0x\(String(UInt(bitPattern: frame.raw.texture), radix: 16))" - ) - ) + public func resize(_ extent: RenderTargetExtent) async throws { + try await performCompletion { session, completion in + var value = extent.nativeInput.native + return mln_render_session_resize(session, &value, completion) } } - public var isClosed: Bool { - frame == nil - } - - public func withBackendPointers(_ body: (MetalOwnedTextureFrameView) throws - -> Void) throws - { - guard let frame else { - throw MaplibreError( - kind: .invalidState, - rawStatus: nil, - diagnostic: "Metal texture frame is closed" - ) + public func setMetalBorrowedTextureTarget( + _ descriptor: MetalBorrowedTextureDescriptor + ) async throws { + let future = try mapNativeFailure { + try descriptor.nativeInput.withNativeDescriptor { descriptor in + try startCompletion { session, completion in + mln_metal_borrowed_texture_set_target( + session, descriptor, completion + ) + } + } } - let scope = NativeFrameScope(isFrameLive: { [weak self] in - self?.frame != nil - }) - let view = MetalOwnedTextureFrameView( - texture: FrameNativePointer( - bitPattern: UInt(bitPattern: frame.raw.texture), - scope: scope, - diagnosticName: "Metal texture" - ), - device: FrameNativePointer( - bitPattern: UInt(bitPattern: frame.raw.device), - scope: scope, - diagnosticName: "Metal device" - ) - ) - defer { scope.close() } - try body(view) + try await future.value() } - public func close() throws { - guard var frame else { return } - try mapNativeFailure { - try releaseFrame(&frame) + public func setMetalSurfaceTarget( + _ descriptor: MetalSurfaceDescriptor + ) async throws { + let future = try mapNativeFailure { + try descriptor.nativeInput.withNativeDescriptor { descriptor in + try startCompletion { session, completion in + mln_metal_surface_set_target(session, descriptor, completion) + } + } } - self.frame = nil + try await future.value() } -} - -public final class VulkanOwnedTextureFrameHandle { - private let releaseFrame: (inout NativeVulkanOwnedTextureFrame) throws -> Void - private var frame: NativeVulkanOwnedTextureFrame? - init(session: RenderSessionHandle, frame: NativeVulkanOwnedTextureFrame) { - releaseFrame = { frame in - try withUnsafePointer(to: &frame.raw) { rawFrame in - try checkStatus(mln_vulkan_owned_texture_release_frame( - session.requireLiveHandle().raw, - rawFrame - )) + public func setVulkanSurfaceTarget( + _ descriptor: VulkanSurfaceDescriptor + ) async throws { + let future = try mapNativeFailure { + try descriptor.nativeInput.withNativeDescriptor { descriptor in + try startCompletion { session, completion in + mln_vulkan_surface_set_target(session, descriptor, completion) + } } } - self.frame = frame - } - - init( - frame: NativeVulkanOwnedTextureFrame, - releaseFrame: @escaping (inout NativeVulkanOwnedTextureFrame) throws -> Void - ) { - self.releaseFrame = releaseFrame - self.frame = frame + try await future.value() } - deinit { - if let frame { - NativeHandleLeakReporter.report( - NativeHandleLeak( - typeName: "VulkanOwnedTextureFrameHandle", - handle: 0, - detail: "image 0x\(String(UInt(bitPattern: frame.raw.image), radix: 16))" - ) - ) + public func setOpenGLSurfaceTarget( + _ descriptor: OpenGLSurfaceDescriptor + ) async throws { + let future = try mapNativeFailure { + try descriptor.nativeInput.withNativeDescriptor { descriptor in + try startCompletion { session, completion in + mln_opengl_surface_set_target(session, descriptor, completion) + } + } } + try await future.value() } - public var isClosed: Bool { - frame == nil - } - - public func withBackendPointers(_ body: (VulkanOwnedTextureFrameView) throws - -> Void) throws - { - guard let frame else { - throw MaplibreError( - kind: .invalidState, - rawStatus: nil, - diagnostic: "Vulkan texture frame is closed" - ) + public func setWebGPUSurfaceTarget( + _ descriptor: WebGPUSurfaceDescriptor + ) async throws { + let future = try mapNativeFailure { + try descriptor.nativeInput.withNativeDescriptor { descriptor in + try startCompletion { session, completion in + mln_webgpu_surface_set_target(session, descriptor, completion) + } + } } - let scope = NativeFrameScope(isFrameLive: { [weak self] in - self?.frame != nil - }) - let view = VulkanOwnedTextureFrameView( - image: FrameNativePointer( - bitPattern: UInt(bitPattern: frame.raw.image), - scope: scope, - diagnosticName: "Vulkan image" - ), - imageView: FrameNativePointer( - bitPattern: UInt(bitPattern: frame.raw.image_view), - scope: scope, - diagnosticName: "Vulkan image view" - ) - ) - defer { scope.close() } - try body(view) + try await future.value() } - public func close() throws { - guard var frame else { return } - try mapNativeFailure { - try releaseFrame(&frame) + public func barrier() async throws { + try await performCompletion { + mln_render_session_barrier($0, $1) } - self.frame = nil } -} -public final class OpenGLOwnedTextureFrameHandle { - private let releaseFrame: (inout NativeOpenGLOwnedTextureFrame) throws -> Void - private var frame: NativeOpenGLOwnedTextureFrame? + public func reduceMemoryUse() async throws { + try await performCompletion(mln_render_session_reduce_memory_use) + } - init(session: RenderSessionHandle, frame: NativeOpenGLOwnedTextureFrame) { - releaseFrame = { frame in - try withUnsafePointer(to: &frame.raw) { rawFrame in - try checkStatus(mln_opengl_owned_texture_release_frame( - session.requireLiveHandle().raw, - rawFrame - )) - } - } - self.frame = frame + public func clearData() async throws { + try await performCompletion(mln_render_session_clear_data) } - init( - frame: NativeOpenGLOwnedTextureFrame, - releaseFrame: @escaping (inout NativeOpenGLOwnedTextureFrame) throws -> Void - ) { - self.releaseFrame = releaseFrame - self.frame = frame + public func dumpDebugLogs() async throws { + try await performCompletion(mln_render_session_dump_debug_logs) } - deinit { - if let frame { - NativeHandleLeakReporter.report( - NativeHandleLeak( - typeName: "OpenGLOwnedTextureFrameHandle", - handle: 0, - detail: "texture 0x\(String(UInt(frame.raw.texture), radix: 16))" - ) + public func readPremultipliedRGBA8() async throws + -> PremultipliedRGBA8Image + { + let future = try startCompletion( + mln_texture_read_premultiplied_rgba8 + ) { result in + let value: mln_texture_readback_result = try NativeCompletion + .value(result) + return try PremultipliedRGBA8Image( + info: TextureImageInfo(native: NativeTextureImageInfo(value.info)), + data: NativeCompletion.dataView(value.data) ) } + return try await future.value() } - public var isClosed: Bool { - frame == nil + public func detach() async throws { + try await performCompletion(mln_render_session_detach) + frameWake.setHandler(nil) + driverWorkWake.setHandler(nil) } - public func withBackendPointers(_ body: (OpenGLOwnedTextureFrameView) throws - -> Void) throws - { - guard let frame else { - throw MaplibreError( - kind: .invalidState, - rawStatus: nil, - diagnostic: "OpenGL texture frame is closed" - ) + /// Irreversibly closes control and mailboxes without a graphics call. + /// + /// The call waits for the map's in-flight tile work before returning, so no + /// library thread touches the session's target or device afterward and the + /// host may destroy its graphics objects immediately. Do not call it from a + /// MapLibre worker callback. + public func abandon() throws -> RenderAbandonResult { + try mapNativeFailure { + try handle.withLive { session in + var value = mln_render_abandon_result() + value.size = UInt32(MemoryLayout.size) + try checkStatus(mln_render_session_abandon(session.raw, &value)) + frameWake.setHandler(nil) + driverWorkWake.setHandler(nil) + guard let disposition = RenderAbandonResult.Disposition( + rawValue: value.disposition + ) else { + throw NativeStatusFailure.swiftNativeError( + "Unknown render abandonment disposition" + ) + } + return RenderAbandonResult( + disposition: disposition, + quarantinedResourceCount: value.quarantined_resource_count + ) + } } - let scope = NativeFrameScope(isFrameLive: { [weak self] in - self?.frame != nil - }) - let view = OpenGLOwnedTextureFrameView( - texture: FrameOpenGLTextureName(frame.raw.texture, scope: scope), - target: frame.raw.target, - scope: scope - ) - defer { scope.close() } - try body(view) } public func close() throws { - guard var frame else { return } try mapNativeFailure { - try releaseFrame(&frame) + try handle.closeOnce { session in + frameWake.setHandler(nil) + driverWorkWake.setHandler(nil) + try checkStatus(mln_render_session_destroy(session.raw)) + } } - self.frame = nil } -} -/// A reference to a map for the sole purpose of attaching a render session. -/// -/// Produced by ``MapHandle/attachRef()``. Attaching runs on the render -/// session's thread rather than the map's, so it lives here instead of on the -/// non-`Sendable` ``MapHandle``. -/// -/// This carries no Swift retention of the map. A reference that outlives its -/// map throws rather than attaching. Close the session before the map: -/// dropping the last ``MapHandle`` reference while a session is still attached -/// leaks the native map, since the destroy fails and `deinit` can only report -/// it. -public struct MapAttachRef: Sendable { - private let handle: NativeHandleBox - - init(handle: NativeHandleBox) { - self.handle = handle + func startCompletion( + _ body: (mln_render_session, UnsafePointer) -> mln_status, + convert: @escaping (UnsafePointer) throws -> Value + ) throws -> NativeFuture { + try handle.withLive { session in + try NativeCompletion.start( + { completion in body(session.raw, completion) }, + convert: convert + ) + } } - /// Whether the map this reference names has been closed. - public var isMapClosed: Bool { - handle.isClosed + func startCompletion( + _ body: (mln_render_session, UnsafePointer) -> mln_status + ) throws -> NativeFuture { + try startCompletion(body) { _ in () } } - /// The map's handle id, which the C API validates on every attach. A released - /// id is rejected rather than binding the session to a later map. - func mapHandle() throws -> NativeMapHandle { - try handle.requireLive() + func performCompletion( + _ body: (mln_render_session, UnsafePointer) -> mln_status + ) async throws { + try await mapNativeFailure { try startCompletion(body) }.value() } } -public extension MapAttachRef { - func attachMetalSurface(_ descriptor: MetalSurfaceDescriptor) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.metalSurfaceAttach( - map: map, - descriptor: nativeDescriptor - ) - } +public extension MapHandle { + private typealias StartedRenderSession = ( + attachment: NativeRender.Attachment, + frameWake: NativeWakeState, + driverWorkWake: NativeWakeState + ) + + private func startRenderSession( + options: RenderSessionAttachOptions, + _ start: (UnsafePointer) throws + -> NativeRender.Attachment + ) throws -> StartedRenderSession { + let frameWake = NativeWakeState() + let driverWorkWake = NativeWakeState() + let frameDescriptor = frameWake.makeDescriptor() + let driverDescriptor = driverWorkWake.makeDescriptor() + do { + let attachment = try mapNativeFailure { + try options.withNative( + frameWake: frameDescriptor, + driverWorkWake: driverDescriptor, + start + ) + } + return (attachment, frameWake, driverWorkWake) + } catch { + frameWake.releaseRejectedDescriptor() + driverWorkWake.releaseRejectedDescriptor() + throw error } - return try RenderSessionHandle(handle: session) } - func attachVulkanSurface(_ descriptor: VulkanSurfaceDescriptor) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.vulkanSurfaceAttach( - map: map, - descriptor: nativeDescriptor - ) - } + private func finishRenderSession( + _ started: StartedRenderSession + ) throws -> RenderSessionAttachment { + let session = try RenderSessionHandle( + session: started.attachment.session, + frameWake: started.frameWake, + driverWorkWake: started.driverWorkWake + ) + let completed = Task { [session] in + try await mapNativeFailure { + try await started.attachment.completion.value() + } + withExtendedLifetime(session) {} } - return try RenderSessionHandle(handle: session) - } - - func attachOpenGLSurface(_ descriptor: OpenGLSurfaceDescriptor) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.openGLSurfaceAttach( - map: map, - descriptor: nativeDescriptor - ) - } + return RenderSessionAttachment(session: session, completion: completed) + } + + func attachMetalSurface( + _ descriptor: MetalSurfaceDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.metalSurfaceAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) - } - - func attachMetalOwnedTexture(_ descriptor: MetalOwnedTextureDescriptor) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.metalOwnedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + return try finishRenderSession(attachment) + } + + func attachVulkanSurface( + _ descriptor: VulkanSurfaceDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.vulkanSurfaceAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } + } + return try finishRenderSession(attachment) + } + + func attachOpenGLSurface( + _ descriptor: OpenGLSurfaceDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.openGLSurfaceAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } + } + return try finishRenderSession(attachment) + } + + func attachMetalOwnedTexture( + _ descriptor: MetalOwnedTextureDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.metalOwnedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } func attachMetalBorrowedTexture( - _ descriptor: MetalBorrowedTextureDescriptor - ) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.metalBorrowedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + _ descriptor: MetalBorrowedTextureDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.metalBorrowedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } func attachVulkanOwnedTexture( - _ descriptor: VulkanOwnedTextureDescriptor - ) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.vulkanOwnedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + _ descriptor: VulkanOwnedTextureDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.vulkanOwnedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } func attachVulkanBorrowedTexture( - _ descriptor: VulkanBorrowedTextureDescriptor - ) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.vulkanBorrowedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + _ descriptor: VulkanBorrowedTextureDescriptor, + options: RenderSessionAttachOptions + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.vulkanBorrowedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } func attachOpenGLOwnedTexture( - _ descriptor: OpenGLOwnedTextureDescriptor - ) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.openGLOwnedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + _ descriptor: OpenGLOwnedTextureDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.openGLOwnedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } func attachOpenGLBorrowedTexture( - _ descriptor: OpenGLBorrowedTextureDescriptor - ) throws - -> RenderSessionHandle - { - let session = try mapNativeFailure { - let map = try mapHandle() - return try descriptor.nativeInput - .withNativeDescriptor { nativeDescriptor in - try NativeRender.openGLBorrowedTextureAttach( - map: map, - descriptor: nativeDescriptor - ) - } + _ descriptor: OpenGLBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.openGLBorrowedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } + } + return try finishRenderSession(attachment) + } + + func attachWebGPUSurface( + _ descriptor: WebGPUSurfaceDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.webGPUSurfaceAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } + } + return try finishRenderSession(attachment) + } + + func attachWebGPUOwnedTexture( + _ descriptor: WebGPUOwnedTextureDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.webGPUOwnedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } + } + return try finishRenderSession(attachment) + } + + func attachWebGPUBorrowedTexture( + _ descriptor: WebGPUBorrowedTextureDescriptor, + options: RenderSessionAttachOptions = .init(driver: .callerGraphicsThread) + ) throws -> RenderSessionAttachment { + let attachment = try descriptor.nativeInput.withNativeDescriptor { value in + try startRenderSession(options: options) { options in + try NativeRender.webGPUBorrowedTextureAttachStart( + map: requireLiveHandle(), + descriptor: value, + options: options + ) + } } - return try RenderSessionHandle(handle: session) + return try finishRenderSession(attachment) } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Runtime.swift b/bindings/swift/Sources/MaplibreNativeFFI/Runtime.swift index cce3848b4..e4acd028c 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Runtime.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Runtime.swift @@ -49,7 +49,6 @@ public enum RuntimeEventType: Sendable, Hashable { case offlineRegionStatusChanged case offlineRegionResponseError case offlineRegionTileCountLimitExceeded - case offlineOperationCompleted case mapCameraTransitionFinished case unknown(UInt32) @@ -76,7 +75,6 @@ public enum RuntimeEventType: Sendable, Hashable { case 19: .offlineRegionStatusChanged case 20: .offlineRegionResponseError case 21: .offlineRegionTileCountLimitExceeded - case 22: .offlineOperationCompleted case 23: .mapCameraTransitionFinished default: .unknown(rawValue) } @@ -91,8 +89,7 @@ public enum RuntimeEventType: Sendable, Hashable { /// runtime-originated ones, ignoring the rest, so ``all`` is a value both /// accept and a handle reports every bit last set. /// -/// An unselected event type is never built and never queued, so it neither -/// reaches a batch nor wakes a parked ``RuntimeHandle/pump(timeout:budget:)``. +/// An unselected event type is never built and never queued. public struct RuntimeEventMask: OptionSet, Sendable, Hashable { public let rawValue: UInt64 @@ -150,8 +147,6 @@ public struct RuntimeEventMask: OptionSet, Sendable, Hashable { native(MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR) public static let offlineRegionTileCountLimitExceeded = native(MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED) - public static let offlineOperationCompleted = - native(MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED) /// Every map-originated event type, which is what /// ``MapHandle/setEventMask(_:)`` reads. @@ -383,22 +378,6 @@ public struct OfflineRegionTileCountLimitEvent: Equatable, Sendable { } } -public struct OfflineOperationCompletedEvent: Equatable, Sendable { - public let operationId: UInt64 - public let operationKind: UInt32 - public let resultKind: UInt32 - public let resultStatus: Int32 - public let found: Bool - - init(native: NativeOfflineOperationCompletedEvent) { - operationId = native.operationId - operationKind = native.operationKind - resultKind = native.resultKind - resultStatus = native.resultStatus - found = native.found - } -} - /// Payload of a `.mapCameraTransitionFinished` event. public struct CameraTransitionFinishedEvent: Equatable, Sendable { /// The `AnimationOptions.transitionId` that started the finished transition. @@ -417,7 +396,6 @@ public enum RuntimeEventPayload: Equatable, Sendable { case offlineRegionStatus(OfflineRegionStatusEvent) case offlineRegionResponseError(OfflineRegionResponseErrorEvent) case offlineRegionTileCountLimit(OfflineRegionTileCountLimitEvent) - case offlineOperationCompleted(OfflineOperationCompletedEvent) case cameraTransitionFinished(CameraTransitionFinishedEvent) /// A payload kind this version of the binding does not name, carrying the /// payload's fixed byte window so a host forwards it unchanged. @@ -445,11 +423,6 @@ public enum RuntimeEventPayload: Equatable, Sendable { .offlineRegionTileCountLimit( OfflineRegionTileCountLimitEvent(native: event) ) - case let .offlineOperationCompleted(event): - self = - .offlineOperationCompleted( - OfflineOperationCompletedEvent(native: event) - ) case let .cameraTransitionFinished(event): self = .cameraTransitionFinished( @@ -472,16 +445,13 @@ public struct RuntimeEvent: Equatable, Sendable { /// - `.mapLoadingFailed` carries the ordinal of MapLibre Native's internal /// map load error kind, which this API leaves unnamed; `message` holds the /// failure text. - /// - `.offlineOperationCompleted` carries the operation result as a native - /// status value, the same value the payload reports in `resultStatus`. /// - Every other event type leaves it 0. public let code: Int32 /// Text this event carries, empty when it carries none. /// /// It is the failure text of `.mapLoadingFailed`, `.mapRenderError`, - /// `.mapStillImageFailed`, `.offlineRegionResponseError`, and a failed - /// `.offlineOperationCompleted`, the image ID of `.mapStyleImageMissing`, and - /// the source ID of `.mapTileAction`. + /// `.mapStillImageFailed`, and `.offlineRegionResponseError`, the image ID of + /// `.mapStyleImageMissing`, and the source ID of `.mapTileAction`. public let message: String public let payload: RuntimeEventPayload @@ -499,254 +469,245 @@ public struct RuntimeEvent: Equatable, Sendable { /// One drained batch of runtime events. /// -/// The C API lends its batch until the next drain; this is a copy of it, so a -/// host keeps events, their messages, and their payloads for as long as it -/// likes. +/// This is a copy of an owned C event batch, so a host keeps events, their +/// messages, and their payloads for as long as it likes. public struct RuntimeEventBatch: Equatable, Sendable { /// The drained events, in queue order. public let events: [RuntimeEvent] - /// Events still queued after this batch. A nonzero value means another drain - /// reports more events. - public let remainingCount: Int } -public final class RuntimeHandle { +public final class RuntimeHandle: @unchecked Sendable { private let handle: NativeHandleBox - private var resourceTransform: NativeResourceTransformState? - private var httpHeaderTransform: NativeHttpHeaderTransformState? - private var resourceProvider: NativeResourceProviderState? + private let eventWake: NativeWakeState public init(options: RuntimeOptions = RuntimeOptions()) throws { - let runtime = try mapNativeFailure { - try options.nativeInput.withNativeOptions { nativeOptions in - try NativeRuntime.create(nativeOptions) + let eventWake = NativeWakeState() + let wake = eventWake.makeDescriptor() + do { + let runtime = try mapNativeFailure { + try options.nativeInput.withNativeOptions( + eventWake: wake + ) { nativeOptions in + try NativeRuntime.create(nativeOptions) + } } + handle = try NativeHandleBox( + typeName: "RuntimeHandle", + handle: runtime + ) + self.eventWake = eventWake + } catch { + eventWake.releaseRejectedDescriptor() + throw error } - handle = try NativeHandleBox(typeName: "RuntimeHandle", handle: runtime) } public var isClosed: Bool { handle.isClosed } - public func close() throws { - try handle.closeOnce { handle in - try checkStatus(mln_runtime_destroy(handle.raw)) + /// Releases this runtime's public native handle and waits for its native + /// teardown. + /// + /// The call returns once every accepted submission, including released maps' + /// teardown, has finished and the runtime's threads and resources are gone, + /// so a host that awaits it may exit the process without racing native + /// teardown. Closing an already closed runtime returns without waiting. + public func close() async throws { + guard let teardown = try startClose() else { return } + try await mapNativeFailure { try await teardown.value() } + } + + /// Releases the native handle and returns the future for its teardown, or nil + /// when this runtime was already closed. + private func startClose() throws -> NativeFuture? { + var teardown: NativeFuture? + try mapNativeFailure { + try handle.closeOnce { teardown = try NativeRuntime.release($0) } } - resourceTransform = nil - httpHeaderTransform = nil - resourceProvider = nil + return teardown } - func requireLiveHandle() throws -> NativeRuntimeHandle { - try handle.requireLive() + /// Closes and waits for native teardown without an async context, so a test + /// leaves no native thread running past its own end. + func closeBlockingForTests() throws { + guard let teardown = try startClose() else { return } + try mapNativeFailure { try teardown.valueBlocking() } } - /// Advances this runtime: parks the owner thread when `timeout` allows it, - /// then drains the owner-thread task queues. Drain queued runtime events with - /// ``drainEvents(maxEvents:)`` afterwards. - /// - /// `timeout` is in seconds. Zero drains and returns, a positive value parks - /// for up to that long, and `nil` parks until a wake arrives. Timers and - /// ready file descriptors wake the runtime only when they queue owner-thread - /// work, so pass a bounded timeout to cap how long a call waits. - /// - /// `budget` bounds the drain, in seconds. `nil` drains without a bound. Zero - /// or a positive value stops the drain at the first task boundary after that - /// long, measured from the start of the drain. The first queued task always - /// runs, so a bounded pump always makes progress, and tasks left behind set - /// the wake flag, so the next pump returns without parking and continues - /// them. The budget bounds the task queues alone: expired timers and ready - /// file descriptors are serviced regardless, and a task runs to completion - /// once started, so one long task can overrun the budget. - /// - /// A non-zero timeout blocks the calling thread. Call it outside any lock - /// that a thread signalling a `WakeSource` takes. A call never parks while - /// this runtime holds undrained events. - public func pump( - timeout: TimeInterval? = 0, - budget: TimeInterval? = nil - ) throws { - try mapNativeFailure { - let timeoutMilliseconds: Int64 - if let timeout { - // A negative or non-finite timeout collapses to no wait; `nil` is the - // spelling for an unbounded park. The upper clamp keeps the conversion - // inside Int64. - let milliseconds = timeout.isFinite ? (timeout * 1000).rounded() : 0 - timeoutMilliseconds = Int64(min(max(milliseconds, 0), 9.0e18)) - } else { - timeoutMilliseconds = -1 + /// Waits until every command accepted before this call has committed. + public func barrier() async throws { + try await mapNativeFailure { + let runtime = try requireLiveHandle() + try await NativeCompletion.unit { completion in + mln_runtime_barrier(runtime.raw, completion) } - let budgetMilliseconds: Int64 - if let budget { - // A negative or non-finite budget collapses to zero; `nil` is the - // spelling for an unbounded drain. The upper clamp keeps the - // conversion inside Int64. - let milliseconds = budget.isFinite ? (budget * 1000).rounded() : 0 - budgetMilliseconds = Int64(min(max(milliseconds, 0), 9.0e18)) - } else { - budgetMilliseconds = -1 - } - try checkStatus( - mln_runtime_pump( - handle.requireLive().raw, - timeoutMilliseconds, - budgetMilliseconds - ) - ) } } - /// Acquires a wake source that releases this runtime's parked owner thread. - /// The returned source is usable from any thread, and the caller closes it. - public func wakeSource() throws -> WakeSource { - try mapNativeFailure { - var source = mln_wake_source() - try checkStatus(mln_runtime_wake_source_acquire( - handle.requireLive().raw, - &source - )) - return try WakeSource(handle: NativeWakeSourceHandle(raw: source)) - } + func requireLiveHandle() throws -> NativeRuntimeHandle { + try handle.requireLive() } - /// Drains this runtime's queued events, copying every event out of the - /// runtime-owned arena before the call returns. + /// Drains this runtime's queued events, copying every event out of the owned + /// native batch before the call returns. /// /// Events arrive in queue order, from this runtime and from every map it - /// owns. `maxEvents` bounds the drain: zero drains everything, and a positive - /// value drains at most that many and reports the rest through - /// ``RuntimeEventBatch/remainingCount``. - /// - /// Draining is a queue operation that runs no owner-thread work. Call - /// ``pump(timeout:budget:)`` to advance the runtime, then drain what it - /// produced. - public func drainEvents(maxEvents: Int = 0) throws -> RuntimeEventBatch { + /// owns. One drain takes the whole queue. + public func drainEvents() throws -> RuntimeEventBatch { try mapNativeFailure { - guard maxEvents >= 0 else { - throw NativeStatusFailure - .swiftInvalidArgument("maxEvents cannot be negative") - } - let batch = try NativeRuntime.drainEvents( - handle.requireLive(), - maxEvents: maxEvents - ) + let batch = try NativeRuntime.drainEvents(handle.requireLive()) return RuntimeEventBatch( - events: batch.events.map { RuntimeEvent(native: $0) }, - remainingCount: batch.remainingCount + events: batch.events.map { RuntimeEvent(native: $0) } ) } } - /// Selects which runtime-originated event types this runtime queues. + /// Schedules the receiver when runtime events become ready to drain. /// - /// The call reads the bits in ``RuntimeEventMask/allRuntimeEvents`` and - /// ignores the rest, so ``RuntimeEventMask/all`` selects every one of them. - /// Narrowing gates later events and keeps queued ones, so a host drains what - /// it already caused. + /// The callback may coalesce and carries no event payload. It should schedule + /// ``drainEvents()`` on the host execution context that consumes + /// runtime events. + public func setEventReadyHandler( + _ handler: (@Sendable () -> Void)? + ) { + eventWake.setHandler(handler) + } + + /// Selects which runtime-originated event types this runtime queues. /// - /// Region status, response error, and tile count limit events also require an - /// observed region, so this mask narrows that subscription rather than - /// replacing it. public func setEventMask(_ mask: RuntimeEventMask) throws { - try mapNativeFailure { - try NativeRuntime.setEventMask(handle.requireLive(), mask: mask.rawValue) - } + try mapNativeFailure { try NativeRuntime.setEventMask( + handle.requireLive(), + mask: mask.rawValue + ) } } - /// The mask last set. A runtime that has not been narrowed reports - /// ``RuntimeEventMask/all``. Read it, change one bit, and write it back to - /// leave the other bits alone. public var eventMask: RuntimeEventMask { get throws { try mapNativeFailure { - try RuntimeEventMask( - rawValue: NativeRuntime.eventMask(handle.requireLive()) - ) + try RuntimeEventMask(rawValue: NativeRuntime + .eventMask(handle.requireLive())) } } } - public func setResourceTransform( - _ callback: @escaping @Sendable (ResourceTransformRequest) -> String? - ) throws { - let replacement = NativeResourceTransformState { nativeRequest in - callback(ResourceTransformRequest(native: nativeRequest)) + @discardableResult + public func setResourceTransform(_ callback: @escaping @Sendable ( + ResourceTransformRequest + ) + -> String?) async throws -> CommandCompletion + { + let replacement = NativeResourceTransformState { + callback(ResourceTransformRequest(native: $0)) } - try mapNativeFailure { - try replacement.withDescriptor { descriptor in - try checkStatus(mln_runtime_set_resource_transform( - handle.requireLive().raw, - descriptor - )) - } - } - resourceTransform = replacement + let future = try replacement.withDescriptor { try startCallbackSet( + $0, + mln_runtime_set_resource_transform + ) } + return try await mapNativeFailure { try await future.value() } } - public func clearResourceTransform() throws { - try mapNativeFailure { - try checkStatus(mln_runtime_clear_resource_transform(handle - .requireLive().raw)) - } - resourceTransform = nil + @discardableResult + public func clearResourceTransform() async throws -> CommandCompletion { + try await submitCallbackClear(mln_runtime_clear_resource_transform) } - public func setHttpHeaderTransform( - _ callback: @escaping @Sendable (HttpHeaderTransformRequest) -> [HttpHeader] - ) throws { + @discardableResult + public func setHttpHeaderTransform(_ callback: @escaping @Sendable ( + HttpHeaderTransformRequest + ) + -> [HttpHeader]) async throws -> CommandCompletion + { let replacement = NativeHttpHeaderTransformState(callback) + let future = try replacement.withDescriptor { try startCallbackSet( + $0, + mln_runtime_set_http_header_transform + ) } + return try await mapNativeFailure { try await future.value() } + } + + @discardableResult + public func clearHttpHeaderTransform() async throws -> CommandCompletion { + try await submitCallbackClear(mln_runtime_clear_http_header_transform) + } + + @discardableResult + public func setResourceProvider(_ callback: @escaping @Sendable ( + ResourceRequest, + ResourceRequestHandle + ) -> ResourceProviderDecision) async throws -> CommandCompletion { + let replacement = NativeResourceProviderState { request, handle in + switch callback( + ResourceRequest(native: request), + ResourceRequestHandle(state: handle) + ) { + case .passThrough: return 0 + case .handle: return 1 + } + } + let future = try replacement.withDescriptor { try startCallbackSet( + $0, + mln_runtime_set_resource_provider + ) } + return try await mapNativeFailure { try await future.value() } + } + + @discardableResult + public func clearResourceProvider() async throws -> CommandCompletion { + try await submitCallbackClear(mln_runtime_clear_resource_provider) + } + + private func startCallbackSet( + _ descriptor: UnsafePointer, + _ submit: ( + mln_runtime, + UnsafePointer, + UnsafePointer + ) -> mln_status + ) throws -> NativeFuture { try mapNativeFailure { - try replacement.withDescriptor { descriptor in - try checkStatus(mln_runtime_set_http_header_transform( - handle.requireLive().raw, - descriptor - )) + let runtime = try handle.requireLive() + return try NativeCompletion.startCommand { completion in + submit(runtime.raw, descriptor, completion) } } - httpHeaderTransform = replacement } - public func clearHttpHeaderTransform() throws { - try mapNativeFailure { - try checkStatus(mln_runtime_clear_http_header_transform(handle - .requireLive().raw)) - } - httpHeaderTransform = nil - } - - public func setResourceProvider( - _ callback: @escaping @Sendable (ResourceRequest, ResourceRequestHandle) - -> ResourceProviderDecision - ) throws { - let replacement = - NativeResourceProviderState { nativeRequest, nativeHandle in - let request = ResourceRequest(native: nativeRequest) - let handle = ResourceRequestHandle(state: nativeHandle) - switch callback(request, handle) { - case .passThrough: - return 0 - case .handle: - return 1 - } - } - try mapNativeFailure { - try replacement.withDescriptor { descriptor in - try checkStatus(mln_runtime_set_resource_provider( - handle.requireLive().raw, - descriptor - )) + private func submitCallbackClear(_ submit: ( + mln_runtime, + UnsafePointer + ) -> mln_status) async throws -> CommandCompletion { + try await mapNativeFailure { + let runtime = try handle.requireLive() + return try await NativeCompletion.command { completion in + submit(runtime.raw, completion) } } - resourceProvider = replacement } +} - public func clearResourceProvider() throws { - try mapNativeFailure { - try checkStatus(mln_runtime_clear_resource_provider(handle - .requireLive().raw)) +public enum CommandDisposition: Sendable, Hashable { + case committed + case superseded + case failed + case cancelled + case unknown(UInt32) + + static func fromNative(_ raw: UInt32) -> Self { + switch raw { + case MLN_COMMAND_DISPOSITION_COMMITTED.rawValue: .committed + case MLN_COMMAND_DISPOSITION_SUPERSEDED.rawValue: .superseded + case MLN_COMMAND_DISPOSITION_FAILED.rawValue: .failed + case MLN_COMMAND_DISPOSITION_CANCELLED.rawValue: .cancelled + default: .unknown(raw) } - resourceProvider = nil } } + +public struct CommandCompletion: Sendable, Hashable { + public let disposition: CommandDisposition + public let generation: UInt64 + public let rawStatus: Int32 + public let diagnostic: String +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Style.swift b/bindings/swift/Sources/MaplibreNativeFFI/Style.swift index f650a903b..a5d65af46 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Style.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Style.swift @@ -432,6 +432,21 @@ public struct StyleSourceInfo: Equatable, Sendable { } } +/// A copied snapshot of the fixed metadata for one style layer. +public struct StyleLayerInfo: Equatable, Sendable { + /// The layer's style-spec type string, such as `"fill"`. + public let type: String + /// Lowest zoom at which the layer draws; `-infinity` with no lower bound. + public let minZoom: Double + /// Highest zoom at which the layer draws; `infinity` with no upper bound. + public let maxZoom: Double + public let visibility: StyleLayerVisibility + /// The layer's source ID, absent when the layer has no source. + public let sourceId: String? + /// The layer's source-layer ID, absent when the layer carries none. + public let sourceLayer: String? +} + public enum LocationIndicatorImageKind: UInt32, Sendable, Hashable { case top = 0 case bearing = 1 @@ -547,7 +562,7 @@ public struct CustomMvtVectorSourceOptions: Sendable { /// GeoJSON source. /// /// Preparation needs no runtime or map and is callable from any thread, so a -/// host can prepare data off the map owner thread. Install calls borrow the +/// host can prepare data concurrently with map work. Install calls borrow the /// handle, so one prepared value may be installed on any number of sources and /// closed at any time afterward; closing never invalidates a source the data /// was installed on. @@ -588,9 +603,7 @@ public final class GeoJSONSourceDataHandle: @unchecked Sendable { } } - /// Borrows the live handle for the duration of `use`, so a concurrent - /// `close()` waits for in-flight installs instead of retiring the native - /// handle under them. + /// Runs `use` after checking that this wrapper still owns the handle. func withLiveHandle( _ use: (NativeGeoJSONSourceDataHandle) throws -> T ) throws -> T { @@ -599,171 +612,139 @@ public final class GeoJSONSourceDataHandle: @unchecked Sendable { } public extension MapHandle { - func addStyleSourceJSON(sourceId: String, sourceJSON: Data) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_add_style_source_json( - requireLiveHandle().raw, - arena.view(sourceId), - arena.view(sourceJSON) - )) + @discardableResult + func addStyleSourceJSON( + sourceId: String, + sourceJSON: Data + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_add_style_source_json( + $0, $1.view(sourceId), $1.view(sourceJSON), $2 + ) } } - @discardableResult func removeStyleSource(_ sourceId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.removeSource( - requireLiveHandle(), - sourceId: arena.view(sourceId) - ) + @discardableResult + func removeStyleSource( + _ sourceId: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_remove_style_source($0, $1.view(sourceId), $2) } } - func styleSourceExists(_ sourceId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.sourceExists( - requireLiveHandle(), - sourceId: arena.view(sourceId) - ) - } + func styleSourceInfo(_ sourceId: String) async throws -> StyleSourceInfo? { + try await styleQuery( + { mln_map_get_style_source_info($0, $1.view(sourceId), $2) }, + convert: Self.copyStyleSourceInfo + ) } - func styleSourceType(_ sourceId: String) throws -> StyleSourceType? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.sourceType( - requireLiveHandle(), - sourceId: arena.view(sourceId) - ).map(StyleSourceType.init(rawValue:)) - } + func styleSourceAttribution(_ sourceId: String) async throws -> String? { + try await styleQuery( + { mln_map_copy_style_source_attribution($0, $1.view(sourceId), $2) }, + convert: Self.copyOptionalString + ) } - func styleSourceInfo(_ sourceId: String) throws -> StyleSourceInfo? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.sourceInfo( - requireLiveHandle(), - sourceId: arena.view(sourceId) - ).map(StyleSourceInfo.init(native:)) - } + func styleSourceURL(_ sourceId: String) async throws -> String? { + try await styleQuery( + { mln_map_copy_style_source_url($0, $1.view(sourceId), $2) }, + convert: Self.copyOptionalString + ) } - func styleSourceAttribution(_ sourceId: String) throws -> String? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.sourceAttribution( - requireLiveHandle(), - sourceId: arena.view(sourceId) - ) - } + func styleSourceTileURLs(_ sourceId: String) async throws -> [String]? { + guard let info = try await styleSourceInfo(sourceId) else { return nil } + return info.tileJSON?.tileURLs } - func styleSourceIds() throws -> [String] { - try mapNativeFailure { try NativeStyle.sourceIds(requireLiveHandle()) } + func styleSourceIds() async throws -> [String] { + try await styleQuery( + { mln_map_list_style_source_ids($0, $2) }, + convert: Self.copyStrings + ) } - /// Adds a GeoJSON source that loads data from a URL. `options` is fixed when - /// the source is created. + @discardableResult func addGeoJSONSourceURL( sourceId: String, url: String, options: StyleGeoJSONSourceOptions = StyleGeoJSONSourceOptions() - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try options.nativeOptions.withNativeOptions { options in - try checkStatus(mln_map_add_geojson_source_url( - requireLiveHandle().raw, - arena.view(sourceId), - arena.view(url), - options - )) + return try options.nativeOptions.withNativeOptions { options in + try startCommand { + mln_map_add_geojson_source_url( + $0, arena.view(sourceId), arena.view(url), options, $1 + ) + } } } + return try await mapNativeFailure { try await future.value() } } - /// Adds a GeoJSON source with prepared inline data. The call borrows the - /// handle, and the source adopts the options the data was prepared with, - /// fixed for the lifetime of the source. + @discardableResult func addGeoJSONSourceData( sourceId: String, data: GeoJSONSourceDataHandle - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try data.withLiveHandle { prepared in - try checkStatus(mln_map_add_geojson_source_data( - requireLiveHandle().raw, - arena.view(sourceId), - prepared.raw - )) + return try data.withLiveHandle { prepared in + try startCommand { + mln_map_add_geojson_source_data( + $0, arena.view(sourceId), prepared.raw, $1 + ) + } } } + return try await mapNativeFailure { try await future.value() } } - func setGeoJSONSourceURL(sourceId: String, url: String) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_geojson_source_url( - requireLiveHandle().raw, - arena.view(sourceId), - arena.view(url) - )) + @discardableResult + func setGeoJSONSourceURL( + sourceId: String, + url: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_geojson_source_url( + $0, $1.view(sourceId), $1.view(url), $2 + ) } } - /// Updates one GeoJSON source with prepared inline data. The call borrows - /// the handle, which makes this a cheap install: the expensive parse and - /// tiling already happened when the data was prepared. - /// - /// The data must have been prepared with options equal to the options the - /// source was added with; cluster aggregation expressions compare by parsed - /// equality. A mismatch is rejected. + @discardableResult func setGeoJSONSourceData( sourceId: String, data: GeoJSONSourceDataHandle - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try data.withLiveHandle { prepared in - try checkStatus(mln_map_set_geojson_source_data( - requireLiveHandle().raw, - arena.view(sourceId), - prepared.raw - )) + return try data.withLiveHandle { prepared in + try startCommand { + mln_map_set_geojson_source_data( + $0, arena.view(sourceId), prepared.raw, $1 + ) + } } } + return try await mapNativeFailure { try await future.value() } } - /// Overrides one GeoJSON source's synchronous tiling at runtime. While - /// enabled, the source slices requested tiles inline during the update pass, - /// as if its options had set - /// ``StyleGeoJSONSourceOptions/synchronousTiling``; disabling it restores - /// the option the source was added with. + @discardableResult func setGeoJSONSourceSynchronousTiling( sourceId: String, enabled: Bool - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_geojson_source_synchronous_tiling( - requireLiveHandle().raw, - arena.view(sourceId), - enabled - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_geojson_source_synchronous_tiling( + $0, $1.view(sourceId), enabled, $2 + ) } } @@ -775,21 +756,20 @@ public extension MapHandle { mln_map, mln_buffer_view, mln_buffer_view, - UnsafePointer? + UnsafePointer?, + UnsafePointer ) -> mln_status - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try options.nativeOptions.withNativeOptions { options in - try checkStatus(add( - requireLiveHandle().raw, - arena.view(sourceId), - arena.view(url), - options - )) + return try options.nativeOptions.withNativeOptions { options in + try startCommand { + add($0, arena.view(sourceId), arena.view(url), options, $1) + } } } + return try await mapNativeFailure { try await future.value() } } private func addTiledSourceTiles( @@ -801,123 +781,111 @@ public extension MapHandle { mln_buffer_view, UnsafePointer?, Int, - UnsafePointer? + UnsafePointer?, + UnsafePointer ) -> mln_status - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - let tileViews = tiles.map { arena.view($0) } - try tileViews.withUnsafeBufferPointer { tiles in + let views = tiles.map { arena.view($0) } + return try views.withUnsafeBufferPointer { views in try options.nativeOptions.withNativeOptions { options in - try checkStatus(add( - requireLiveHandle().raw, - arena.view(sourceId), - tiles.baseAddress, - tiles.count, - options - )) + try startCommand { + add( + $0, arena.view(sourceId), views.baseAddress, views.count, + options, $1 + ) + } } } } + return try await mapNativeFailure { try await future.value() } } + @discardableResult func addVectorSourceURL( sourceId: String, url: String, options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceURL( - sourceId: sourceId, - url: url, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceURL( + sourceId: sourceId, url: url, options: options, add: mln_map_add_vector_source_url ) } + @discardableResult func addVectorSourceTiles( sourceId: String, tiles: [String], options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceTiles( - sourceId: sourceId, - tiles: tiles, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceTiles( + sourceId: sourceId, tiles: tiles, options: options, add: mln_map_add_vector_source_tiles ) } + @discardableResult func addRasterSourceURL( sourceId: String, url: String, options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceURL( - sourceId: sourceId, - url: url, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceURL( + sourceId: sourceId, url: url, options: options, add: mln_map_add_raster_source_url ) } + @discardableResult func addRasterSourceTiles( sourceId: String, tiles: [String], options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceTiles( - sourceId: sourceId, - tiles: tiles, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceTiles( + sourceId: sourceId, tiles: tiles, options: options, add: mln_map_add_raster_source_tiles ) } + @discardableResult func addRasterDEMSourceURL( sourceId: String, url: String, options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceURL( - sourceId: sourceId, - url: url, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceURL( + sourceId: sourceId, url: url, options: options, add: mln_map_add_raster_dem_source_url ) } + @discardableResult func addRasterDEMSourceTiles( sourceId: String, tiles: [String], options: StyleTileSourceOptions = StyleTileSourceOptions() - ) throws { - try addTiledSourceTiles( - sourceId: sourceId, - tiles: tiles, - options: options, + ) async throws -> CommandCompletion { + try await addTiledSourceTiles( + sourceId: sourceId, tiles: tiles, options: options, add: mln_map_add_raster_dem_source_tiles ) } - /// Adds a custom geometry source, whose tile callbacks run on native worker - /// threads. - /// - /// The source's callbacks stay alive until the C API stops referencing them, - /// which is when the source is removed, when a style load drops it, or when - /// the map is destroyed. The C API then releases them, waiting for any tile - /// callback still running, so a closure is never entered afterwards. + @discardableResult func addCustomGeometrySource( sourceId: String, options: CustomGeometrySourceOptions - ) throws { - let fetchTile: NativeCustomGeometrySourceCallbacks - .TileCallback = { tileId in - options.fetchTile(CanonicalTileID(native: tileId)) - } + ) async throws -> CommandCompletion { + let fetchTile: NativeCustomGeometrySourceCallbacks.TileCallback = { + options.fetchTile(CanonicalTileID(native: $0)) + } let cancelTile: NativeCustomGeometrySourceCallbacks.TileCallback? if let callback = options.cancelTile { - cancelTile = { tileId in callback(CanonicalTileID(native: tileId)) } + cancelTile = { callback(CanonicalTileID(native: $0)) } } else { cancelTile = nil } @@ -925,85 +893,76 @@ public extension MapHandle { fetchTile: fetchTile, cancelTile: cancelTile ) + let future: NativeFuture do { - try mapNativeFailure { + future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try options.nativeOptions(callbacks: callbacks) - .withNativeOptions { nativeOptions in - try checkStatus(mln_map_add_custom_geometry_source( - requireLiveHandle().raw, - arena.view(sourceId), - nativeOptions - )) + return try options.nativeOptions(callbacks: callbacks) + .withNativeOptions { native in + try startCommand { + mln_map_add_custom_geometry_source( + $0, arena.view(sourceId), native, $1 + ) + } } } } catch { - // A rejected add is the one case the C API never releases, because it - // never took the callbacks on. callbacks.release() throw error } + return try await mapNativeFailure { try await future.value() } } + @discardableResult func setCustomGeometrySourceTileData( sourceId: String, tileId: CanonicalTileID, data: Data - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_custom_geometry_source_tile_data( - requireLiveHandle().raw, - arena.view(sourceId), - tileId.nativeTileID.native, - arena.view(data) - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_custom_geometry_source_tile_data( + $0, $1.view(sourceId), tileId.nativeTileID.native, $1.view(data), $2 + ) } } + @discardableResult func invalidateCustomGeometrySourceTile( sourceId: String, tileId: CanonicalTileID - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_invalidate_custom_geometry_source_tile( - requireLiveHandle().raw, - arena.view(sourceId), - tileId.nativeTileID.native - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_invalidate_custom_geometry_source_tile( + $0, $1.view(sourceId), tileId.nativeTileID.native, $2 + ) } } + @discardableResult func invalidateCustomGeometrySourceRegion( sourceId: String, bounds: LatLngBounds - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_invalidate_custom_geometry_source_region( - requireLiveHandle().raw, - arena.view(sourceId), - bounds.nativeInput.native - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_invalidate_custom_geometry_source_region( + $0, $1.view(sourceId), bounds.nativeInput.native, $2 + ) } } - /// Adds a custom MVT vector source, whose tile callbacks run on native worker - /// threads. + /// Adds a custom MVT vector source, whose tile callbacks run on native + /// worker threads. /// /// The source's callbacks stay alive until the C API stops referencing them, /// which is when the source is removed, when a style load drops it, or when /// the map is destroyed. The C API then releases them, waiting for any tile /// callback still running, so a closure is never entered afterwards. + @discardableResult func addCustomMvtVectorSource( sourceId: String, options: CustomMvtVectorSourceOptions - ) throws { + ) async throws -> CommandCompletion { let fetchTile: NativeCustomMvtVectorSourceCallbacks .TileCallback = { tileId in options.fetchTile(CanonicalTileID(native: tileId)) @@ -1018,17 +977,18 @@ public extension MapHandle { fetchTile: fetchTile, cancelTile: cancelTile ) + let future: NativeFuture do { - try mapNativeFailure { + future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try options.nativeOptions(callbacks: callbacks) - .withNativeOptions { nativeOptions in - try checkStatus(mln_map_add_custom_mvt_vector_source( - requireLiveHandle().raw, - arena.view(sourceId), - nativeOptions - )) + return try options.nativeOptions(callbacks: callbacks) + .withNativeOptions { native in + try startCommand { + mln_map_add_custom_mvt_vector_source( + $0, arena.view(sourceId), native, $1 + ) + } } } } catch { @@ -1037,682 +997,714 @@ public extension MapHandle { callbacks.release() throw error } + return try await mapNativeFailure { try await future.value() } } + @discardableResult func setCustomMvtVectorSourceTileData( sourceId: String, tileId: CanonicalTileID, data: Data - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_custom_mvt_vector_source_tile_data( - requireLiveHandle().raw, - arena.view(sourceId), - tileId.nativeTileID.native, - arena.view(data) - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_custom_mvt_vector_source_tile_data( + $0, $1.view(sourceId), tileId.nativeTileID.native, $1.view(data), $2 + ) } } + @discardableResult func setCustomMvtVectorSourceTileError( sourceId: String, tileId: CanonicalTileID, message: String - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_custom_mvt_vector_source_tile_error( - requireLiveHandle().raw, - arena.view(sourceId), - tileId.nativeTileID.native, - arena.view(message) - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_custom_mvt_vector_source_tile_error( + $0, $1.view(sourceId), tileId.nativeTileID.native, $1.view(message), $2 + ) } } + @discardableResult func invalidateCustomMvtVectorSourceTile( sourceId: String, tileId: CanonicalTileID - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_invalidate_custom_mvt_vector_source_tile( - requireLiveHandle().raw, - arena.view(sourceId), - tileId.nativeTileID.native - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_invalidate_custom_mvt_vector_source_tile( + $0, $1.view(sourceId), tileId.nativeTileID.native, $2 + ) } } + @discardableResult func setStyleImage( imageId: String, image: StyleRGBA8Image, options: StyleImageOptions = StyleImageOptions() - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try image.nativeImage.withNativeImage { image in + return try image.nativeImage.withNativeImage { image in try options.nativeOptions.withNativeOptions { options in - try checkStatus(mln_map_set_style_image( - requireLiveHandle().raw, - arena.view(imageId), - image, - options - )) + try startCommand { + mln_map_set_style_image( + $0, arena.view(imageId), image, options, $1 + ) + } } } } + return try await mapNativeFailure { try await future.value() } } - @discardableResult func removeStyleImage(_ imageId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.removeImage( - requireLiveHandle(), - imageId: arena.view(imageId) - ) + @discardableResult + func removeStyleImage( + _ imageId: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_remove_style_image($0, $1.view(imageId), $2) } } - func styleImageExists(_ imageId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.imageExists( - requireLiveHandle(), - imageId: arena.view(imageId) - ) - } + func styleImageInfo(_ imageId: String) async throws -> StyleImageInfo? { + try await styleImageResult(imageId).map(\.info) } - func styleImageInfo(_ imageId: String) throws -> StyleImageInfo? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.imageInfo( - requireLiveHandle(), - imageId: arena.view(imageId) - ).map(StyleImageInfo.init(native:)) - } + func styleImage(_ imageId: String) async throws -> StyleImage? { + try await styleImageResult(imageId) } - /// Copies one runtime style image's stretchable intervals, or nil when no - /// image carries `imageId`. func styleImageStretches( _ imageId: String - ) throws -> (stretchX: [ImageStretch], stretchY: [ImageStretch])? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.copyImageStretches( - requireLiveHandle(), - imageId: arena.view(imageId) + ) async throws -> (stretchX: [ImageStretch], stretchY: [ImageStretch])? { + try await styleQuery( + { mln_map_get_style_image_info($0, $1.view(imageId), $2) } + ) { result in + guard result.pointee.value_count > 0 else { return nil } + let raw: mln_style_image_result = try NativeCompletion.value(result) + return try ( + Self.copyStretches(raw.stretch_x, count: raw.stretch_x_count), + Self.copyStretches(raw.stretch_y, count: raw.stretch_y_count) ) } } - func styleImage(_ imageId: String) throws -> StyleImage? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - let imageIdView = arena.view(imageId) - guard let info = try NativeStyle.imageInfo( - requireLiveHandle(), - imageId: imageIdView - ) else { return nil } - guard let pixels = try NativeStyle.copyImagePremultipliedRGBA8( - requireLiveHandle(), - imageId: imageIdView, - capacity: info.byteLength - ).0 else { return nil } - return StyleImage(info: StyleImageInfo(native: info), pixels: pixels) - } + private func styleImageResult(_ imageId: String) async throws -> StyleImage? { + try await styleQuery( + { mln_map_get_style_image_info($0, $1.view(imageId), $2) }, + convert: Self.copyStyleImage + ) } + @discardableResult func addImageSourceURL( sourceId: String, coordinates: [LatLng], url: String - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try NativeStyle.addImageSourceURL( - requireLiveHandle(), - sourceId: arena.view(sourceId), - coordinates: coordinates.map(\.nativeInput), - url: arena.view(url) + ) async throws -> CommandCompletion { + try await imageSourceCommand(coordinates) { + mln_map_add_image_source_url( + $0, $1.view(sourceId), $2, $3, $1.view(url), $4 ) } } + @discardableResult func addImageSourceImage( sourceId: String, coordinates: [LatLng], image: StyleRGBA8Image - ) throws { - try mapNativeFailure { + ) async throws -> CommandCompletion { + let raw = try imageSourceCoordinates(coordinates) + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try image.nativeImage.withNativeImage { image in - try NativeStyle.addImageSourceImage( - requireLiveHandle(), - sourceId: arena.view(sourceId), - coordinates: coordinates.map(\.nativeInput), - image: image - ) + return try raw.withUnsafeBufferPointer { coordinates in + try image.nativeImage.withNativeImage { image in + try startCommand { + mln_map_add_image_source_image( + $0, arena.view(sourceId), coordinates.baseAddress, + coordinates.count, image, $1 + ) + } + } } } + return try await mapNativeFailure { try await future.value() } } - func setImageSourceURL(sourceId: String, url: String) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_image_source_url( - requireLiveHandle().raw, - arena.view(sourceId), - arena.view(url) - )) + @discardableResult + func setImageSourceURL( + sourceId: String, + url: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_image_source_url( + $0, $1.view(sourceId), $1.view(url), $2 + ) } } - func setImageSourceImage(sourceId: String, image: StyleRGBA8Image) throws { - try mapNativeFailure { + @discardableResult + func setImageSourceImage( + sourceId: String, + image: StyleRGBA8Image + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try image.nativeImage.withNativeImage { image in - try checkStatus(mln_map_set_image_source_image( - requireLiveHandle().raw, - arena.view(sourceId), - image - )) + return try image.nativeImage.withNativeImage { image in + try startCommand { + mln_map_set_image_source_image($0, arena.view(sourceId), image, $1) + } } } + return try await mapNativeFailure { try await future.value() } } + @discardableResult func setImageSourceCoordinates( sourceId: String, coordinates: [LatLng] - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try NativeStyle.setImageSourceCoordinates( - requireLiveHandle(), - sourceId: arena.view(sourceId), - coordinates: coordinates.map(\.nativeInput) + ) async throws -> CommandCompletion { + try await imageSourceCommand(coordinates) { + mln_map_set_image_source_coordinates( + $0, $1.view(sourceId), $2, $3, $4 ) } } - func imageSourceCoordinates(sourceId: String) throws -> [LatLng]? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.imageSourceCoordinates( - requireLiveHandle(), - sourceId: arena.view(sourceId) - )?.map(LatLng.init(native:)) + func imageSourceCoordinates(sourceId: String) async throws -> [LatLng]? { + try await styleQuery( + { mln_map_get_image_source_coordinates($0, $1.view(sourceId), $2) } + ) { result in + let values = try NativeCompletion.values(result, as: mln_lat_lng.self) + guard !values.isEmpty else { return nil } + guard values.count == 4 else { + throw NativeStatusFailure.swiftNativeError( + "image source completion did not contain four coordinates" + ) + } + return values.map { LatLng(native: NativeLatLng($0)) } } } + @discardableResult func addHillshadeLayer( layerId: String, sourceId: String, beforeLayerId: String? = nil - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_add_hillshade_layer( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(sourceId), - arena.view(beforeLayerId ?? "") - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_add_hillshade_layer( + $0, $1.view(layerId), $1.view(sourceId), + $1.view(beforeLayerId ?? ""), $2 + ) } } + @discardableResult func addColorReliefLayer( layerId: String, sourceId: String, beforeLayerId: String? = nil - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_add_color_relief_layer( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(sourceId), - arena.view(beforeLayerId ?? "") - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_add_color_relief_layer( + $0, $1.view(layerId), $1.view(sourceId), + $1.view(beforeLayerId ?? ""), $2 + ) } } + @discardableResult func addLocationIndicatorLayer( layerId: String, beforeLayerId: String? = nil - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_add_location_indicator_layer( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(beforeLayerId ?? "") - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_add_location_indicator_layer( + $0, $1.view(layerId), $1.view(beforeLayerId ?? ""), $2 + ) } } + @discardableResult func setLocationIndicatorLocation( layerId: String, coordinate: LatLng, altitude: Double - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_location_indicator_location( - requireLiveHandle().raw, - arena.view(layerId), - coordinate.nativeInput.native, - altitude - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_location_indicator_location( + $0, $1.view(layerId), coordinate.nativeInput.native, altitude, $2 + ) } } - func setLocationIndicatorBearing(layerId: String, bearing: Double) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_location_indicator_bearing( - requireLiveHandle().raw, - arena.view(layerId), - bearing - )) + @discardableResult + func setLocationIndicatorBearing( + layerId: String, + bearing: Double + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_location_indicator_bearing( + $0, $1.view(layerId), bearing, $2 + ) } } + @discardableResult func setLocationIndicatorAccuracyRadius( layerId: String, radius: Double - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_location_indicator_accuracy_radius( - requireLiveHandle().raw, - arena.view(layerId), - radius - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_location_indicator_accuracy_radius( + $0, $1.view(layerId), radius, $2 + ) } } + @discardableResult func setLocationIndicatorImageName( layerId: String, kind: LocationIndicatorImageKind, imageId: String - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_location_indicator_image_name( - requireLiveHandle().raw, - arena.view(layerId), - kind.rawValue, - arena.view(imageId) - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_location_indicator_image_name( + $0, $1.view(layerId), kind.rawValue, $1.view(imageId), $2 + ) } } + @discardableResult func addStyleLayerJSON( _ layerJSON: Data, beforeLayerId: String? = nil - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_add_style_layer_json( - requireLiveHandle().raw, - arena.view(layerJSON), - arena.view(beforeLayerId ?? "") - )) - } - } - - @discardableResult func removeStyleLayer(_ layerId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.removeLayer( - requireLiveHandle(), - layerId: arena.view(layerId) - ) - } - } - - func styleLayerExists(_ layerId: String) throws -> Bool { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.layerExists( - requireLiveHandle(), - layerId: arena.view(layerId) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_add_style_layer_json( + $0, $1.view(layerJSON), $1.view(beforeLayerId ?? ""), $2 ) } } - func styleLayerType(_ layerId: String) throws -> String? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.layerType( - requireLiveHandle(), - layerId: arena.view(layerId) - ) + @discardableResult + func removeStyleLayer( + _ layerId: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_remove_style_layer($0, $1.view(layerId), $2) } } - func styleLayerIds() throws -> [String] { - try mapNativeFailure { try NativeStyle.layerIds(requireLiveHandle()) } + func styleLayerInfo(_ layerId: String) async throws -> StyleLayerInfo? { + try await styleQuery( + { mln_map_get_style_layer_info($0, $1.view(layerId), $2) }, + convert: Self.copyStyleLayerInfo + ) } - func moveStyleLayer(_ layerId: String, beforeLayerId: String? = nil) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_move_style_layer( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(beforeLayerId ?? "") - )) - } + func styleLayerIds() async throws -> [String] { + try await styleQuery( + { mln_map_list_style_layer_ids($0, $2) }, + convert: Self.copyStrings + ) } - func styleLayerJSON(_ layerId: String) throws -> Data? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.layerJSON( - requireLiveHandle(), - layerId: arena.view(layerId) + @discardableResult + func moveStyleLayer( + _ layerId: String, + beforeLayerId: String? = nil + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_move_style_layer( + $0, $1.view(layerId), $1.view(beforeLayerId ?? ""), $2 ) } } - func setStyleLightJSON(_ lightJSON: Data) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_style_light_json( - requireLiveHandle().raw, - arena.view(lightJSON) - )) - } + func styleLayerJSON(_ layerId: String) async throws -> Data? { + try await styleQuery( + { mln_map_get_style_layer_json($0, $1.view(layerId), $2) }, + convert: Self.copyOptionalData + ) } - func setStyleLightProperty(_ propertyName: String, value: Data) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_style_light_property( - requireLiveHandle().raw, - arena.view(propertyName), - arena.view(value) - )) + @discardableResult + func setStyleLightJSON( + _ lightJSON: Data + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_style_light_json($0, $1.view(lightJSON), $2) } } - func styleLightProperty(_ propertyName: String) throws -> Data? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.lightProperty( - requireLiveHandle(), - propertyName: arena.view(propertyName) + @discardableResult + func setStyleLightProperty( + _ propertyName: String, + value: Data + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_style_light_property( + $0, $1.view(propertyName), $1.view(value), $2 ) } } - /// Replaces the style's global transition options; absent fields clear the - /// style-wide override rather than merging. Loading a style replaces these - /// options, so apply an override after the style loads. - func setStyleTransitionOptions(_ options: StyleTransitionOptions) throws { - try mapNativeFailure { - try NativeStyle.setTransitionOptions( - requireLiveHandle(), - options: options.nativeOptions - ) + func styleLightProperty(_ propertyName: String) async throws -> Data? { + try await styleQuery( + { mln_map_get_style_light_property($0, $1.view(propertyName), $2) }, + convert: Self.copyOptionalData + ) + } + + @discardableResult + func setStyleTransitionOptions( + _ options: StyleTransitionOptions + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { + try options.nativeOptions.withNativeOptions { native in + try startCommand { + mln_map_set_style_transition_options($0, native, $1) + } + } } + return try await mapNativeFailure { try await future.value() } } - /// Reads the style's global transition options. - func styleTransitionOptions() throws -> StyleTransitionOptions { - try mapNativeFailure { + func styleTransitionOptions() async throws -> StyleTransitionOptions { + try await styleQuery( + { mln_map_get_style_transition_options($0, $2) } + ) { result in try StyleTransitionOptions( - native: NativeStyle.transitionOptions(requireLiveHandle()) + native: NativeStyleTransitionOptions( + NativeCompletion.value(result) + ) ) } } + @discardableResult func setLayerProperty( layerId: String, propertyName: String, value: Data - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_property( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(propertyName), - arena.view(value) - )) + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_property( + $0, $1.view(layerId), $1.view(propertyName), $1.view(value), $2 + ) } } - func layerProperty(layerId: String, - propertyName: String) throws -> Data? - { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.layerProperty( - requireLiveHandle(), - layerId: arena.view(layerId), - propertyName: arena.view(propertyName) - ) - } + func layerProperty( + layerId: String, + propertyName: String + ) async throws -> Data? { + try await styleQuery( + { + mln_map_get_layer_property( + $0, $1.view(layerId), $1.view(propertyName), $2 + ) + }, + convert: Self.copyOptionalData + ) } - func setLayerFilter(layerId: String, filter: Data?) throws { - try mapNativeFailure { + @discardableResult + func setLayerFilter( + layerId: String, + filter: Data? + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } guard let filter else { - return try checkStatus(mln_map_set_layer_filter( - requireLiveHandle().raw, arena.view(layerId), nil - )) + return try startCommand { + mln_map_set_layer_filter($0, arena.view(layerId), nil, $1) + } } - var filterView = arena.view(filter) - try withUnsafePointer(to: &filterView) { filter in - try checkStatus(mln_map_set_layer_filter( - requireLiveHandle().raw, arena.view(layerId), filter - )) + var view = arena.view(filter) + return try withUnsafePointer(to: &view) { view in + try startCommand { + mln_map_set_layer_filter($0, arena.view(layerId), view, $1) + } } } + return try await mapNativeFailure { try await future.value() } } - func layerFilter(_ layerId: String) throws -> Data? { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.layerFilter( - requireLiveHandle(), - layerId: arena.view(layerId) + func layerFilter(_ layerId: String) async throws -> Data? { + try await styleQuery( + { mln_map_get_layer_filter($0, $1.view(layerId), $2) }, + convert: Self.copyOptionalData + ) + } + + @discardableResult + func setLayerSourceLayer( + layerId: String, + sourceLayer: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_source_layer( + $0, $1.view(layerId), $1.view(sourceLayer), $2 ) } } - /// Sets one layer's source-layer ID. Layer types that take no source, such as - /// background, are rejected. - func setLayerSourceLayer(layerId: String, sourceLayer: String) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_source_layer( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(sourceLayer) - )) + func layerSourceLayer(_ layerId: String) async throws -> String { + try await styleQuery( + { mln_map_copy_layer_source_layer($0, $1.view(layerId), $2) }, + convert: NativeCompletion.string + ) + } + + @discardableResult + func setLayerSourceId( + layerId: String, + sourceId: String + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_source_id( + $0, $1.view(layerId), $1.view(sourceId), $2 + ) } } - /// Copies one layer's source-layer ID, empty when the layer carries none. - func layerSourceLayer(_ layerId: String) throws -> String { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - return try NativeStyle.copyLayerText( - requireLiveHandle(), - layerId: arena.view(layerId), - copy: mln_map_copy_layer_source_layer + func layerSourceId(_ layerId: String) async throws -> String { + try await styleQuery( + { mln_map_copy_layer_source_id($0, $1.view(layerId), $2) }, + convert: NativeCompletion.string + ) + } + + @discardableResult + func setLayerMinZoom( + layerId: String, + minZoom: Double + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_min_zoom($0, $1.view(layerId), minZoom, $2) + } + } + + @discardableResult + func setLayerMaxZoom( + layerId: String, + maxZoom: Double + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_max_zoom($0, $1.view(layerId), maxZoom, $2) + } + } + + @discardableResult + func setLayerVisibility( + layerId: String, + visibility: StyleLayerVisibility + ) async throws -> CommandCompletion { + try await styleCommand { + mln_map_set_layer_visibility( + $0, $1.view(layerId), visibility.rawValue, $2 ) } } - /// Sets one layer's source ID. Layer types that take no source, such as - /// background, are rejected. The named source need not exist yet. - func setLayerSourceId(layerId: String, sourceId: String) throws { - try mapNativeFailure { + private func styleCommand( + _ body: ( + mln_map, + NativeInputArena, + UnsafePointer + ) -> mln_status + ) async throws -> CommandCompletion { + let future = try mapNativeFailure { let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_source_id( - requireLiveHandle().raw, - arena.view(layerId), - arena.view(sourceId) - )) + return try startCommand { body($0, arena, $1) } } + return try await mapNativeFailure { try await future.value() } } - /// Copies one layer's source ID, empty when the layer carries none. - func layerSourceId(_ layerId: String) throws -> String { - try mapNativeFailure { + private func styleQuery( + _ body: ( + mln_map, + NativeInputArena, + UnsafePointer + ) -> mln_status, + convert: @escaping ( + UnsafePointer + ) throws -> Value + ) async throws -> Value { + let future = try mapNativeFailure { + let map = try requireLiveHandle() let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - return try NativeStyle.copyLayerText( - requireLiveHandle(), - layerId: arena.view(layerId), - copy: mln_map_copy_layer_source_id + return try NativeCompletion.start( + { body(map.raw, arena, $0) }, + convert: convert ) } + return try await mapNativeFailure { try await future.value() } } - /// Sets the lowest zoom at which one layer draws. Pass `-.infinity` for no - /// lower bound. - func setLayerMinZoom(layerId: String, minZoom: Double) throws { - try mapNativeFailure { + private func imageSourceCommand( + _ coordinates: [LatLng], + _ body: ( + mln_map, + NativeInputArena, + UnsafePointer?, + Int, + UnsafePointer + ) -> mln_status + ) async throws -> CommandCompletion { + let raw = try imageSourceCoordinates(coordinates) + let future = try mapNativeFailure { + let map = try requireLiveHandle() let arena = NativeInputArena() defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_min_zoom( - requireLiveHandle().raw, - arena.view(layerId), - minZoom - )) + return try raw.withUnsafeBufferPointer { coordinates in + try NativeCompletion.startCommand { + body( + map.raw, arena, coordinates.baseAddress, coordinates.count, $0 + ) + } + } } + return try await mapNativeFailure { try await future.value() } } - /// Reads the lowest zoom at which one layer draws. A layer with no lower - /// bound - /// reports `-.infinity`. - func layerMinZoom(_ layerId: String) throws -> Double { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - let handle = try requireLiveHandle() - let layerView = arena.view(layerId) - return try NativeMemory.withTemporary(0.0) { outZoom in - try checkStatus(mln_map_get_layer_min_zoom( - handle.raw, - layerView, - outZoom - )) - }.value + private func imageSourceCoordinates( + _ coordinates: [LatLng] + ) throws -> [mln_lat_lng] { + guard coordinates.count == 4 else { + throw MaplibreError.invalidArgument( + "image source coordinates must contain exactly 4 coordinates" + ) } + return coordinates.map { $0.nativeInput.native } } - /// Sets the highest zoom at which one layer draws. Pass `.infinity` for no - /// upper bound. - func setLayerMaxZoom(layerId: String, maxZoom: Double) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_max_zoom( - requireLiveHandle().raw, - arena.view(layerId), - maxZoom - )) + private static func copyOptionalData( + _ result: UnsafePointer + ) throws -> Data? { + guard result.pointee.value_count > 0 else { return nil } + return try NativeCompletion.data(result) + } + + private static func copyOptionalString( + _ result: UnsafePointer + ) throws -> String? { + guard result.pointee.value_count > 0 else { return nil } + return try NativeCompletion.string(result) + } + + private static func copyStrings( + _ result: UnsafePointer + ) throws -> [String] { + try NativeCompletion.values(result, as: mln_buffer_view.self).map { + try NativeString.copyUTF8(data: $0.data, size: $0.size) } } - /// Reads the highest zoom at which one layer draws. A layer with no upper - /// bound reports `.infinity`. - func layerMaxZoom(_ layerId: String) throws -> Double { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - let handle = try requireLiveHandle() - let layerView = arena.view(layerId) - return try NativeMemory.withTemporary(0.0) { outZoom in - try checkStatus(mln_map_get_layer_max_zoom( - handle.raw, - layerView, - outZoom - )) - }.value + private static func copyStyleSourceInfo( + _ result: UnsafePointer + ) throws -> StyleSourceInfo? { + guard result.pointee.value_count > 0 else { return nil } + let raw: mln_style_source_result = try NativeCompletion.value(result) + let has: (UInt32) -> Bool = { (raw.info.fields & $0) != 0 } + let attribution = raw.info.has_attribution + ? try NativeString.copyUTF8( + data: raw.attribution.data, size: raw.attribution.size + ) : nil + let url = has(MLN_STYLE_SOURCE_INFO_URL.rawValue) + ? try NativeString.copyUTF8(data: raw.url.data, size: raw.url.size) : nil + let tileURLs: [String] + if raw.tile_url_count == 0 { + tileURLs = [] + } else { + guard let values = raw.tile_urls else { + throw NativeStatusFailure.swiftNativeError( + "source completion returned a null tile URL array" + ) + } + tileURLs = try UnsafeBufferPointer( + start: values, count: raw.tile_url_count + ).map { try NativeString.copyUTF8(data: $0.data, size: $0.size) } } + return StyleSourceInfo(native: NativeStyle.sourceInfo( + fixed: raw.info, + attribution: attribution, + url: url, + tileURLs: tileURLs + )) } - /// Sets whether one layer draws. - func setLayerVisibility( - layerId: String, - visibility: StyleLayerVisibility - ) throws { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - try checkStatus(mln_map_set_layer_visibility( - requireLiveHandle().raw, - arena.view(layerId), - visibility.rawValue + private static func copyStyleLayerInfo( + _ result: UnsafePointer + ) throws -> StyleLayerInfo? { + guard result.pointee.value_count > 0 else { return nil } + let raw: mln_style_layer_result = try NativeCompletion.value(result) + let has: (UInt32) -> Bool = { (raw.info.fields & $0) != 0 } + return try StyleLayerInfo( + type: NativeString.copyUTF8( + data: raw.info.type.data, size: raw.info.type.size + ), + minZoom: raw.info.min_zoom, + maxZoom: raw.info.max_zoom, + visibility: StyleLayerVisibility(rawValue: raw.info.visibility), + sourceId: has(MLN_STYLE_LAYER_INFO_SOURCE_ID.rawValue) + ? NativeString.copyUTF8( + data: raw.source_id.data, size: raw.source_id.size + ) : nil, + sourceLayer: has(MLN_STYLE_LAYER_INFO_SOURCE_LAYER.rawValue) + ? NativeString.copyUTF8( + data: raw.source_layer.data, size: raw.source_layer.size + ) : nil + ) + } + + private static func copyStyleImage( + _ result: UnsafePointer + ) throws -> StyleImage? { + guard result.pointee.value_count > 0 else { return nil } + let raw: mln_style_image_result = try NativeCompletion.value(result) + let info = StyleImageInfo(native: NativeStyleImageInfo(raw.info)) + let pixels: [UInt8] + if raw.pixels.size == 0 { + pixels = [] + } else { + guard let data = raw.pixels.data else { + throw NativeStatusFailure.swiftNativeError( + "style image completion returned null pixels" + ) + } + pixels = Array(UnsafeBufferPointer( + start: data.assumingMemoryBound(to: UInt8.self), + count: raw.pixels.size )) } + return StyleImage(info: info, pixels: pixels) } - /// Reads whether one layer draws. - func layerVisibility(_ layerId: String) throws -> StyleLayerVisibility { - try mapNativeFailure { - let arena = NativeInputArena() - defer { withExtendedLifetime(arena) {} } - let handle = try requireLiveHandle() - let layerView = arena.view(layerId) - let raw = try NativeMemory.withTemporary(UInt32(0)) { outVisibility in - try checkStatus(mln_map_get_layer_visibility( - handle.raw, - layerView, - outVisibility - )) - }.value - return StyleLayerVisibility(rawValue: raw) + private static func copyStretches( + _ values: UnsafePointer?, + count: Int + ) throws -> [ImageStretch] { + guard count > 0 else { return [] } + guard let values else { + throw NativeStatusFailure.swiftNativeError( + "style image completion returned a null stretch array" + ) + } + return UnsafeBufferPointer(start: values, count: count).map { + ImageStretch(from: $0.from, to: $0.to) } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/LoggingCallbacks.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/LoggingCallbacks.swift index 9fc031139..8868a214e 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/LoggingCallbacks.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/LoggingCallbacks.swift @@ -38,43 +38,46 @@ private func logCallbackTrampoline( return box.invoke(record) ? 1 : 0 } -enum LoggingCallbackState { - private static let lock = NSLock() - private nonisolated(unsafe) static var retainedBox: Unmanaged? +private func releaseLogCallback(_ userData: UnsafeMutableRawPointer?) { + guard let userData else { return } + Unmanaged.fromOpaque(userData).release() +} +enum LoggingCallbackState { static func set(_ callback: @escaping @Sendable (NativeLogRecord) -> Bool) throws { let replacement = Unmanaged.passRetained(LogCallbackBox(callback)) - var old: Unmanaged? do { - try lock.withLock { - try checkStatus(mln_log_set_callback( - logCallbackTrampoline, - replacement.toOpaque() - )) - old = retainedBox - retainedBox = replacement - } + try checkStatus(mln_log_set_callback( + logCallbackTrampoline, + replacement.toOpaque(), + releaseLogCallback + )) } catch { replacement.release() throw error } - old?.release() } static func clear() throws { - var old: Unmanaged? - try lock.withLock { - try checkStatus(mln_log_clear_callback()) - old = retainedBox - retainedBox = nil - } - old?.release() + try checkStatus(mln_log_clear_callback()) } - static func invokeForTesting(_ record: NativeLogRecord) -> Bool? { - let box = lock.withLock { retainedBox?.takeUnretainedValue() } - return box?.invoke(record) + static func invokeForTesting( + _ callback: @escaping @Sendable (NativeLogRecord) -> Bool, + record: NativeLogRecord + ) -> Bool { + let box = Unmanaged.passRetained(LogCallbackBox(callback)) + defer { box.release() } + return record.message.withCString { message in + logCallbackTrampoline( + userData: box.toOpaque(), + severity: record.severity, + event: record.event, + code: record.code, + message: message + ) != 0 + } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/MapStructs.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/MapStructs.swift index 80c03757f..bacdd34ce 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/MapStructs.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/MapStructs.swift @@ -94,9 +94,11 @@ struct NativeMapOptionsInput: Equatable { _ body: (UnsafePointer) throws -> Result ) throws -> Result { var options = mln_map_options_default() - options.width = width - options.height = height - options.scale_factor = scaleFactor + options.initial_extent = mln_logical_extent( + width: width, + height: height, + scale_factor: scaleFactor + ) options.map_mode = mapMode options.fast_pfor_enabled = fastPFOREnabled options.event_mask = eventMask diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeCompletion.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeCompletion.swift new file mode 100644 index 000000000..fabea75bf --- /dev/null +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeCompletion.swift @@ -0,0 +1,236 @@ +internal import CMaplibreNativeC +import Foundation + +private protocol AnyNativeCompletionState: AnyObject { + func complete(_ result: UnsafePointer) +} + +private final class NativeCompletionState: + AnyNativeCompletionState, @unchecked Sendable +{ + private let lock = NSCondition() + private let convert: (UnsafePointer) throws -> Value + private let acceptErrorStatus: Bool + private var result: Result? + private var waiter: CheckedContinuation? + + init( + acceptErrorStatus: Bool = false, + convert: @escaping (UnsafePointer) throws -> Value + ) { + self.acceptErrorStatus = acceptErrorStatus + self.convert = convert + } + + func complete(_ native: UnsafePointer) { + let converted: Result + do { + let raw = native.pointee + guard raw.status == MLN_STATUS_OK || acceptErrorStatus else { + let diagnostic = try NativeString.copyUTF8( + data: raw.diagnostic.data, + size: raw.diagnostic.size + ) + throw NativeStatusFailure( + rawStatus: raw.status.rawValue, + diagnostic: diagnostic + ) + } + converted = try .success(convert(native)) + } catch { + converted = .failure(error) + } + + let waiter = lock.withLock { () -> CheckedContinuation? in + if let waiter = self.waiter { + self.waiter = nil + return waiter + } + result = converted + lock.broadcast() + return nil + } + waiter?.resume(with: converted) + } + + func value() async throws -> Value { + try await withCheckedThrowingContinuation { continuation in + let completed = lock.withLock { () -> Result? in + if let result { + self.result = nil + return result + } + precondition(waiter == nil) + waiter = continuation + return nil + } + if let completed { continuation.resume(with: completed) } + } + } + + /// Blocks the calling thread until the completion runs. + /// + /// Native runs the completion on its own thread, so this suits a host that + /// must not return before native work finishes, such as one tearing a runtime + /// down before process exit. + func valueBlocking() throws -> Value { + lock.lock() + defer { lock.unlock() } + while result == nil { + lock.wait() + } + let completed = result + result = nil + return try completed!.get() + } +} + +struct NativeFuture { + fileprivate let state: NativeCompletionState + + func value() async throws -> Value { + try await state.value() + } + + func valueBlocking() throws -> Value { + try state.valueBlocking() + } +} + +enum NativeCompletion { + /// Starts native work synchronously and returns its one-shot async value. + static func start( + _ call: (UnsafePointer) -> mln_status, + acceptErrorStatus: Bool = false, + convert: @escaping (UnsafePointer) throws -> Value + ) throws -> NativeFuture { + let state = NativeCompletionState( + acceptErrorStatus: acceptErrorStatus, + convert: convert + ) + let retained = Unmanaged.passRetained(state) + var descriptor = mln_completion() + descriptor.size = UInt32(MemoryLayout.size) + descriptor.callback = { userData, result in + guard let userData, let result else { return } + let state = Unmanaged.fromOpaque(userData) + .takeUnretainedValue() as! AnyNativeCompletionState + state.complete(result) + } + descriptor.user_data = retained.toOpaque() + descriptor.release_user_data = { userData in + guard let userData else { return } + Unmanaged.fromOpaque(userData).release() + } + + let status = withUnsafePointer(to: &descriptor, call) + if status != MLN_STATUS_OK { + retained.release() + try checkStatus(status) + } + return NativeFuture(state: state) + } + + static func submit( + _ call: (UnsafePointer) -> mln_status, + convert: @escaping (UnsafePointer) throws -> Value + ) async throws -> Value { + try await start(call, convert: convert).value() + } + + static func startUnit( + _ call: (UnsafePointer) -> mln_status + ) throws -> NativeFuture { + try start(call) { _ in () } + } + + static func unit( + _ call: (UnsafePointer) -> mln_status + ) async throws { + try await startUnit(call).value() + } + + static func startCommand( + _ call: (UnsafePointer) -> mln_status + ) throws -> NativeFuture { + try start(call, acceptErrorStatus: true) { result in + try CommandCompletion( + disposition: CommandDisposition.fromNative(result.pointee.disposition), + generation: result.pointee.generation, + rawStatus: result.pointee.status.rawValue, + diagnostic: NativeString.copyUTF8( + data: result.pointee.diagnostic.data, + size: result.pointee.diagnostic.size + ) + ) + } + } + + static func command( + _ call: (UnsafePointer) -> mln_status + ) async throws -> CommandCompletion { + try await startCommand(call).value() + } + + static func value( + _ result: UnsafePointer, + as _: Value.Type = Value.self + ) throws -> Value { + guard result.pointee.value_count == 1, + let value = result.pointee.value + else { + throw NativeStatusFailure.swiftNativeError( + "native completion returned no value" + ) + } + return value.load(as: Value.self) + } + + static func values( + _ result: UnsafePointer, + as _: Value.Type = Value.self + ) throws -> UnsafeBufferPointer { + guard result.pointee.value_count > 0 else { + return UnsafeBufferPointer(start: nil, count: 0) + } + guard let value = result.pointee.value else { + throw NativeStatusFailure.swiftNativeError( + "native completion returned a null array" + ) + } + return UnsafeBufferPointer( + start: value.assumingMemoryBound(to: Value.self), + count: result.pointee.value_count + ) + } + + static func data( + _ result: UnsafePointer + ) throws -> Data { + let view: mln_buffer_view = try value(result) + guard view.size > 0 else { return Data() } + guard let bytes = view.data else { + throw NativeStatusFailure.swiftNativeError( + "native completion returned a null buffer" + ) + } + return Data(bytes: bytes, count: view.size) + } + + static func dataView(_ view: mln_buffer_view) throws -> Data { + guard view.size > 0 else { return Data() } + guard let bytes = view.data else { + throw NativeStatusFailure.swiftNativeError( + "native completion returned a null buffer" + ) + } + return Data(bytes: bytes, count: view.size) + } + + static func string( + _ result: UnsafePointer + ) throws -> String { + let view: mln_buffer_view = try value(result) + return try NativeString.copyUTF8(data: view.data, size: view.size) + } +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandleState.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandleState.swift index 58107c6e8..56a241a36 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandleState.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandleState.swift @@ -10,7 +10,6 @@ final class NativeHandleState: @unchecked Sendable { private let typeName: String private let lock = NSCondition() private var state: State - private var activeUses = 0 init(typeName: String, handle: Handle) throws { guard !handle.isNull else { @@ -58,25 +57,11 @@ final class NativeHandleState: @unchecked Sendable { } } - /// Runs `use` with the handle and holds off release until it returns. Use - /// this for handles the host may use and release from different threads; - /// handles confined to one thread can call native after `requireLive()`. - /// - /// `use` runs outside the lock, so concurrent uses proceed together. Calling - /// `closeOnce` from inside `use` on the same thread deadlocks. + /// Runs `use` after checking that this wrapper still owns the handle. The C + /// API leases its native object for each entry point, so concurrent release + /// does not need a second binding-side active-use lease. func withLive(_ use: (Handle) throws -> T) throws -> T { - let handle = try lock.withLock { - let handle = try requireLiveLocked() - activeUses += 1 - return handle - } - defer { - lock.withLock { - activeUses -= 1 - lock.broadcast() - } - } - return try use(handle) + try use(requireLive()) } func closeOnce(_ destroy: (Handle) throws -> Void) throws { @@ -84,11 +69,6 @@ final class NativeHandleState: @unchecked Sendable { switch state { case let .live(handle): state = .closing(handle) - // Uses that already passed their liveness check still hold the handle, - // so wait for them before destroying it. - while activeUses > 0 { - lock.wait() - } return handle case .closing: throw NativeStatusFailure( diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandles.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandles.swift index 58b0d86af..60e866678 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandles.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeHandles.swift @@ -57,10 +57,6 @@ struct NativeGeoJSONSourceDataHandle: NativeHandle { let raw: UInt64 } -struct NativeWakeSourceHandle: NativeHandle { - let raw: UInt64 -} - struct NativeResourceRequestHandle: NativeHandle { let raw: UInt64 } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeMap.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeMap.swift index a1f0546d6..31293429f 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeMap.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeMap.swift @@ -4,95 +4,75 @@ enum NativeMap { static func create( runtime: NativeRuntimeHandle, options: UnsafePointer - ) throws -> NativeMapHandle { - try NativeHandleFactory - .create(nullDiagnostic: "mln_map_create returned a null map") { outHandle in - try checkStatus(mln_map_create(runtime.raw, options, outHandle)) - } - } - - static func setEventMask(_ map: NativeMapHandle, mask: UInt64) throws { - try checkStatus(mln_map_set_event_mask(map.raw, mask)) - } - - static func eventMask(_ map: NativeMapHandle) throws -> UInt64 { - try NativeMemory.withTemporary(UInt64(0)) { mask in - try checkStatus(mln_map_get_event_mask(map.raw, mask)) - }.value - } - - static func debugOptions(_ map: NativeMapHandle) throws -> UInt32 { - try NativeMemory.withTemporary(UInt32(0)) { options in - try checkStatus(mln_map_get_debug_options(map.raw, options)) - }.value - } - - static func renderingStatsViewEnabled(_ map: NativeMapHandle) throws -> Bool { - try NativeMemory.withTemporary(false) { enabled in - try checkStatus(mln_map_get_rendering_stats_view_enabled( - map.raw, - enabled - )) - }.value - } - - static func isFullyLoaded(_ map: NativeMapHandle) throws -> Bool { - try NativeMemory.withTemporary(false) { loaded in - try checkStatus(mln_map_is_fully_loaded(map.raw, loaded)) - }.value - } - - static func isGestureInProgress(_ map: NativeMapHandle) throws -> Bool { - try NativeMemory.withTemporary(false) { inProgress in - try checkStatus(mln_map_is_gesture_in_progress(map.raw, inProgress)) - }.value + ) throws -> NativeFuture { + try NativeCompletion.start( + { mln_map_create(runtime.raw, options, $0) } + ) { result in + try NativeMapHandle(raw: NativeCompletion.value(result, as: mln_map.self)) + } } - static func size(_ map: NativeMapHandle) throws - -> (width: UInt32, height: UInt32, scaleFactor: Double) - { - var width: UInt32 = 0 - var height: UInt32 = 0 - var scaleFactor: Double = 0 - try checkStatus(mln_map_get_size(map.raw, &width, &height, &scaleFactor)) - return (width: width, height: height, scaleFactor: scaleFactor) + static func release(_ map: NativeMapHandle) throws -> NativeFuture { + try NativeCompletion.startUnit { completion in + mln_map_release(map.raw, completion) + } } - static func viewportOptions(_ map: NativeMapHandle) throws - -> mln_map_viewport_options - { - var options = mln_map_viewport_options_default() - try checkStatus(mln_map_get_viewport_options(map.raw, &options)) - return options + static func snapshot(_ map: NativeMapHandle) throws -> mln_map_snapshot { + var snapshot = mln_map_snapshot() + snapshot.size = UInt32(MemoryLayout.size) + try checkStatus(mln_map_snapshot_get(map.raw, &snapshot)) + return snapshot } - static func tileOptions(_ map: NativeMapHandle) throws - -> mln_map_tile_options + static func cameraSnapshot(_ map: NativeMapHandle) throws + -> (camera: mln_camera_options, generation: UInt64) { - var options = mln_map_tile_options_default() - try checkStatus(mln_map_get_tile_options(map.raw, &options)) - return options + var camera = mln_camera_options_default() + var generation: UInt64 = 0 + try checkStatus(mln_map_camera_snapshot_get( + map.raw, + &camera, + &generation + )) + return (camera, generation) + } + + static func cameraQuery( + _ map: NativeMapHandle + ) throws -> NativeFuture { + try NativeCompletion.start( + { mln_map_camera_query(map.raw, $0) } + ) { result in + let value: mln_camera_query_result = try NativeCompletion.value(result) + return CameraSnapshot( + generation: value.generation, + camera: CameraOptions(native: NativeCameraOptionsInput(value.camera)) + ) + } } - static func camera(_ map: NativeMapHandle) throws -> mln_camera_options { - var camera = mln_camera_options_default() - try checkStatus(mln_map_get_camera(map.raw, &camera)) - return camera + static func requestStillImage( + _ map: NativeMapHandle + ) throws -> NativeFuture { + try NativeCompletion.startUnit { + mln_map_request_still_image(map.raw, $0) + } } static func cameraForLatLngBounds( _ map: NativeMapHandle, bounds: NativeLatLngBounds, fitOptions: UnsafePointer? - ) throws -> mln_camera_options { - var camera = mln_camera_options_default() - try checkStatus(mln_map_camera_for_lat_lng_bounds( - map.raw, - bounds.native, - fitOptions, - &camera - )) - return camera + ) throws -> NativeFuture { + try camera { + mln_map_camera_for_lat_lng_bounds( + map.raw, + bounds.native, + fitOptions, + $0 + ) + } } static func cameraForLatLngs( @@ -100,147 +80,127 @@ enum NativeMap { coordinates: UnsafePointer?, count: Int, fitOptions: UnsafePointer? - ) throws -> mln_camera_options { - var camera = mln_camera_options_default() - try checkStatus(mln_map_camera_for_lat_lngs( - map.raw, - coordinates, - count, - fitOptions, - &camera - )) - return camera + ) throws -> NativeFuture { + try camera { + mln_map_camera_for_lat_lngs( + map.raw, + coordinates, + count, + fitOptions, + $0 + ) + } } static func cameraForGeometry( _ map: NativeMapHandle, geometry: mln_buffer_view, fitOptions: UnsafePointer? - ) throws -> mln_camera_options { - var camera = mln_camera_options_default() - try checkStatus(mln_map_camera_for_geometry( - map.raw, - geometry, - fitOptions, - &camera - )) - return camera + ) throws -> NativeFuture { + try camera { + mln_map_camera_for_geometry(map.raw, geometry, fitOptions, $0) + } + } + + private static func camera( + _ start: (UnsafePointer) -> mln_status + ) throws -> NativeFuture { + try NativeCompletion.start(start) { result in + let value: mln_camera_options = try NativeCompletion.value(result) + return CameraOptions(native: NativeCameraOptionsInput(value)) + } } static func latLngBoundsForCamera( _ map: NativeMapHandle, camera: UnsafePointer, unwrapped: Bool - ) throws -> NativeLatLngBounds { - let output = try NativeMemory - .withTemporary(mln_lat_lng_bounds()) { bounds in + ) throws -> NativeFuture { + try NativeCompletion.start( + { completion in if unwrapped { - try checkStatus(mln_map_lat_lng_bounds_for_camera_unwrapped( + mln_map_lat_lng_bounds_for_camera_unwrapped( map.raw, camera, - bounds - )) + completion + ) } else { - try checkStatus(mln_map_lat_lng_bounds_for_camera( - map.raw, - camera, - bounds - )) + mln_map_lat_lng_bounds_for_camera(map.raw, camera, completion) } } - return NativeLatLngBounds(output.value) - } - - static func bounds(_ map: NativeMapHandle) throws -> mln_bound_options { - var bounds = mln_bound_options_default() - try checkStatus(mln_map_get_bounds(map.raw, &bounds)) - return bounds - } - - static func freeCameraOptions(_ map: NativeMapHandle) throws - -> mln_free_camera_options - { - var options = mln_free_camera_options_default() - try checkStatus(mln_map_get_free_camera_options(map.raw, &options)) - return options - } - - static func projectionMode(_ map: NativeMapHandle) throws - -> mln_projection_mode - { - var mode = mln_projection_mode_default() - try checkStatus(mln_map_get_projection_mode(map.raw, &mode)) - return mode + ) { result in + let value: mln_lat_lng_bounds = try NativeCompletion.value(result) + return LatLngBounds(native: NativeLatLngBounds(value)) + } } - static func pixelForLatLng(_ map: NativeMapHandle, - coordinate: NativeLatLng) throws - -> NativeScreenPoint - { - let output = try NativeMemory.withTemporary(mln_screen_point()) { point in - try checkStatus(mln_map_pixel_for_lat_lng( - map.raw, - coordinate.native, - point - )) + static func pixelForLatLng( + _ map: NativeMapHandle, + coordinate: NativeLatLng + ) throws -> NativeFuture { + try NativeCompletion.start( + { mln_map_pixel_for_lat_lng(map.raw, coordinate.native, $0) } + ) { result in + let value: mln_screen_point = try NativeCompletion.value(result) + return ScreenPoint(native: NativeScreenPoint(value)) } - return NativeScreenPoint(output.value) } - static func latLngForPixel(_ map: NativeMapHandle, - point: NativeScreenPoint) throws -> NativeLatLng - { - let output = try NativeMemory.withTemporary(mln_lat_lng()) { coordinate in - try checkStatus(mln_map_lat_lng_for_pixel( - map.raw, - point.native, - coordinate - )) + static func latLngForPixel( + _ map: NativeMapHandle, + point: NativeScreenPoint + ) throws -> NativeFuture { + try NativeCompletion.start( + { mln_map_lat_lng_for_pixel(map.raw, point.native, $0) } + ) { result in + let value: mln_lat_lng = try NativeCompletion.value(result) + return LatLng(native: NativeLatLng(value)) } - return NativeLatLng(output.value) } static func pixelsForLatLngs( _ map: NativeMapHandle, coordinates: [NativeLatLng] - ) throws -> [NativeScreenPoint] { - let rawCoordinates = coordinates.map(\.native) - var rawPoints = [mln_screen_point]( - repeating: mln_screen_point(), - count: rawCoordinates.count - ) - try rawCoordinates.withUnsafeBufferPointer { coordinates in - try rawPoints.withUnsafeMutableBufferPointer { points in - try checkStatus(mln_map_pixels_for_lat_lngs( - map.raw, - coordinates.baseAddress, - coordinates.count, - points.baseAddress - )) + ) throws -> NativeFuture<[ScreenPoint]> { + let raw = coordinates.map(\.native) + return try raw.withUnsafeBufferPointer { coordinates in + try NativeCompletion.start( + { + mln_map_pixels_for_lat_lngs( + map.raw, + coordinates.baseAddress, + coordinates.count, + $0 + ) + } + ) { result in + try NativeCompletion.values(result, as: mln_screen_point.self).map { + ScreenPoint(native: NativeScreenPoint($0)) + } } } - return rawPoints.map(NativeScreenPoint.init) } static func latLngsForPixels( _ map: NativeMapHandle, points: [NativeScreenPoint] - ) throws -> [NativeLatLng] { - let rawPoints = points.map(\.native) - var rawCoordinates = [mln_lat_lng]( - repeating: mln_lat_lng(), - count: rawPoints.count - ) - try rawPoints.withUnsafeBufferPointer { points in - try rawCoordinates.withUnsafeMutableBufferPointer { coordinates in - try checkStatus(mln_map_lat_lngs_for_pixels( - map.raw, - points.baseAddress, - points.count, - coordinates.baseAddress - )) + ) throws -> NativeFuture<[LatLng]> { + let raw = points.map(\.native) + return try raw.withUnsafeBufferPointer { points in + try NativeCompletion.start( + { + mln_map_lat_lngs_for_pixels( + map.raw, + points.baseAddress, + points.count, + $0 + ) + } + ) { result in + try NativeCompletion.values(result, as: mln_lat_lng.self).map { + LatLng(native: NativeLatLng($0)) + } } } - return rawCoordinates.map(NativeLatLng.init) } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeOffline.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeOffline.swift deleted file mode 100644 index 41ad33eca..000000000 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeOffline.swift +++ /dev/null @@ -1,316 +0,0 @@ -internal import CMaplibreNativeC -import Foundation - -enum NativeOffline { - static func runAmbientCacheOperationStart( - _ runtime: NativeRuntimeHandle, - operation: UInt32 - ) throws -> UInt64 { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_run_ambient_cache_operation_start( - runtime.raw, - operation, - operationId - )) - }.value - } - - static func setMaximumAmbientCacheSizeStart( - _ runtime: NativeRuntimeHandle, - size: UInt64 - ) throws -> UInt64 { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_set_maximum_ambient_cache_size_start( - runtime.raw, - size, - operationId - )) - }.value - } - - static func regionCreateStart( - _ runtime: NativeRuntimeHandle, - definition: UnsafePointer, - metadata: Data - ) throws -> UInt64 { - try metadata.withUnsafeBytes { bytes in - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_create_start( - runtime.raw, - definition, - bytes.bindMemory(to: UInt8.self).baseAddress, - bytes.count, - operationId - )) - }.value - } - } - - static func regionGetStart(_ runtime: NativeRuntimeHandle, - regionId: Int64) throws -> UInt64 - { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_get_start( - runtime.raw, - regionId, - operationId - )) - }.value - } - - static func regionsListStart(_ runtime: NativeRuntimeHandle) throws - -> UInt64 - { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_regions_list_start( - runtime.raw, - operationId - )) - }.value - } - - static func regionsMergeDatabaseStart( - _ runtime: NativeRuntimeHandle, - sideDatabasePath: String - ) throws -> UInt64 { - try NativeString.withCString(sideDatabasePath) { path in - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_regions_merge_database_start( - runtime.raw, - path, - operationId - )) - }.value - } - } - - static func regionUpdateMetadataStart( - _ runtime: NativeRuntimeHandle, - regionId: Int64, - metadata: Data - ) throws -> UInt64 { - try metadata.withUnsafeBytes { bytes in - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_update_metadata_start( - runtime.raw, - regionId, - bytes.bindMemory(to: UInt8.self).baseAddress, - bytes.count, - operationId - )) - }.value - } - } - - static func regionGetStatusStart(_ runtime: NativeRuntimeHandle, - regionId: Int64) throws -> UInt64 - { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_get_status_start( - runtime.raw, - regionId, - operationId - )) - }.value - } - - static func regionSetObservedStart( - _ runtime: NativeRuntimeHandle, - regionId: Int64, - observed: Bool - ) throws -> UInt64 { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_set_observed_start( - runtime.raw, - regionId, - observed, - operationId - )) - }.value - } - - static func regionSetDownloadStateStart( - _ runtime: NativeRuntimeHandle, - regionId: Int64, - state: UInt32 - ) throws -> UInt64 { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_set_download_state_start( - runtime.raw, - regionId, - state, - operationId - )) - }.value - } - - static func regionInvalidateStart(_ runtime: NativeRuntimeHandle, - regionId: Int64) throws -> UInt64 - { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_invalidate_start( - runtime.raw, - regionId, - operationId - )) - }.value - } - - static func regionDeleteStart(_ runtime: NativeRuntimeHandle, - regionId: Int64) throws -> UInt64 - { - try NativeMemory.withTemporary(UInt64(0)) { operationId in - try checkStatus(mln_runtime_offline_region_delete_start( - runtime.raw, - regionId, - operationId - )) - }.value - } - - static func regionCreateTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> NativeOfflineRegionInfo { - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_runtime_offline_region_create_take_result( - runtime.raw, - operationId, - outHandle - )) - }.value - let snapshot = NativeOfflineRegionSnapshotHandle(raw: snapshotValue) - guard !snapshot.isNull else { throw NativeStatusFailure( - rawStatus: 0, - diagnostic: "offline region create result was null" - ) } - defer { mln_offline_region_snapshot_destroy(snapshot.raw) } - return try offlineRegionSnapshotCopy(snapshot) - } - - static func regionGetTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> NativeOfflineRegionInfo? { - var found = false - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_runtime_offline_region_get_take_result( - runtime.raw, - operationId, - outHandle, - outFound - )) - found = outFound.pointee - } - }.value - let snapshot = NativeOfflineRegionSnapshotHandle(raw: snapshotValue) - guard found, !snapshot.isNull else { return nil } - defer { mln_offline_region_snapshot_destroy(snapshot.raw) } - return try offlineRegionSnapshotCopy(snapshot) - } - - static func regionsListTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> [NativeOfflineRegionInfo] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_runtime_offline_regions_list_take_result( - runtime.raw, - operationId, - outHandle - )) - }.value - let list = NativeOfflineRegionListHandle(raw: listValue) - guard !list.isNull else { throw NativeStatusFailure( - rawStatus: 0, - diagnostic: "offline region list result was null" - ) } - defer { mln_offline_region_list_destroy(list.raw) } - return try offlineRegionListCopy(list) - } - - static func regionsMergeDatabaseTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> [NativeOfflineRegionInfo] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_runtime_offline_regions_merge_database_take_result( - runtime.raw, - operationId, - outHandle - )) - }.value - let list = NativeOfflineRegionListHandle(raw: listValue) - guard !list.isNull else { throw NativeStatusFailure( - rawStatus: 0, - diagnostic: "offline merge result list was null" - ) } - defer { mln_offline_region_list_destroy(list.raw) } - return try offlineRegionListCopy(list) - } - - static func regionUpdateMetadataTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> NativeOfflineRegionInfo { - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_runtime_offline_region_update_metadata_take_result( - runtime.raw, - operationId, - outHandle - )) - }.value - let snapshot = NativeOfflineRegionSnapshotHandle(raw: snapshotValue) - guard !snapshot.isNull else { throw NativeStatusFailure( - rawStatus: 0, - diagnostic: "offline update metadata result was null" - ) } - defer { mln_offline_region_snapshot_destroy(snapshot.raw) } - return try offlineRegionSnapshotCopy(snapshot) - } - - static func regionGetStatusTakeResult( - _ runtime: NativeRuntimeHandle, - operationId: UInt64 - ) throws -> NativeOfflineRegionStatus { - var status = mln_offline_region_status() - status.size = UInt32(MemoryLayout.size) - try checkStatus(mln_runtime_offline_region_get_status_take_result( - runtime.raw, - operationId, - &status - )) - return NativeOfflineRegionStatus(status) - } - - private static func offlineRegionSnapshotCopy( - _ snapshot: NativeOfflineRegionSnapshotHandle - ) throws - -> NativeOfflineRegionInfo - { - var info = mln_offline_region_info() - info.size = UInt32(MemoryLayout.size) - try checkStatus(mln_offline_region_snapshot_get(snapshot.raw, &info)) - return try NativeOfflineRegionInfo(copying: info) - } - - private static func offlineRegionListCopy( - _ list: NativeOfflineRegionListHandle - ) throws - -> [NativeOfflineRegionInfo] - { - let count = try NativeMemory.withTemporary(0) { count in - try checkStatus(mln_offline_region_list_count(list.raw, count)) - }.value - return try (0 ..< count).map { index in - var info = mln_offline_region_info() - info.size = UInt32(MemoryLayout.size) - try checkStatus(mln_offline_region_list_get(list.raw, index, &info)) - return try NativeOfflineRegionInfo(copying: info) - } - } -} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeProjection.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeProjection.swift index e1d7dd32a..604b6a9e0 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeProjection.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeProjection.swift @@ -1,53 +1,6 @@ internal import CMaplibreNativeC enum NativeProjection { - static func create(_ map: NativeMapHandle) throws - -> NativeMapProjectionHandle - { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_map_projection_create returned a null projection" - ) { outHandle in - try checkStatus(mln_map_projection_create(map.raw, outHandle)) - } - } - - static func camera(_ projection: NativeMapProjectionHandle) throws - -> mln_camera_options - { - var camera = mln_camera_options_default() - try checkStatus(mln_map_projection_get_camera(projection.raw, &camera)) - return camera - } - - static func pixelForLatLng( - _ projection: NativeMapProjectionHandle, - coordinate: mln_lat_lng - ) throws -> mln_screen_point { - let output = try NativeMemory.withTemporary(mln_screen_point()) { point in - try checkStatus(mln_map_projection_pixel_for_lat_lng( - projection.raw, - coordinate, - point - )) - } - return output.value - } - - static func latLngForPixel( - _ projection: NativeMapProjectionHandle, - point: mln_screen_point - ) throws -> mln_lat_lng { - let output = try NativeMemory.withTemporary(mln_lat_lng()) { coordinate in - try checkStatus(mln_map_projection_lat_lng_for_pixel( - projection.raw, - point, - coordinate - )) - } - return output.value - } - static func projectedMetersForLatLng(_ coordinate: NativeLatLng) throws -> NativeProjectedMeters { diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeQuery.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeQuery.swift index faa57205b..80372d388 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeQuery.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeQuery.swift @@ -2,107 +2,31 @@ internal import CMaplibreNativeC import Foundation enum NativeQuery { - static func renderedFeatures( - session: NativeRenderSessionHandle, - geometry: UnsafePointer, - options: UnsafePointer? + static func copyQueriedFeatures( + _ result: UnsafePointer ) throws -> [QueriedFeature] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { output in - try checkStatus(mln_render_session_query_rendered_features( - session.raw, geometry, options, output - )) - }.value - let list = NativeQueriedFeatureListHandle(raw: listValue) - return try copyQueriedFeatureList(list) - } - - static func sourceFeatures( - session: NativeRenderSessionHandle, - sourceId: mln_buffer_view, - options: UnsafePointer? - ) throws -> [QueriedFeature] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { output in - try checkStatus(mln_render_session_query_source_features( - session.raw, sourceId, options, output - )) - }.value - let list = NativeQueriedFeatureListHandle(raw: listValue) - return try copyQueriedFeatureList(list) - } - - static func featureExtensions( - session: NativeRenderSessionHandle, - sourceId: mln_buffer_view, - feature: mln_buffer_view, - extensionName: mln_buffer_view, - extensionField: mln_buffer_view, - arguments: UnsafePointer? - ) throws -> Data { - try copyResult { output in - try checkStatus(mln_render_session_query_feature_extensions( - session.raw, sourceId, feature, extensionName, extensionField, - arguments, output - )) + try NativeCompletion.values(result, as: mln_queried_feature.self).map { + try copyQueriedFeature($0) } } - static func featureState( - _ map: NativeMapHandle, - selector: UnsafePointer - ) throws -> Data { - try copyResult { output in - try checkStatus(mln_map_get_feature_state( - map.raw, selector, output - )) - } - } - - private static func copyResult( - _ body: (UnsafeMutablePointer) throws -> Void - ) throws -> Data { - let raw = try NativeMemory.withTemporary(UInt64(0), body).value - return try NativeMemory.copyBuffer(NativeBufferHandle(raw: raw)) - } - - private static func copyQueriedFeatureList( - _ list: NativeQueriedFeatureListHandle - ) throws -> [QueriedFeature] { - guard !list.isNull else { - throw NativeStatusFailure - .swiftNativeError("queried feature list was null") - } - defer { mln_queried_feature_list_destroy(list.raw) } - let count = try NativeMemory.withTemporary(0) { count in - try checkStatus(mln_queried_feature_list_count(list.raw, count)) - }.value - return try (0 ..< count).map { index in - var hit = mln_queried_feature_default() - try checkStatus(mln_queried_feature_list_get(list.raw, index, &hit)) - return try copyQueriedFeature(hit) - } - } - - private static func copyQueriedFeature(_ hit: mln_queried_feature) throws - -> QueriedFeature - { + private static func copyQueriedFeature( + _ hit: mln_queried_feature + ) throws -> QueriedFeature { let sourceId = hasField(hit, MLN_QUERIED_FEATURE_SOURCE_ID.rawValue) ? try NativeString.copyUTF8( data: hit.source_id.data, size: hit.source_id.size - ) - : nil + ) : nil let sourceLayerId = hasField( hit, MLN_QUERIED_FEATURE_SOURCE_LAYER_ID.rawValue - ) - ? try NativeString.copyUTF8( - data: hit.source_layer_id.data, - size: hit.source_layer_id.size - ) - : nil + ) ? try NativeString.copyUTF8( + data: hit.source_layer_id.data, + size: hit.source_layer_id.size + ) : nil let state = hasField(hit, MLN_QUERIED_FEATURE_STATE.rawValue) - ? try copyView(hit.state) - : nil + ? try copyView(hit.state) : nil return try QueriedFeature( feature: copyView(hit.feature), sourceId: sourceId, diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRender.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRender.swift index 15bcc726b..983d484d8 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRender.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRender.swift @@ -1,195 +1,220 @@ internal import CMaplibreNativeC enum NativeRender { - static func metalSurfaceAttach( - map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_metal_surface_attach returned a null session" - ) { outHandle in - try checkStatus(mln_metal_surface_attach( - map.raw, - descriptor, - outHandle - )) - } + typealias Attachment = ( + session: NativeRenderSessionHandle, + completion: NativeFuture + ) + + private static func attachment( + _ body: (UnsafeMutablePointer, + UnsafePointer) -> mln_status + ) throws -> Attachment { + var session: mln_render_session = 0 + let completion = try NativeCompletion.startUnit { completion in + body(&session, completion) + } + guard session != 0 else { + if session != 0 { _ = mln_render_session_destroy(session) } + throw NativeStatusFailure.swiftNativeError( + "render attachment returned a null session" + ) + } + return ( + NativeRenderSessionHandle(raw: session), + completion + ) } - static func vulkanSurfaceAttach( + static func metalSurfaceAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_vulkan_surface_attach returned a null session" - ) { outHandle in - try checkStatus(mln_vulkan_surface_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_metal_surface_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func openGLSurfaceAttach( + static func vulkanSurfaceAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_opengl_surface_attach returned a null session" - ) { outHandle in - try checkStatus(mln_opengl_surface_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_vulkan_surface_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func textureReadPremultipliedRGBA8( - session: NativeRenderSessionHandle, - data: UnsafeMutablePointer?, - capacity: Int - ) throws -> mln_texture_image_info { - var info = mln_texture_image_info_default() - try checkStatus(mln_texture_read_premultiplied_rgba8( - session.raw, - data, - capacity, - &info - )) - return info + static func openGLSurfaceAttachStart( + map: NativeMapHandle, + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_opengl_surface_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func metalOwnedTextureAttach( + static func metalOwnedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_metal_owned_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_metal_owned_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_metal_owned_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func metalBorrowedTextureAttach( + static func metalBorrowedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_metal_borrowed_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_metal_borrowed_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_metal_borrowed_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func vulkanOwnedTextureAttach( + static func vulkanOwnedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_vulkan_owned_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_vulkan_owned_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_vulkan_owned_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func vulkanBorrowedTextureAttach( + static func vulkanBorrowedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_vulkan_borrowed_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_vulkan_borrowed_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_vulkan_borrowed_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func openGLOwnedTextureAttach( + static func openGLOwnedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_opengl_owned_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_opengl_owned_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_opengl_owned_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func openGLBorrowedTextureAttach( + static func openGLBorrowedTextureAttachStart( map: NativeMapHandle, - descriptor: UnsafePointer - ) throws -> NativeRenderSessionHandle { - try NativeHandleFactory - .create( - nullDiagnostic: "mln_opengl_borrowed_texture_attach returned a null session" - ) { outHandle in - try checkStatus(mln_opengl_borrowed_texture_attach( - map.raw, - descriptor, - outHandle - )) - } + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_opengl_borrowed_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func metalOwnedTextureAcquireFrame( - _ session: NativeRenderSessionHandle - ) throws - -> mln_metal_owned_texture_frame - { - var frame = mln_metal_owned_texture_frame() - frame.size = UInt32(MemoryLayout.size) - try checkStatus(mln_metal_owned_texture_acquire_frame(session.raw, &frame)) - return frame + static func webGPUSurfaceAttachStart( + map: NativeMapHandle, + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_webgpu_surface_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func vulkanOwnedTextureAcquireFrame( - _ session: NativeRenderSessionHandle - ) throws - -> mln_vulkan_owned_texture_frame - { - var frame = mln_vulkan_owned_texture_frame() - frame.size = UInt32(MemoryLayout.size) - try checkStatus(mln_vulkan_owned_texture_acquire_frame(session.raw, &frame)) - return frame + static func webGPUOwnedTextureAttachStart( + map: NativeMapHandle, + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_webgpu_owned_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } - static func openGLOwnedTextureAcquireFrame( - _ session: NativeRenderSessionHandle - ) throws - -> mln_opengl_owned_texture_frame - { - var frame = mln_opengl_owned_texture_frame() - frame.size = UInt32(MemoryLayout.size) - try checkStatus(mln_opengl_owned_texture_acquire_frame(session.raw, &frame)) - return frame + static func webGPUBorrowedTextureAttachStart( + map: NativeMapHandle, + descriptor: UnsafePointer, + options: UnsafePointer + ) throws -> Attachment { + try attachment { session, completion in + mln_webgpu_borrowed_texture_attach( + map.raw, + descriptor, + options, + session, + completion + ) + } } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRuntime.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRuntime.swift index 95d54f82d..a019fca7d 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRuntime.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeRuntime.swift @@ -4,19 +4,30 @@ enum NativeRuntime { static func create(_ options: UnsafePointer) throws -> NativeRuntimeHandle { - try NativeHandleFactory - .create(nullDiagnostic: "mln_runtime_create returned a null runtime") { outHandle in - try checkStatus(mln_runtime_create(options, outHandle)) - } + try NativeHandleFactory.create( + nullDiagnostic: "mln_runtime_create returned a null runtime" + ) { runtime in + try checkStatus(mln_runtime_create(options, runtime)) + } } - static func drainEvents( - _ runtime: NativeRuntimeHandle, - maxEvents: Int - ) throws -> NativeRuntimeEventBatch { - var batch = mln_runtime_event_batch_default() - try checkStatus(mln_runtime_drain_events(runtime.raw, maxEvents, &batch)) - return try NativeRuntimeEventBatch(copying: batch) + /// Releases a runtime and returns the future for its native teardown. + static func release(_ runtime: NativeRuntimeHandle) throws + -> NativeFuture + { + try NativeCompletion.startUnit { mln_runtime_release(runtime.raw, $0) } + } + + static func drainEvents(_ runtime: NativeRuntimeHandle) throws + -> NativeRuntimeEventBatch + { + var batch: mln_event_batch = 0 + try checkStatus(mln_runtime_drain_events(runtime.raw, &batch)) + defer { mln_event_batch_release(batch) } + var view = mln_runtime_event_batch_view() + view.size = UInt32(MemoryLayout.size) + try checkStatus(mln_event_batch_get(batch, &view)) + return try NativeRuntimeEventBatch(copying: view) } static func setEventMask( diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeString.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeString.swift index 6ce1f1814..642c5d5ca 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeString.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeString.swift @@ -15,6 +15,21 @@ enum NativeString { } static func copyUTF8(data: UnsafeRawPointer?, size: Int) throws -> String { + try copyCUTF8( + data: data?.assumingMemoryBound(to: CChar.self), + size: size + ) + } + + static func copyUTF8(data: UnsafePointer?, + size: UInt) throws -> String + { + try copyCUTF8(data: data, size: Int(size)) + } + + private static func copyCUTF8(data: UnsafePointer?, + size: Int) throws -> String + { guard size > 0 else { return "" } guard let data else { throw NativeStringError( @@ -22,7 +37,7 @@ enum NativeString { ) } let bytes = UnsafeBufferPointer( - start: data.assumingMemoryBound(to: UInt8.self), + start: UnsafeRawPointer(data).assumingMemoryBound(to: UInt8.self), count: size ) guard let text = String(bytes: bytes, encoding: .utf8) else { diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeStyle.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeStyle.swift index 9d1474dcf..d5ead9311 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeStyle.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeStyle.swift @@ -1,5 +1,4 @@ internal import CMaplibreNativeC -import Foundation enum NativeStyle { static func createGeoJSONSourceData( @@ -19,697 +18,34 @@ enum NativeStyle { } } - static func copyMapData( - _ map: NativeMapHandle, - copy: ( - mln_map, - UnsafeMutablePointer?, - Int, - UnsafeMutablePointer - ) -> mln_status - ) throws -> Data { - let required = try NativeMemory.withTemporary(0) { size in - try checkStatus(copy(map.raw, nil, 0, size)) - }.value - guard required > 0 else { return Data() } - var bytes = [UInt8](repeating: 0, count: required) - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { size in - try checkStatus(copy(map.raw, buffer.baseAddress, required, size)) - }.value - } - guard size <= bytes.count else { - throw NativeStatusFailure.swiftNativeError( - "native data size exceeded caller buffer" - ) - } - return Data(bytes[0 ..< size]) - } - - static func removeSource(_ map: NativeMapHandle, - sourceId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { removed in - try checkStatus(mln_map_remove_style_source(map.raw, sourceId, removed)) - }.value - } - - static func sourceExists(_ map: NativeMapHandle, - sourceId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { exists in - try checkStatus(mln_map_style_source_exists(map.raw, sourceId, exists)) - }.value - } - - static func sourceType(_ map: NativeMapHandle, - sourceId: mln_buffer_view) throws -> UInt32? - { - var type = UInt32(0) - let found = try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_source_type( - map.raw, - sourceId, - &type, - found - )) - }.value - return found ? type : nil - } - - static func sourceInfo(_ map: NativeMapHandle, - sourceId: mln_buffer_view) throws - -> NativeStyleSourceInfo? - { - var info = mln_style_source_info() - info.size = UInt32(MemoryLayout.size) - let found = try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_source_info( - map.raw, - sourceId, - &info, - found - )) - }.value - guard found else { return nil } - - let attribution = info.has_attribution - ? try copySourceAttribution( - map, - sourceId: sourceId, - capacity: info.attribution_size - ).0 - : nil - let url = hasSourceInfoField(info, MLN_STYLE_SOURCE_INFO_URL.rawValue) - ? try copySourceURL(map, sourceId: sourceId, capacity: info.url_size).0 - : nil - let tileJSON: NativeStyleSourceTileJSON? - if hasSourceInfoField(info, MLN_STYLE_SOURCE_INFO_TILEJSON.rawValue) { - let tileURLs = try sourceTileURLs(map, sourceId: sourceId) ?? [] - tileJSON = NativeStyleSourceTileJSON( + static func sourceInfo( + fixed info: mln_style_source_info, + attribution: String?, + url: String?, + tileURLs: [String] + ) -> NativeStyleSourceInfo { + let has: (UInt32) -> Bool = { (info.fields & $0) != 0 } + let tileJSON = has(MLN_STYLE_SOURCE_INFO_TILEJSON.rawValue) + ? NativeStyleSourceTileJSON( tileURLs: tileURLs, minZoom: info.min_zoom, maxZoom: info.max_zoom, scheme: info.scheme, - bounds: hasSourceInfoField(info, MLN_STYLE_SOURCE_INFO_BOUNDS.rawValue) - ? NativeLatLngBounds(info.bounds) - : nil - ) - } else { - tileJSON = nil - } - + bounds: has(MLN_STYLE_SOURCE_INFO_BOUNDS.rawValue) + ? NativeLatLngBounds(info.bounds) : nil + ) : nil return NativeStyleSourceInfo( type: info.type, isVolatile: info.is_volatile, attribution: attribution, url: url, tileJSON: tileJSON, - tileSize: hasSourceInfoField( - info, - MLN_STYLE_SOURCE_INFO_TILE_SIZE.rawValue - ) ? info.tile_size : nil, - vectorEncoding: hasSourceInfoField( - info, - MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING.rawValue - ) ? info.vector_encoding : nil, - rasterEncoding: hasSourceInfoField( - info, - MLN_STYLE_SOURCE_INFO_RASTER_ENCODING.rawValue - ) ? info.raster_encoding : nil + tileSize: has(MLN_STYLE_SOURCE_INFO_TILE_SIZE.rawValue) + ? info.tile_size : nil, + vectorEncoding: has(MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING.rawValue) + ? info.vector_encoding : nil, + rasterEncoding: has(MLN_STYLE_SOURCE_INFO_RASTER_ENCODING.rawValue) + ? info.raster_encoding : nil ) } - - static func sourceAttribution( - _ map: NativeMapHandle, - sourceId: mln_buffer_view - ) throws -> String? { - var info = mln_style_source_info() - info.size = UInt32(MemoryLayout.size) - let found = try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_source_info( - map.raw, - sourceId, - &info, - found - )) - }.value - guard found, info.has_attribution else { return nil } - return try copySourceAttribution( - map, - sourceId: sourceId, - capacity: info.attribution_size - ).0 - } - - private static func hasSourceInfoField( - _ info: mln_style_source_info, - _ field: UInt32 - ) -> Bool { - (info.fields & field) != 0 - } - - static func copySourceAttribution( - _ map: NativeMapHandle, - sourceId: mln_buffer_view, - capacity: Int - ) throws -> (String?, Int) { - var bytes = [UInt8](repeating: 0, count: capacity) - var found = false - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { outSize in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_map_copy_style_source_attribution( - map.raw, - sourceId, - buffer.baseAddress, - capacity, - outSize, - outFound - )) - found = outFound.pointee - } - }.value - } - guard found else { return (nil, size) } - guard size <= capacity else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native style source attribution size exceeded caller buffer" - ) - } - let attribution = try bytes.withUnsafeBufferPointer { buffer in - try NativeString.copyUTF8( - data: buffer.baseAddress.map(UnsafeRawPointer.init), - size: size - ) - } - return (attribution, size) - } - - static func copySourceURL( - _ map: NativeMapHandle, - sourceId: mln_buffer_view, - capacity: Int - ) throws -> (String?, Int) { - var bytes = [UInt8](repeating: 0, count: capacity) - var found = false - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { outSize in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_map_copy_style_source_url( - map.raw, - sourceId, - buffer.baseAddress, - capacity, - outSize, - outFound - )) - found = outFound.pointee - } - }.value - } - guard found else { return (nil, size) } - guard size <= capacity else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native style source URL size exceeded caller buffer" - ) - } - let url = try bytes.withUnsafeBufferPointer { buffer in - try NativeString.copyUTF8( - data: buffer.baseAddress.map(UnsafeRawPointer.init), - size: size - ) - } - return (url, size) - } - - static func sourceTileURLs( - _ map: NativeMapHandle, - sourceId: mln_buffer_view - ) throws -> [String]? { - var found = false - let listValue = try NativeMemory.withTemporary(UInt64(0)) { outHandle in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_map_get_style_source_tile_urls( - map.raw, - sourceId, - outHandle, - outFound - )) - found = outFound.pointee - } - }.value - guard found else { return nil } - let list = NativeStyleStringListHandle(raw: listValue) - guard !list.isNull else { - throw NativeStatusFailure.swiftNativeError("tile URL list was null") - } - defer { mln_style_string_list_destroy(list.raw) } - let count = try NativeMemory.withTemporary(0) { count in - try checkStatus(mln_style_string_list_count(list.raw, count)) - }.value - return try (0 ..< count).map { index in - let output = try NativeMemory.withTemporary(mln_buffer_view()) { value in - try checkStatus(mln_style_string_list_get(list.raw, index, value)) - } - return try NativeString.copyUTF8( - data: output.value.data, - size: output.value.size - ) - } - } - - static func sourceIds(_ map: NativeMapHandle) throws -> [String] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_map_list_style_source_ids(map.raw, outHandle)) - }.value - let list = NativeStyleIdListHandle(raw: listValue) - guard !list.isNull - else { - throw NativeStatusFailure.swiftNativeError("source ID list was null") - } - defer { mln_style_id_list_destroy(list.raw) } - return try copyStyleIdList(list) - } - - static func removeImage(_ map: NativeMapHandle, - imageId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { removed in - try checkStatus(mln_map_remove_style_image(map.raw, imageId, removed)) - }.value - } - - static func setTransitionOptions( - _ map: NativeMapHandle, - options: NativeStyleTransitionOptions - ) throws { - try options.withNativeOptions { native in - try checkStatus(mln_map_set_style_transition_options(map.raw, native)) - } - } - - static func transitionOptions(_ map: NativeMapHandle) throws - -> NativeStyleTransitionOptions - { - var options = mln_style_transition_options_default() - try checkStatus(mln_map_get_style_transition_options(map.raw, &options)) - return NativeStyleTransitionOptions(options) - } - - static func imageExists(_ map: NativeMapHandle, - imageId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { exists in - try checkStatus(mln_map_style_image_exists(map.raw, imageId, exists)) - }.value - } - - static func imageInfo(_ map: NativeMapHandle, - imageId: mln_buffer_view) throws - -> NativeStyleImageInfo? - { - var info = mln_style_image_info_default() - let found = try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_image_info( - map.raw, - imageId, - &info, - found - )) - }.value - return found ? NativeStyleImageInfo(info) : nil - } - - static func copyImagePremultipliedRGBA8( - _ map: NativeMapHandle, - imageId: mln_buffer_view, - capacity: Int - ) throws -> ([UInt8]?, Int) { - var bytes = [UInt8](repeating: 0, count: capacity) - var found = false - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { outSize in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_map_copy_style_image_premultiplied_rgba8( - map.raw, - imageId, - buffer.baseAddress, - capacity, - outSize, - outFound - )) - found = outFound.pointee - } - }.value - } - guard found else { return (nil, size) } - guard size <= capacity else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native style image byte size exceeded caller buffer" - ) - } - return (Array(bytes.prefix(size)), size) - } - - static func addImageSourceURL( - _ map: NativeMapHandle, - sourceId: mln_buffer_view, - coordinates: [NativeLatLng], - url: mln_buffer_view - ) throws { - try validateImageSourceCoordinates(coordinates) - let rawCoordinates = coordinates.map(\.native) - try rawCoordinates.withUnsafeBufferPointer { coordinates in - try checkStatus(mln_map_add_image_source_url( - map.raw, - sourceId, - coordinates.baseAddress, - coordinates.count, - url - )) - } - } - - static func addImageSourceImage( - _ map: NativeMapHandle, - sourceId: mln_buffer_view, - coordinates: [NativeLatLng], - image: UnsafePointer - ) throws { - try validateImageSourceCoordinates(coordinates) - let rawCoordinates = coordinates.map(\.native) - try rawCoordinates.withUnsafeBufferPointer { coordinates in - try checkStatus(mln_map_add_image_source_image( - map.raw, - sourceId, - coordinates.baseAddress, - coordinates.count, - image - )) - } - } - - static func setImageSourceCoordinates( - _ map: NativeMapHandle, - sourceId: mln_buffer_view, - coordinates: [NativeLatLng] - ) throws { - try validateImageSourceCoordinates(coordinates) - let rawCoordinates = coordinates.map(\.native) - try rawCoordinates.withUnsafeBufferPointer { coordinates in - try checkStatus(mln_map_set_image_source_coordinates( - map.raw, - sourceId, - coordinates.baseAddress, - coordinates.count - )) - } - } - - static func imageSourceCoordinates( - _ map: NativeMapHandle, - sourceId: mln_buffer_view - ) throws -> [NativeLatLng]? { - var coordinates = [mln_lat_lng](repeating: mln_lat_lng(), count: 4) - var found = false - let count = try coordinates.withUnsafeMutableBufferPointer { coordinates in - try NativeMemory.withTemporary(0) { count in - try NativeMemory.withTemporary(false) { outFound in - try checkStatus(mln_map_get_image_source_coordinates( - map.raw, - sourceId, - coordinates.baseAddress, - coordinates.count, - count, - outFound - )) - found = outFound.pointee - } - }.value - } - guard found else { return nil } - guard count == coordinates.count else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native image source coordinate count did not match Swift image source invariant" - ) - } - return coordinates.map(NativeLatLng.init) - } - - private static func validateImageSourceCoordinates( - _ coordinates: [NativeLatLng] - ) throws { - guard coordinates.count == 4 else { - throw NativeStatusFailure - .swiftInvalidArgument( - "image source coordinates must contain exactly 4 coordinates" - ) - } - } - - static func removeLayer(_ map: NativeMapHandle, - layerId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { removed in - try checkStatus(mln_map_remove_style_layer(map.raw, layerId, removed)) - }.value - } - - static func layerExists(_ map: NativeMapHandle, - layerId: mln_buffer_view) throws -> Bool - { - try NativeMemory.withTemporary(false) { exists in - try checkStatus(mln_map_style_layer_exists(map.raw, layerId, exists)) - }.value - } - - static func layerType(_ map: NativeMapHandle, - layerId: mln_buffer_view) throws -> String? - { - var layerType = mln_buffer_view() - let found = try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_layer_type( - map.raw, - layerId, - &layerType, - found - )) - }.value - return found ? try NativeString.copyUTF8( - data: layerType.data, - size: layerType.size - ) : nil - } - - static func layerIds(_ map: NativeMapHandle) throws -> [String] { - let listValue = try NativeMemory.withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_map_list_style_layer_ids(map.raw, outHandle)) - }.value - let list = NativeStyleIdListHandle(raw: listValue) - guard !list.isNull - else { - throw NativeStatusFailure.swiftNativeError("layer ID list was null") - } - defer { mln_style_id_list_destroy(list.raw) } - return try copyStyleIdList(list) - } - - static func layerJSON(_ map: NativeMapHandle, - layerId: mln_buffer_view) throws -> Data? - { - let output = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try NativeMemory.withTemporary(false) { found in - try checkStatus(mln_map_get_style_layer_json( - map.raw, - layerId, - outHandle, - found - )) - if !found.pointee { outHandle.pointee = 0 } - } - }.value - let buffer = NativeBufferHandle(raw: output) - return buffer.isNull ? nil : try NativeMemory.copyBuffer(buffer) - } - - static func lightProperty( - _ map: NativeMapHandle, - propertyName: mln_buffer_view - ) throws -> Data? { - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_map_get_style_light_property( - map.raw, - propertyName, - outHandle - )) - }.value - let buffer = NativeBufferHandle(raw: snapshotValue) - return buffer.isNull ? nil : try NativeMemory.copyBuffer(buffer) - } - - static func layerProperty( - _ map: NativeMapHandle, - layerId: mln_buffer_view, - propertyName: mln_buffer_view - ) throws -> Data? { - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_map_get_layer_property( - map.raw, - layerId, - propertyName, - outHandle - )) - }.value - let buffer = NativeBufferHandle(raw: snapshotValue) - return buffer.isNull ? nil : try NativeMemory.copyBuffer(buffer) - } - - static func layerFilter(_ map: NativeMapHandle, - layerId: mln_buffer_view) throws -> Data? - { - let snapshotValue = try NativeMemory - .withTemporary(UInt64(0)) { outHandle in - try checkStatus(mln_map_get_layer_filter(map.raw, layerId, outHandle)) - }.value - let buffer = NativeBufferHandle(raw: snapshotValue) - return buffer.isNull ? nil : try NativeMemory.copyBuffer(buffer) - } - - /// Probes the required interval counts, then copies. Null arrays with zero - /// capacity are a size probe the C API answers with OK. - static func copyImageStretches( - _ map: NativeMapHandle, - imageId: mln_buffer_view - ) throws -> ([ImageStretch], [ImageStretch])? { - var xCount = 0 - var yCount = 0 - var found = false - try checkStatus(mln_map_copy_style_image_stretches( - map.raw, imageId, nil, 0, &xCount, nil, 0, &yCount, &found - )) - guard found else { return nil } - - var rawX = [mln_image_stretch]( - repeating: mln_image_stretch(from: 0, to: 0), count: xCount - ) - var rawY = [mln_image_stretch]( - repeating: mln_image_stretch(from: 0, to: 0), count: yCount - ) - try rawX.withUnsafeMutableBufferPointer { bufferX in - try rawY.withUnsafeMutableBufferPointer { bufferY in - try checkStatus(mln_map_copy_style_image_stretches( - map.raw, - imageId, - bufferX.baseAddress, - bufferX.count, - &xCount, - bufferY.baseAddress, - bufferY.count, - &yCount, - &found - )) - } - } - let toPublic = { (raw: [mln_image_stretch]) -> [ImageStretch] in - raw.map { ImageStretch(from: $0.from, to: $0.to) } - } - return (toPublic(rawX), toPublic(rawY)) - } - - /// Probes the required byte length, then copies. A null buffer with zero - /// capacity is a size probe the C API answers with OK. - static func copyMapText( - _ map: NativeMapHandle, - copy: ( - mln_map, - UnsafeMutablePointer?, - Int, - UnsafeMutablePointer - ) -> mln_status - ) throws -> String { - let required = try NativeMemory.withTemporary(0) { outSize in - try checkStatus(copy(map.raw, nil, 0, outSize)) - }.value - guard required > 0 else { return "" } - - var bytes = [CChar](repeating: 0, count: required) - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { outSize in - try checkStatus(copy(map.raw, buffer.baseAddress, required, outSize)) - }.value - } - guard size <= required else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native text size exceeded caller buffer" - ) - } - return try bytes.withUnsafeBufferPointer { buffer in - try NativeString.copyUTF8(data: buffer.baseAddress, size: size) - } - } - - static func copyLayerText( - _ map: NativeMapHandle, - layerId: mln_buffer_view, - copy: ( - mln_map, - mln_buffer_view, - UnsafeMutablePointer?, - Int, - UnsafeMutablePointer - ) -> mln_status - ) throws -> String { - let required = try NativeMemory.withTemporary(0) { outSize in - try checkStatus(copy(map.raw, layerId, nil, 0, outSize)) - }.value - guard required > 0 else { return "" } - - var bytes = [CChar](repeating: 0, count: required) - let size = try bytes.withUnsafeMutableBufferPointer { buffer in - try NativeMemory.withTemporary(0) { outSize in - try checkStatus(copy( - map.raw, - layerId, - buffer.baseAddress, - required, - outSize - )) - }.value - } - guard size <= required else { - throw NativeStatusFailure( - rawStatus: MLN_STATUS_NATIVE_ERROR.rawValue, - diagnostic: "native layer text size exceeded caller buffer" - ) - } - return try bytes.withUnsafeBufferPointer { buffer in - try NativeString.copyUTF8(data: buffer.baseAddress, size: size) - } - } - - private static func copyStyleIdList(_ list: NativeStyleIdListHandle) throws - -> [String] - { - let count = try NativeMemory.withTemporary(0) { count in - try checkStatus(mln_style_id_list_count(list.raw, count)) - }.value - return try (0 ..< count).map { index in - let output = try NativeMemory.withTemporary(mln_buffer_view()) { value in - try checkStatus(mln_style_id_list_get(list.raw, index, value)) - } - return try NativeString.copyUTF8( - data: output.value.data, - size: output.value.size - ) - } - } } diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeWake.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeWake.swift new file mode 100644 index 000000000..925b91519 --- /dev/null +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/NativeWake.swift @@ -0,0 +1,49 @@ +internal import CMaplibreNativeC +import Foundation + +/// Mutable host-side receiver state owned by one native wake descriptor. +final class NativeWakeState: @unchecked Sendable { + private let lock = NSLock() + private var handler: (@Sendable () -> Void)? + private var retainedContext: UnsafeMutableRawPointer? + + func setHandler(_ handler: (@Sendable () -> Void)?) { + lock.withLock { self.handler = handler } + } + + func makeDescriptor() -> mln_wake { + let context = Unmanaged.passRetained(self).toOpaque() + lock.withLock { + precondition(retainedContext == nil) + retainedContext = context + } + var wake = mln_wake() + wake.size = UInt32(MemoryLayout.size) + wake.callback = { userData in + guard let userData else { return } + let state = Unmanaged.fromOpaque(userData) + .takeUnretainedValue() + state.lock.withLock { state.handler }?() + } + wake.user_data = context + wake.release_user_data = { userData in + guard let userData else { return } + let state = Unmanaged.fromOpaque(userData) + .takeUnretainedValue() + state.lock.withLock { state.retainedContext = nil } + Unmanaged.fromOpaque(userData).release() + } + return wake + } + + /// Releases a descriptor that an owning native call rejected. + func releaseRejectedDescriptor() { + let context = lock.withLock { () -> UnsafeMutableRawPointer? in + defer { retainedContext = nil } + return retainedContext + } + if let context { + Unmanaged.fromOpaque(context).release() + } + } +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/RenderStructs.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/RenderStructs.swift index 0490ca8d9..4e45ef627 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/RenderStructs.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/RenderStructs.swift @@ -139,10 +139,35 @@ struct NativeEglContextDescriptor: Equatable { } } +struct NativeWebGLContextDescriptor: Equatable { + enum Kind: UInt32, Equatable { + case existing = 0 + case transferredCanvas = 1 + } + + let kind: Kind + let context: Int32 + let canvasSelector: String + + func withNative( + _ body: (mln_webgl_context_descriptor) throws -> Result + ) throws -> Result { + try NativeString.withStringView(canvasSelector) { selector in + try body(mln_webgl_context_descriptor( + size: UInt32(MemoryLayout.size), + kind: kind.rawValue, + context: context, + canvas_selector: selector + )) + } + } +} + struct NativeOpenGLContextDescriptor: Equatable { enum Platform: Equatable { case wgl(NativeWglContextDescriptor) case egl(NativeEglContextDescriptor) + case webGL(NativeWebGLContextDescriptor) } let platform: Platform @@ -156,7 +181,9 @@ struct NativeOpenGLContextDescriptor: Equatable { self.ownershipRawValue = ownershipRawValue } - var native: mln_opengl_context_descriptor { + func withNative( + _ body: (mln_opengl_context_descriptor) throws -> Result + ) throws -> Result { var descriptor = mln_opengl_context_descriptor() descriptor.size = UInt32(MemoryLayout.size) descriptor.ownership = mln_opengl_context_ownership( @@ -166,11 +193,18 @@ struct NativeOpenGLContextDescriptor: Equatable { case let .wgl(context): descriptor.platform = MLN_OPENGL_CONTEXT_PLATFORM_WGL descriptor.data.wgl = context.native + return try body(descriptor) case let .egl(context): descriptor.platform = MLN_OPENGL_CONTEXT_PLATFORM_EGL descriptor.data.egl = context.native + return try body(descriptor) + case let .webGL(context): + return try context.withNative { context in + descriptor.platform = MLN_OPENGL_CONTEXT_PLATFORM_WEBGL + descriptor.data.webgl = context + return try body(descriptor) + } } - return descriptor } } @@ -214,11 +248,13 @@ struct NativeOpenGLSurfaceDescriptorInput: Equatable { func withNativeDescriptor( _ body: (UnsafePointer) throws -> Result ) throws -> Result { - var descriptor = mln_opengl_surface_descriptor_default() - descriptor.extent = extent.native - descriptor.context = context.native - descriptor.surface = UnsafeMutableRawPointer(bitPattern: surfaceAddress) - return try withUnsafePointer(to: &descriptor, body) + try context.withNative { context in + var descriptor = mln_opengl_surface_descriptor_default() + descriptor.extent = extent.native + descriptor.context = context + descriptor.surface = UnsafeMutableRawPointer(bitPattern: surfaceAddress) + return try withUnsafePointer(to: &descriptor, body) + } } } @@ -322,10 +358,12 @@ struct NativeOpenGLOwnedTextureDescriptorInput: Equatable { _ body: (UnsafePointer) throws -> Result ) throws -> Result { - var descriptor = mln_opengl_owned_texture_descriptor_default() - descriptor.extent = extent.native - descriptor.context = context.native - return try withUnsafePointer(to: &descriptor, body) + try context.withNative { context in + var descriptor = mln_opengl_owned_texture_descriptor_default() + descriptor.extent = extent.native + descriptor.context = context + return try withUnsafePointer(to: &descriptor, body) + } } } @@ -341,14 +379,16 @@ struct NativeOpenGLBorrowedTextureDescriptorInput: Equatable { _ body: (UnsafePointer) throws -> Result ) throws -> Result { - var descriptor = mln_opengl_borrowed_texture_descriptor_default() - descriptor.extent = extent.native - descriptor.physical_width = physicalWidth - descriptor.physical_height = physicalHeight - descriptor.context = context.native - descriptor.texture = texture - descriptor.target = target - return try withUnsafePointer(to: &descriptor, body) + try context.withNative { context in + var descriptor = mln_opengl_borrowed_texture_descriptor_default() + descriptor.extent = extent.native + descriptor.physical_width = physicalWidth + descriptor.physical_height = physicalHeight + descriptor.context = context + descriptor.texture = texture + descriptor.target = target + return try withUnsafePointer(to: &descriptor, body) + } } } @@ -375,3 +415,76 @@ struct NativeOpenGLOwnedTextureFrame { self.raw = raw } } + +struct NativeWebGPUContextDescriptor: Equatable { + let instanceAddress: UInt + let deviceAddress: UInt + let queueAddress: UInt + + var native: mln_webgpu_context_descriptor { + mln_webgpu_context_descriptor( + size: UInt32(MemoryLayout.size), + instance: UnsafeMutableRawPointer(bitPattern: instanceAddress), + device: UnsafeMutableRawPointer(bitPattern: deviceAddress), + queue: UnsafeMutableRawPointer(bitPattern: queueAddress) + ) + } +} + +struct NativeWebGPUSurfaceDescriptorInput: Equatable { + let extent: NativeRenderTargetExtent + let context: NativeWebGPUContextDescriptor + let surfaceAddress: UInt + let format: UInt32 + + func withNativeDescriptor( + _ body: (UnsafePointer) throws -> Result + ) throws -> Result { + var value = mln_webgpu_surface_descriptor_default() + value.extent = extent.native + value.context = context.native + value.surface = UnsafeMutableRawPointer(bitPattern: surfaceAddress) + value.format = format + return try withUnsafePointer(to: &value, body) + } +} + +struct NativeWebGPUOwnedTextureDescriptorInput: Equatable { + let extent: NativeRenderTargetExtent + let context: NativeWebGPUContextDescriptor + + func withNativeDescriptor( + _ body: (UnsafePointer) throws + -> Result + ) throws -> Result { + var value = mln_webgpu_owned_texture_descriptor_default() + value.extent = extent.native + value.context = context.native + return try withUnsafePointer(to: &value, body) + } +} + +struct NativeWebGPUBorrowedTextureDescriptorInput: Equatable { + let extent: NativeRenderTargetExtent + let physicalWidth: UInt32 + let physicalHeight: UInt32 + let context: NativeWebGPUContextDescriptor + let textureAddress: UInt + let textureViewAddress: UInt + let format: UInt32 + + func withNativeDescriptor( + _ body: (UnsafePointer) throws + -> Result + ) throws -> Result { + var value = mln_webgpu_borrowed_texture_descriptor_default() + value.extent = extent.native + value.physical_width = physicalWidth + value.physical_height = physicalHeight + value.context = context.native + value.texture = UnsafeMutableRawPointer(bitPattern: textureAddress) + value.texture_view = UnsafeMutableRawPointer(bitPattern: textureViewAddress) + value.format = format + return try withUnsafePointer(to: &value, body) + } +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/ResourceCallbacks.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/ResourceCallbacks.swift index 4c4119876..f7811f86f 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/ResourceCallbacks.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/ResourceCallbacks.swift @@ -354,16 +354,12 @@ private func resourceTransformTrampoline( } final class NativeResourceTransformState: @unchecked Sendable { - private let retainedBox: Unmanaged + private let box: NativeResourceTransformBox init(_ callback: @escaping @Sendable (NativeResourceTransformRequest) -> String?) { - retainedBox = Unmanaged.passRetained(NativeResourceTransformBox(callback)) - } - - deinit { - retainedBox.release() + box = NativeResourceTransformBox(callback) } func invokeForTesting(kind: UInt32, @@ -371,7 +367,7 @@ final class NativeResourceTransformState: @unchecked Sendable { { var response = mln_resource_transform_response() let status = url.withCString { url in - retainedBox.takeUnretainedValue().invoke( + box.invoke( kind: kind, url: url, outResponse: &response @@ -386,11 +382,23 @@ final class NativeResourceTransformState: @unchecked Sendable { var transform = mln_resource_transform() transform.size = UInt32(MemoryLayout.size) transform.callback = resourceTransformTrampoline - transform.user_data = retainedBox.toOpaque() - return try withUnsafePointer(to: &transform, body) + let retained = Unmanaged.passRetained(box) + transform.user_data = retained.toOpaque() + transform.release_user_data = releaseResourceTransform + do { + return try withUnsafePointer(to: &transform, body) + } catch { + retained.release() + throw error + } } } +private func releaseResourceTransform(_ userData: UnsafeMutableRawPointer?) { + guard let userData else { return } + Unmanaged.fromOpaque(userData).release() +} + private final class NativeHttpHeaderTransformBox: @unchecked Sendable { private let callback: @Sendable (HttpHeaderTransformRequest) -> [HttpHeader] @@ -454,27 +462,37 @@ private func httpHeaderTransformTrampoline( } final class NativeHttpHeaderTransformState: @unchecked Sendable { - private let retainedBox: Unmanaged + private let box: NativeHttpHeaderTransformBox init(_ callback: @escaping @Sendable (HttpHeaderTransformRequest) -> [HttpHeader]) { - retainedBox = Unmanaged.passRetained(NativeHttpHeaderTransformBox(callback)) + box = NativeHttpHeaderTransformBox(callback) } - deinit { retainedBox.release() } - func withDescriptor( _ body: (UnsafePointer) throws -> Result ) throws -> Result { var transform = mln_http_header_transform() transform.size = UInt32(MemoryLayout.size) transform.callback = httpHeaderTransformTrampoline - transform.user_data = retainedBox.toOpaque() - return try withUnsafePointer(to: &transform, body) + let retained = Unmanaged.passRetained(box) + transform.user_data = retained.toOpaque() + transform.release_user_data = releaseHttpHeaderTransform + do { + return try withUnsafePointer(to: &transform, body) + } catch { + retained.release() + throw error + } } } +private func releaseHttpHeaderTransform(_ userData: UnsafeMutableRawPointer?) { + guard let userData else { return } + Unmanaged.fromOpaque(userData).release() +} + private final class NativeResourceProviderBox: @unchecked Sendable { private let callback: @Sendable ( NativeResourceRequest, @@ -530,7 +548,7 @@ private func resourceProviderTrampoline( } final class NativeResourceProviderState: @unchecked Sendable { - private let retainedBox: Unmanaged + private let box: NativeResourceProviderBox init( handleFunctions: NativeResourceRequestHandleFunctions = .native, @@ -539,23 +557,17 @@ final class NativeResourceProviderState: @unchecked Sendable { NativeResourceRequestHandleState ) -> UInt32 ) { - retainedBox = Unmanaged.passRetained( - NativeResourceProviderBox( - handleFunctions: handleFunctions, - callback: callback - ) + box = NativeResourceProviderBox( + handleFunctions: handleFunctions, + callback: callback ) } - deinit { - retainedBox.release() - } - func invokeForTesting(request: mln_resource_request, rawHandle: mln_resource_request_handle) -> UInt32 { withUnsafePointer(to: request) { request in - retainedBox.takeUnretainedValue().invoke( + box.invoke( request: request, rawHandle: rawHandle ) @@ -568,7 +580,19 @@ final class NativeResourceProviderState: @unchecked Sendable { var provider = mln_resource_provider() provider.size = UInt32(MemoryLayout.size) provider.callback = resourceProviderTrampoline - provider.user_data = retainedBox.toOpaque() - return try withUnsafePointer(to: &provider, body) + let retained = Unmanaged.passRetained(box) + provider.user_data = retained.toOpaque() + provider.release_user_data = releaseResourceProvider + do { + return try withUnsafePointer(to: &provider, body) + } catch { + retained.release() + throw error + } } } + +private func releaseResourceProvider(_ userData: UnsafeMutableRawPointer?) { + guard let userData else { return } + Unmanaged.fromOpaque(userData).release() +} diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/RuntimeStructs.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/RuntimeStructs.swift index 77e6dad63..3da9fda54 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/RuntimeStructs.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/RuntimeStructs.swift @@ -16,6 +16,7 @@ struct NativeRuntimeOptionsInput: Equatable { } func withNativeOptions( + eventWake: mln_wake, _ body: (UnsafePointer) throws -> Result ) throws -> Result { try NativeString.withOptionalCString(assetPath) { assetPath in @@ -24,6 +25,7 @@ struct NativeRuntimeOptionsInput: Equatable { options.asset_path = assetPath options.cache_path = cachePath options.event_mask = eventMask + options.event_wake = eventWake return try withUnsafePointer(to: &options, body) } } @@ -156,22 +158,6 @@ struct NativeOfflineRegionTileCountLimitEvent: Equatable { } } -struct NativeOfflineOperationCompletedEvent: Equatable { - let operationId: UInt64 - let operationKind: UInt32 - let resultKind: UInt32 - let resultStatus: Int32 - let found: Bool - - init(_ raw: mln_runtime_event_offline_operation_completed) { - operationId = raw.operation_id - operationKind = raw.operation_kind - resultKind = raw.result_kind - resultStatus = raw.result_status - found = raw.found - } -} - enum NativeRuntimeEventPayload: Equatable { case none case renderFrame(NativeRenderFrameEvent) @@ -180,7 +166,6 @@ enum NativeRuntimeEventPayload: Equatable { case offlineRegionStatus(NativeOfflineRegionStatusEvent) case offlineRegionResponseError(NativeOfflineRegionResponseErrorEvent) case offlineRegionTileCountLimit(NativeOfflineRegionTileCountLimitEvent) - case offlineOperationCompleted(NativeOfflineOperationCompletedEvent) case cameraTransitionFinished(NativeCameraTransitionFinishedEvent) /// A payload kind this binding does not name, carrying the payload union's /// fixed byte window copied out of the batch. @@ -196,18 +181,15 @@ struct NativeRuntimeEvent: Equatable { let payload: NativeRuntimeEventPayload } -/// One drained batch of runtime events, copied out of the arena the C API lends -/// until the next drain. +/// One drained batch of runtime events copied from an owned native batch. struct NativeRuntimeEventBatch: Equatable { let events: [NativeRuntimeEvent] - let remainingCount: Int - init(copying raw: mln_runtime_event_batch) throws { + init(copying raw: mln_runtime_event_batch_view) throws { events = try Self.copyEvents(raw) - remainingCount = raw.remaining_count } - private static func copyEvents(_ raw: mln_runtime_event_batch) throws + private static func copyEvents(_ raw: mln_runtime_event_batch_view) throws -> [NativeRuntimeEvent] { guard raw.event_count > 0 else { return [] } @@ -290,12 +272,6 @@ struct NativeRuntimeEventBatch: Equatable { raw.payload.offline_region_tile_count_limit ) ) - case MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED.rawValue: - return .offlineOperationCompleted( - NativeOfflineOperationCompletedEvent( - raw.payload.offline_operation_completed - ) - ) case MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED.rawValue: return .cameraTransitionFinished( NativeCameraTransitionFinishedEvent( diff --git a/bindings/swift/Sources/MaplibreNativeFFI/Support/StyleStructs.swift b/bindings/swift/Sources/MaplibreNativeFFI/Support/StyleStructs.swift index 309dc26a8..398a8bc03 100644 --- a/bindings/swift/Sources/MaplibreNativeFFI/Support/StyleStructs.swift +++ b/bindings/swift/Sources/MaplibreNativeFFI/Support/StyleStructs.swift @@ -436,9 +436,9 @@ private final class NativeCustomGeometrySourceCallbackBox: @unchecked Sendable { /// API as `user_data`. /// /// The C API owns the retain once it accepts the source, and gives it back by -/// invoking the release callback exactly once, on the map owner thread, when it -/// stops referencing the pointer. A rejected add is the one case that never -/// releases, so the caller releases it with ``release()`` there. +/// invoking the release callback exactly once when it stops referencing the +/// pointer. A rejected add is the one case that never releases, so the caller +/// releases it with ``release()`` there. struct NativeCustomGeometrySourceCallbacks: @unchecked Sendable { typealias TileCallback = @Sendable (NativeCanonicalTileID) -> Void diff --git a/bindings/swift/Sources/MaplibreNativeFFI/WakeSource.swift b/bindings/swift/Sources/MaplibreNativeFFI/WakeSource.swift deleted file mode 100644 index f0a6f343c..000000000 --- a/bindings/swift/Sources/MaplibreNativeFFI/WakeSource.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -internal import CMaplibreNativeC - -/// Releases a runtime owner thread parked in -/// `RuntimeHandle.pump(timeout:budget:)`. -/// -/// A wake source is usable from any thread, and stays usable after its runtime -/// closes. The unchecked conformance rests on `NativeHandleState` ordering -/// signal against close, and on the C API allowing signal and destroy from any -/// thread. -public final class WakeSource: @unchecked Sendable { - private let state: NativeHandleState - - init(handle: NativeWakeSourceHandle) throws { - state = try NativeHandleState(typeName: "WakeSource", handle: handle) - } - - public var isClosed: Bool { - state.isClosed - } - - /// Sets the runtime's wake flag and releases the parked owner thread. A - /// signal raised while the owner thread runs makes the next - /// `RuntimeHandle.pump(timeout:budget:)` return without parking. Signalling - /// after - /// the runtime closes succeeds and does nothing. - public func signal() throws { - try mapNativeFailure { - try state.withLive { source in - try checkStatus(mln_wake_source_signal(source.raw)) - } - } - } - - /// Releases the wake source. - public func close() throws { - try mapNativeFailure { - try state.closeOnce { source in - mln_wake_source_destroy(source.raw) - } - } - } -} diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/HandleIdentityTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/HandleIdentityTests.swift index 7ffa0567f..4be9c3cbd 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/HandleIdentityTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/HandleIdentityTests.swift @@ -3,58 +3,38 @@ import Foundation @testable import MaplibreNativeFFI import Testing -private final class CapturedFailure: @unchecked Sendable { - private let lock = NSLock() - private var failure: NativeStatusFailure? - - func store(_ value: NativeStatusFailure) { - lock.withLock { failure = value } - } - - func value() -> NativeStatusFailure? { - lock.withLock { failure } - } -} - // Handle-identity behaviour the C API owns, reached through the internal // handle accessors because the safe public API has no way to express these // calls. -private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { - try MapHandle( - runtime: runtime, - options: MapOptions( - width: 64, - height: 64, - scaleFactor: 1.0, - mode: .continuous - ) - ) +private func makeMap(_ runtime: RuntimeHandle) async throws -> MapHandle { + try await MapHandle(runtime: runtime, + options: MapOptions( + width: 64, + height: 64, + scaleFactor: 1.0, + mode: .continuous + )) } private func mapSize(_ map: NativeMapHandle) throws { - var width: UInt32 = 0 - var height: UInt32 = 0 - var scaleFactor: Double = 0 - try checkStatus( - mln_map_get_size(map.raw, &width, &height, &scaleFactor) - ) + _ = try NativeMap.snapshot(map) } /// BND-045. -@Test func releasedMapIdReplayedAfterANewMapIsReportedStale() throws { +@Test func releasedMapIdReplayedAfterANewMapIsReportedStale() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } + defer { try? runtime.closeBlockingForTests() } - let first = try makeMap(runtime) + let first = try await makeMap(runtime) let released = try first.requireLiveHandle() - try first.close() + try await first.close() // The released slot is the one the next map takes, so the replayed id // names a retired generation of a slot that is live again. - let second = try makeMap(runtime) - defer { try? second.close() } + let second = try await makeMap(runtime) + defer { try? second.closeBlockingForTests() } #expect(throws: NativeStatusFailure.self) { try mapSize(released) } do { @@ -69,18 +49,20 @@ private func mapSize(_ map: NativeMapHandle) throws { } /// BND-047. -@Test func mapIdPassedToARuntimeOperationIsRejectedOnItsKind() throws { +@Test func mapIdPassedToARuntimeOperationIsRejectedOnItsKind() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } // NativeMapHandle and NativeRuntimeHandle are distinct types, so this call // has no expression in the safe API and needs the raw id. let wrongKind = try map.requireLiveHandle().raw + let state = NativeCompletionStateForTest() + var completion = state.descriptor do { - try checkStatus(mln_runtime_pump(wrongKind, 0, -1)) + try checkStatus(mln_runtime_barrier(wrongKind, &completion)) Issue.record("a map id should not name a runtime") } catch let failure as NativeStatusFailure { #expect(failure.rawStatus == MLN_STATUS_INVALID_ARGUMENT.rawValue) @@ -89,30 +71,11 @@ private func mapSize(_ map: NativeMapHandle) throws { } } -/// BND-049. -@Test func liveMapIdCalledFromAnotherThreadReportsWrongThread() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } - - let live = try map.requireLiveHandle() - let captured = CapturedFailure() - let thread = Thread { - do { - try mapSize(live) - } catch let failure as NativeStatusFailure { - captured.store(failure) - } catch {} - } - thread.start() - while !thread.isFinished { - usleep(1000) - } - - // The id is live, so the owner-thread rule decides rather than identity. - let failure = try #require(captured.value()) - #expect(failure.rawStatus == MLN_STATUS_WRONG_THREAD.rawValue) - #expect(!failure.diagnostic.contains("stale")) +private final class NativeCompletionStateForTest { + private(set) lazy var descriptor = mln_completion( + size: UInt32(MemoryLayout.size), + callback: { _, _ in }, + user_data: nil, + release_user_data: nil + ) } diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/LoggingTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/LoggingTests.swift index a71bef4ff..d1272d8e7 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/LoggingTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/LoggingTests.swift @@ -30,7 +30,11 @@ private final class LogRecords: @unchecked Sendable { } #expect( LoggingCallbackState.invokeForTesting( - NativeLogRecord(severity: 1, event: 0, code: 7, message: "first") + { record in + first.append(LogRecord(native: record)) + return true + }, + record: NativeLogRecord(severity: 1, event: 0, code: 7, message: "first") ) == true ) @@ -40,18 +44,17 @@ private final class LogRecords: @unchecked Sendable { } #expect( LoggingCallbackState.invokeForTesting( - NativeLogRecord(severity: 2, event: 3, code: 8, message: "second") + { record in + second.append(LogRecord(native: record)) + return false + }, + record: NativeLogRecord(severity: 2, event: 3, code: 8, message: "second") ) == false ) try Maplibre.clearLogCallback() - #expect( - LoggingCallbackState.invokeForTesting( - NativeLogRecord(severity: 3, event: 4, code: 9, message: "cleared") - ) == nil - ) - let syntheticMessages = Set(["first", "second", "cleared"]) + let syntheticMessages = Set(["first", "second"]) #expect(first.snapshot() .filter { syntheticMessages.contains($0.message) } == [LogRecord( severity: .info, diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/MapHandleTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/MapHandleTests.swift index c69b218cf..c482e342c 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/MapHandleTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/MapHandleTests.swift @@ -1,308 +1,176 @@ +import CMaplibreNativeC +import Foundation @testable import MaplibreNativeFFI import Testing -private struct CameraEventTally { - var finishedTransitionIds: [UInt64] = [] - var lastDidChangeMode: CameraChangeMode? - /// The types this batch carried, in queue order. - var types: [RuntimeEventType] = [] -} - -private func drainCameraEvents(_ runtime: RuntimeHandle) throws - -> CameraEventTally -{ - var tally = CameraEventTally() - for event in try runtime.drainEvents().events { - tally.types.append(event.type) - switch event.type { - case .mapCameraTransitionFinished: - guard case let .cameraTransitionFinished(payload) = event.payload else { - Issue.record("unexpected transition payload: \(event.payload)") - continue - } - tally.finishedTransitionIds.append(payload.transitionId) - case .mapCameraDidChange: - tally.lastDidChangeMode = CameraChangeMode - .fromNative(UInt32(bitPattern: event.code)) - default: - break - } +@Test func mapOptionsMaterializeInitialExtentAndFastPFOR() throws { + try MapOptions( + width: 256, + height: 128, + scaleFactor: 2, + fastPFOREnabled: true + ).nativeInput.withNativeOptions { native in + #expect(native.pointee.initial_extent.width == 256) + #expect(native.pointee.initial_extent.height == 128) + #expect(native.pointee.initial_extent.scale_factor == 2) + #expect(native.pointee.fast_pfor_enabled) } - return tally } -/// A creation mask is in force while the map initializes. -@Test func aMapCreatedWithANarrowedMaskNeverQueuesAClearedType() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions( - width: 64, - height: 64, - eventMask: [.mapStyleLoaded] - ) +@Test func mapLifecycleCommandsSnapshotsAndOrderedCameraQuery() async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - defer { try? map.close() } - - #expect(try map.eventMask == [.mapStyleLoaded]) - #expect(try runtime.drainEvents().events.isEmpty) - - try map.setStyleJSON(emptyStyleJSON) - try map.jump(to: CameraOptions(zoom: 6)) - try runtime.pump() - - let types = try Set(runtime.drainEvents().events.map(\.type)) - #expect(types == [.mapStyleLoaded]) -} - -@Test func mapOptionsMaterializeFastPFORDecoding() throws { - try MapOptions(width: 256, height: 256).nativeInput - .withNativeOptions { native in - #expect(native.pointee.fast_pfor_enabled == false) - } - try MapOptions(width: 256, height: 256, fastPFOREnabled: true).nativeInput - .withNativeOptions { native in - #expect(native.pointee.fast_pfor_enabled == true) - } -} - -@Test func mapCreateCameraStyleAndClose() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( runtime: runtime, - options: MapOptions( - width: 256, - height: 256, - scaleFactor: 1.0, - mode: .continuous - ) + options: MapOptions(width: 256, height: 128, scaleFactor: 2) ) - try map.setStyleURL("https://tiles.openfreemap.org/styles/bright") - try map.jump( - to: CameraOptions( + let initial = try map.snapshot() + #expect(initial.logicalExtent == MapLogicalExtent( + width: 256, + height: 128, + scaleFactor: 2 + )) + + // Cross an actor/task suspension before using the any-thread map handle. + await Task.yield() + let cameraCommand = try await map.updateCamera(CameraUpdate( + camera: CameraOptions( center: LatLng(latitude: 37.7749, longitude: -122.4194), - zoom: 12, - bearing: 5, - pitch: 10 + zoom: 12 ) - ) - let camera = try map.camera() - #expect(abs((camera.center?.latitude ?? 0) - 37.7749) < 0.000001) - #expect(abs((camera.center?.longitude ?? 0) - -122.4194) < 0.000001) - #expect(abs((camera.zoom ?? 0) - 12) < 0.000001) - - try map.cancelTransitions() - try map.moveBy(deltaX: 1, deltaY: 2) - try map.moveBy( - deltaX: -1, - deltaY: -2, - animation: AnimationOptions(durationMilliseconds: 1) - ) - try map.scaleBy(1.01, anchor: ScreenPoint(x: 128, y: 128)) - try map.scaleBy( - 1.0 / 1.01, - anchor: nil, - animation: AnimationOptions(durationMilliseconds: 1) - ) - try map.ease( - to: CameraOptions(bearing: 0, pitch: 0), - animation: AnimationOptions(durationMilliseconds: 1) - ) - - #expect(try map.isGestureInProgress() == false) - try map.setGestureInProgress(true) - try map.moveBy(deltaX: 8, deltaY: -4) - #expect(try map.isGestureInProgress() == true) - try map.setGestureInProgress(false) - #expect(try map.isGestureInProgress() == false) - - try map.close() + )) + let resizeCommand = try await map.resize(to: MapLogicalExtent( + width: 512, + height: 256, + scaleFactor: 2 + )) + #expect(cameraCommand.disposition == .committed) + #expect(resizeCommand.generation >= cameraCommand.generation) + + let ordered = try await map.queryCamera() + #expect(abs((ordered.camera.center?.latitude ?? 0) - 37.7749) < 0.000001) + #expect(abs((ordered.camera.center?.longitude ?? 0) - -122.4194) < 0.000001) + #expect(abs((ordered.camera.zoom ?? 0) - 12) < 0.000001) + + let resized = try map.snapshot() + #expect(resized.generation >= ordered.generation) + #expect(resized.logicalExtent.width == 512) + #expect(resized.logicalExtent.height == 256) + + try await map.close() #expect(map.isClosed) + try await runtime.close() + #expect(runtime.isClosed) } -@Test func mapSizeReportsCreationExtentAndPixelRatio() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 256, scaleFactor: 2.0) +@Test func cameraSnapshotIsSynchronousAndOrderedQueryObservesCommands( +) async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - defer { try? map.close() } - - let size = try map.size() - #expect(size.width == 512) - #expect(size.height == 256) - #expect(size.scaleFactor == 2.0) -} - -/// Covers the terminal outcomes a headless map reaches without rendering -/// frames. Running a transition to completion needs a live render session. -@Test func cameraTransitionIdReportsTerminalOutcomesOnce() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( runtime: runtime, - options: MapOptions( - width: 256, - height: 256, - scaleFactor: 1.0, - mode: .continuous - ) - ) - defer { try? map.close() } - - var camera = CameraOptions( - center: LatLng(latitude: 37.7749, longitude: -122.4194), - zoom: 11, - bearing: 12, - pitch: 30 + options: MapOptions(width: 64, height: 64) ) + defer { try? map.closeBlockingForTests() } - // MapLibre resizes the map inside its own constructor, so drop the camera - // events of the initial sizing before the batch that matters. - _ = try runtime.drainEvents() - - // A zero-duration ease resolves inside the call, so its event lands ahead of - // the immediate did-change event. - try map.ease( - to: camera, - animation: AnimationOptions(durationMilliseconds: 0, transitionId: 7) - ) - var tally = try drainCameraEvents(runtime) - #expect(tally.finishedTransitionIds == [7]) - #expect(tally.lastDidChangeMode == .immediate) + let before = try map.cameraSnapshot() + _ = try await map.updateCamera(CameraUpdate( + mode: .ease, + camera: CameraOptions(zoom: 6), + animation: AnimationOptions(durationMilliseconds: 0, transitionId: 10), + gesturePhase: .end + )) - // One batch keeps queue order, so a host reads the transition's own event - // before the camera change that ended it. - let finishedIndex = try #require( - tally.types.firstIndex(of: .mapCameraTransitionFinished) - ) - let didChangeIndex = try #require( - tally.types.firstIndex(of: .mapCameraDidChange) - ) - #expect(finishedIndex < didChangeIndex) + await Task.yield() + let ordered = try await map.queryCamera() + #expect(ordered.generation >= before.generation) + #expect(abs((ordered.camera.zoom ?? 0) - 6) < 0.000001) +} - // A running transition stays silent until it releases the camera. - camera.zoom = 12 - try map.ease( - to: camera, - animation: AnimationOptions(durationMilliseconds: 5000, transitionId: 11) +/// A committed mutation's finished event reports the generation its commit +/// published, and a snapshot at or past that generation observes the value. +@Test func snapshotObservesACommittedCommandAtItsGeneration() async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - tally = try drainCameraEvents(runtime) - #expect(tally.finishedTransitionIds.isEmpty) - - // A later camera command supersedes it and reports the superseded identity. - camera.zoom = 13 - try map.ease( - to: camera, - animation: AnimationOptions(durationMilliseconds: 5000, transitionId: 12) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( + runtime: runtime, + options: MapOptions(width: 64, height: 64) ) - tally = try drainCameraEvents(runtime) - #expect(tally.finishedTransitionIds == [11]) - #expect(tally.lastDidChangeMode == .animated) + defer { try? map.closeBlockingForTests() } - try map.cancelTransitions() - tally = try drainCameraEvents(runtime) - #expect(tally.finishedTransitionIds == [12]) + let command = try await map.setDebugOptions([.tileBorders]) + let result = command + #expect(rawCommandDisposition(result) == MLN_COMMAND_DISPOSITION_COMMITTED + .rawValue) + #expect(result.generation > 0) - // A transition started without an identity reports nothing when cancelled. - camera.zoom = 14 - try map.ease( - to: camera, - animation: AnimationOptions(durationMilliseconds: 5000) - ) - try map.cancelTransitions() - tally = try drainCameraEvents(runtime) - #expect(tally.finishedTransitionIds.isEmpty) + let snapshot = try map.snapshot() + #expect(snapshot.generation >= result.generation) + #expect(snapshot.debugOptions == [.tileBorders]) } -@Test func styleURLRejectsEmbeddedNULAsPublicInvalidArgument() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( +/// The snapshot's tile, bound, and free-camera fields observe their set +/// commands. +@Test func snapshotFieldsRoundTripThroughTheirSetCommands() async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") + ) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( runtime: runtime, options: MapOptions(width: 64, height: 64) ) - defer { try? map.close() } - - do { - try map.setStyleURL("https://example.test/style\0oops") - Issue.record("embedded NUL should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - #expect(error.rawStatus == nil) - #expect(error.diagnostic.contains("embedded NUL")) - } catch { - Issue.record("unexpected error: \(error)") - } + defer { try? map.closeBlockingForTests() } + + _ = try await map.setTileOptions(MapTileOptions( + prefetchZoomDelta: 5, + lodScale: 2 + )) + _ = try await map.setBounds(BoundOptions( + minZoom: 2, + maxZoom: 15, + maxPitch: 45 + )) + // The altitude sits inside the bound zoom range, so it is not clamped. + _ = try await map.setFreeCameraOptions(FreeCameraOptions( + position: Vec3(x: 0.25, y: 0.5, z: 0.01) + )) + let statsCommand = try await map.setRenderingStatsViewEnabled(true) + #expect(try await commandDisposition( + statsCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + + let snapshot = try map.snapshot() + #expect(snapshot.tileOptions.prefetchZoomDelta == 5) + #expect(snapshot.tileOptions.lodScale == 2) + #expect(snapshot.bounds.minZoom == 2) + #expect(snapshot.bounds.maxZoom == 15) + #expect(snapshot.bounds.maxPitch == 45) + let position = try #require(snapshot.freeCameraOptions.position) + #expect(abs(position.x - 0.25) < 0.000001) + #expect(abs(position.y - 0.5) < 0.000001) + #expect(abs(position.z - 0.01) < 0.000001) + #expect(snapshot.renderingStatsViewEnabled) } -@Test func cameraBoundsDistinguishUnboundedFromWorldBounds() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 256, height: 256) +@Test func requestRepaintReturnsACommittedCompletion() async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - defer { try? map.close() } - - func longitudeAfterJump(to longitude: Double) throws -> Double { - try map.jump(to: CameraOptions( - center: LatLng(latitude: 0, longitude: longitude), - zoom: 2 - )) - return try map.camera().center?.longitude ?? .nan - } - - #expect(try map.bounds().bounds == .unbounded) - // An unbounded map wraps across the antimeridian. - #expect(try abs(longitudeAfterJump(to: 200) - -160) < 0.000001) - - try map.setBounds(BoundOptions(bounds: .bounded(LatLngBounds( - southwest: LatLng(latitude: -90, longitude: -180), - northeast: LatLng(latitude: 90, longitude: 180) - )))) - - if case let .bounded(reported) = try map.bounds().bounds { - #expect(abs(reported.northeast.longitude - 180) < 0.000001) - } else { - Issue.record("world bounds should report a bounded constraint") - } - // World bounds clamp at the antimeridian instead of wrapping. - #expect(try abs(longitudeAfterJump(to: 200) - 180) < 0.000001) - - try map.setBounds(BoundOptions(bounds: .unbounded)) - #expect(try map.bounds().bounds == .unbounded) - // Releasing the constraint restores antimeridian wrapping. - #expect(try abs(longitudeAfterJump(to: 200) - -160) < 0.000001) -} - -@Test func closedMapReportsSwiftOwnedStateError() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( runtime: runtime, - options: MapOptions(width: 64, height: 64) + options: MapOptions(width: 64, height: 64, mode: .continuous) ) - try map.close() + defer { try? map.closeBlockingForTests() } - do { - try map.requestRepaint() - Issue.record("closed map should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } catch { - Issue.record("unexpected error: \(error)") - } + let command = try await map.requestRepaint() + #expect(command.disposition == .committed) } diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/OfflineTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/OfflineTests.swift index 9cbb4c207..fdfca96cd 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/OfflineTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/OfflineTests.swift @@ -115,43 +115,22 @@ import Testing } } -@Test func setMaximumAmbientCacheSizeReportsCompletion() throws { +@Test func setMaximumAmbientCacheSizeReportsCompletion() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } + defer { try? runtime.closeBlockingForTests() } - let operationId = try runtime.setMaximumAmbientCacheSizeStart(8 << 20) - #expect(operationId != 0) - - let event = try pumpUntilEvent( - runtime, - waitingFor: "the cache size operation to complete" - ) { event in - guard case let .offlineOperationCompleted(completed) = event.payload else { - return false - } - return completed.operationId == operationId - } - guard case let .offlineOperationCompleted(completed) = try #require(event) - .payload - else { - Issue.record("the completion event lost its payload") - return - } - // MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE; the public API - // carries operation kinds as raw C values. - #expect(completed.operationKind == 12) - #expect(completed.resultStatus == 0) - try runtime.discardOfflineOperation(operationId) + try await runtime.setMaximumAmbientCacheSize(8 << 20) } -@Test func closedRuntimeRejectsOfflineCallsThroughSwiftHandleState() throws { +@Test func closedRuntimeRejectsOfflineCallsThroughSwiftHandleState( +) async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - try runtime.close() + try await runtime.close() do { - _ = try runtime.offlineRegionsListStart() + _ = try await runtime.offlineRegions() Issue.record("closed runtime should throw") } catch let error as MaplibreError { #expect(error.kind == .invalidState) diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/ProjectionTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/ProjectionTests.swift index 0072e8429..e0d2af620 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/ProjectionTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/ProjectionTests.swift @@ -1,3 +1,4 @@ +import Foundation @testable import MaplibreNativeFFI import Testing @@ -10,36 +11,53 @@ import Testing #expect(abs(roundTripped.longitude - coordinate.longitude) < 0.000001) } -@Test func mapProjectionCameraAndCoordinateConversion() async throws { +/// A projection created after a camera command observes that command, every +/// later call is synchronous, a setter changes later conversions, and close is +/// synchronous. +@Test func mapProjectionIsSynchronousAfterCreation() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 256, height: 256) - ) - defer { try? map.close() } - try map.jump(to: CameraOptions( - center: LatLng(latitude: 0, longitude: 0), - zoom: 1 - )) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 256, height: 256)) + defer { try? map.closeBlockingForTests() } + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions( + center: LatLng(latitude: 10, longitude: 20), + zoom: 3 + ))) + + // Creation is ordered after the accepted camera command, so the copied + // transform observes it without a barrier. + let projection = try await MapProjectionHandle(map: map) + let created = try projection.camera() + #expect(abs((created.center?.latitude ?? 0) - 10) < 0.000001) + #expect(abs((created.center?.longitude ?? 0) - 20) < 0.000001) + #expect(abs((created.zoom ?? 0) - 3) < 0.000001) + + // A synchronous conversion round-trips within tolerance. + let point = try projection.pixel(for: LatLng(latitude: 10, longitude: 20)) + let coordinate = try projection.latLng(for: point) + #expect(abs(coordinate.latitude - 10) < 0.000001) + #expect(abs(coordinate.longitude - 20) < 0.000001) - let projection = try MapProjectionHandle(map: map) + // A setter applies before returning and changes later conversions. try projection.setCamera(CameraOptions( center: LatLng(latitude: 1, longitude: 2), - zoom: 2 + zoom: 5 )) - let camera = try projection.camera() - #expect(abs((camera.center?.latitude ?? 0) - 1) < 0.000001) - #expect(abs((camera.center?.longitude ?? 0) - 2) < 0.000001) + let updated = try projection.camera() + #expect(abs((updated.center?.latitude ?? 0) - 1) < 0.000001) + #expect(abs((updated.center?.longitude ?? 0) - 2) < 0.000001) + let moved = try projection.pixel(for: LatLng(latitude: 10, longitude: 20)) + #expect(abs(moved.x - point.x) > 1 || abs(moved.y - point.y) > 1) - let point = try projection.pixel(for: LatLng(latitude: 1, longitude: 2)) - let coordinate = try projection.latLng(for: point) - #expect(abs(coordinate.latitude - 1) < 0.000001) - #expect(abs(coordinate.longitude - 2) < 0.000001) + try await map.close() + try await runtime.close() + let detached = try projection.latLng(for: moved) + #expect(abs(detached.latitude - 10) < 0.000001) + #expect(abs(detached.longitude - 20) < 0.000001) - try map.close() - try runtime.close() + // Close is synchronous and works from any thread. try await Task.detached { _ = try projection.camera() try projection.close() @@ -47,17 +65,35 @@ import Testing #expect(projection.isClosed) } +/// Projection calls are internally serialized, so a second thread converts +/// through the same live handle. +@Test func mapProjectionIsUsableFromASecondThread() async throws { + let runtime = + try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 256, height: 256)) + defer { try? map.closeBlockingForTests() } + let projection = try await MapProjectionHandle(map: map) + defer { try? projection.close() } + + let expected = try projection.pixel(for: LatLng(latitude: 5, longitude: 6)) + let result = try await Task.detached { + try projection.pixel(for: LatLng(latitude: 5, longitude: 6)) + }.value + #expect(abs(result.x - expected.x) < 0.000001) + #expect(abs(result.y - expected.y) < 0.000001) +} + @Test func mapProjectionSetVisibleCoordinatesRejectsEmptyInputBeforeCallingC( -) throws { +) async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 256, height: 256) - ) - defer { try? map.close() } - let projection = try MapProjectionHandle(map: map) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 256, height: 256)) + defer { try? map.closeBlockingForTests() } + let projection = try await MapProjectionHandle(map: map) defer { try? projection.close() } do { diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/QueryTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/QueryTests.swift index a96efdcf4..25542da9c 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/QueryTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/QueryTests.swift @@ -122,3 +122,47 @@ import Testing #expect(stateKey == "hover") } } + +private func featureStateObject( + _ map: MapHandle, + selector: FeatureStateSelector +) async throws -> [String: Any] { + let bytes = try await map.featureState(selector: selector) + return try #require( + JSONSerialization.jsonObject(with: bytes) as? [String: Any] + ) +} + +/// Feature state belongs to the map store: a committed set reads back through +/// an ordered get, a keyed remove drops only that key, and a source-wide +/// remove clears the rest, all without a render session or a loaded source. +@Test func mapFeatureStateRoundTripsWithoutARenderSession() async throws { + let runtime = + try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } + + let feature = FeatureStateSelector(sourceId: "geo", featureId: "f1") + // Missing state reads as an empty JSON object. + #expect(try await featureStateObject(map, selector: feature).isEmpty) + + try await map.setFeatureState( + selector: feature, + state: Data(#"{"hover":true,"rank":2}"#.utf8) + ) + let stored = try await featureStateObject(map, selector: feature) + #expect(stored["hover"] as? Bool == true) + #expect(stored["rank"] as? Int == 2) + + try await map.removeFeatureState(selector: FeatureStateSelector( + sourceId: "geo", featureId: "f1", stateKey: "hover" + )) + let trimmed = try await featureStateObject(map, selector: feature) + #expect(Set(trimmed.keys) == ["rank"]) + + try await map + .removeFeatureState(selector: FeatureStateSelector(sourceId: "geo")) + #expect(try await featureStateObject(map, selector: feature).isEmpty) +} diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/RenderTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/RenderTests.swift index a1552a17c..2556822c5 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/RenderTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/RenderTests.swift @@ -1,405 +1,123 @@ import CMaplibreNativeC -import Foundation @testable import MaplibreNativeFFI import Testing - -private final class RenderCounter: @unchecked Sendable { - private let lock = NSLock() - private var count = 0 - - func increment() { - lock.withLock { count += 1 } - } - - func value() -> Int { - lock.withLock { count } - } -} - -private final class RenderLeakBox: @unchecked Sendable { - private let lock = NSLock() - private var leaks: [NativeHandleLeak] = [] - - func append(_ leak: NativeHandleLeak) { - lock.withLock { leaks.append(leak) } - } - - func value() -> [NativeHandleLeak] { - lock.withLock { leaks } - } -} - -@Test func renderTargetDescriptorsMaterializeNativePointersAndExtents() throws { - let extent = RenderTargetExtent(width: 640, height: 480, scaleFactor: 2) - let metalSurface = MetalSurfaceDescriptor( - extent: extent, - context: MetalContextDescriptor(device: NativePointer(bitPattern: 0x10)), - layer: NativePointer(bitPattern: 0x20) - ) - try metalSurface.nativeInput.withNativeDescriptor { descriptor in - #expect(descriptor.pointee.extent.width == 640) - #expect(descriptor.pointee.extent.height == 480) - #expect(descriptor.pointee.extent.scale_factor == 2) - #expect(UInt(bitPattern: descriptor.pointee.context.device) == 0x10) - #expect(UInt(bitPattern: descriptor.pointee.layer) == 0x20) - } - - let vulkanContext = VulkanContextDescriptor( - instance: NativePointer(bitPattern: 0x30), - physicalDevice: NativePointer(bitPattern: 0x40), - device: NativePointer(bitPattern: 0x50), - graphicsQueue: NativePointer(bitPattern: 0x60), - graphicsQueueFamilyIndex: 7, - getInstanceProcAddr: NativePointer(bitPattern: 0x90), - getDeviceProcAddr: NativePointer(bitPattern: 0xA0) - ) - let vulkanTexture = VulkanBorrowedTextureDescriptor( - extent: extent, - physicalWidth: 65, - physicalHeight: 33, - context: vulkanContext, - image: NativePointer(bitPattern: 0x70), - imageView: NativePointer(bitPattern: 0x80), - format: 44, - initialLayout: 1, - finalLayout: 2 - ) - try vulkanTexture.nativeInput.withNativeDescriptor { descriptor in - #expect(UInt(bitPattern: descriptor.pointee.context.instance) == 0x30) - #expect(UInt(bitPattern: descriptor.pointee.context.physical_device) == - 0x40) - #expect(UInt(bitPattern: descriptor.pointee.context.device) == 0x50) - #expect(UInt(bitPattern: descriptor.pointee.context.graphics_queue) == 0x60) - #expect(descriptor.pointee.context.graphics_queue_family_index == 7) - #expect(UInt( - bitPattern: descriptor.pointee.context.get_instance_proc_addr - ) == - 0x90) - #expect(UInt(bitPattern: descriptor.pointee.context.get_device_proc_addr) == - 0xA0) - #expect(UInt(bitPattern: descriptor.pointee.image) == 0x70) - #expect(UInt(bitPattern: descriptor.pointee.image_view) == 0x80) - #expect(descriptor.pointee.format == 44) - #expect(descriptor.pointee.initial_layout == 1) - #expect(descriptor.pointee.final_layout == 2) - } - - let wgl = OpenGLContextDescriptor.wgl( - WglContextDescriptor( - deviceContext: NativePointer(bitPattern: 0x110), - shareContext: NativePointer(bitPattern: 0x120), - getProcAddress: NativePointer(bitPattern: 0x130) +#if canImport(Metal) + import Metal +#endif + +#if canImport(Metal) + @Test(.enabled( + if: Maplibre.supportedRenderBackends().contains(.metal), + "The selected native preset does not provide Metal." + )) func metalCallerDriverCanServiceAttachmentBeforeCompletion( + ) async throws { + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - ) - let openGLSurface = OpenGLSurfaceDescriptor( - extent: extent, - context: wgl, - surface: NativePointer(bitPattern: 0x140) - ) - try openGLSurface.nativeInput.withNativeDescriptor { descriptor in - #expect(descriptor.pointee.extent.width == 640) - #expect(descriptor.pointee.context - .platform == MLN_OPENGL_CONTEXT_PLATFORM_WGL) - #expect(UInt(bitPattern: descriptor.pointee.context.data.wgl - .device_context) == 0x110) - #expect(UInt( - bitPattern: descriptor.pointee.context.data.wgl.share_context - ) == - 0x120) - #expect(UInt(bitPattern: descriptor.pointee.context.data.wgl - .get_proc_address) == 0x130) - #expect(descriptor.pointee.context - .ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED) - #expect(UInt(bitPattern: descriptor.pointee.surface) == 0x140) - } - - let dedicatedEgl = OpenGLContextDescriptor.egl( - EglContextDescriptor( - display: NativePointer(bitPattern: 0x310), - config: NativePointer(bitPattern: 0x320), - shareContext: .null, - clientAPI: .gles - ), - ownership: .dedicated - ) - let dedicatedSurface = OpenGLSurfaceDescriptor( - extent: extent, - context: dedicatedEgl, - surface: NativePointer(bitPattern: 0x340) - ) - try dedicatedSurface.nativeInput.withNativeDescriptor { descriptor in - #expect(descriptor.pointee.context - .ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED) - #expect(descriptor.pointee.context.data.egl.share_context == nil) - #expect(descriptor.pointee.context.data.egl - .client_api == MLN_OPENGL_CLIENT_API_GLES) - } - - let unknownEgl = OpenGLContextDescriptor.egl( - EglContextDescriptor( - display: NativePointer(bitPattern: 0x410), - config: NativePointer(bitPattern: 0x420), - shareContext: .null, - clientAPI: .unknown(97) - ), - ownership: .unknown(98) - ) - let unknownSurface = OpenGLSurfaceDescriptor( - extent: extent, - context: unknownEgl, - surface: NativePointer(bitPattern: 0x440) - ) - try unknownSurface.nativeInput.withNativeDescriptor { descriptor in - #expect(descriptor.pointee.context.ownership.rawValue == 98) - #expect(descriptor.pointee.context.data.egl.client_api.rawValue == 97) - } - - let egl = OpenGLContextDescriptor.egl( - EglContextDescriptor( - display: NativePointer(bitPattern: 0x210), - config: NativePointer(bitPattern: 0x220), - shareContext: NativePointer(bitPattern: 0x230), - getProcAddress: NativePointer(bitPattern: 0x240) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle( + runtime: runtime, + options: MapOptions(width: 32, height: 32) ) - ) - let openGLTexture = OpenGLBorrowedTextureDescriptor( - extent: extent, - physicalWidth: 65, - physicalHeight: 33, - context: egl, - texture: 33, - target: 0x0DE1 - ) - try openGLTexture.nativeInput.withNativeDescriptor { descriptor in - #expect(descriptor.pointee.context - .platform == MLN_OPENGL_CONTEXT_PLATFORM_EGL) - #expect(UInt(bitPattern: descriptor.pointee.context.data.egl.display) == - 0x210) - #expect(UInt(bitPattern: descriptor.pointee.context.data.egl.config) == - 0x220) - #expect(UInt( - bitPattern: descriptor.pointee.context.data.egl.share_context - ) == - 0x230) - #expect(UInt(bitPattern: descriptor.pointee.context.data.egl - .get_proc_address) == 0x240) - #expect(descriptor.pointee.context.data.egl - .client_api == MLN_OPENGL_CLIENT_API_UNSPECIFIED) - #expect(descriptor.pointee.texture == 33) - #expect(descriptor.pointee.target == 0x0DE1) - } -} - -@Test func metalOwnedTextureFrameInvalidatesAfterClose() throws { - let releases = RenderCounter() - var raw = mln_metal_owned_texture_frame() - raw.size = UInt32(MemoryLayout.size) - raw.texture = UnsafeMutableRawPointer(bitPattern: 0x1234) - raw.device = UnsafeMutableRawPointer(bitPattern: 0x5678) - let frame = - MetalOwnedTextureFrameHandle(frame: NativeMetalOwnedTextureFrame( - raw - )) { _ in - releases.increment() + defer { try? map.close() } + let device = try #require(MTLCreateSystemDefaultDevice()) + let devicePointer = NativePointer(bitPattern: UInt(bitPattern: + Unmanaged.passUnretained(device as AnyObject).toOpaque())) + + let attachment = try map.attachMetalOwnedTexture( + MetalOwnedTextureDescriptor( + extent: RenderTargetExtent(width: 32, height: 32, scaleFactor: 1), + context: MetalContextDescriptor(device: devicePointer) + ), + options: RenderSessionAttachOptions(driver: .callerGraphicsThread) + ) + let session = attachment.session + defer { + _ = try? session.abandon() + try? session.close() } - var capturedView: MetalOwnedTextureFrameView? - var escapedTexture: FrameNativePointer? - try frame.withBackendPointers { view in - capturedView = view - let texture = try view.texture - let device = try view.device - escapedTexture = texture - #expect(try texture.addressBitPattern == 0x1234) - #expect(try device.addressBitPattern == 0x5678) - } - do { - _ = try capturedView?.texture - Issue.record("frame view access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - do { - _ = try escapedTexture?.addressBitPattern - Issue.record("escaped frame pointer access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - - try frame.close() - try frame.close() - - #expect(frame.isClosed) - #expect(releases.value() == 1) - do { - try frame.withBackendPointers { _ in } - Issue.record("closed frame access should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } + #expect(try session.snapshot().state == RenderSessionState.attaching) + #expect(try session.serviceDriverWork(maxWork: 0) > 0) + try await attachment.completion.value + #expect(try session.snapshot().state == RenderSessionState.attached) + withExtendedLifetime(device) {} + } +#endif + +@Test func frameDemandPreservesCoalescingAndTimeout() { + let demand = FrameDemand( + options: [.ifNeeded, .present], + token: 41, + coalescingBoundary: 7, + timeoutNanoseconds: 2000 + ).native + + #expect(demand.flags == 3) + #expect(demand.token == 41) + #expect(demand.coalescing_boundary == 7) + #expect(demand.timeout_ns == 2000) } -@Test func vulkanOwnedTextureFrameInvalidatesAfterClose() throws { - let releases = RenderCounter() - var raw = mln_vulkan_owned_texture_frame() - raw.size = UInt32(MemoryLayout.size) - raw.image = UnsafeMutableRawPointer(bitPattern: 0x1234) - raw.image_view = UnsafeMutableRawPointer(bitPattern: 0x5678) - let frame = - VulkanOwnedTextureFrameHandle(frame: NativeVulkanOwnedTextureFrame( - raw - )) { _ in - releases.increment() - } - - var capturedView: VulkanOwnedTextureFrameView? - var escapedImage: FrameNativePointer? - try frame.withBackendPointers { view in - capturedView = view - let image = try view.image - let imageView = try view.imageView - escapedImage = image - #expect(try image.addressBitPattern == 0x1234) - #expect(try imageView.addressBitPattern == 0x5678) - } - do { - _ = try capturedView?.image - Issue.record("frame view access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - do { - _ = try escapedImage?.addressBitPattern - Issue.record("escaped frame pointer access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - - try frame.close() - try frame.close() - - #expect(frame.isClosed) - #expect(releases.value() == 1) - do { - try frame.withBackendPointers { _ in } - Issue.record("closed frame access should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } +@Test func renderResultsPreserveEveryTerminalDisposition() { + #expect(RenderResult.fromNative(0) == .rendered) + #expect(RenderResult.fromNative(1) == .noUpdate) + #expect(RenderResult.fromNative(2) == .sizePending) + #expect(RenderResult.fromNative(3) == .targetNotReady) + #expect(RenderResult.fromNative(4) == .superseded) + #expect(RenderResult.fromNative(5) == .deadlineMissed) + #expect(RenderResult.fromNative(99) == .unknown(99)) } -@Test func openGLOwnedTextureFrameInvalidatesAfterClose() throws { - let releases = RenderCounter() - var raw = mln_opengl_owned_texture_frame() - raw.size = UInt32(MemoryLayout.size) - raw.texture = 77 - raw.target = 0x0DE1 - let frame = - OpenGLOwnedTextureFrameHandle(frame: NativeOpenGLOwnedTextureFrame( - raw - )) { _ in - releases.increment() - } - - var capturedView: OpenGLOwnedTextureFrameView? - var escapedTexture: FrameOpenGLTextureName? - try frame.withBackendPointers { view in - capturedView = view - let texture = try view.texture - let target = try view.target - escapedTexture = texture - #expect(try texture.value == 77) - #expect(target == 0x0DE1) - } - do { - _ = try capturedView?.texture - Issue.record("frame view access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - do { - _ = try escapedTexture?.value - Issue.record("escaped OpenGL texture access after scope should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } - - try frame.close() - try frame.close() - - #expect(frame.isClosed) - #expect(releases.value() == 1) - do { - try frame.withBackendPointers { _ in } - Issue.record("closed frame access should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidState) - #expect(error.rawStatus == nil) - } +@Test func renderLifecycleStatesPreserveTargetLossAndAbandonment() { + #expect(RenderSessionState(rawValue: 1) == .attaching) + #expect(RenderSessionState(rawValue: 2) == .attached) + #expect(RenderSessionState(rawValue: 3) == .detaching) + #expect(RenderSessionState(rawValue: 4) == .detached) + #expect(RenderSessionState(rawValue: 5) == .targetLost) + #expect(RenderSessionState(rawValue: 6) == .abandoned) + #expect(RenderAbandonResult.Disposition(rawValue: 1) == .quarantined) } -@Test func ownedTextureFrameAllowsRetryAfterFailedRelease() throws { - struct ReleaseFailure: Error {} - - let releases = RenderCounter() - var raw = mln_opengl_owned_texture_frame() - raw.size = UInt32(MemoryLayout.size) - raw.texture = 77 - raw.target = 0x0DE1 - let frame = - OpenGLOwnedTextureFrameHandle(frame: NativeOpenGLOwnedTextureFrame( - raw - )) { _ in - releases.increment() - if releases.value() == 1 { - throw ReleaseFailure() - } - } +@Test func gpuSynchronizationUsesFrozenKinds() { + #expect(GPUSynchronization.cpuComplete.native.kind == + MLN_GPU_SYNC_CPU_COMPLETE.rawValue) + #expect(GPUSynchronization.metalSharedEvent( + NativePointer(bitPattern: 0x1234), value: 9 + ).native.kind == MLN_GPU_SYNC_METAL_SHARED_EVENT.rawValue) + #expect(GPUSynchronization.vulkanTimelineSemaphore( + NativePointer(bitPattern: 0x5678), value: 11 + ).native.kind == MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE.rawValue) +} - do { - try frame.close() - Issue.record("failed release should throw") - } catch is ReleaseFailure {} +@Test func transferredWebGLCanvasMaterializesWorkerDescriptor() throws { + let input = OpenGLContextDescriptor.webGL( + .transferredCanvas(selector: "#map") + ).nativeInput - #expect(!frame.isClosed) - try frame.close() - #expect(frame.isClosed) - #expect(releases.value() == 2) + try input.withNative { descriptor in + #expect(descriptor.platform == MLN_OPENGL_CONTEXT_PLATFORM_WEBGL) + #expect(descriptor.data.webgl.kind == + MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS.rawValue) + #expect(descriptor.data.webgl.context == 0) + #expect(descriptor.data.webgl.canvas_selector.size == 4) + } } -@Test func textureFrameDeinitReportsLeakWithoutRelease() { - let releases = RenderCounter() - let leaks = RenderLeakBox() - - NativeHandleLeakTestSupport.withHandler({ leak in - leaks.append(leak) - }) { - do { - var raw = mln_metal_owned_texture_frame() - raw.size = UInt32(MemoryLayout.size) - raw.texture = UnsafeMutableRawPointer(bitPattern: 0x1234) - raw.device = UnsafeMutableRawPointer(bitPattern: 0x5678) - _ = - MetalOwnedTextureFrameHandle(frame: NativeMetalOwnedTextureFrame( - raw - )) { _ in - releases.increment() - } +@Test func attachmentOptionsCarryDirectWakes() { + let options = RenderSessionAttachOptions( + driver: .coreWorker, + requestedTextureRingDepth: 3 + ) + var frameWake = mln_wake() + frameWake.user_data = UnsafeMutableRawPointer(bitPattern: 17) + var driverWake = mln_wake() + driverWake.user_data = UnsafeMutableRawPointer(bitPattern: 23) + options + .withNative(frameWake: frameWake, driverWorkWake: driverWake) { native in + #expect(native.pointee.driver == MLN_RENDER_DRIVER_CORE_WORKER.rawValue) + #expect(native.pointee.requested_texture_ring_depth == 3) + #expect(native.pointee.frame_wake.user_data == frameWake.user_data) + #expect(native.pointee.driver_work_wake.user_data == driverWake.user_data) } - - #expect(releases.value() == 0) - #expect(leaks.value() == [NativeHandleLeak( - typeName: "MetalOwnedTextureFrameHandle", - handle: 0, - detail: "texture 0x1234" - )]) - } } diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTestSupport.swift b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTestSupport.swift index 52b4234a5..7c507191f 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTestSupport.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTestSupport.swift @@ -3,32 +3,57 @@ import Foundation @testable import MaplibreNativeFFI import Testing -/// Pumps and drains until an event `isMatch` accepts arrives, and returns it. -/// Records an issue naming `subject` and returns `nil` when nothing matches -/// before the deadline. -func pumpUntilEvent( +/// Drains until an event `isMatch` accepts arrives, and returns it. +/// Records an issue naming `subject` and returns `nil` at the deadline. +func drainUntilEvent( _ runtime: RuntimeHandle, waitingFor subject: String, timeout: TimeInterval = 10, where isMatch: (RuntimeEvent) -> Bool -) throws -> RuntimeEvent? { +) async throws -> RuntimeEvent? { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { - try runtime.pump() + try await runtime.barrier() for event in try runtime.drainEvents().events where isMatch(event) { return event } - Thread.sleep(forTimeInterval: 0.001) + try await Task.sleep(nanoseconds: 1_000_000) } Issue.record("timed out waiting for \(subject)") return nil } -/// Pumps and drains until the runtime stops producing events, so a park that -/// follows is released by whatever the test raises next. -func pumpUntilQuiet(_ runtime: RuntimeHandle, iterations: Int = 100) throws { +func rawCommandDisposition(_ completion: CommandCompletion) -> UInt32 { + switch completion.disposition { + case .committed: MLN_COMMAND_DISPOSITION_COMMITTED.rawValue + case .superseded: MLN_COMMAND_DISPOSITION_SUPERSEDED.rawValue + case .failed: MLN_COMMAND_DISPOSITION_FAILED.rawValue + case .cancelled: MLN_COMMAND_DISPOSITION_CANCELLED.rawValue + case let .unknown(raw): raw + } +} + +func expectCommandFailure(_ completion: CommandCompletion, status: mln_status) { + #expect(completion.disposition == .failed) + #expect(completion.rawStatus == status.rawValue) + #expect(!completion.diagnostic.isEmpty) +} + +func commandDisposition( + _ completion: CommandCompletion, + runtime: RuntimeHandle +) async throws -> UInt32? { + _ = runtime + return rawCommandDisposition(completion) +} + +/// Drains until autonomous execution stops producing events. +func drainUntilQuiet( + _ runtime: RuntimeHandle, + iterations: Int = 100 +) async throws { for _ in 0 ..< iterations { - try runtime.pump() + try await runtime.barrier() if try runtime.drainEvents().events.isEmpty { return } } Issue.record("the runtime kept producing events while idle") @@ -51,9 +76,6 @@ private final class CapturedFailure: @unchecked Sendable { } /// Runs `body` on another thread and returns the native failure it threw. -/// -/// The public handle types are deliberately not `Sendable`, so a test reaches -/// the C API's owner-thread rule through the native handle a handle lends out. func failureFromAnotherThread( _ body: @escaping @Sendable () throws -> Void ) -> NativeStatusFailure? { @@ -80,7 +102,7 @@ func rawRuntimeEvent( source: UInt64 = 1, code: Int32 = 0, payloadType: UInt32 = MLN_RUNTIME_EVENT_PAYLOAD_NONE.rawValue, - messageOffset: UInt32 = 0, + messageOffset: UInt64 = 0, messageSize: UInt32 = 0 ) -> mln_runtime_event { var event = mln_runtime_event() @@ -98,11 +120,11 @@ func rawRuntimeEvent( /// and reports where each message starts. func packMessageArena( _ messages: [String] -) -> (bytes: [UInt8], offsets: [UInt32]) { +) -> (bytes: [UInt8], offsets: [UInt64]) { var bytes: [UInt8] = [] - var offsets: [UInt32] = [] + var offsets: [UInt64] = [] for message in messages { - offsets.append(UInt32(bytes.count)) + offsets.append(UInt64(bytes.count)) bytes.append(contentsOf: Array(message.utf8)) bytes.append(0) } @@ -111,7 +133,7 @@ func packMessageArena( /// One batch a test built over storage it owns. struct SynthesizedEventBatch { - let batch: mln_runtime_event_batch + let batch: mln_runtime_event_batch_view /// The event records, for a test that overwrites them after a decode. let records: UnsafeMutableRawBufferPointer /// The message arena, for the same reason. @@ -126,15 +148,14 @@ func withSynthesizedEventBatch( events: [mln_runtime_event], stride: Int = MemoryLayout.size, messages: [UInt8] = [], - remainingCount: Int = 0, payloadWindows: [Int: [UInt8]] = [:], _ body: (SynthesizedEventBatch) throws -> Result ) throws -> Result { let payloadOffset = try #require( MemoryLayout.offset(of: \.payload) ) - // The C API lends an array of event records, so the storage behind one has to - // carry the record alignment; a `UInt64` element buffer does. An empty + // The view borrows an array of event records, so the storage behind one has + // to carry the record alignment; a `UInt64` element buffer does. An empty // allocation has no base address, and the C API spells an empty array or // arena as a null pointer, so one spare element keeps both in one code path. var recordStorage = [UInt64]( @@ -157,7 +178,8 @@ func withSynthesizedEventBatch( } } - var batch = mln_runtime_event_batch_default() + var batch = mln_runtime_event_batch_view() + batch.size = UInt32(MemoryLayout.size) batch.event_size = UInt32(stride) batch.events = events.isEmpty ? nil @@ -169,7 +191,6 @@ func withSynthesizedEventBatch( : UnsafeRawPointer(#require(arena.baseAddress)) .assumingMemoryBound(to: CChar.self) batch.messages_size = messages.count - batch.remaining_count = remainingCount return try body(SynthesizedEventBatch( batch: batch, records: records, diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTests.swift index 626012581..c8288a850 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeEventTests.swift @@ -3,12 +3,15 @@ import Foundation @testable import MaplibreNativeFFI import Testing -private func makeRuntime() throws -> RuntimeHandle { +private func makeRuntime() async throws -> RuntimeHandle { try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) } -private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { - try MapHandle(runtime: runtime, options: MapOptions(width: 64, height: 64)) +private func makeMap(_ runtime: RuntimeHandle) async throws -> MapHandle { + try await MapHandle( + runtime: runtime, + options: MapOptions(width: 64, height: 64) + ) } // MARK: - Decoding a batch @@ -37,8 +40,7 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { let batch = try withSynthesizedEventBatch( events: events, stride: MemoryLayout.size + 16, - messages: arena.bytes, - remainingCount: 4 + messages: arena.bytes ) { synthesized in try NativeRuntimeEventBatch(copying: synthesized.batch) } @@ -47,7 +49,6 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { #expect(batch.events.map(\.sourceId) == [0x11, 0x22, 0x33]) #expect(batch.events.map(\.message) == ["first", "second", ""]) #expect(batch.events.map(\.code) == [0, -3, 0]) - #expect(batch.remainingCount == 4) } /// BND-083. An event type, source kind, and payload kind this version does not @@ -97,11 +98,11 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { /// BND-086. A map-sourced event names its map by the id the C API delivered, /// whether or not this process still holds a ``MapHandle`` for it, so a host /// keeps the identity it needs to route or forward the event. -@Test func aMapSourcedEventKeepsAnIdNoLiveMapHandleClaims() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +@Test func aMapSourcedEventKeepsAnIdNoLiveMapHandleClaims() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } let batch = try withSynthesizedEventBatch( events: [rawRuntimeEvent(type: 4, source: 0xDEAD_BEEF)] @@ -192,16 +193,20 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { #expect(native.pointee.event_mask == 0) } - try RuntimeOptions().nativeInput.withNativeOptions { native in + let wake = mln_wake() + try RuntimeOptions().nativeInput.withNativeOptions(eventWake: wake) { + native in #expect( native.pointee.event_mask == mln_runtime_options_default().event_mask ) + #expect(native.pointee.event_wake.callback == nil) } try RuntimeOptions(eventMask: .allRuntimeEvents).nativeInput - .withNativeOptions { native in + .withNativeOptions(eventWake: wake) { native in #expect( native.pointee.event_mask == RuntimeEventMask.allRuntimeEvents.rawValue ) + #expect(native.pointee.event_wake.callback == nil) } } @@ -209,71 +214,41 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { /// BND-090. One drain reports every event a style load produced, from the map /// that produced them. -@Test func oneDrainReportsEveryEventAStyleLoadProduced() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +@Test func oneDrainReportsEveryEventAStyleLoadProduced() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } _ = try runtime.drainEvents() - try map.setStyleJSON(emptyStyleJSON) - try runtime.pump() + try await map.setStyleJSON(emptyStyleJSON) + try await runtime.barrier() let batch = try runtime.drainEvents() #expect(batch.events.count > 1) - #expect(batch.remainingCount == 0) #expect(batch.events.map(\.type).contains(.mapStyleLoaded)) #expect(batch.events.allSatisfy { map.isSource(of: $0) }) } -/// A bounded drain reports what stayed queued, so a host that takes a slice per -/// iteration learns to come back, and reaches zero when it does. -@Test func aBoundedDrainReportsTheEventsItLeftQueued() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } - _ = try runtime.drainEvents() - - try map.setStyleJSON(emptyStyleJSON) - try runtime.pump() - let bounded = try runtime.drainEvents(maxEvents: 1) - - #expect(bounded.events.count == 1) - #expect(bounded.remainingCount > 0) - - var drained = bounded.events.count - var remaining = bounded.remainingCount - while remaining > 0 { - let batch = try runtime.drainEvents(maxEvents: 2) - #expect(batch.events.count <= 2) - #expect(!batch.events.isEmpty) - drained += batch.events.count - remaining = batch.remainingCount - } - #expect(drained > 1) - #expect(try runtime.drainEvents().events.isEmpty) -} - /// BND-082, BND-092. An event copied out of a batch keeps its message and /// payload after the drain that ends the batch's window. -@Test func anEventTakenOutOfABatchOutlivesTheNextDrain() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +@Test func anEventTakenOutOfABatchOutlivesTheNextDrain() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } _ = try runtime.drainEvents() - try? map.setStyleJSON(Data(#"{"version":8,"#.utf8)) - try runtime.pump() + _ = try? await map.setStyleJSON(Data(#"{"version":8,"#.utf8)) + try await runtime.barrier() let failure = try #require( try runtime.drainEvents().events.first { $0.type == .mapLoadingFailed } ) #expect(!failure.message.isEmpty) let copied = failure - try map.setStyleJSON(emptyStyleJSON) - try runtime.pump() + try await map.setStyleJSON(emptyStyleJSON) + try await runtime.barrier() let second = try runtime.drainEvents() #expect(second.events.map(\.type).contains(.mapStyleLoaded)) @@ -285,19 +260,19 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { /// event type and deliver each type the test drives, while a bit outside `all` /// names no event type and is rejected rather than silently kept. @Test func theDefaultMaskSelectsEveryEventTypeAndAnUnknownBitIsRejected( -) throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +) async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } #expect(try runtime.eventMask == .all) #expect(try map.eventMask == .all) _ = try runtime.drainEvents() - try map.setStyleJSON(emptyStyleJSON) - try map.jump(to: CameraOptions(zoom: 4)) - try runtime.pump() + try await map.setStyleJSON(emptyStyleJSON) + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions(zoom: 4))) + try await runtime.barrier() let types = try Set(runtime.drainEvents().events.map(\.type)) #expect(types.contains(.mapStyleLoaded)) @@ -305,56 +280,65 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { #expect(types.contains(.mapRenderUpdateAvailable)) let unnamed = RuntimeEventMask(rawValue: 1 << 63) - for setMask in [runtime.setEventMask, map.setEventMask] { - do { - try setMask(unnamed) - Issue.record("a mask bit outside all should be rejected") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - } + do { + try runtime.setEventMask(unnamed) + Issue.record("a runtime mask bit outside all should be rejected") + } catch let error as MaplibreError { + #expect(error.kind == .invalidArgument) } do { - _ = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64, eventMask: unnamed) - ) + _ = try await map.setEventMask(unnamed) + Issue.record("a map mask bit outside all should be rejected") + } catch let error as MaplibreError { + #expect(error.kind == .invalidArgument) + } + do { + _ = try await MapHandle(runtime: runtime, + options: MapOptions( + width: 64, + height: 64, + eventMask: unnamed + )) Issue.record("a mask bit outside all should be rejected on creation") } catch let error as MaplibreError { #expect(error.kind == .invalidArgument) } } -/// Narrowing a mask gates later events and keeps queued ones, so an event the -/// host caused before the change still reaches a drain that runs after it. -@Test func aQueuedEventSurvivesANarrowedMask() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +/// Once an event is queued, a later mask command does not remove it. +@Test func aQueuedEventSurvivesANarrowedMask() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } _ = try runtime.drainEvents() - try map.setStyleJSON(emptyStyleJSON) - try map.setEventMask(RuntimeEventMask.all.subtracting(.mapStyleLoaded)) - + _ = try await map.setStyleJSON(emptyStyleJSON) + try await runtime.barrier() + _ = try await map + .setEventMask(RuntimeEventMask.all.subtracting(.mapStyleLoaded)) + try await runtime.barrier() #expect(try runtime.drainEvents().events.map(\.type) .contains(.mapStyleLoaded)) } /// A read-modify-write cycle keeps every bit it did not touch, on both handles. -@Test func anEventMaskRoundTripsThroughBothHandles() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } +@Test func anEventMaskRoundTripsThroughBothHandles() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } try runtime.setEventMask(.all) - try map.setEventMask(.all) + _ = try await map.setEventMask(.all) + try await runtime.barrier() #expect(try runtime.eventMask == .all) #expect(try map.eventMask == .all) var mapMask = try map.eventMask mapMask.remove(.mapRenderUpdateAvailable) - try map.setEventMask(mapMask) + _ = try await map.setEventMask(mapMask) + try await runtime.barrier() let readBackMapMask = try map.eventMask #expect(readBackMapMask == mapMask) #expect(readBackMapMask.contains(.mapStyleLoaded)) @@ -365,34 +349,20 @@ private func makeMap(_ runtime: RuntimeHandle) throws -> MapHandle { try runtime.setEventMask(runtimeMask) let readBackRuntimeMask = try runtime.eventMask #expect(readBackRuntimeMask == runtimeMask) - #expect(readBackRuntimeMask.contains(.offlineOperationCompleted)) #expect(!readBackRuntimeMask.contains(.offlineRegionStatusChanged)) } -/// The drain and both mask setters are owner-thread calls, and the C API says -/// so rather than reading queue state from the wrong thread. -@Test func drainingAndMaskingFromAnotherThreadReportWrongThread() throws { - let runtime = try makeRuntime() - defer { try? runtime.close() } - let map = try makeMap(runtime) - defer { try? map.close() } - - let liveRuntime = try runtime.requireLiveHandle() - let liveMap = try map.requireLiveHandle() - let failures = [ - failureFromAnotherThread { - _ = try NativeRuntime.drainEvents(liveRuntime, maxEvents: 0) - }, - failureFromAnotherThread { - try NativeRuntime.setEventMask(liveRuntime, mask: 0) - }, - failureFromAnotherThread { - try NativeMap.setEventMask(liveMap, mask: 0) - }, - ] +/// Runtime and map operations remain valid after Swift resumes on another task. +@Test func runtimeAndMapAreUsableAfterTaskResumption() async throws { + let runtime = try await makeRuntime() + defer { try? runtime.closeBlockingForTests() } + let map = try await makeMap(runtime) + defer { try? map.closeBlockingForTests() } - for failure in failures { - let reported = try #require(failure) - #expect(reported.rawStatus == MLN_STATUS_WRONG_THREAD.rawValue) - } + await Task.yield() + try runtime.setEventMask(.all) + try await map.setEventMask(.all) + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions(zoom: 3))) + let camera = try await map.queryCamera() + #expect(camera.camera.zoom == 3) } diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeTests.swift index 08ab4598a..796a007c4 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/RuntimeTests.swift @@ -65,65 +65,69 @@ private final class ResourceHandleStateCapture: @unchecked Sendable { } } -@Test func runtimeCreateRunDrainAndClose() throws { +@Test func runtimeCreateRunDrainAndClose() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - try runtime.pump() + try await runtime.barrier() _ = try runtime.drainEvents() - try runtime.close() + try await runtime.close() #expect(runtime.isClosed) } -@Test func runtimeResourceTransformCanInstallAndClear() throws { +@Test func runtimeResourceTransformCanInstallAndClear() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } + defer { try? runtime.closeBlockingForTests() } - try runtime.setResourceTransform { request in + try await runtime.setResourceTransform { request in request.url.replacingOccurrences( of: "example.test", with: "example.invalid" ) } - try runtime.clearResourceTransform() + try await runtime.clearResourceTransform() } -/// Requests a style URL whose scheme no file source serves, pumps until the +/// Requests a style URL whose scheme no file source serves, drains until the /// matching loading failure arrives, and returns its message. The failure /// proves the request reached the network file source, where the /// runtime-scoped resource provider applies. +private func waitForSemaphore( + _ semaphore: DispatchSemaphore, + timeout: DispatchTime +) -> DispatchTimeoutResult { + semaphore.wait(timeout: timeout) +} + private func loadProbeStyle( runtime: RuntimeHandle, map: MapHandle, styleURL: String -) throws -> String? { - try map.setStyleURL(styleURL) - return try pumpUntilEvent( +) async throws -> String? { + _ = try await map.setStyleURL(styleURL) + return try await drainUntilEvent( runtime, waitingFor: "a loading failure for \(styleURL)" - ) { event in - event.type == .mapLoadingFailed && event.message.contains(styleURL) - }?.message + ) { $0.type == .mapLoadingFailed }?.message } -@Test func runtimeResourceProviderIsConsultedUntilReplacedAndCleared() throws { +@Test func runtimeResourceProviderIsConsultedUntilReplacedAndCleared( +) async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } let firstCalls = ResourceProviderCallCounter() - try runtime.setResourceProvider { _, _ in + try await runtime.setResourceProvider { _, _ in firstCalls.recordCall() return .passThrough } - let firstFailure = try loadProbeStyle( + let firstFailure = try await loadProbeStyle( runtime: runtime, map: map, styleURL: "jar:file:/packaged/first.json" @@ -133,13 +137,13 @@ private func loadProbeStyle( // The previous provider stops being consulted once the call returns. let secondCalls = ResourceProviderCallCounter() - try runtime.setResourceProvider { _, _ in + try await runtime.setResourceProvider { _, _ in secondCalls.recordCall() return .passThrough } let firstCallsAfterReplace = firstCalls.callCount - let secondFailure = try loadProbeStyle( + let secondFailure = try await loadProbeStyle( runtime: runtime, map: map, styleURL: "jar:file:/packaged/second.json" @@ -148,10 +152,10 @@ private func loadProbeStyle( #expect(secondCalls.callCount > 0) #expect(firstCalls.callCount == firstCallsAfterReplace) - try runtime.clearResourceProvider() + try await runtime.clearResourceProvider() let secondCallsAfterClear = secondCalls.callCount - let clearedFailure = try loadProbeStyle( + let clearedFailure = try await loadProbeStyle( runtime: runtime, map: map, styleURL: "jar:file:/packaged/third.json" @@ -161,7 +165,7 @@ private func loadProbeStyle( #expect(secondCalls.callCount == secondCallsAfterClear) // Clearing an already cleared provider stays a successful no-op. - try runtime.clearResourceProvider() + try await runtime.clearResourceProvider() } private let providerStyleJSON = #"{"version":8,"sources":{},"layers":[]}"# @@ -182,13 +186,13 @@ private final class ResolvedURLCapture: @unchecked Sendable { /// BND-155: the default tile server's `maplibre:` scheme alias reaches the /// provider as the alias, alongside the URL the built-in network path would /// have fetched. -@Test func resourceProviderSeesSchemeAliasAndItsResolvedURL() throws { +@Test func resourceProviderSeesSchemeAliasAndItsResolvedURL() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } + defer { try? runtime.closeBlockingForTests() } let resolved = ResolvedURLCapture() - try runtime.setResourceProvider { request, handle in + try await runtime.setResourceProvider { request, handle in guard request.requestedUrl == "maplibre://maps/style" else { return .passThrough } @@ -200,14 +204,12 @@ private final class ResolvedURLCapture: @unchecked Sendable { return .handle } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } - try map.setStyleURL("maplibre://maps/style") - let loaded = try pumpUntilEvent( + try await map.setStyleURL("maplibre://maps/style") + let loaded = try await drainUntilEvent( runtime, waitingFor: "the provider-served style to load" ) { $0.type == .mapStyleLoaded } @@ -341,7 +343,7 @@ private final class ResolvedURLCapture: @unchecked Sendable { #expect(counters.snapshot().release == 1) } -@Test func resourceRequestReleaseWaitsForCancellationCheck() throws { +@Test func resourceRequestReleaseWaitsForCancellationCheck() async throws { let counters = ResourceCounters() let cancellationStarted = DispatchSemaphore(value: 0) let allowCancellationReturn = DispatchSemaphore(value: 0) @@ -371,21 +373,30 @@ private final class ResolvedURLCapture: @unchecked Sendable { cancellationFinished.signal() }.start() - #expect(cancellationStarted.wait(timeout: .now() + .seconds(5)) == .success) + #expect(await Task.detached { + waitForSemaphore(cancellationStarted, timeout: .now() + .seconds(5)) + }.value == .success) Thread { releaseStarted.signal() state.release() releaseFinished.signal() }.start() - #expect(releaseStarted.wait(timeout: .now() + .seconds(5)) == .success) - #expect(releaseFinished - .wait(timeout: .now() + .milliseconds(100)) == .timedOut) + #expect(await Task.detached { + waitForSemaphore(releaseStarted, timeout: .now() + .seconds(5)) + }.value == .success) + #expect(await Task.detached { + waitForSemaphore(releaseFinished, timeout: .now() + .milliseconds(100)) + }.value == .timedOut) #expect(counters.snapshot().release == 0) allowCancellationReturn.signal() - #expect(cancellationFinished.wait(timeout: .now() + .seconds(5)) == .success) - #expect(releaseFinished.wait(timeout: .now() + .seconds(5)) == .success) + #expect(await Task.detached { + waitForSemaphore(cancellationFinished, timeout: .now() + .seconds(5)) + }.value == .success) + #expect(await Task.detached { + waitForSemaphore(releaseFinished, timeout: .now() + .seconds(5)) + }.value == .success) switch cancellationResult.load() { case let .success(isCancelled): diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/StyleTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/StyleTests.swift index 7dc236a55..2db383f48 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/StyleTests.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/StyleTests.swift @@ -73,22 +73,21 @@ import Testing #expect(publicInfo.rasterEncoding?.rawValue == 703) } -@Test func sourceInspectionCopiesReconstructibleMetadata() throws { +@Test func sourceInspectionCopiesReconstructibleMetadata() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(jsonData(#"{"version":8,"sources":{},"layers":[]}"#)) + try await map + .setStyleJSON(jsonData(#"{"version":8,"sources":{},"layers":[]}"#)) let bounds = LatLngBounds( southwest: LatLng(latitude: -1, longitude: -2), northeast: LatLng(latitude: 3, longitude: 4) ) - try map.addVectorSourceTiles( + try await map.addVectorSourceTiles( sourceId: "inline", tiles: [ "https://a.example/{z}/{x}/{y}.mvt", @@ -104,7 +103,7 @@ import Testing vectorEncoding: .mlt ) ) - try map.addVectorSourceURL( + try await map.addVectorSourceURL( sourceId: "remote", url: "https://example.com/source.json" ) @@ -112,9 +111,9 @@ import Testing data: jsonData(#"{"type":"FeatureCollection","features":[]}"#) ) defer { try? emptyCollection.close() } - try map.addGeoJSONSourceData(sourceId: "data", data: emptyCollection) + try await map.addGeoJSONSourceData(sourceId: "data", data: emptyCollection) - let inline = try #require(try map.styleSourceInfo("inline")) + let inline = try #require(try await map.styleSourceInfo("inline")) #expect(inline.type == .vector) #expect(inline.url == nil) #expect(inline.attribution == "© inline") @@ -132,21 +131,25 @@ import Testing #expect(inline.vectorEncoding == .mlt) #expect(inline.rasterEncoding == nil) - let remote = try #require(try map.styleSourceInfo("remote")) + let remote = try #require(try await map.styleSourceInfo("remote")) #expect(remote.url == "https://example.com/source.json") #expect(remote.tileJSON == nil) #expect(remote.attribution == nil) - let data = try #require(try map.styleSourceInfo("data")) + let data = try #require(try await map.styleSourceInfo("data")) #expect(data.url == nil) #expect(data.tileJSON == nil) #expect(data.tileSize == nil) #expect(data.vectorEncoding == nil) #expect(data.rasterEncoding == nil) - #expect(try map.styleSourceInfo("missing") == nil) + #expect(try await map.styleSourceInfo("missing") == nil) - #expect(try map.removeStyleSource("inline")) - try map.close() + let removeCommand = try await map.removeStyleSource("inline") + #expect(try await commandDisposition( + removeCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + #expect(try await map.styleSourceInfo("inline") == nil) + try await map.close() // Every nested string and value remains valid after its native source and // owning map are gone. @@ -183,30 +186,6 @@ import Testing } } -@Test func imageSourceCoordinatesRejectInvalidCountBeforeCallingC() throws { - let map = SyntheticHandles.map() - let sourceId = mln_buffer_view() - let coordinate = NativeLatLng(latitude: 1, longitude: 2) - - do { - try NativeStyle.addImageSourceURL( - map, - sourceId: sourceId, - coordinates: [coordinate], - url: mln_buffer_view() - ) - Issue.record("invalid coordinate count should throw") - } catch let failure as NativeStatusFailure { - #expect(!failure.isNativeStatus) - #expect(failure.rawStatus == MLN_STATUS_INVALID_ARGUMENT.rawValue) - #expect(failure - .diagnostic == - "image source coordinates must contain exactly 4 coordinates") - } catch { - Issue.record("unexpected error: \(error)") - } -} - @Test func customGeometryOptionsRetainAndInvokeTileCallbacks() throws { final class TileBox: @unchecked Sendable { var fetched: [NativeCanonicalTileID] = [] @@ -256,11 +235,18 @@ import Testing #expect(box.cancelled == [NativeCanonicalTileID(z: 4, x: 5, y: 6)]) } -/// The C API calls the release callback on the map owner thread while a tile -/// callback can still be running on a worker thread, so the release waits for -/// that call rather than freeing the state under it. +/// The C API can call the release callback while a tile callback is still +/// running on another thread, so the release waits for that call rather than +/// freeing the state under it. +private func waitForSemaphore( + _ semaphore: DispatchSemaphore, + timeout: DispatchTime +) -> DispatchTimeoutResult { + semaphore.wait(timeout: timeout) +} + @Test func customGeometryCallbacksWaitForInFlightInvocationBeforeRelease( -) throws { +) async throws { // The C ABI callbacks an options struct carries, held past the struct's // lifetime so a test calls them the way the C API does. final class NativeCallbacks: @unchecked Sendable { @@ -274,7 +260,7 @@ import Testing fetchTile = try #require(options.pointee.fetch_tile) releaseUserData = try #require(options.pointee.release_user_data) userDataAddress = - try UInt(bitPattern: #require(options.pointee.user_data)) + UInt(bitPattern: options.pointee.user_data) } private var userData: UnsafeMutableRawPointer? { @@ -307,20 +293,29 @@ import Testing native.fetch() invocationFinished.signal() }.start() - #expect(entered.wait(timeout: .now() + .seconds(5)) == .success) + #expect(await Task.detached { + waitForSemaphore(entered, timeout: .now() + .seconds(5)) + }.value == .success) Thread { releaseStarted.signal() native.release() releaseFinished.signal() }.start() - #expect(releaseStarted.wait(timeout: .now() + .seconds(5)) == .success) - #expect(releaseFinished - .wait(timeout: .now() + .milliseconds(100)) == .timedOut) + #expect(await Task.detached { + waitForSemaphore(releaseStarted, timeout: .now() + .seconds(5)) + }.value == .success) + #expect(await Task.detached { + waitForSemaphore(releaseFinished, timeout: .now() + .milliseconds(100)) + }.value == .timedOut) allowReturn.signal() - #expect(invocationFinished.wait(timeout: .now() + .seconds(5)) == .success) - #expect(releaseFinished.wait(timeout: .now() + .seconds(5)) == .success) + #expect(await Task.detached { + waitForSemaphore(invocationFinished, timeout: .now() + .seconds(5)) + }.value == .success) + #expect(await Task.detached { + waitForSemaphore(releaseFinished, timeout: .now() + .seconds(5)) + }.value == .success) } /// Counts the custom geometry source callback states the C API has released. @@ -357,9 +352,9 @@ private func addSourceReportingItsRelease( to map: MapHandle, sourceId: String = "custom", counter: ReleaseCounter -) throws { +) async throws -> CommandCompletion { let sentinel = ReleaseSentinel(counter) - try map.addCustomGeometrySource( + return try await map.addCustomGeometrySource( sourceId: sourceId, options: CustomGeometrySourceOptions(fetchTile: { _ in withExtendedLifetime(sentinel) {} @@ -370,11 +365,11 @@ private func addSourceReportingItsRelease( /// A style load drops the sources the previous style held, and the C API /// releases their callback state without a map-style-loaded event, so a host /// that never selected that event type still gets its state freed. -@Test func aStyleLoadReleasesADroppedCustomGeometrySource() throws { +@Test func aStyleLoadReleasesADroppedCustomGeometrySource() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - try runtime.setResourceProvider { request, handle in + defer { try? runtime.closeBlockingForTests() } + try await runtime.setResourceProvider { request, handle in guard request.requestedUrl == "maplibre://maps/replacement" else { return .passThrough } @@ -382,70 +377,71 @@ private func addSourceReportingItsRelease( return .handle } let narrowed = RuntimeEventMask.all.subtracting(.mapStyleLoaded) - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64, eventMask: narrowed) - ) - defer { try? map.close() } - - try map.setStyleJSON(emptyStyleJSON) + let map = try await MapHandle(runtime: runtime, + options: MapOptions( + width: 64, + height: 64, + eventMask: narrowed + )) + defer { try? map.closeBlockingForTests() } + + try await map.setStyleJSON(emptyStyleJSON) let counter = ReleaseCounter() - try addSourceReportingItsRelease(to: map, counter: counter) + _ = try await addSourceReportingItsRelease(to: map, counter: counter) #expect(counter.value == 0) - try map.setStyleURL("maplibre://maps/replacement") + try await map.setStyleURL("maplibre://maps/replacement") var styleLoadedReported = false let deadline = Date().addingTimeInterval(10) while Date() < deadline, counter.value == 0 { - try runtime.pump() + try await runtime.barrier() styleLoadedReported = try styleLoadedReported || runtime.drainEvents() .events.contains { $0.type == .mapStyleLoaded } - Thread.sleep(forTimeInterval: 0.001) + try await Task.sleep(nanoseconds: 1_000_000) } #expect(counter.value == 1) #expect(!styleLoadedReported) #expect(try map.eventMask == narrowed) - #expect(try !map.styleSourceExists("custom")) + #expect(try await map.styleSourceInfo("custom") == nil) } /// Removing a custom geometry source releases its callback state, and does so /// once. -@Test func removingACustomGeometrySourceReleasesItsCallbacks() throws { +@Test func removingACustomGeometrySourceReleasesItsCallbacks() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(emptyStyleJSON) + try await map.setStyleJSON(emptyStyleJSON) let counter = ReleaseCounter() - try addSourceReportingItsRelease(to: map, counter: counter) + _ = try await addSourceReportingItsRelease(to: map, counter: counter) - #expect(try map.removeStyleSource("custom")) + let removeCommand = try await map.removeStyleSource("custom") + #expect(try await commandDisposition( + removeCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) #expect(counter.value == 1) - try map.close() + try await map.close() #expect(counter.value == 1) } /// Destroying a map releases the callback state of the sources it still holds. -@Test func closingAMapReleasesItsCustomGeometrySources() throws { +@Test func closingAMapReleasesItsCustomGeometrySources() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(emptyStyleJSON) + try await map.setStyleJSON(emptyStyleJSON) let counter = ReleaseCounter() for sourceId in ["first", "second"] { - try addSourceReportingItsRelease( + _ = try await addSourceReportingItsRelease( to: map, sourceId: sourceId, counter: counter @@ -453,43 +449,53 @@ private func addSourceReportingItsRelease( } #expect(counter.value == 0) - try map.close() + try await map.close() #expect(counter.value == 2) } -/// The C API never releases the callback state of an add it rejected, so this -/// binding frees it there itself. -@Test func aRejectedCustomGeometrySourceAddReleasesItsCallbacks() throws { +/// Accepted commands own callback state. A command rejected by the map worker +/// releases that state before a subsequent runtime barrier completes. +@Test func aRejectedCustomGeometrySourceAddReleasesItsCallbacks() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } - - try map.setStyleJSON(emptyStyleJSON) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } + + let styleCommand = try await map.setStyleJSON(emptyStyleJSON) + #expect(try await commandDisposition( + styleCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) let accepted = ReleaseCounter() - try addSourceReportingItsRelease(to: map, counter: accepted) + let acceptedCommand = try await addSourceReportingItsRelease( + to: map, counter: accepted + ) + #expect(try await commandDisposition( + acceptedCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) let rejected = ReleaseCounter() - #expect(throws: MaplibreError.self) { - try addSourceReportingItsRelease(to: map, counter: rejected) - } + try expectCommandFailure( + await addSourceReportingItsRelease(to: map, counter: rejected), + status: MLN_STATUS_INVALID_ARGUMENT + ) + try await runtime.barrier() #expect(rejected.value == 1) #expect(accepted.value == 0) } +/// Adds a custom MVT vector source whose callback state reports its own +/// release through `counter`. private func addMvtSourceReportingItsRelease( to map: MapHandle, sourceId: String = "custom-mvt", counter: ReleaseCounter -) throws { +) async throws -> CommandCompletion { let sentinel = ReleaseSentinel(counter) - try map.addCustomMvtVectorSource( + return try await map.addCustomMvtVectorSource( sourceId: sourceId, options: CustomMvtVectorSourceOptions(fetchTile: { _ in withExtendedLifetime(sentinel) {} @@ -535,104 +541,113 @@ private func addMvtSourceReportingItsRelease( #expect(box.cancelled == [NativeCanonicalTileID(z: 4, x: 5, y: 6)]) } -@Test func customMvtVectorSourcesCanBeAddedInspectedAndReleased() throws { +@Test func customMvtVectorSourcesCanBeAddedInspectedAndReleased( +) async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(emptyStyleJSON) + try await map.setStyleJSON(emptyStyleJSON) let counter = ReleaseCounter() - try addMvtSourceReportingItsRelease(to: map, counter: counter) - #expect(try map.styleSourceExists("custom-mvt")) - #expect(try map.styleSourceType("custom-mvt") == .customMVTVector) + let addCommand = try await addMvtSourceReportingItsRelease( + to: map, counter: counter + ) + #expect(try await commandDisposition( + addCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + #expect(try await map.styleSourceInfo("custom-mvt")? + .type == .customMVTVector) let tileId = CanonicalTileID(z: 0, x: 0, y: 0) - try map.setCustomMvtVectorSourceTileData( + try await map.setCustomMvtVectorSourceTileData( sourceId: "custom-mvt", tileId: tileId, data: Data() ) - try map.setCustomMvtVectorSourceTileError( + try await map.setCustomMvtVectorSourceTileError( sourceId: "custom-mvt", tileId: tileId, message: "tile missing" ) - try map.invalidateCustomMvtVectorSourceTile( + try await map.invalidateCustomMvtVectorSourceTile( sourceId: "custom-mvt", tileId: tileId ) - #expect(try map.removeStyleSource("custom-mvt")) + let removeCommand = try await map.removeStyleSource("custom-mvt") + #expect(try await commandDisposition( + removeCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) #expect(counter.value == 1) } -@Test func aRejectedCustomMvtVectorSourceAddReleasesItsCallbacks() throws { +@Test func aRejectedCustomMvtVectorSourceAddReleasesItsCallbacks( +) async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 64, height: 64) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 64, height: 64)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(emptyStyleJSON) + try await map.setStyleJSON(emptyStyleJSON) let accepted = ReleaseCounter() - try addMvtSourceReportingItsRelease(to: map, counter: accepted) + let acceptedCommand = try await addMvtSourceReportingItsRelease( + to: map, counter: accepted + ) + #expect(try await commandDisposition( + acceptedCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) let rejected = ReleaseCounter() - #expect(throws: MaplibreError.self) { - try addMvtSourceReportingItsRelease(to: map, counter: rejected) - } + try expectCommandFailure( + await addMvtSourceReportingItsRelease(to: map, counter: rejected), + status: MLN_STATUS_INVALID_ARGUMENT + ) + try await runtime.barrier() #expect(rejected.value == 1) #expect(accepted.value == 0) } -@Test func loadedStyleDocumentAndURLReadBackWhatWasLoaded() throws { +@Test func loadedStyleDocumentAndURLReadBackWhatWasLoaded() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } // Nothing parsed and nothing requested yet. - #expect(try map.loadedStyleJSON() == Data()) - #expect(try map.styleURL() == "") + #expect(try await map.loadedStyleJSON() == Data()) + #expect(try await map.styleURL() == "") // The document reads back byte-for-byte, so it can be reloaded unchanged. let styleJSON = jsonData(#"{"version":8,"sources":{},"layers":[]}"#) - try map.setStyleJSON(styleJSON) - #expect(try map.loadedStyleJSON() == styleJSON) + try await map.setStyleJSON(styleJSON) + #expect(try await map.loadedStyleJSON() == styleJSON) // Inline JSON clears the URL. - #expect(try map.styleURL() == "") + #expect(try await map.styleURL() == "") // The URL is request state, recorded before the load can succeed, while the // document still reports the style that last parsed. - try map.setStyleURL("https://example.com/style.json") - #expect(try map.styleURL() == "https://example.com/style.json") - #expect(try map.loadedStyleJSON() == styleJSON) + try await map.setStyleURL("https://example.com/style.json") + #expect(try await map.styleURL() == "https://example.com/style.json") + #expect(try await map.loadedStyleJSON() == styleJSON) } -@Test func closedMapRejectsStyleCallsThroughSwiftHandleState() throws { +@Test func closedMapRejectsStyleCallsThroughSwiftHandleState() async throws { let runtime = try RuntimeHandle() - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - try map.close() + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + try await map.close() do { - _ = try map.styleLayerIds() + _ = try await map.styleLayerIds() Issue.record("closed map should throw") } catch let error as MaplibreError { #expect(error.kind == .invalidState) @@ -642,17 +657,15 @@ private func addMvtSourceReportingItsRelease( } } -@Test func ninePatchStyleImageRoundTripsStretchContentAndTextFit() throws { +@Test func ninePatchStyleImageRoundTripsStretchContentAndTextFit() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(jsonData(""" + try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) @@ -668,9 +681,9 @@ private func addMvtSourceReportingItsRelease( content: ImageContent(left: 0.5, top: 0.5, right: 1.5, bottom: 1.5), textFitHeight: .proportional ) - try map.setStyleImage(imageId: "patch", image: image, options: options) + try await map.setStyleImage(imageId: "patch", image: image, options: options) - let info = try #require(try map.styleImageInfo("patch")) + let info = try #require(try await map.styleImageInfo("patch")) #expect(info.stretchXCount == 1) #expect(info.stretchYCount == 2) #expect(info.content?.right == 1.5) @@ -678,71 +691,163 @@ private func addMvtSourceReportingItsRelease( #expect(info.textFitWidth == nil) #expect(info.textFitHeight == .proportional) - let stretches = try #require(try map.styleImageStretches("patch")) + let stretches = try #require(try await map.styleImageStretches("patch")) #expect(stretches.stretchX == [ImageStretch(from: 0, to: 1)]) #expect( stretches.stretchY == [ ImageStretch(from: 0, to: 1), ImageStretch(from: 1, to: 2), ] ) - #expect(try map.styleImageStretches("missing") == nil) + #expect(try await map.styleImageStretches("missing") == nil) // A backwards interval is rejected by C. - #expect(throws: MaplibreError.self) { - try map.setStyleImage( + do { + _ = try await map.setStyleImage( imageId: "bad", image: image, options: StyleImageOptions(stretchX: [ImageStretch(from: 2, to: 1)]) ) - } + Issue.record("a backwards stretch interval should fail") + } catch is MaplibreError {} } -@Test func layerBaseAccessorsRoundTripThroughNativeMap() throws { +@Test func layerBaseAccessorsRoundTripThroughNativeMap() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(jsonData(""" - {"version":8,"sources":{"geo":{"type":"geojson","data": {"type":"FeatureCollection","features":[]}}}, "layers":[{"id":"bg","type":"background"}, {"id":"fill","type":"fill","source":"geo"}]} + let styleCommand = try await map.setStyleJSON(jsonData(""" + {"version":8,"sources":{"geo":{"type":"geojson","data":{"type":"FeatureCollection","features":[]}}},"layers":[{"id":"bg","type":"background"},{"id":"fill","type":"fill","source":"geo"}]} """)) + #expect(try await commandDisposition( + styleCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) - #expect(try map.layerSourceLayer("fill") == "") - try map.setLayerSourceLayer(layerId: "fill", sourceLayer: "roads") - #expect(try map.layerSourceLayer("fill") == "roads") - #expect(try map.layerSourceId("fill") == "geo") + #expect(try await map.layerSourceLayer("fill") == "") + let sourceLayerCommand = try await map.setLayerSourceLayer( + layerId: "fill", sourceLayer: "roads" + ) + #expect(try await commandDisposition( + sourceLayerCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + #expect(try await map.layerSourceLayer("fill") == "roads") + #expect(try await map.layerSourceId("fill") == "geo") + + try expectCommandFailure( + await map.setLayerSourceLayer(layerId: "bg", sourceLayer: "roads"), + status: MLN_STATUS_INVALID_ARGUMENT + ) + #expect(try await map.layerSourceLayer("bg") == "") + #expect(try await map.layerSourceId("bg") == "") + + // The layer-info aggregate reports the unbounded zoom range, the layer + // type, the visibility, and the source strings its sizes describe. + let unbounded = try #require(try await map.styleLayerInfo("fill")) + #expect(unbounded.type == "fill") + #expect(unbounded.minZoom == -Double.infinity) + #expect(unbounded.maxZoom == Double.infinity) + #expect(unbounded.visibility == .visible) + #expect(unbounded.sourceId == "geo") + #expect(unbounded.sourceLayer == "roads") + + let background = try #require(try await map.styleLayerInfo("bg")) + #expect(background.type == "background") + #expect(background.sourceId == nil) + #expect(background.sourceLayer == nil) + + let minZoomCommand = try await map.setLayerMinZoom( + layerId: "fill", + minZoom: 4 + ) + #expect(try await commandDisposition( + minZoomCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + let maxZoomCommand = try await map.setLayerMaxZoom( + layerId: "fill", maxZoom: 12.5 + ) + #expect(try await commandDisposition( + maxZoomCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + let visibilityCommand = try await map.setLayerVisibility( + layerId: "fill", visibility: .none + ) + #expect(try await commandDisposition( + visibilityCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + let bounded = try #require(try await map.styleLayerInfo("fill")) + #expect(bounded.minZoom == 4) + #expect(bounded.maxZoom == 12.5) + #expect(bounded.visibility == .none) + + try expectCommandFailure( + await map.setLayerVisibility( + layerId: "fill", + visibility: StyleLayerVisibility(rawValue: 900) + ), + status: MLN_STATUS_INVALID_ARGUMENT + ) + #expect(try await map.styleLayerInfo("fill")? + .visibility == StyleLayerVisibility.none) - // A layer type that takes no source is rejected rather than silently ignored. - #expect(throws: MaplibreError.self) { - try map.setLayerSourceLayer(layerId: "bg", sourceLayer: "roads") - } - #expect(try map.layerSourceId("bg") == "") + #expect(try await map.styleLayerInfo("missing") == nil) +} - // An unset zoom range crosses the boundary as infinities. - #expect(try map.layerMinZoom("fill") == -Double.infinity) - #expect(try map.layerMaxZoom("fill") == Double.infinity) - try map.setLayerMinZoom(layerId: "fill", minZoom: 4) - try map.setLayerMaxZoom(layerId: "fill", maxZoom: 12.5) - #expect(try map.layerMinZoom("fill") == 4) - #expect(try map.layerMaxZoom("fill") == 12.5) +/// A removal command commits when the object existed, and fails with +/// `MLN_STATUS_NOT_FOUND` when nothing has the ID. The info getters' found +/// flag re-checks existence. +@Test func styleRemovalsCommitOrFailWithNotFound() async throws { + let runtime = + try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } - #expect(try map.layerVisibility("fill") == .visible) - try map.setLayerVisibility(layerId: "fill", visibility: .none) - #expect(try map.layerVisibility("fill") == StyleLayerVisibility.none) + let styleCommand = try await map.setStyleJSON(jsonData(""" + {"version":8,"sources":{"geo":{"type":"geojson","data":{"type":"FeatureCollection","features":[]}}},"layers":[{"id":"fill","type":"fill","source":"geo"}]} + """)) + #expect(try await commandDisposition( + styleCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + try await map.setStyleImage( + imageId: "marker", + image: StyleRGBA8Image(width: 1, height: 1, stride: 4, + pixels: [0, 0, 0, 0]) + ) - // An unknown raw visibility passes through to C, which rejects it. - #expect(throws: MaplibreError.self) { - try map.setLayerVisibility( - layerId: "fill", - visibility: StyleLayerVisibility(rawValue: 900) - ) + // A source still used by a layer fails with invalid-state. + try expectCommandFailure( + await map.removeStyleSource("geo"), + status: MLN_STATUS_INVALID_STATE + ) + #expect(try await map.styleSourceInfo("geo") != nil) + + // Existing objects commit their removal, re-checked through info getters. + // Each wait drains and discards unrelated events, so submit one at a time. + let removals: [(String, (MapHandle) async throws -> CommandCompletion)] = [ + ("layer", { try await $0.removeStyleLayer("fill") }), + ("source", { try await $0.removeStyleSource("geo") }), + ("image", { try await $0.removeStyleImage("marker") }), + ] + for (subject, remove) in removals { + let command = try await remove(map) + #expect(try await commandDisposition( + command, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue, "removing a \(subject)") } - #expect(throws: MaplibreError.self) { - _ = try map.layerMinZoom("missing") + #expect(try await map.styleLayerInfo("fill") == nil) + #expect(try await map.styleSourceInfo("geo") == nil) + #expect(try await map.styleImageInfo("marker") == nil) + + // Removing a missing object resolves with a failed NOT_FOUND completion. + for (_, remove) in removals { + try expectCommandFailure( + await remove(map), + status: MLN_STATUS_NOT_FOUND + ) } } @@ -836,19 +941,20 @@ private func addMvtSourceReportingItsRelease( /// One prepared handle installs on any number of sources, and the source /// adopts the options the data was prepared with. -@Test func preparedGeoJSONSourceDataAddsAndUpdatesAcrossSources() throws { +@Test func preparedGeoJSONSourceDataAddsAndUpdatesAcrossSources() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 512) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 512, height: 512)) + defer { try? map.closeBlockingForTests() } - try map.setStyleJSON(jsonData(""" + let styleCommand = try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) + #expect(try await commandDisposition( + styleCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) let clustered = try GeoJSONSourceDataHandle( data: nearbyPoints(), @@ -856,10 +962,24 @@ private func addMvtSourceReportingItsRelease( ) defer { try? clustered.close() } - try map.addGeoJSONSourceData(sourceId: "first", data: clustered) - try map.addGeoJSONSourceData(sourceId: "second", data: clustered) - #expect(try map.styleSourceType("first") == .geoJSON) - #expect(try map.styleSourceType("second") == .geoJSON) + let firstAdd = try await map.addGeoJSONSourceData( + sourceId: "first", + data: clustered + ) + #expect(try await commandDisposition( + firstAdd, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + let secondAdd = try await map.addGeoJSONSourceData( + sourceId: "second", + data: clustered + ) + #expect(try await commandDisposition( + secondAdd, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + #expect(try #require(try await map.styleSourceInfo("first")) + .type == .geoJSON) + #expect(try #require(try await map.styleSourceInfo("second")) + .type == .geoJSON) // A cheap install of already-prepared data updates both sources, because // the sources adopted the options the data was prepared with. @@ -868,11 +988,23 @@ private func addMvtSourceReportingItsRelease( options: clusterOptions() ) defer { try? replacement.close() } - try map.setGeoJSONSourceData(sourceId: "first", data: replacement) - try map.setGeoJSONSourceData(sourceId: "second", data: replacement) + let firstSet = try await map.setGeoJSONSourceData( + sourceId: "first", + data: replacement + ) + #expect(try await commandDisposition( + firstSet, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + let secondSet = try await map.setGeoJSONSourceData( + sourceId: "second", + data: replacement + ) + #expect(try await commandDisposition( + secondSet, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) // Cluster aggregations are part of the options-equality requirement, so - // data prepared with different cluster_properties is rejected. + // data prepared with different cluster_properties fails on the map thread. var reaggregated = clusterOptions() reaggregated.clusterProperties = jsonData( #"{"weight_max":["max",["get","weight"]]}"# @@ -882,26 +1014,25 @@ private func addMvtSourceReportingItsRelease( options: reaggregated ) defer { try? differentAggregation.close() } - do { - try map.setGeoJSONSourceData(sourceId: "first", data: differentAggregation) - Issue.record("different cluster aggregations should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - } + try expectCommandFailure( + await map.setGeoJSONSourceData( + sourceId: "first", + data: differentAggregation + ), + status: MLN_STATUS_INVALID_ARGUMENT + ) } /// A set rejects data whose baked-in options differ from the options the /// source was added with, because they would tile inconsistently. -@Test func setGeoJSONSourceDataRejectsMismatchedOptions() throws { +@Test func setGeoJSONSourceDataRejectsMismatchedOptions() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 512) - ) - defer { try? map.close() } - try map.setStyleJSON(jsonData(""" + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 512, height: 512)) + defer { try? map.closeBlockingForTests() } + try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) @@ -910,38 +1041,49 @@ private func addMvtSourceReportingItsRelease( options: clusterOptions() ) defer { try? clustered.close() } - try map.addGeoJSONSourceData(sourceId: "clustered", data: clustered) + let addCommand = try await map.addGeoJSONSourceData( + sourceId: "clustered", + data: clustered + ) + #expect(try await commandDisposition( + addCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) let unclustered = try GeoJSONSourceDataHandle(data: nearbyPoints()) defer { try? unclustered.close() } - do { - try map.setGeoJSONSourceData(sourceId: "clustered", data: unclustered) - Issue.record("mismatched options should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - } catch { - Issue.record("unexpected error: \(error)") - } + // The options mismatch is validated on the map thread, so the install is + // accepted here and fails asynchronously through command completion. + try expectCommandFailure( + await map.setGeoJSONSourceData( + sourceId: "clustered", + data: unclustered + ), + status: MLN_STATUS_INVALID_ARGUMENT + ) } /// Sources keep their own reference, so closing the handle never invalidates /// a source, while the closed handle itself stops installing. -@Test func closedGeoJSONSourceDataStopsInstallingButKeepsSources() throws { +@Test func closedGeoJSONSourceDataStopsInstallingButKeepsSources() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 512) - ) - defer { try? map.close() } - try map.setStyleJSON(jsonData(""" + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 512, height: 512)) + defer { try? map.closeBlockingForTests() } + try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) let prepared = try GeoJSONSourceDataHandle(data: nearbyPoints()) - try map.addGeoJSONSourceData(sourceId: "kept", data: prepared) + let addCommand = try await map.addGeoJSONSourceData( + sourceId: "kept", + data: prepared + ) + #expect(try await commandDisposition( + addCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) #expect(!prepared.isClosed) try prepared.close() @@ -950,12 +1092,12 @@ private func addMvtSourceReportingItsRelease( try prepared.close() // The source outlives the handle that seeded it. - #expect(try map.styleSourceExists("kept")) - #expect(try map.styleSourceType("kept") == .geoJSON) + #expect(try #require(try await map.styleSourceInfo("kept")) + .type == .geoJSON) // The closed handle fails in Swift handle state before reaching native. do { - try map.addGeoJSONSourceData(sourceId: "late", data: prepared) + try await map.addGeoJSONSourceData(sourceId: "late", data: prepared) Issue.record("closed prepared data should throw") } catch let error as MaplibreError { #expect(error.kind == .invalidState) @@ -963,78 +1105,94 @@ private func addMvtSourceReportingItsRelease( } catch { Issue.record("unexpected error: \(error)") } - #expect(try !map.styleSourceExists("late")) + #expect(try await map.styleSourceInfo("late") == nil) } /// The runtime override slices tiles inline while enabled and restores the /// source's own option when disabled. -@Test func synchronousTilingOverrideTogglesPerSource() throws { +@Test func synchronousTilingOverrideTogglesPerSource() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 512) - ) - defer { try? map.close() } - try map.setStyleJSON(jsonData(""" + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 512, height: 512)) + defer { try? map.closeBlockingForTests() } + try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) let prepared = try GeoJSONSourceDataHandle(data: nearbyPoints()) defer { try? prepared.close() } - try map.addGeoJSONSourceData(sourceId: "tracked", data: prepared) + let addCommand = try await map.addGeoJSONSourceData( + sourceId: "tracked", + data: prepared + ) + #expect(try await commandDisposition( + addCommand, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + + let enable = try await map.setGeoJSONSourceSynchronousTiling( + sourceId: "tracked", + enabled: true + ) + #expect(try await commandDisposition( + enable, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) - try map.setGeoJSONSourceSynchronousTiling(sourceId: "tracked", enabled: true) // Installs under the override still take prepared data. - try map.setGeoJSONSourceData(sourceId: "tracked", data: prepared) - try map.setGeoJSONSourceSynchronousTiling( + let install = try await map.setGeoJSONSourceData( sourceId: "tracked", - enabled: false + data: prepared ) + #expect(try await commandDisposition( + install, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) - // A source that does not exist is rejected. - do { - try map.setGeoJSONSourceSynchronousTiling( + let disable = try await map.setGeoJSONSourceSynchronousTiling( + sourceId: "tracked", + enabled: false + ) + #expect(try await commandDisposition( + disable, runtime: runtime + ) == MLN_COMMAND_DISPOSITION_COMMITTED.rawValue) + + // A source that does not exist fails on the map thread, asynchronously + // through command completion. + try expectCommandFailure( + await map.setGeoJSONSourceSynchronousTiling( sourceId: "missing", enabled: true - ) - Issue.record("missing source should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - } catch { - Issue.record("unexpected error: \(error)") - } + ), + status: MLN_STATUS_INVALID_ARGUMENT + ) } -@Test func styleTransitionOptionsRoundTripThroughTheCAPI() throws { +@Test func styleTransitionOptionsRoundTripThroughTheCAPI() async throws { let runtime = try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - defer { try? runtime.close() } - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 1, height: 1) - ) - defer { try? map.close() } + defer { try? runtime.closeBlockingForTests() } + let map = try await MapHandle(runtime: runtime, + options: MapOptions(width: 1, height: 1)) + defer { try? map.closeBlockingForTests() } // A map with no style yet reports no duration or delay. The placement flag // always reports, because MapLibre Native always holds a value for it. - let empty = try map.styleTransitionOptions() + let empty = try await map.styleTransitionOptions() #expect(empty.durationMilliseconds == nil) #expect(empty.delayMilliseconds == nil) #expect(empty.enablePlacementTransitions == true) // The style parser fills in its own 300ms duration for a style that declares // no transition. - try map.setStyleJSON(jsonData(""" + try await map.setStyleJSON(jsonData(""" {"version":8,"sources":{},"layers":[]} """)) - let parsed = try map.styleTransitionOptions() + let parsed = try await map.styleTransitionOptions() #expect(parsed.durationMilliseconds == 300) #expect(parsed.delayMilliseconds == nil) - try map.setStyleJSON(jsonData(transitionStyleJSON)) - let declared = try map.styleTransitionOptions() + try await map.setStyleJSON(jsonData(transitionStyleJSON)) + let declared = try await map.styleTransitionOptions() #expect(declared.durationMilliseconds == 750) #expect(declared.delayMilliseconds == 100) #expect(declared.enablePlacementTransitions == true) @@ -1045,29 +1203,27 @@ private func addMvtSourceReportingItsRelease( durationMilliseconds: 0, enablePlacementTransitions: false ) - try map.setStyleTransitionOptions(options) - #expect(try map.styleTransitionOptions() == options) + try await map.setStyleTransitionOptions(options) + #expect(try await map.styleTransitionOptions() == options) // Omitting the flag leaves the cross-fade on rather than clearing it. - try map.setStyleTransitionOptions( + try await map.setStyleTransitionOptions( StyleTransitionOptions(durationMilliseconds: 250) ) - #expect(try map.styleTransitionOptions().enablePlacementTransitions == true) + #expect(try await map.styleTransitionOptions() + .enablePlacementTransitions == true) // Loading a style replaces the override with what that style declares. - try map.setStyleJSON(jsonData(transitionStyleJSON)) - #expect(try map.styleTransitionOptions() == declared) + try await map.setStyleJSON(jsonData(transitionStyleJSON)) + #expect(try await map.styleTransitionOptions() == declared) - do { - try map.setStyleTransitionOptions( + try expectCommandFailure( + await map.setStyleTransitionOptions( StyleTransitionOptions(delayMilliseconds: -1) - ) - Issue.record("a negative delay should throw") - } catch let error as MaplibreError { - #expect(error.kind == .invalidArgument) - } catch { - Issue.record("unexpected error: \(error)") - } + ), + status: MLN_STATUS_INVALID_ARGUMENT + ) + #expect(try await map.styleTransitionOptions() == declared) } private let transitionStyleJSON = """ diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/SyntheticHandles.swift b/bindings/swift/Tests/MaplibreNativeFFITests/SyntheticHandles.swift index a57387399..534f2422a 100644 --- a/bindings/swift/Tests/MaplibreNativeFFITests/SyntheticHandles.swift +++ b/bindings/swift/Tests/MaplibreNativeFFITests/SyntheticHandles.swift @@ -24,10 +24,6 @@ enum SyntheticHandles { NativeRenderSessionHandle(raw: kind(0x04) | ordinal) } - static func wakeSource(_ ordinal: UInt64 = 1) -> NativeWakeSourceHandle { - NativeWakeSourceHandle(raw: kind(0x0B) | ordinal) - } - static func resourceRequest(_ ordinal: UInt64 = 1) -> NativeResourceRequestHandle { diff --git a/bindings/swift/Tests/MaplibreNativeFFITests/WakeSourceTests.swift b/bindings/swift/Tests/MaplibreNativeFFITests/WakeSourceTests.swift deleted file mode 100644 index fd21aab88..000000000 --- a/bindings/swift/Tests/MaplibreNativeFFITests/WakeSourceTests.swift +++ /dev/null @@ -1,160 +0,0 @@ -import Dispatch -import Foundation -@testable import MaplibreNativeFFI -import Testing - -private let parkTimeout: TimeInterval = 10 - -/// Well below parkTimeout, and far above the scheduling noise a loaded CI -/// machine adds to a condition-variable wake. -private let promptReturn: TimeInterval = 5 - -@Test func parkedOwnerThreadWakesForNativeWorkAndForAWakeSource() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - let map = try MapHandle( - runtime: runtime, - options: MapOptions(width: 512, height: 512) - ) - try pumpUntilQuiet(runtime) - - // The style is malformed, so native reports the failure from its own threads - // and it reaches the parked owner thread. - try map.setStyleURL("unsupported://style.json") - var loadingFailed = false - let loadStarted = Date() - for _ in 0 ..< 20 where !loadingFailed { - try runtime.pump(timeout: parkTimeout) - #expect( - Date().timeIntervalSince(loadStarted) < promptReturn, - "parks sat out their timeouts while loading was pending" - ) - for event in try runtime.drainEvents().events - where event.type == .mapLoadingFailed - { - loadingFailed = true - } - } - #expect(loadingFailed) - - // The park a cross-thread signal releases has no other work to end it. - let source = try runtime.wakeSource() - try pumpUntilQuiet(runtime) - let signalled = DispatchSemaphore(value: 0) - DispatchQueue.global().async { - Thread.sleep(forTimeInterval: 0.02) - try? source.signal() - signalled.signal() - } - let parkStarted = Date() - try runtime.pump(timeout: parkTimeout) - #expect( - Date().timeIntervalSince(parkStarted) < promptReturn, - "the parked owner thread timed out instead of taking the signal" - ) - signalled.wait() - - // A wake source stays usable after its runtime closes, so hosts tear the two - // down in either order. - try map.close() - try runtime.close() - try source.signal() - try source.close() - #expect(source.isClosed) -} - -@Test func pumpClearsTheWakeFlagItReturnsOn() throws { - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - let source = try runtime.wakeSource() - try pumpUntilQuiet(runtime) - - try source.signal() - let signalledStarted = Date() - try runtime.pump(timeout: parkTimeout) - #expect( - Date().timeIntervalSince(signalledStarted) < promptReturn, - "a pump waited even though the wake flag was set" - ) - - // The pump above cleared the wake flag, so this one waits its full timeout. - let idleStarted = Date() - try runtime.pump(timeout: 0.2) - #expect( - Date().timeIntervalSince(idleStarted) >= 0.1, - "the first pump left the wake flag set" - ) - - try source.close() - try runtime.close() -} - -/// BND-197. The wake source is the one handle a host may use and release from -/// different threads, so `NativeHandleState` has to order the two. -@Test func closeWaitsForAUseInFlightOnAnotherThread() throws { - let state = try NativeHandleState( - typeName: "TestHandle", - handle: SyntheticHandles.wakeSource(1) - ) - let entered = DispatchSemaphore(value: 0) - let releaseUse = DispatchSemaphore(value: 0) - let closeReturned = DispatchSemaphore(value: 0) - let destroys = DestroyCounter() - - let useThread = Thread { - try? state.withLive { _ in - entered.signal() - releaseUse.wait() - #expect( - destroys.value() == 0, - "the use should never observe a destroyed handle" - ) - } - } - useThread.start() - #expect(entered.wait(timeout: .now() + 5) == .success) - - let closeThread = Thread { - try? state.closeOnce { _ in destroys.increment() } - closeReturned.signal() - } - closeThread.start() - - #expect( - closeReturned.wait(timeout: .now() + 0.2) == .timedOut, - "close should wait for the use" - ) - #expect(destroys.value() == 0) - - releaseUse.signal() - #expect(closeReturned.wait(timeout: .now() + 5) == .success) - #expect(destroys.value() == 1) - #expect(state.isClosed) -} - -/// BND-197. A use that starts once close has begun is turned away by the -/// wrapper rather than reaching native with a retired id. -@Test func aUseStartingAfterCloseBeginsIsRefused() throws { - let state = try NativeHandleState( - typeName: "TestHandle", - handle: SyntheticHandles.wakeSource(2) - ) - try state.closeOnce { _ in } - - #expect(throws: NativeStatusFailure.self) { - try state.withLive { _ in } - } -} - -private final class DestroyCounter: @unchecked Sendable { - private let lock = NSLock() - private var count = 0 - - func increment() { - lock.withLock { count += 1 } - } - - func value() -> Int { - lock.withLock { count } - } -} diff --git a/bindings/zig/mise.toml b/bindings/zig/mise.toml index 1e010374e..dfa858268 100644 --- a/bindings/zig/mise.toml +++ b/bindings/zig/mise.toml @@ -28,11 +28,12 @@ zig build --build-file ../../build.zig \ [tasks.test] extends = "ffi:preset" -# The preset selects the runner: an iOS or tvOS simulator preset spawns the -# suite on a simulator, and host presets run in process. +# The preset selects the runner: Android presets use their matching emulator, +# an iOS or tvOS simulator preset spawns the suite on a simulator, and host +# presets run in process. run = ''' case "$usage_preset" in - android-x64-*) + android-*) source "$MISE_MONOREPO_ROOT/scripts/zig-cross-env.sh" "$usage_preset" exec zig build test --build-file ../../build.zig \ -Dnative-install-dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" \ @@ -40,10 +41,6 @@ case "$usage_preset" in ${zig_cross_args[@]+"${zig_cross_args[@]}"} \ --test-timeout 180s --summary all --verbose ;; - android-*) - echo "The Android emulator runs x64 guests only; check $usage_preset with //bindings/zig:build and test an Android x64 preset instead." >&2 - exit 2 - ;; ios-simulator-*) source "$MISE_MONOREPO_ROOT/scripts/zig-cross-env.sh" "$usage_preset" mise run //:ios-simulator:boot diff --git a/bindings/zig/src/completion.zig b/bindings/zig/src/completion.zig new file mode 100644 index 000000000..859af562e --- /dev/null +++ b/bindings/zig/src/completion.zig @@ -0,0 +1,259 @@ +const std = @import("std"); + +const c = @import("c.zig").raw; +const diagnostics = @import("diagnostics.zig"); +const status = @import("status.zig"); + +pub const CommandDisposition = union(enum) { + committed, + superseded, + failed, + cancelled, + unknown: u32, + + fn fromRaw(raw: u32) CommandDisposition { + return switch (raw) { + c.MLN_COMMAND_DISPOSITION_COMMITTED => .committed, + c.MLN_COMMAND_DISPOSITION_SUPERSEDED => .superseded, + c.MLN_COMMAND_DISPOSITION_FAILED => .failed, + c.MLN_COMMAND_DISPOSITION_CANCELLED => .cancelled, + else => .{ .unknown = raw }, + }; + } +}; + +pub const CommandCompletion = struct { + disposition: CommandDisposition, + generation: u64, + raw_status: i32, + + /// Returns the native error carried by a failed or cancelled command. + pub fn statusError(self: CommandCompletion) status.NativeStatusError!void { + return status.errorFromRawStatus(self.raw_status); + } +}; + +pub fn Future(comptime T: type) type { + return struct { + const Self = @This(); + const Copy = *const fn (*const c.mln_completion_result, ?*anyopaque) status.Error!T; + + const State = struct { + refs: std.atomic.Value(usize) = .init(2), + completed: std.atomic.Value(u32) = .init(0), + consumed: std.atomic.Value(bool) = .init(false), + raw_status: i32 = c.MLN_STATUS_OK, + diagnostic: []u8 = &.{}, + value: ?T = null, + conversion_error: ?status.Error = null, + copy: Copy, + copy_context: ?*anyopaque = null, + release_copy_context: ?*const fn (?*anyopaque) void = null, + + fn release(state: *State) void { + if (state.refs.fetchSub(1, .acq_rel) != 1) return; + if (state.diagnostic.len != 0) std.heap.smp_allocator.free(state.diagnostic); + if (state.release_copy_context) |release_context| release_context(state.copy_context); + std.heap.smp_allocator.destroy(state); + } + }; + + state: ?*State, + + pub fn poll(self: *const Self) status.BindingError!bool { + const state = self.state orelse return error.ClosedHandle; + return state.completed.load(.acquire) != 0; + } + + /// Returns the copied terminal diagnostic until this future is deinitialized. + pub fn diagnostic(self: *const Self) status.BindingError![]const u8 { + const state = self.state orelse return error.ClosedHandle; + return state.diagnostic; + } + + pub fn wait(self: *Self, diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error!T { + const state = self.state orelse return error.ClosedHandle; + while (state.completed.load(.acquire) == 0) std.Thread.yield() catch {}; + if (state.consumed.swap(true, .acq_rel)) return error.AlreadyCompleted; + if (state.diagnostic.len != 0) { + if (diagnostic_store) |store| try store.set(state.raw_status, state.diagnostic); + } + if (state.conversion_error) |err| return err; + if (T != CommandCompletion) try status.errorFromRawStatus(state.raw_status); + return state.value orelse unreachable; + } + + pub fn deinit(self: *Self) void { + const state = self.state orelse return; + self.state = null; + state.release(); + } + + fn callback(user_data: ?*anyopaque, raw: [*c]const c.mln_completion_result) callconv(.c) void { + const state: *State = @ptrCast(@alignCast(user_data orelse return)); + const result: *const c.mln_completion_result = @ptrCast(raw orelse return); + state.raw_status = result.status; + if (result.diagnostic.data != null and result.diagnostic.size != 0) { + const bytes = @as([*]const u8, @ptrCast(result.diagnostic.data))[0..result.diagnostic.size]; + state.diagnostic = std.heap.smp_allocator.dupe(u8, bytes) catch { + state.conversion_error = error.OutOfMemory; + state.completed.store(1, .release); + return; + }; + } + if (result.status == c.MLN_STATUS_OK or T == CommandCompletion) { + state.value = state.copy(result, state.copy_context) catch |err| { + state.conversion_error = err; + state.completed.store(1, .release); + return; + }; + } + state.completed.store(1, .release); + } + + fn releaseUserData(user_data: ?*anyopaque) callconv(.c) void { + const state: *State = @ptrCast(@alignCast(user_data orelse return)); + state.release(); + } + }; +} + +/// Returns a future that already carries `value`, for work the binding +/// satisfied without a native submission. The caller deinitializes it like any +/// other future. +pub fn completed(comptime T: type, terminal_value: T) std.mem.Allocator.Error!Future(T) { + const FutureType = Future(T); + const state = try std.heap.smp_allocator.create(FutureType.State); + state.* = .{ + .refs = .init(1), + .completed = .init(1), + .value = terminal_value, + .copy = struct { + fn copyResult(_: *const c.mln_completion_result, _: ?*anyopaque) status.Error!T { + unreachable; + } + }.copyResult, + }; + return .{ .state = state }; +} + +pub fn submit( + comptime T: type, + diagnostic_store: ?*diagnostics.DiagnosticStore, + comptime copy: *const fn (*const c.mln_completion_result) status.Error!T, + context: anytype, + comptime start: anytype, +) status.Error!Future(T) { + return submitWithCopyContext(T, void, diagnostic_store, struct { + fn copyResult(result: *const c.mln_completion_result, _: *void) status.Error!T { + return copy(result); + } + }.copyResult, {}, context, start); +} + +pub fn submitWithCopyContext( + comptime T: type, + comptime CopyContext: type, + diagnostic_store: ?*diagnostics.DiagnosticStore, + comptime copy: *const fn (*const c.mln_completion_result, *CopyContext) status.Error!T, + copy_context: CopyContext, + context: anytype, + comptime start: anytype, +) status.Error!Future(T) { + const FutureType = Future(T); + const owned_copy_context = try std.heap.smp_allocator.create(CopyContext); + owned_copy_context.* = copy_context; + const state = std.heap.smp_allocator.create(FutureType.State) catch |err| { + std.heap.smp_allocator.destroy(owned_copy_context); + return err; + }; + state.* = .{ + .copy = struct { + fn copyResult(result: *const c.mln_completion_result, erased: ?*anyopaque) status.Error!T { + const typed: *CopyContext = @ptrCast(@alignCast(erased orelse return error.NativeError)); + return copy(result, typed); + } + }.copyResult, + .copy_context = @ptrCast(owned_copy_context), + .release_copy_context = struct { + fn release(erased: ?*anyopaque) void { + const typed: *CopyContext = @ptrCast(@alignCast(erased orelse return)); + std.heap.smp_allocator.destroy(typed); + } + }.release, + }; + var future = FutureType{ .state = state }; + const descriptor = c.mln_completion{ + .size = @sizeOf(c.mln_completion), + .callback = FutureType.callback, + .user_data = state, + .release_user_data = FutureType.releaseUserData, + }; + status.checkStatus(start(context, &descriptor), diagnostic_store) catch |err| { + state.release(); + future.deinit(); + return err; + }; + return future; +} + +pub fn Submission(comptime T: type, comptime Output: type) type { + return struct { future: Future(T), output: Output }; +} + +pub fn submitWithOutput( + comptime T: type, + comptime Output: type, + diagnostic_store: ?*diagnostics.DiagnosticStore, + comptime copy: *const fn (*const c.mln_completion_result) status.Error!T, + initial_output: Output, + context: anytype, + comptime start: anytype, +) status.Error!Submission(T, Output) { + const FutureType = Future(T); + const state = try std.heap.smp_allocator.create(FutureType.State); + state.* = .{ + .copy = struct { + fn copyResult(result: *const c.mln_completion_result, _: ?*anyopaque) status.Error!T { + return copy(result); + } + }.copyResult, + }; + var future = FutureType{ .state = state }; + const descriptor = c.mln_completion{ + .size = @sizeOf(c.mln_completion), + .callback = FutureType.callback, + .user_data = state, + .release_user_data = FutureType.releaseUserData, + }; + var output = initial_output; + status.checkStatus(start(context, &output, &descriptor), diagnostic_store) catch |err| { + state.release(); + future.deinit(); + return err; + }; + return .{ .future = future, .output = output }; +} + +pub fn unit(result: *const c.mln_completion_result) status.Error!void { + if (result.value != null or result.value_count != 0) return error.NativeError; +} + +pub fn command(result: *const c.mln_completion_result) status.Error!CommandCompletion { + if (result.value != null or result.value_count != 0) return error.NativeError; + return .{ + .disposition = CommandDisposition.fromRaw(result.disposition), + .generation = result.generation, + .raw_status = result.status, + }; +} + +pub fn value(comptime T: type) *const fn (*const c.mln_completion_result) status.Error!T { + return struct { + fn copy(result: *const c.mln_completion_result) status.Error!T { + if (result.value == null or result.value_count != 1) return error.NativeError; + const pointer: *align(1) const T = @ptrCast(result.value.?); + return pointer.*; + } + }.copy; +} diff --git a/bindings/zig/src/logging.zig b/bindings/zig/src/logging.zig index 4a0499135..0ec0bd4ba 100644 --- a/bindings/zig/src/logging.zig +++ b/bindings/zig/src/logging.zig @@ -101,28 +101,16 @@ const LogCallbackState = struct { context: ?*anyopaque, }; -var log_callback_state: std.atomic.Value(usize) = std.atomic.Value(usize).init(0); - pub fn setLogCallback(callback: LogCallback, diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error!void { const replacement = try std.heap.smp_allocator.create(LogCallbackState); replacement.* = .{ .handler = callback.handler, .context = callback.context }; errdefer std.heap.smp_allocator.destroy(replacement); - try status.checkStatus(c.mln_log_set_callback(logTrampoline, replacement), diagnostic_store); - const previous = log_callback_state.swap(@intFromPtr(replacement), .seq_cst); - if (previous != 0) { - const previous_state: *LogCallbackState = @ptrFromInt(previous); - std.heap.smp_allocator.destroy(previous_state); - } + try status.checkStatus(c.mln_log_set_callback(logTrampoline, replacement, releaseLogCallback), diagnostic_store); } pub fn clearLogCallback(diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error!void { try status.checkStatus(c.mln_log_clear_callback(), diagnostic_store); - const previous = log_callback_state.swap(0, .seq_cst); - if (previous != 0) { - const previous_state: *LogCallbackState = @ptrFromInt(previous); - std.heap.smp_allocator.destroy(previous_state); - } } pub fn setAsyncLogSeverityMask(mask: LogSeverityMask, diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error!void { @@ -142,6 +130,11 @@ fn logTrampoline(user_data: ?*anyopaque, severity: u32, event: u32, code: i64, m })) 1 else 0; } +fn releaseLogCallback(user_data: ?*anyopaque) callconv(.c) void { + const callback_state: *LogCallbackState = @ptrCast(@alignCast(user_data orelse return)); + std.heap.smp_allocator.destroy(callback_state); +} + test "log raw domains preserve unknown values" { try std.testing.expect(std.meta.eql(LogSeverity.fromRaw(0xbeef), LogSeverity{ .unknown = 0xbeef })); try std.testing.expect(std.meta.eql(LogEvent.fromRaw(0xfeed), LogEvent{ .unknown = 0xfeed })); diff --git a/bindings/zig/src/map.zig b/bindings/zig/src/map.zig index 29c994d1a..4490b077a 100644 --- a/bindings/zig/src/map.zig +++ b/bindings/zig/src/map.zig @@ -1,6 +1,7 @@ const std = @import("std"); const c = @import("c.zig").raw; +const completion = @import("completion.zig"); const diagnostics = @import("diagnostics.zig"); const native_temp = @import("native_temp.zig"); const runtime_module = @import("runtime.zig"); @@ -30,11 +31,13 @@ const MapState = struct { diagnostic_store: ?*diagnostics.DiagnosticStore, attached_render_sessions: std.atomic.Value(usize), closing: bool, + runtime: RuntimeHandle, }; pub const RenderSessionRegistration = struct { native: c.mln_map, diagnostic_store: ?*diagnostics.DiagnosticStore, + runtime: RuntimeHandle, }; var custom_geometry_state_registry_lock = std.Io.Mutex.init; @@ -63,36 +66,166 @@ pub const MapMode = enum { /// Map creation options. A null field takes the C API default. pub const MapOptions = struct { - /// Initial logical width in UI pixels, replaced by the extent of the first - /// attached render session. width: ?u32 = null, - /// Initial logical height in UI pixels, replaced by the extent of the first - /// attached render session. height: ?u32 = null, - /// UI-to-device pixel scale, fixed for the lifetime of the map, selecting - /// sprites, glyphs, and raster tiles. A render session attached or resized - /// with a different scale factor logs a warning and still renders styled - /// imagery chosen for this density. scale_factor: ?f64 = null, mode: ?MapMode = null, - /// Decodes MapLibre Tile (MLT) tiles whose integer streams use FastPFOR - /// encodings, fixed for the lifetime of the map. A map created with this - /// false decodes every other MLT encoding and logs a tile parse warning for - /// the FastPFOR ones. fast_pfor_enabled: ?bool = null, - /// Map-originated event types this map queues. A null takes the C API - /// default, which selects every type the library reports, including the types - /// this binding does not name. Set it to be narrow from the map's first style - /// load, which is the load that produces the most tile and frame events. See - /// `MapHandle.setEventMask`. event_mask: ?runtime_module.RuntimeEventMask = null, }; +pub const CameraUpdateMode = enum { + jump, + ease, + fly, + + fn toRaw(self: CameraUpdateMode) u32 { + return switch (self) { + .jump => c.MLN_CAMERA_UPDATE_MODE_JUMP, + .ease => c.MLN_CAMERA_UPDATE_MODE_EASE, + .fly => c.MLN_CAMERA_UPDATE_MODE_FLY, + }; + } +}; + +pub const GesturePhase = enum { + none, + begin, + update, + end, + cancel, + + fn toRaw(self: GesturePhase) u32 { + return switch (self) { + .none => c.MLN_GESTURE_PHASE_NONE, + .begin => c.MLN_GESTURE_PHASE_BEGIN, + .update => c.MLN_GESTURE_PHASE_UPDATE, + .end => c.MLN_GESTURE_PHASE_END, + .cancel => c.MLN_GESTURE_PHASE_CANCEL, + }; + } +}; + +pub const CameraUpdate = struct { + mode: CameraUpdateMode = .jump, + camera: values.CameraOptions = .{}, + animation: values.AnimationOptions = .{}, + gesture_phase: GesturePhase = .none, +}; + +pub const CameraDeltaKind = enum { + move, + scale, + bearing, + pitch, + + fn toRaw(self: CameraDeltaKind) u32 { + return switch (self) { + .move => c.MLN_CAMERA_DELTA_MOVE, + .scale => c.MLN_CAMERA_DELTA_SCALE, + .bearing => c.MLN_CAMERA_DELTA_BEARING, + .pitch => c.MLN_CAMERA_DELTA_PITCH, + }; + } +}; + +pub const CameraDelta = struct { + kind: CameraDeltaKind = .move, + offset: values.ScreenPoint = .{ .x = 0, .y = 0 }, + amount: f64 = 0, + anchor: ?values.ScreenPoint = null, + animation: values.AnimationOptions = .{}, +}; + +pub const CameraSnapshot = struct { + generation: u64, + camera: values.CameraOptions, +}; + +pub const ScreenPointList = struct { + allocator: std.mem.Allocator, + items: []values.ScreenPoint, + + pub fn deinit(self: *ScreenPointList) void { + self.allocator.free(self.items); + self.items = &.{}; + } +}; + +pub const LatLngList = struct { + allocator: std.mem.Allocator, + items: []values.LatLng, + + pub fn deinit(self: *LatLngList) void { + self.allocator.free(self.items); + self.items = &.{}; + } +}; + +/// Immutable map state copied from the latest published generation. +/// +/// Every committed map command publishes a new generation in its completion, +/// so a snapshot whose generation is at or past that value observes the +/// commit. +pub const MapSnapshot = struct { + generation: u64, + camera: values.CameraOptions, + width: u32, + height: u32, + scale_factor: f64, + debug_options: values.DebugOptions, + projection_mode: values.ProjectionMode, + viewport: values.ViewportOptions, + tile: values.TileOptions, + bounds: values.BoundOptions, + free_camera: values.FreeCameraOptions, + /// True once every requested style and tile resource finished loading. + fully_loaded: bool, + rendering_stats_view_enabled: bool, + repaint_demand: bool, + event_mask: runtime_module.RuntimeEventMask, + latest_render_update_generation: u64, +}; + pub const CanonicalTileId = struct { z: u32, x: u32, y: u32, }; +pub const StyleSourceMetadata = struct { + source_type: values.StyleSourceType, + id_size: usize, + is_volatile: bool, + has_attribution: bool, + has_url: bool, + has_tile_json: bool, + min_zoom: f64, + max_zoom: f64, + scheme: values.StyleTileScheme, + bounds: ?values.LatLngBounds, + tile_count: usize, + tile_size: ?u32, + vector_encoding: ?values.StyleVectorTileEncoding, + raster_encoding: ?values.StyleRasterDemEncoding, +}; + +/// Fixed layer metadata returned by `MapHandle.getStyleLayerInfoTakeResult`. +pub const StyleLayerInfo = struct { + /// Style-spec layer type name. It views a static native string that stays + /// valid for the life of the process. + layer_type: []const u8, + /// Lowest zoom at which the layer draws; -inf with no lower bound. + min_zoom: f64, + /// Highest zoom at which the layer draws; +inf with no upper bound. + max_zoom: f64, + visibility: values.StyleLayerVisibility, + /// Source ID byte length, null when the layer names no source. It sizes + /// buffers for `copyLayerSourceId`. + source_id_size: ?usize, + /// Source-layer byte length, null when the layer names no source layer. It + /// sizes buffers for `copyLayerSourceLayer`. + source_layer_size: ?usize, +}; pub const CustomGeometrySourceTileCallback = *const fn ( context: ?*anyopaque, @@ -107,7 +240,7 @@ pub const CustomGeometrySourceOptions = struct { cancel_tile: ?CustomGeometrySourceTileCallback = null, /// Invoked once with `context` after the map stops referencing this source: /// on an explicit removal, on a style load that leaves a style without the - /// source, and on the map's own destruction. It runs on the map owner thread, + /// source, and on the map's own destruction. It runs on the runtime worker /// after the last tile callback returns, and never runs for an add that /// failed. A host frees `context` here instead of tracking style loads. release_context: ?CustomGeometrySourceReleaseCallback = null, @@ -201,11 +334,11 @@ pub const FeatureStateSelector = struct { pub const MapHandle = enum(c.mln_map) { _, - pub fn create(runtime: *RuntimeHandle, options: MapOptions) status.Error!MapHandle { + pub fn create(runtime: *RuntimeHandle, options: MapOptions) status.Error!completion.Future(MapHandle) { var native_options = c.mln_map_options_default(); - if (options.width) |value| native_options.width = value; - if (options.height) |value| native_options.height = value; - if (options.scale_factor) |value| native_options.scale_factor = value; + if (options.width) |value| native_options.initial_extent.width = value; + if (options.height) |value| native_options.initial_extent.height = value; + if (options.scale_factor) |value| native_options.initial_extent.scale_factor = value; if (options.mode) |value| native_options.map_mode = value.toRaw(); if (options.fast_pfor_enabled) |value| native_options.fast_pfor_enabled = value; if (options.event_mask) |value| { @@ -214,135 +347,125 @@ pub const MapHandle = enum(c.mln_map) { const runtime_lease = try runtime_module.lease(runtime); defer runtime_lease.release(); - var map: c.mln_map = 0; const diagnostic_store = runtime_lease.diagnostic_store; - try status.checkStatus( - c.mln_map_create(runtime_lease.native, &native_options, &map), - diagnostic_store, - ); - errdefer _ = c.mln_map_destroy(map); - - const map_registration = try runtime_module.registerMap(runtime, map); - errdefer runtime_module.unregisterMap(map_registration.registry, map); - - const map_state = try std.heap.smp_allocator.create(MapState); - map_state.* = .{ - .runtime_registry = map_registration.registry, - .id_value = map_registration.id, - .diagnostic_store = diagnostic_store, - .attached_render_sessions = std.atomic.Value(usize).init(0), - .closing = false, + const CopyContext = struct { + runtime: RuntimeHandle, + diagnostic_store: ?*diagnostics.DiagnosticStore, }; - errdefer std.heap.smp_allocator.destroy(map_state); - - return try registerMapState(map, map_state); + const StartContext = struct { runtime: c.mln_runtime, options: c.mln_map_options }; + return completion.submitWithCopyContext(MapHandle, CopyContext, diagnostic_store, struct { + fn copyResult(result: *const c.mln_completion_result, context: *CopyContext) status.Error!MapHandle { + const map = try completion.value(c.mln_map)(result); + var runtime_handle = context.runtime; + const registration = try runtime_module.registerMap(&runtime_handle, map); + errdefer runtime_module.unregisterMap(registration.registry, map); + const state = try std.heap.smp_allocator.create(MapState); + state.* = .{ + .runtime_registry = registration.registry, + .id_value = registration.id, + .diagnostic_store = context.diagnostic_store, + .runtime = context.runtime, + .attached_render_sessions = std.atomic.Value(usize).init(0), + .closing = false, + }; + errdefer std.heap.smp_allocator.destroy(state); + return registerMapState(map, state); + } + }.copyResult, .{ + .runtime = runtime.*, + .diagnostic_store = diagnostic_store, + }, StartContext{ + .runtime = runtime_lease.native, + .options = native_options, + }, struct { + fn start(context: StartContext, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_map_create(context.runtime, &context.options, descriptor); + } + }.start); } pub fn id(self: *MapHandle) status.BindingError!values.MapId { return mapIdForHandle(self); } - /// Loads inline style JSON. - /// - /// Malformed JSON is reported twice: as a synchronous parse error here and - /// as a map-loading-failed runtime event. A well-formed style that MapLibre - /// rejects semantically produces neither; MapLibre logs it and renders what - /// it can. + /// Accepts an ordered inline-style command. pub fn setStyleJson( self: *MapHandle, allocator: std.mem.Allocator, json: []const u8, - ) status.Error!void { - const native_map = try native(self); + ) status.Error!completion.Future(completion.CommandCompletion) { _ = allocator; - try status.checkStatus(c.mln_map_set_style_json(native_map, stringView(json)), diagnosticStore(self)); + return submitCommand(self, c.mln_map_set_style_json, .{ try native(self), stringView(json) }); } + /// Accepts an ordered per-feature-state command. The committed command + /// requests a map repaint. pub fn setFeatureState( self: *MapHandle, allocator: std.mem.Allocator, selector: FeatureStateSelector, feature_state: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_selector = try featureStateSelectorToNative(&temp, selector); - const raw_state = try temp.stringView(feature_state); - try status.checkStatus( - c.mln_map_set_feature_state(try native(self), &raw_selector, raw_state), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_feature_state, .{ + try native(self), + &raw_selector, + try temp.stringView(feature_state), + }); } + /// Starts an ordered read of per-feature state. The read copies the map + /// store, not the last rendered frame; missing feature state resolves to an + /// empty JSON object. pub fn getFeatureState( self: *MapHandle, allocator: std.mem.Allocator, selector: FeatureStateSelector, - ) status.Error!values.OwnedString { + ) status.Error!completion.Future(values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_selector = try featureStateSelectorToNative(&temp, selector); - var buffer: c.mln_buffer = 0; - try status.checkStatus( - c.mln_map_get_feature_state(try native(self), &raw_selector, &buffer), - diagnosticStore(self), - ); - return (try native_temp.copyOwnedBuffer(allocator, buffer, diagnosticStore(self))) orelse error.NativeError; + return submitAllocatedQuery(values.OwnedString, self, allocator, copyOwnedStringResult, c.mln_map_get_feature_state, .{ + try native(self), + &raw_selector, + }); } + /// Accepts an ordered feature-state removal command. The committed command + /// requests a map repaint. pub fn removeFeatureState( self: *MapHandle, allocator: std.mem.Allocator, selector: FeatureStateSelector, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_selector = try featureStateSelectorToNative(&temp, selector); - try status.checkStatus( - c.mln_map_remove_feature_state(try native(self), &raw_selector), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_remove_feature_state, .{ + try native(self), + &raw_selector, + }); } - /// Loads a style URL. - /// - /// Loading is asynchronous: a missing, unreachable, or malformed style - /// still returns success here and reports through a map-loading-failed - /// runtime event. A well-formed style that MapLibre rejects semantically - /// produces neither; MapLibre logs it and renders what it can. + /// Accepts an ordered style-URL command. pub fn setStyleUrl( self: *MapHandle, allocator: std.mem.Allocator, url: []const u8, - ) status.Error!void { - const native_map = try native(self); + ) status.Error!completion.Future(completion.CommandCompletion) { const url_z = try nulTerminated(allocator, url, diagnosticStore(self), "style URL contains embedded NUL"); defer allocator.free(url_z); - try status.checkStatus(c.mln_map_set_style_url(native_map, url_z.ptr), diagnosticStore(self)); + return submitCommand(self, c.mln_map_set_style_url, .{ try native(self), url_z.ptr }); } - /// Copies the style document this map's style was last parsed from: the - /// bytes passed to `setStyleJson` or the response body fetched for - /// `setStyleUrl`, not a serialization of the live style. Runtime mutations - /// do not change it, and a failed parse leaves the previous document in - /// place. The value is empty when no document has been parsed. - pub fn copyLoadedStyleJson( - self: *MapHandle, - allocator: std.mem.Allocator, - ) status.Error!values.OwnedString { - return self.copyMapText(allocator, c.mln_map_copy_loaded_style_json); + pub fn loadedStyleJson(self: *MapHandle, allocator: std.mem.Allocator) status.Error!completion.Future(values.OwnedString) { + return submitAllocatedQuery(values.OwnedString, self, allocator, copyOwnedStringResult, c.mln_map_loaded_style_json, .{try native(self)}); } - /// Copies the URL this map's style was last requested from. `setStyleUrl` - /// records it when the request is made, before the response arrives, and - /// `setStyleJson` clears it, so it can disagree with `copyLoadedStyleJson` - /// while a load is in flight. The value is empty for an inline-JSON style, - /// a map with no style, and an empty URL alike. - pub fn copyStyleUrl( - self: *MapHandle, - allocator: std.mem.Allocator, - ) status.Error!values.OwnedString { - return self.copyMapText(allocator, c.mln_map_copy_style_url); + pub fn styleUrl(self: *MapHandle, allocator: std.mem.Allocator) status.Error!completion.Future(values.OwnedString) { + return submitAllocatedQuery(values.OwnedString, self, allocator, copyOwnedStringResult, c.mln_map_style_url, .{try native(self)}); } pub fn setLayerProperty( @@ -351,18 +474,15 @@ pub const MapHandle = enum(c.mln_map) { layer_id: []const u8, property_name: []const u8, value: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_property( - try native(self), - try temp.stringView(layer_id), - try temp.stringView(property_name), - try temp.stringView(value), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_layer_property, .{ + try native(self), + try temp.stringView(layer_id), + try temp.stringView(property_name), + try temp.stringView(value), + }); } pub fn getLayerProperty( @@ -370,20 +490,12 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_id: []const u8, property_name: []const u8, - ) status.Error!?values.OwnedString { + ) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var buffer: c.mln_buffer = 0; - try status.checkStatus( - c.mln_map_get_layer_property( - try native(self), - try temp.stringView(layer_id), - try temp.stringView(property_name), - &buffer, - ), - diagnosticStore(self), - ); - return try native_temp.copyOwnedBuffer(allocator, buffer, diagnosticStore(self)); + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_get_layer_property, .{ + try native(self), try temp.stringView(layer_id), try temp.stringView(property_name), + }); } pub fn setLayerFilter( @@ -391,268 +503,115 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_id: []const u8, filter: ?[]const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var filter_view = if (filter) |value| try temp.stringView(value) else undefined; const filter_ptr = if (filter != null) &filter_view else null; - try status.checkStatus( - c.mln_map_set_layer_filter(try native(self), try temp.stringView(layer_id), filter_ptr), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_layer_filter, .{ + try native(self), + try temp.stringView(layer_id), + filter_ptr, + }); } pub fn getLayerFilter( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, - ) status.Error!?values.OwnedString { + ) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var buffer: c.mln_buffer = 0; - try status.checkStatus( - c.mln_map_get_layer_filter(try native(self), try temp.stringView(layer_id), &buffer), - diagnosticStore(self), - ); - return try native_temp.copyOwnedBuffer(allocator, buffer, diagnosticStore(self)); + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_get_layer_filter, .{ try native(self), try temp.stringView(layer_id) }); } - /// Sets one layer's source-layer ID. Layer types that take no source, such as background, are - /// rejected. pub fn setLayerSourceLayer( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, source_layer: []const u8, - ) status.Error!void { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_source_layer( - try native(self), - try temp.stringView(layer_id), - try temp.stringView(source_layer), - ), - diagnosticStore(self), - ); + ) status.Error!completion.Future(completion.CommandCompletion) { + return self.setLayerStringCommand(allocator, layer_id, source_layer, c.mln_map_set_layer_source_layer); } - /// Copies one layer's source-layer ID. The value is empty when the layer carries none. pub fn copyLayerSourceLayer( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, - ) status.Error!values.OwnedString { - return self.copyLayerText(allocator, layer_id, c.mln_map_copy_layer_source_layer); + ) status.Error!completion.Future(values.OwnedString) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitAllocatedQuery(values.OwnedString, self, allocator, copyOwnedStringResult, c.mln_map_copy_layer_source_layer, .{ try native(self), try temp.stringView(layer_id) }); } - /// Sets one layer's source ID. Layer types that take no source, such as background, are - /// rejected. The named source need not exist yet. pub fn setLayerSourceId( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, source_id: []const u8, - ) status.Error!void { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_source_id( - try native(self), - try temp.stringView(layer_id), - try temp.stringView(source_id), - ), - diagnosticStore(self), - ); + ) status.Error!completion.Future(completion.CommandCompletion) { + return self.setLayerStringCommand(allocator, layer_id, source_id, c.mln_map_set_layer_source_id); } - /// Copies one layer's source ID. The value is empty when the layer carries none. pub fn copyLayerSourceId( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, - ) status.Error!values.OwnedString { - return self.copyLayerText(allocator, layer_id, c.mln_map_copy_layer_source_id); - } - - /// A null buffer with zero capacity is a size probe the C API answers with - /// OK. - fn copyMapText( - self: *MapHandle, - allocator: std.mem.Allocator, - copy: *const fn (c.mln_map, ?[*]u8, usize, *usize) callconv(.c) c.mln_status, - ) status.Error!values.OwnedString { - var required: usize = 0; - try status.checkStatus( - copy(try native(self), null, 0, &required), - diagnosticStore(self), - ); - if (required == 0) { - return .{ .allocator = allocator, .value = try allocator.dupe(u8, "") }; - } - - const buffer = try allocator.alloc(u8, required); - errdefer allocator.free(buffer); - var copied: usize = 0; - try status.checkStatus( - copy(try native(self), buffer.ptr, buffer.len, &copied), - diagnosticStore(self), - ); - if (copied != buffer.len) { - const exact = try allocator.dupe(u8, buffer[0..copied]); - allocator.free(buffer); - return .{ .allocator = allocator, .value = exact }; - } - return .{ .allocator = allocator, .value = buffer }; - } - - fn copyLayerText( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - copy: *const fn (c.mln_map, c.mln_buffer_view, ?[*]u8, usize, *usize) callconv(.c) c.mln_status, - ) status.Error!values.OwnedString { + ) status.Error!completion.Future(values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - const layer_view = try temp.stringView(layer_id); - - var required: usize = 0; - try status.checkStatus( - copy(try native(self), layer_view, null, 0, &required), - diagnosticStore(self), - ); - if (required == 0) { - return .{ .allocator = allocator, .value = try allocator.dupe(u8, "") }; - } - - const buffer = try allocator.alloc(u8, required); - errdefer allocator.free(buffer); - var copied: usize = 0; - try status.checkStatus( - copy(try native(self), layer_view, buffer.ptr, buffer.len, &copied), - diagnosticStore(self), - ); - if (copied != buffer.len) { - const exact = try allocator.dupe(u8, buffer[0..copied]); - allocator.free(buffer); - return .{ .allocator = allocator, .value = exact }; - } - return .{ .allocator = allocator, .value = buffer }; + return submitAllocatedQuery(values.OwnedString, self, allocator, copyOwnedStringResult, c.mln_map_copy_layer_source_id, .{ try native(self), try temp.stringView(layer_id) }); } - /// Sets the lowest zoom at which one layer draws. Pass -inf for no lower bound. - pub fn setLayerMinZoom( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - min_zoom: f64, - ) status.Error!void { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_min_zoom(try native(self), try temp.stringView(layer_id), min_zoom), - diagnosticStore(self), - ); - } - - /// Reads the lowest zoom at which one layer draws. A layer with no lower bound reports -inf. - pub fn getLayerMinZoom( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - ) status.Error!f64 { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - var min_zoom: f64 = 0; - try status.checkStatus( - c.mln_map_get_layer_min_zoom(try native(self), try temp.stringView(layer_id), &min_zoom), - diagnosticStore(self), - ); - return min_zoom; - } - - /// Sets the highest zoom at which one layer draws. Pass inf for no upper bound. - pub fn setLayerMaxZoom( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - max_zoom: f64, - ) status.Error!void { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_max_zoom(try native(self), try temp.stringView(layer_id), max_zoom), - diagnosticStore(self), - ); + pub fn setLayerMinZoom(self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, value: f64) status.Error!completion.Future(completion.CommandCompletion) { + return self.setLayerNumberCommand(allocator, layer_id, value, c.mln_map_set_layer_min_zoom); } - /// Reads the highest zoom at which one layer draws. A layer with no upper bound reports inf. - pub fn getLayerMaxZoom( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - ) status.Error!f64 { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - var max_zoom: f64 = 0; - try status.checkStatus( - c.mln_map_get_layer_max_zoom(try native(self), try temp.stringView(layer_id), &max_zoom), - diagnosticStore(self), - ); - return max_zoom; + pub fn setLayerMaxZoom(self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, value: f64) status.Error!completion.Future(completion.CommandCompletion) { + return self.setLayerNumberCommand(allocator, layer_id, value, c.mln_map_set_layer_max_zoom); } - /// Sets whether one layer draws. pub fn setLayerVisibility( self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8, visibility: values.StyleLayerVisibility, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_layer_visibility( - try native(self), - try temp.stringView(layer_id), - visibility.toRaw(), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_layer_visibility, .{ + try native(self), + try temp.stringView(layer_id), + visibility.toRaw(), + }); } - /// Reads whether one layer draws. - pub fn getLayerVisibility( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - ) status.Error!values.StyleLayerVisibility { + pub fn getStyleLayerInfo(self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8) status.Error!completion.Future(?StyleLayerInfo) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var visibility: u32 = 0; - try status.checkStatus( - c.mln_map_get_layer_visibility( - try native(self), - try temp.stringView(layer_id), - &visibility, - ), - diagnosticStore(self), - ); - return values.StyleLayerVisibility.fromRaw(visibility); + return submitQuery(?StyleLayerInfo, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!?StyleLayerInfo { + const raw_result = (try optionalCompletionValue(c.mln_style_layer_result, result)) orelse return null; + const raw = raw_result.info; + const type_data: [*]const u8 = @ptrCast(raw.type.data orelse return error.NativeError); + return .{ + .layer_type = type_data[0..raw.type.size], + .min_zoom = raw.min_zoom, + .max_zoom = raw.max_zoom, + .visibility = values.StyleLayerVisibility.fromRaw(raw.visibility), + .source_id_size = if ((raw.fields & c.MLN_STYLE_LAYER_INFO_SOURCE_ID) != 0) raw.source_id_size else null, + .source_layer_size = if ((raw.fields & c.MLN_STYLE_LAYER_INFO_SOURCE_LAYER) != 0) raw.source_layer_size else null, + }; + } + }.copyResult, c.mln_map_get_style_layer_info, .{ try native(self), try temp.stringView(layer_id) }); } - pub fn listStyleSourceIds(self: *MapHandle, allocator: std.mem.Allocator) status.Error!values.StringList { - var list: c.mln_style_id_list = 0; - try status.checkStatus(c.mln_map_list_style_source_ids(try native(self), &list), diagnosticStore(self)); - defer c.mln_style_id_list_destroy(list); - return try copyStyleIdList(allocator, list, diagnosticStore(self)); + pub fn listStyleSourceIds(self: *MapHandle, allocator: std.mem.Allocator) status.Error!completion.Future(values.StringList) { + return submitAllocatedQuery(values.StringList, self, allocator, copyStringListResult, c.mln_map_list_style_source_ids, .{try native(self)}); } - pub fn listStyleLayerIds(self: *MapHandle, allocator: std.mem.Allocator) status.Error!values.StringList { - var list: c.mln_style_id_list = 0; - try status.checkStatus(c.mln_map_list_style_layer_ids(try native(self), &list), diagnosticStore(self)); - defer c.mln_style_id_list_destroy(list); - return try copyStyleIdList(allocator, list, diagnosticStore(self)); + pub fn listStyleLayerIds(self: *MapHandle, allocator: std.mem.Allocator) status.Error!completion.Future(values.StringList) { + return submitAllocatedQuery(values.StringList, self, allocator, copyStringListResult, c.mln_map_list_style_layer_ids, .{try native(self)}); } pub fn addStyleSourceJson( @@ -660,218 +619,67 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, source_json: []const u8, - ) status.Error!void { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_style_source_json(try native(self), try temp.stringView(source_id), try temp.stringView(source_json)), - diagnosticStore(self), - ); - } - - pub fn removeStyleSource(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!bool { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - var removed = false; - try status.checkStatus( - c.mln_map_remove_style_source(try native(self), try temp.stringView(source_id), &removed), - diagnosticStore(self), - ); - return removed; + ) status.Error!completion.Future(completion.CommandCompletion) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitCommand(self, c.mln_map_add_style_source_json, .{ + try native(self), + try temp.stringView(source_id), + try temp.stringView(source_json), + }); + } + + /// Accepts an ordered source-removal command and returns its runtime-wide + /// ID. The command's finished event reports a committed removal, a + /// `NotFound` failure when no source has the ID, and an `InvalidState` + /// failure when a layer still uses the source. + pub fn removeStyleSource(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(completion.CommandCompletion) { + return self.removeStyleObjectCommand(allocator, source_id, c.mln_map_remove_style_source); + } + + pub fn getStyleSourceInfo(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(?StyleSourceMetadata) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitQuery(?StyleSourceMetadata, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!?StyleSourceMetadata { + const raw_result = (try optionalCompletionValue(c.mln_style_source_result, result)) orelse return null; + const raw = raw_result.info; + return .{ + .source_type = values.styleSourceTypeFromNative(raw.type), + .id_size = raw.id_size, + .is_volatile = raw.is_volatile, + .has_attribution = raw.has_attribution, + .has_url = (raw.fields & c.MLN_STYLE_SOURCE_INFO_URL) != 0, + .has_tile_json = (raw.fields & c.MLN_STYLE_SOURCE_INFO_TILEJSON) != 0, + .min_zoom = raw.min_zoom, + .max_zoom = raw.max_zoom, + .scheme = values.StyleTileScheme.fromRaw(raw.scheme), + .bounds = if ((raw.fields & c.MLN_STYLE_SOURCE_INFO_BOUNDS) != 0) values.latLngBoundsFromNative(raw.bounds) else null, + .tile_count = raw_result.tile_url_count, + .tile_size = if ((raw.fields & c.MLN_STYLE_SOURCE_INFO_TILE_SIZE) != 0) raw.tile_size else null, + .vector_encoding = if ((raw.fields & c.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) != 0) values.StyleVectorTileEncoding.fromRaw(raw.vector_encoding) else null, + .raster_encoding = if ((raw.fields & c.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) != 0) values.StyleRasterDemEncoding.fromRaw(raw.raster_encoding) else null, + }; + } + }.copyResult, c.mln_map_get_style_source_info, .{ try native(self), try temp.stringView(source_id) }); } - pub fn styleSourceExists(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!bool { + pub fn copyStyleSourceAttribution(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var exists = false; - try status.checkStatus( - c.mln_map_style_source_exists(try native(self), try temp.stringView(source_id), &exists), - diagnosticStore(self), - ); - return exists; + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_copy_style_source_attribution, .{ try native(self), try temp.stringView(source_id) }); } - pub fn getStyleSourceType( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?values.StyleSourceType { + pub fn copyStyleSourceUrl(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var raw_type: u32 = c.MLN_STYLE_SOURCE_TYPE_UNKNOWN; - var found = false; - try status.checkStatus( - c.mln_map_get_style_source_type(try native(self), try temp.stringView(source_id), &raw_type, &found), - diagnosticStore(self), - ); - if (!found) return null; - return values.styleSourceTypeFromNative(raw_type); - } - - /// Copies one source's retained metadata into a value that stays valid - /// independently of the map. - pub fn getStyleSourceInfo( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?values.StyleSourceInfo { - const raw_info = (try self.getNativeStyleSourceInfo(allocator, source_id)) orelse return null; - - var attribution: ?[]const u8 = null; - errdefer if (attribution) |value| allocator.free(value); - if (raw_info.has_attribution) { - const copied = (try self.copyStyleSourceText( - allocator, - source_id, - c.mln_map_copy_style_source_attribution, - )) orelse return error.UnknownStatus; - attribution = copied.value; - } - - var url: ?[]const u8 = null; - errdefer if (url) |value| allocator.free(value); - if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_URL) != 0) { - const copied = (try self.copyStyleSourceText( - allocator, - source_id, - c.mln_map_copy_style_source_url, - )) orelse return error.UnknownStatus; - url = copied.value; - } - - var tile_urls: ?values.StringList = null; - errdefer if (tile_urls) |*list| list.deinit(); - if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_TILEJSON) != 0) { - var raw_list: c.mln_style_string_list = 0; - var found = false; - try status.checkStatus( - c.mln_map_get_style_source_tile_urls( - try native(self), - stringView(source_id), - &raw_list, - &found, - ), - diagnosticStore(self), - ); - defer c.mln_style_string_list_destroy(raw_list); - if (!found) return error.UnknownStatus; - tile_urls = try copyStyleStringList(allocator, raw_list, diagnosticStore(self)); - } - - return .{ - .allocator = allocator, - .source_type = values.styleSourceTypeFromNative(raw_info.type), - .id_size = raw_info.id_size, - .is_volatile = raw_info.is_volatile, - .attribution = attribution, - .url = url, - .tile_json = if (tile_urls) |list| .{ - .tile_urls = list.items, - .min_zoom = raw_info.min_zoom, - .max_zoom = raw_info.max_zoom, - .scheme = values.StyleTileScheme.fromRaw(raw_info.scheme), - .bounds = if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_BOUNDS) != 0) - values.latLngBoundsFromNative(raw_info.bounds) - else - null, - } else null, - .tile_size = if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_TILE_SIZE) != 0) raw_info.tile_size else null, - .vector_encoding = if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING) != 0) - values.StyleVectorTileEncoding.fromRaw(raw_info.vector_encoding) - else - null, - .raster_encoding = if ((raw_info.fields & c.MLN_STYLE_SOURCE_INFO_RASTER_ENCODING) != 0) - values.StyleRasterDemEncoding.fromRaw(raw_info.raster_encoding) - else - null, - }; - } - - pub fn copyStyleSourceAttribution( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?values.OwnedString { - const info = (try self.getNativeStyleSourceInfo(allocator, source_id)) orelse return null; - if (!info.has_attribution) return null; - return self.copyStyleSourceText( - allocator, - source_id, - c.mln_map_copy_style_source_attribution, - ); + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_copy_style_source_url, .{ try native(self), try temp.stringView(source_id) }); } - /// Copies one source's retained URL. A source without a URL returns null. - pub fn copyStyleSourceUrl( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?values.OwnedString { - const info = (try self.getNativeStyleSourceInfo(allocator, source_id)) orelse return null; - if ((info.fields & c.MLN_STYLE_SOURCE_INFO_URL) == 0) return null; - return self.copyStyleSourceText(allocator, source_id, c.mln_map_copy_style_source_url); - } - - fn getNativeStyleSourceInfo( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?c.mln_style_source_info { + pub fn getStyleSourceTileUrls(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(values.StringList) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var raw_info = std.mem.zeroes(c.mln_style_source_info); - raw_info.size = @sizeOf(c.mln_style_source_info); - var found = false; - try status.checkStatus( - c.mln_map_get_style_source_info( - try native(self), - try temp.stringView(source_id), - &raw_info, - &found, - ), - diagnosticStore(self), - ); - return if (found) raw_info else null; - } - - fn copyStyleSourceText( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - copy: *const fn (c.mln_map, c.mln_buffer_view, ?[*]u8, usize, *usize, *bool) callconv(.c) c.mln_status, - ) status.Error!?values.OwnedString { - var required: usize = 0; - var found = false; - try status.checkStatus( - copy(try native(self), stringView(source_id), null, 0, &required, &found), - diagnosticStore(self), - ); - if (!found) return null; - - const buffer = try allocator.alloc(u8, required); - errdefer allocator.free(buffer); - var copied_size: usize = 0; - try status.checkStatus( - copy( - try native(self), - stringView(source_id), - if (buffer.len == 0) null else buffer.ptr, - buffer.len, - &copied_size, - &found, - ), - diagnosticStore(self), - ); - if (!found) { - allocator.free(buffer); - return null; - } - if (copied_size != buffer.len) { - const exact = try allocator.dupe(u8, buffer[0..copied_size]); - allocator.free(buffer); - return .{ .allocator = allocator, .value = exact }; - } - return .{ .allocator = allocator, .value = buffer }; + return submitAllocatedQuery(values.StringList, self, allocator, copyStringListResult, c.mln_map_get_style_source_tile_urls, .{ try native(self), try temp.stringView(source_id) }); } pub fn addStyleLayerJson( @@ -879,85 +687,41 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_json: []const u8, before_layer_id: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_style_layer_json(try native(self), try temp.stringView(layer_json), stringView(before_layer_id)), - diagnosticStore(self), - ); - } - - pub fn removeStyleLayer(self: *MapHandle, layer_id: []const u8) status.Error!bool { - var removed = false; - try status.checkStatus( - c.mln_map_remove_style_layer(try native(self), stringView(layer_id), &removed), - diagnosticStore(self), - ); - return removed; - } - - pub fn styleLayerExists(self: *MapHandle, layer_id: []const u8) status.Error!bool { - var exists = false; - try status.checkStatus( - c.mln_map_style_layer_exists(try native(self), stringView(layer_id), &exists), - diagnosticStore(self), - ); - return exists; + return submitCommand(self, c.mln_map_add_style_layer_json, .{ + try native(self), + try temp.stringView(layer_json), + stringView(before_layer_id), + }); } - pub fn moveStyleLayer( - self: *MapHandle, - layer_id: []const u8, - before_layer_id: []const u8, - ) status.Error!void { - try status.checkStatus( - c.mln_map_move_style_layer(try native(self), stringView(layer_id), stringView(before_layer_id)), - diagnosticStore(self), - ); + /// Accepts an ordered layer-removal command and returns its runtime-wide + /// ID. The command's finished event reports a committed removal, and a + /// `NotFound` failure when no layer has the ID. + pub fn removeStyleLayer(self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8) status.Error!completion.Future(completion.CommandCompletion) { + return self.removeStyleObjectCommand(allocator, layer_id, c.mln_map_remove_style_layer); } - pub fn getStyleLayerJson( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - ) status.Error!?values.OwnedString { - var buffer: c.mln_buffer = 0; - var found = false; - try status.checkStatus( - c.mln_map_get_style_layer_json(try native(self), stringView(layer_id), &buffer, &found), - diagnosticStore(self), - ); - if (!found) return null; - return try native_temp.copyOwnedBuffer(allocator, buffer, diagnosticStore(self)) orelse error.NativeError; + pub fn moveStyleLayer(self: *MapHandle, layer_id: []const u8, before_layer_id: []const u8) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_move_style_layer, .{ + try native(self), + stringView(layer_id), + stringView(before_layer_id), + }); } - pub fn getStyleLayerType( - self: *MapHandle, - allocator: std.mem.Allocator, - layer_id: []const u8, - ) status.Error!?values.OwnedString { - var layer_type = c.mln_buffer_view{ .data = null, .size = 0 }; - var found = false; - try status.checkStatus( - c.mln_map_get_style_layer_type(try native(self), stringView(layer_id), &layer_type, &found), - diagnosticStore(self), - ); - if (!found) return null; - const copied = if (layer_type.size == 0) try allocator.dupe(u8, "") else blk: { - const data: [*]const u8 = @ptrCast(layer_type.data orelse return error.NativeError); - break :blk try allocator.dupe(u8, data[0..layer_type.size]); - }; - return .{ .allocator = allocator, .value = copied }; + pub fn getStyleLayerJson(self: *MapHandle, allocator: std.mem.Allocator, layer_id: []const u8) status.Error!completion.Future(?values.OwnedString) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_get_style_layer_json, .{ try native(self), try temp.stringView(layer_id) }); } - pub fn setStyleLightJson(self: *MapHandle, allocator: std.mem.Allocator, value: []const u8) status.Error!void { + pub fn setStyleLightJson(self: *MapHandle, allocator: std.mem.Allocator, value: []const u8) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_style_light_json(try native(self), try temp.stringView(value)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_style_light_json, .{ try native(self), try temp.stringView(value) }); } pub fn setStyleLightProperty( @@ -965,52 +729,33 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, property_name: []const u8, value: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_style_light_property(try native(self), try temp.stringView(property_name), try temp.stringView(value)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_style_light_property, .{ + try native(self), + try temp.stringView(property_name), + try temp.stringView(value), + }); } - pub fn getStyleLightProperty( - self: *MapHandle, - allocator: std.mem.Allocator, - property_name: []const u8, - ) status.Error!?values.OwnedString { + pub fn getStyleLightProperty(self: *MapHandle, allocator: std.mem.Allocator, property_name: []const u8) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var buffer: c.mln_buffer = 0; - try status.checkStatus( - c.mln_map_get_style_light_property(try native(self), try temp.stringView(property_name), &buffer), - diagnosticStore(self), - ); - return try native_temp.copyOwnedBuffer(allocator, buffer, diagnosticStore(self)); + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_get_style_light_property, .{ try native(self), try temp.stringView(property_name) }); } - /// Sets the style's global transition options, replacing the whole - /// configuration: absent duration and delay clear the style-wide override. - /// Loading a style replaces these options, so apply an override afterwards. - pub fn setStyleTransitionOptions( - self: *MapHandle, - options: values.StyleTransitionOptions, - ) status.Error!void { + pub fn setStyleTransitionOptions(self: *MapHandle, options: values.StyleTransitionOptions) status.Error!completion.Future(completion.CommandCompletion) { const raw = values.styleTransitionOptionsToNative(options); - try status.checkStatus( - c.mln_map_set_style_transition_options(try native(self), &raw), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_style_transition_options, .{ try native(self), &raw }); } - /// Reads the style's global transition options. - pub fn getStyleTransitionOptions(self: *MapHandle) status.Error!values.StyleTransitionOptions { - var raw = c.mln_style_transition_options_default(); - try status.checkStatus( - c.mln_map_get_style_transition_options(try native(self), &raw), - diagnosticStore(self), - ); - return values.styleTransitionOptionsFromNative(raw); + pub fn getStyleTransitionOptions(self: *MapHandle) status.Error!completion.Future(values.StyleTransitionOptions) { + return submitQuery(values.StyleTransitionOptions, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!values.StyleTransitionOptions { + return values.styleTransitionOptionsFromNative(try completion.value(c.mln_style_transition_options)(result)); + } + }.copyResult, c.mln_map_get_style_transition_options, .{try native(self)}); } pub fn addVectorSourceUrl( @@ -1019,14 +764,11 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, url: []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_vector_source_url(try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_vector_source_url, .{ try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null }); } pub fn addVectorSourceTiles( @@ -1035,15 +777,12 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tiles: []const []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_tiles = try temp.stringViews(tiles); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_vector_source_tiles(try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_vector_source_tiles, .{ try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null }); } pub fn addRasterSourceUrl( @@ -1052,14 +791,11 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, url: []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_raster_source_url(try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_raster_source_url, .{ try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null }); } pub fn addRasterSourceTiles( @@ -1068,15 +804,12 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tiles: []const []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_tiles = try temp.stringViews(tiles); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_raster_source_tiles(try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_raster_source_tiles, .{ try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null }); } pub fn addRasterDemSourceUrl( @@ -1085,14 +818,11 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, url: []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_raster_dem_source_url(try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_raster_dem_source_url, .{ try native(self), try temp.stringView(source_id), try temp.stringView(url), if (options != null) &raw_options else null }); } pub fn addRasterDemSourceTiles( @@ -1101,15 +831,12 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tiles: []const []const u8, options: ?values.StyleTileSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_tiles = try temp.stringViews(tiles); var raw_options = if (options) |value| try styleTileSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_raster_dem_source_tiles(try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_raster_dem_source_tiles, .{ try native(self), try temp.stringView(source_id), raw_tiles.ptr, raw_tiles.len, if (options != null) &raw_options else null }); } pub fn addHillshadeLayer( @@ -1118,13 +845,10 @@ pub const MapHandle = enum(c.mln_map) { layer_id: []const u8, source_id: []const u8, before_layer_id: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_hillshade_layer(try native(self), try temp.stringView(layer_id), try temp.stringView(source_id), try temp.stringView(before_layer_id)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_hillshade_layer, .{ try native(self), try temp.stringView(layer_id), try temp.stringView(source_id), try temp.stringView(before_layer_id) }); } pub fn addColorReliefLayer( @@ -1133,13 +857,10 @@ pub const MapHandle = enum(c.mln_map) { layer_id: []const u8, source_id: []const u8, before_layer_id: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_color_relief_layer(try native(self), try temp.stringView(layer_id), try temp.stringView(source_id), try temp.stringView(before_layer_id)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_color_relief_layer, .{ try native(self), try temp.stringView(layer_id), try temp.stringView(source_id), try temp.stringView(before_layer_id) }); } pub fn setStyleImage( @@ -1148,164 +869,78 @@ pub const MapHandle = enum(c.mln_map) { image_id: []const u8, image: values.PremultipliedRgba8Image, options: ?values.StyleImageOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_image = values.premultipliedRgba8ImageToNative(image); - // Native borrows the stretch arrays for the call, so they live in this scope. - const stretch_x_count = if (options) |value| - if (value.stretch_x) |stretches| stretches.len else 0 - else - 0; - const stretch_y_count = if (options) |value| - if (value.stretch_y) |stretches| stretches.len else 0 - else - 0; + const stretch_x_count = if (options) |value| if (value.stretch_x) |stretches| stretches.len else 0 else 0; + const stretch_y_count = if (options) |value| if (value.stretch_y) |stretches| stretches.len else 0 else 0; const stretch_x = try allocator.alloc(c.mln_image_stretch, stretch_x_count); defer allocator.free(stretch_x); const stretch_y = try allocator.alloc(c.mln_image_stretch, stretch_y_count); defer allocator.free(stretch_y); - var raw_options = if (options) |value| - values.styleImageOptionsToNative(value, stretch_x, stretch_y) - else - undefined; - try status.checkStatus( - c.mln_map_set_style_image(try native(self), try temp.stringView(image_id), &raw_image, if (options != null) &raw_options else null), - diagnosticStore(self), - ); + var raw_options = if (options) |value| values.styleImageOptionsToNative(value, stretch_x, stretch_y) else undefined; + return submitCommand(self, c.mln_map_set_style_image, .{ + try native(self), + try temp.stringView(image_id), + &raw_image, + if (options != null) &raw_options else null, + }); } - /// Copies one runtime style image's stretchable intervals, or null when no - /// image carries `image_id`. pub fn copyStyleImageStretches( self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8, - ) status.Error!?values.OwnedImageStretches { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - const image_view = try temp.stringView(image_id); - - var x_count: usize = 0; - var y_count: usize = 0; - var found = false; - try status.checkStatus( - c.mln_map_copy_style_image_stretches( - try native(self), - image_view, - null, - 0, - &x_count, - null, - 0, - &y_count, - &found, - ), - diagnosticStore(self), - ); - if (!found) return null; - - const raw_x = try allocator.alloc(c.mln_image_stretch, x_count); - defer allocator.free(raw_x); - const raw_y = try allocator.alloc(c.mln_image_stretch, y_count); - defer allocator.free(raw_y); - try status.checkStatus( - c.mln_map_copy_style_image_stretches( - try native(self), - image_view, - if (raw_x.len == 0) null else raw_x.ptr, - raw_x.len, - &x_count, - if (raw_y.len == 0) null else raw_y.ptr, - raw_y.len, - &y_count, - &found, - ), - diagnosticStore(self), - ); - - const stretch_x = try allocator.alloc(values.ImageStretch, x_count); - errdefer allocator.free(stretch_x); - const stretch_y = try allocator.alloc(values.ImageStretch, y_count); - errdefer allocator.free(stretch_y); - for (raw_x, 0..) |stretch, index| { - stretch_x[index] = .{ .from = stretch.from, .to = stretch.to }; - } - for (raw_y, 0..) |stretch, index| { - stretch_y[index] = .{ .from = stretch.from, .to = stretch.to }; - } - return .{ .allocator = allocator, .stretch_x = stretch_x, .stretch_y = stretch_y }; - } - - pub fn removeStyleImage(self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8) status.Error!bool { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - var removed = false; - try status.checkStatus( - c.mln_map_remove_style_image(try native(self), try temp.stringView(image_id), &removed), - diagnosticStore(self), - ); - return removed; + ) status.Error!completion.Future(?values.OwnedImageStretches) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitAllocatedQuery(?values.OwnedImageStretches, self, allocator, struct { + fn copyResult(result: *const c.mln_completion_result, target: *std.mem.Allocator) status.Error!?values.OwnedImageStretches { + const raw = (try optionalCompletionValue(c.mln_style_image_stretches_result, result)) orelse return null; + const stretch_x = try target.alloc(values.ImageStretch, raw.stretch_x_count); + errdefer target.free(stretch_x); + const stretch_y = try target.alloc(values.ImageStretch, raw.stretch_y_count); + errdefer target.free(stretch_y); + if (raw.stretch_x_count != 0) { + const source = raw.stretch_x orelse return error.NativeError; + for (source[0..raw.stretch_x_count], stretch_x) |stretch, *copy| copy.* = .{ .from = stretch.from, .to = stretch.to }; + } + if (raw.stretch_y_count != 0) { + const source = raw.stretch_y orelse return error.NativeError; + for (source[0..raw.stretch_y_count], stretch_y) |stretch, *copy| copy.* = .{ .from = stretch.from, .to = stretch.to }; + } + return .{ .allocator = target.*, .stretch_x = stretch_x, .stretch_y = stretch_y }; + } + }.copyResult, c.mln_map_copy_style_image_stretches, .{ try native(self), try temp.stringView(image_id) }); } - pub fn styleImageExists(self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8) status.Error!bool { - var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - var exists = false; - try status.checkStatus( - c.mln_map_style_image_exists(try native(self), try temp.stringView(image_id), &exists), - diagnosticStore(self), - ); - return exists; + /// Accepts an ordered image-removal command and returns its runtime-wide + /// ID. The command's finished event reports a committed removal, and a + /// `NotFound` failure when no runtime style image has the ID. + pub fn removeStyleImage(self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8) status.Error!completion.Future(completion.CommandCompletion) { + return self.removeStyleObjectCommand(allocator, image_id, c.mln_map_remove_style_image); } - pub fn getStyleImageInfo( - self: *MapHandle, - allocator: std.mem.Allocator, - image_id: []const u8, - ) status.Error!?values.StyleImageInfo { + pub fn getStyleImageInfo(self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8) status.Error!completion.Future(?values.StyleImageInfo) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var info = c.mln_style_image_info_default(); - var found = false; - try status.checkStatus( - c.mln_map_get_style_image_info(try native(self), try temp.stringView(image_id), &info, &found), - diagnosticStore(self), - ); - if (!found) return null; - return values.styleImageInfoFromNative(info); + return submitQuery(?values.StyleImageInfo, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!?values.StyleImageInfo { + const raw = (try optionalCompletionValue(c.mln_style_image_result, result)) orelse return null; + return values.styleImageInfoFromNative(raw.info); + } + }.copyResult, c.mln_map_get_style_image_info, .{ try native(self), try temp.stringView(image_id) }); } pub fn copyStyleImagePremultipliedRgba8( self: *MapHandle, allocator: std.mem.Allocator, image_id: []const u8, - ) status.Error!?values.OwnedStyleImage { - const info = (try self.getStyleImageInfo(allocator, image_id)) orelse return null; - const pixels = try allocator.alloc(u8, info.byte_length); - errdefer allocator.free(pixels); - + ) status.Error!completion.Future(?values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var copied_size: usize = 0; - var found = false; - try status.checkStatus( - c.mln_map_copy_style_image_premultiplied_rgba8( - try native(self), - try temp.stringView(image_id), - if (pixels.len == 0) null else pixels.ptr, - pixels.len, - &copied_size, - &found, - ), - diagnosticStore(self), - ); - if (!found) { - allocator.free(pixels); - return null; - } - if (copied_size != pixels.len) return error.NativeError; - return .{ .allocator = allocator, .info = info, .pixels = pixels }; + return submitAllocatedQuery(?values.OwnedString, self, allocator, copyOptionalOwnedStringResult, c.mln_map_copy_style_image_premultiplied_rgba8, .{ try native(self), try temp.stringView(image_id) }); } pub fn addImageSourceUrl( @@ -1314,20 +949,17 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, coordinates: [4]values.LatLng, url: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_coordinates = try temp.latLngs(&coordinates); - try status.checkStatus( - c.mln_map_add_image_source_url( - try native(self), - try temp.stringView(source_id), - raw_coordinates.ptr, - raw_coordinates.len, - try temp.stringView(url), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_image_source_url, .{ + try native(self), + try temp.stringView(source_id), + raw_coordinates.ptr, + raw_coordinates.len, + try temp.stringView(url), + }); } pub fn addImageSourceImage( @@ -1336,98 +968,66 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, coordinates: [4]values.LatLng, image: values.PremultipliedRgba8Image, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_coordinates = try temp.latLngs(&coordinates); var raw_image = values.premultipliedRgba8ImageToNative(image); - try status.checkStatus( - c.mln_map_add_image_source_image( - try native(self), - try temp.stringView(source_id), - raw_coordinates.ptr, - raw_coordinates.len, - &raw_image, - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_image_source_image, .{ + try native(self), + try temp.stringView(source_id), + raw_coordinates.ptr, + raw_coordinates.len, + &raw_image, + }); } - pub fn setImageSourceUrl( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - url: []const u8, - ) status.Error!void { + pub fn setImageSourceUrl(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8, url: []const u8) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_image_source_url(try native(self), try temp.stringView(source_id), try temp.stringView(url)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_image_source_url, .{ + try native(self), + try temp.stringView(source_id), + try temp.stringView(url), + }); } - pub fn setImageSourceImage( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - image: values.PremultipliedRgba8Image, - ) status.Error!void { + pub fn setImageSourceImage(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8, image: values.PremultipliedRgba8Image) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_image = values.premultipliedRgba8ImageToNative(image); - try status.checkStatus( - c.mln_map_set_image_source_image(try native(self), try temp.stringView(source_id), &raw_image), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_image_source_image, .{ + try native(self), + try temp.stringView(source_id), + &raw_image, + }); } - pub fn setImageSourceCoordinates( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - coordinates: [4]values.LatLng, - ) status.Error!void { + pub fn setImageSourceCoordinates(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8, coordinates: [4]values.LatLng) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); - defer temp.deinit(); - const raw_coordinates = try temp.latLngs(&coordinates); - try status.checkStatus( - c.mln_map_set_image_source_coordinates( - try native(self), - try temp.stringView(source_id), - raw_coordinates.ptr, - raw_coordinates.len, - ), - diagnosticStore(self), - ); + defer temp.deinit(); + const raw_coordinates = try temp.latLngs(&coordinates); + return submitCommand(self, c.mln_map_set_image_source_coordinates, .{ + try native(self), + try temp.stringView(source_id), + raw_coordinates.ptr, + raw_coordinates.len, + }); } - pub fn getImageSourceCoordinates( - self: *MapHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - ) status.Error!?[4]values.LatLng { + pub fn getImageSourceCoordinates(self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8) status.Error!completion.Future(?[4]values.LatLng) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - var raw_coordinates: [4]c.mln_lat_lng = undefined; - var coordinate_count: usize = 0; - var found = false; - try status.checkStatus( - c.mln_map_get_image_source_coordinates( - try native(self), - try temp.stringView(source_id), - &raw_coordinates, - raw_coordinates.len, - &coordinate_count, - &found, - ), - diagnosticStore(self), - ); - if (!found) return null; - if (coordinate_count != raw_coordinates.len) return error.NativeError; - var coordinates: [4]values.LatLng = undefined; - for (raw_coordinates, &coordinates) |raw_coordinate, *coordinate| coordinate.* = values.latLngFromNative(raw_coordinate); - return coordinates; + return submitQuery(?[4]values.LatLng, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!?[4]values.LatLng { + if (result.value_count == 0) return null; + if (result.value_count != 4) return error.NativeError; + const raw_coordinates = @as([*]align(1) const c.mln_lat_lng, @ptrCast(result.value orelse return error.NativeError))[0..4]; + var coordinates: [4]values.LatLng = undefined; + for (raw_coordinates, &coordinates) |raw, *coordinate| coordinate.* = values.latLngFromNative(raw); + return coordinates; + } + }.copyResult, c.mln_map_get_image_source_coordinates, .{ try native(self), try temp.stringView(source_id) }); } pub fn addLocationIndicatorLayer( @@ -1435,13 +1035,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_id: []const u8, before_layer_id: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_location_indicator_layer(try native(self), try temp.stringView(layer_id), try temp.stringView(before_layer_id)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_location_indicator_layer, .{ try native(self), try temp.stringView(layer_id), try temp.stringView(before_layer_id) }); } pub fn setLocationIndicatorLocation( @@ -1450,18 +1047,15 @@ pub const MapHandle = enum(c.mln_map) { layer_id: []const u8, coordinate: values.LatLng, altitude: f64, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_location_indicator_location( - try native(self), - try temp.stringView(layer_id), - values.latLngToNative(coordinate), - altitude, - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_location_indicator_location, .{ + try native(self), + try temp.stringView(layer_id), + values.latLngToNative(coordinate), + altitude, + }); } pub fn setLocationIndicatorBearing( @@ -1469,13 +1063,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_id: []const u8, bearing: f64, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_location_indicator_bearing(try native(self), try temp.stringView(layer_id), bearing), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_location_indicator_bearing, .{ try native(self), try temp.stringView(layer_id), bearing }); } pub fn setLocationIndicatorAccuracyRadius( @@ -1483,13 +1074,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, layer_id: []const u8, radius: f64, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_location_indicator_accuracy_radius(try native(self), try temp.stringView(layer_id), radius), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_location_indicator_accuracy_radius, .{ try native(self), try temp.stringView(layer_id), radius }); } pub fn setLocationIndicatorImageName( @@ -1498,18 +1086,15 @@ pub const MapHandle = enum(c.mln_map) { layer_id: []const u8, kind: values.LocationIndicatorImageKind, image_id: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_location_indicator_image_name( - try native(self), - try temp.stringView(layer_id), - values.locationIndicatorImageKindToNative(kind), - try temp.stringView(image_id), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_location_indicator_image_name, .{ + try native(self), + try temp.stringView(layer_id), + values.locationIndicatorImageKindToNative(kind), + try temp.stringView(image_id), + }); } /// Adds a GeoJSON source with prepared inline data. The call borrows @@ -1520,35 +1105,26 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, data: GeoJsonSourceDataHandle, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_add_geojson_source_data( - try native(self), - try temp.stringView(source_id), - @intFromEnum(data), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_geojson_source_data, .{ try native(self), try temp.stringView(source_id), @intFromEnum(data) }); } /// Updates one GeoJSON source with prepared inline data. The call borrows /// `data`, which must have been prepared with options equal to the options - /// the source was added with, `cluster_properties` excepted; a mismatch is - /// rejected. + /// the source was added with; a mismatch is rejected. Cluster aggregation + /// expressions compare by parsed equality, so equivalent + /// `cluster_properties` JSON matches regardless of formatting. pub fn setGeoJsonSourceData( self: *MapHandle, allocator: std.mem.Allocator, source_id: []const u8, data: GeoJsonSourceDataHandle, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_geojson_source_data(try native(self), try temp.stringView(source_id), @intFromEnum(data)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_geojson_source_data, .{ try native(self), try temp.stringView(source_id), @intFromEnum(data) }); } /// Overrides one GeoJSON source's synchronous tiling at runtime. While @@ -1560,13 +1136,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, enabled: bool, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_geojson_source_synchronous_tiling(try native(self), try temp.stringView(source_id), enabled), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_geojson_source_synchronous_tiling, .{ try native(self), try temp.stringView(source_id), enabled }); } /// Adds a GeoJSON source that loads from a URL. The options are fixed at @@ -1578,19 +1151,16 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, url: []const u8, options: ?values.StyleGeoJsonSourceOptions, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_options = if (options) |value| try styleGeoJsonSourceOptionsToNative(&temp, value) else undefined; - try status.checkStatus( - c.mln_map_add_geojson_source_url( - try native(self), - try temp.stringView(source_id), - try temp.stringView(url), - if (options != null) &raw_options else null, - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_add_geojson_source_url, .{ + try native(self), + try temp.stringView(source_id), + try temp.stringView(url), + if (options != null) &raw_options else null, + }); } pub fn setGeoJsonSourceUrl( @@ -1598,13 +1168,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, url: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_geojson_source_url(try native(self), try temp.stringView(source_id), try temp.stringView(url)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_geojson_source_url, .{ try native(self), try temp.stringView(source_id), try temp.stringView(url) }); } pub fn addCustomGeometrySource( @@ -1612,10 +1179,7 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, options: CustomGeometrySourceOptions, - ) status.Error!void { - _ = try native(self); - const map_state = try mapStateForHandle(self); - + ) status.Error!completion.Future(completion.CommandCompletion) { const source_state = try std.heap.smp_allocator.create(CustomGeometrySourceState); source_state.* = .{ .fetch_tile = options.fetch_tile, @@ -1633,10 +1197,7 @@ pub const MapHandle = enum(c.mln_map) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var native_options = customGeometrySourceOptionsToNative(options, source_state); - try status.checkStatus( - c.mln_map_add_custom_geometry_source(try native(self), try temp.stringView(source_id), &native_options), - map_state.diagnostic_store, - ); + return submitCommand(self, c.mln_map_add_custom_geometry_source, .{ try native(self), try temp.stringView(source_id), &native_options }); } pub fn setCustomGeometrySourceTileData( @@ -1645,18 +1206,15 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tile_id: CanonicalTileId, data: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_custom_geometry_source_tile_data( - try native(self), - try temp.stringView(source_id), - canonicalTileIdToNative(tile_id), - try temp.stringView(data), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_custom_geometry_source_tile_data, .{ + try native(self), + try temp.stringView(source_id), + canonicalTileIdToNative(tile_id), + try temp.stringView(data), + }); } pub fn invalidateCustomGeometrySourceTile( @@ -1664,13 +1222,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, tile_id: CanonicalTileId, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_invalidate_custom_geometry_source_tile(try native(self), try temp.stringView(source_id), canonicalTileIdToNative(tile_id)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_invalidate_custom_geometry_source_tile, .{ try native(self), try temp.stringView(source_id), canonicalTileIdToNative(tile_id) }); } pub fn invalidateCustomGeometrySourceRegion( @@ -1678,13 +1233,10 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, bounds: values.LatLngBounds, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_invalidate_custom_geometry_source_region(try native(self), try temp.stringView(source_id), values.latLngBoundsToNative(bounds)), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_invalidate_custom_geometry_source_region, .{ try native(self), try temp.stringView(source_id), values.latLngBoundsToNative(bounds) }); } pub fn addCustomMvtVectorSource( @@ -1692,10 +1244,7 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, options: CustomMvtVectorSourceOptions, - ) status.Error!void { - _ = try native(self); - const map_state = try mapStateForHandle(self); - + ) status.Error!completion.Future(completion.CommandCompletion) { const source_state = try std.heap.smp_allocator.create(CustomMvtVectorSourceState); source_state.* = .{ .fetch_tile = options.fetch_tile, @@ -1707,15 +1256,13 @@ pub const MapHandle = enum(c.mln_map) { errdefer std.heap.smp_allocator.destroy(source_state); try registerLiveCustomMvtVectorSourceState(source_state); + // A failed add releases nothing, so this call owns the state it built. errdefer unregisterLiveCustomMvtVectorSourceState(source_state); var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var native_options = customMvtVectorSourceOptionsToNative(options, source_state); - try status.checkStatus( - c.mln_map_add_custom_mvt_vector_source(try native(self), try temp.stringView(source_id), &native_options), - map_state.diagnostic_store, - ); + return submitCommand(self, c.mln_map_add_custom_mvt_vector_source, .{ try native(self), try temp.stringView(source_id), &native_options }); } pub fn setCustomMvtVectorSourceTileData( @@ -1724,18 +1271,15 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tile_id: CanonicalTileId, data: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_custom_mvt_vector_source_tile_data( - try native(self), - try temp.stringView(source_id), - canonicalTileIdToNative(tile_id), - try temp.stringView(data), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_custom_mvt_vector_source_tile_data, .{ + try native(self), + try temp.stringView(source_id), + canonicalTileIdToNative(tile_id), + try temp.stringView(data), + }); } pub fn setCustomMvtVectorSourceTileError( @@ -1744,18 +1288,15 @@ pub const MapHandle = enum(c.mln_map) { source_id: []const u8, tile_id: CanonicalTileId, message: []const u8, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_set_custom_mvt_vector_source_tile_error( - try native(self), - try temp.stringView(source_id), - canonicalTileIdToNative(tile_id), - try temp.stringView(message), - ), - diagnosticStore(self), - ); + return submitCommand(self, c.mln_map_set_custom_mvt_vector_source_tile_error, .{ + try native(self), + try temp.stringView(source_id), + canonicalTileIdToNative(tile_id), + try temp.stringView(message), + }); } pub fn invalidateCustomMvtVectorSourceTile( @@ -1763,300 +1304,195 @@ pub const MapHandle = enum(c.mln_map) { allocator: std.mem.Allocator, source_id: []const u8, tile_id: CanonicalTileId, - ) status.Error!void { + ) status.Error!completion.Future(completion.CommandCompletion) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - try status.checkStatus( - c.mln_map_invalidate_custom_mvt_vector_source_tile(try native(self), try temp.stringView(source_id), canonicalTileIdToNative(tile_id)), - diagnosticStore(self), - ); - } - - pub fn requestRepaint(self: *MapHandle) status.Error!void { - try status.checkStatus(c.mln_map_request_repaint(try native(self)), diagnosticStore(self)); - } - - pub fn setDebugOptions(self: *MapHandle, options: values.DebugOptions) status.Error!void { - try status.checkStatus(c.mln_map_set_debug_options(try native(self), values.debugOptionsToNative(options)), diagnosticStore(self)); + return submitCommand(self, c.mln_map_invalidate_custom_mvt_vector_source_tile, .{ try native(self), try temp.stringView(source_id), canonicalTileIdToNative(tile_id) }); } - pub fn getDebugOptions(self: *MapHandle) status.Error!values.DebugOptions { - var options: u32 = 0; - try status.checkStatus(c.mln_map_get_debug_options(try native(self), &options), diagnosticStore(self)); - return values.debugOptionsFromNative(options); + pub fn requestRepaint(self: *MapHandle) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_request_repaint, .{try native(self)}); } - pub fn setRenderingStatsViewEnabled(self: *MapHandle, enabled: bool) status.Error!void { - try status.checkStatus(c.mln_map_set_rendering_stats_view_enabled(try native(self), enabled), diagnosticStore(self)); + pub fn setDebugOptions(self: *MapHandle, options: values.DebugOptions) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_set_debug_options, .{ try native(self), values.debugOptionsToNative(options) }); } - pub fn getRenderingStatsViewEnabled(self: *MapHandle) status.Error!bool { - var enabled = false; - try status.checkStatus(c.mln_map_get_rendering_stats_view_enabled(try native(self), &enabled), diagnosticStore(self)); - return enabled; + pub fn setRenderingStatsViewEnabled(self: *MapHandle, enabled: bool) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_set_rendering_stats_view_enabled, .{ try native(self), enabled }); } - pub fn isFullyLoaded(self: *MapHandle) status.Error!bool { - var loaded = false; - try status.checkStatus(c.mln_map_is_fully_loaded(try native(self), &loaded), diagnosticStore(self)); - return loaded; + pub fn snapshot(self: *MapHandle) status.Error!MapSnapshot { + var raw = std.mem.zeroes(c.mln_map_snapshot); + raw.size = @sizeOf(c.mln_map_snapshot); + try status.checkStatus(c.mln_map_snapshot_get(try native(self), &raw), diagnosticStore(self)); + return .{ + .generation = raw.generation, + .camera = values.cameraOptionsFromNative(raw.camera), + .width = raw.logical_extent.width, + .height = raw.logical_extent.height, + .scale_factor = raw.logical_extent.scale_factor, + .debug_options = values.debugOptionsFromNative(raw.debug_options), + .projection_mode = values.projectionModeFromNative(raw.projection_mode), + .viewport = try values.viewportOptionsFromNative(raw.viewport), + .tile = try values.tileOptionsFromNative(raw.tile), + .bounds = values.boundOptionsFromNative(raw.bounds), + .free_camera = values.freeCameraOptionsFromNative(raw.free_camera), + .fully_loaded = raw.fully_loaded, + .rendering_stats_view_enabled = raw.rendering_stats_view_enabled, + .repaint_demand = raw.repaint_demand, + .event_mask = runtime_module.eventMaskFromRaw(raw.event_mask), + .latest_render_update_generation = raw.latest_render_update_generation, + }; } - /// Returns the map's logical viewport size in UI pixels and its pixel - /// ratio. The scale factor is fixed for the lifetime of the map and is - /// independent of any render target's scale factor. pub fn getSize(self: *MapHandle) status.Error!struct { width: u32, height: u32, scale_factor: f64 } { - var width: u32 = 0; - var height: u32 = 0; - var scale_factor: f64 = 0; - try status.checkStatus(c.mln_map_get_size(try native(self), &width, &height, &scale_factor), diagnosticStore(self)); - return .{ .width = width, .height = height, .scale_factor = scale_factor }; + const current = try self.snapshot(); + return .{ .width = current.width, .height = current.height, .scale_factor = current.scale_factor }; } - pub fn dumpDebugLogs(self: *MapHandle) status.Error!void { - try status.checkStatus(c.mln_map_dump_debug_logs(try native(self)), diagnosticStore(self)); + pub fn resize(self: *MapHandle, width: u32, height: u32, scale_factor: f64) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_resize, .{ try native(self), c.mln_logical_extent{ .width = width, .height = height, .scale_factor = scale_factor } }); } - pub fn setViewportOptions(self: *MapHandle, options: values.ViewportOptions) status.Error!void { - var raw_options = values.viewportOptionsToNative(options); - try status.checkStatus(c.mln_map_set_viewport_options(try native(self), &raw_options), diagnosticStore(self)); + pub fn dumpDebugLogs(self: *MapHandle) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_dump_debug_logs, .{try native(self)}); } - pub fn getViewportOptions(self: *MapHandle) status.Error!values.ViewportOptions { - var options = c.mln_map_viewport_options_default(); - try status.checkStatus(c.mln_map_get_viewport_options(try native(self), &options), diagnosticStore(self)); - return try values.viewportOptionsFromNative(options); + pub fn setViewportOptions(self: *MapHandle, options: values.ViewportOptions) status.Error!completion.Future(completion.CommandCompletion) { + var raw_options = values.viewportOptionsToNative(options); + return submitCommand(self, c.mln_map_set_viewport_options, .{ try native(self), &raw_options }); } - pub fn setTileOptions(self: *MapHandle, options: values.TileOptions) status.Error!void { + pub fn setTileOptions(self: *MapHandle, options: values.TileOptions) status.Error!completion.Future(completion.CommandCompletion) { var raw_options = values.tileOptionsToNative(options); - try status.checkStatus(c.mln_map_set_tile_options(try native(self), &raw_options), diagnosticStore(self)); - } - - pub fn getTileOptions(self: *MapHandle) status.Error!values.TileOptions { - var options = c.mln_map_tile_options_default(); - try status.checkStatus(c.mln_map_get_tile_options(try native(self), &options), diagnosticStore(self)); - return try values.tileOptionsFromNative(options); + return submitCommand(self, c.mln_map_set_tile_options, .{ try native(self), &raw_options }); } - pub fn getCamera(self: *MapHandle) status.Error!values.CameraOptions { + pub fn cameraSnapshot(self: *MapHandle) status.Error!CameraSnapshot { var camera = c.mln_camera_options_default(); - try status.checkStatus(c.mln_map_get_camera(try native(self), &camera), diagnosticStore(self)); - return values.cameraOptionsFromNative(camera); - } - - pub fn jumpTo(self: *MapHandle, camera: values.CameraOptions) status.Error!void { - var raw_camera = values.cameraOptionsToNative(camera); - try status.checkStatus(c.mln_map_jump_to(try native(self), &raw_camera), diagnosticStore(self)); - } - - pub fn easeTo(self: *MapHandle, camera: values.CameraOptions, animation: ?values.AnimationOptions) status.Error!void { - var raw_camera = values.cameraOptionsToNative(camera); - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus(c.mln_map_ease_to(try native(self), &raw_camera, animation_ptr), diagnosticStore(self)); - } - - pub fn flyTo(self: *MapHandle, camera: values.CameraOptions, animation: ?values.AnimationOptions) status.Error!void { - var raw_camera = values.cameraOptionsToNative(camera); - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus(c.mln_map_fly_to(try native(self), &raw_camera, animation_ptr), diagnosticStore(self)); - } - - pub fn moveBy(self: *MapHandle, delta_x: f64, delta_y: f64) status.Error!void { - try status.checkStatus(c.mln_map_move_by(try native(self), delta_x, delta_y), diagnosticStore(self)); - } - - pub fn moveByAnimated(self: *MapHandle, delta_x: f64, delta_y: f64, animation: ?values.AnimationOptions) status.Error!void { - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus(c.mln_map_move_by_animated(try native(self), delta_x, delta_y, animation_ptr), diagnosticStore(self)); - } - - pub fn scaleBy(self: *MapHandle, scale: f64, anchor: ?values.ScreenPoint) status.Error!void { - var raw_anchor = if (anchor) |point| values.screenPointToNative(point) else undefined; - const anchor_ptr = if (anchor != null) &raw_anchor else null; - try status.checkStatus(c.mln_map_scale_by(try native(self), scale, anchor_ptr), diagnosticStore(self)); - } - - pub fn scaleByAnimated(self: *MapHandle, scale: f64, anchor: ?values.ScreenPoint, animation: ?values.AnimationOptions) status.Error!void { - var raw_anchor = if (anchor) |point| values.screenPointToNative(point) else undefined; - const anchor_ptr = if (anchor != null) &raw_anchor else null; - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus(c.mln_map_scale_by_animated(try native(self), scale, anchor_ptr, animation_ptr), diagnosticStore(self)); - } - - pub fn rotateBy(self: *MapHandle, first: values.ScreenPoint, second: values.ScreenPoint) status.Error!void { + var generation: u64 = 0; try status.checkStatus( - c.mln_map_rotate_by(try native(self), values.screenPointToNative(first), values.screenPointToNative(second)), + c.mln_map_camera_snapshot_get(try native(self), &camera, &generation), diagnosticStore(self), ); + return .{ .generation = generation, .camera = values.cameraOptionsFromNative(camera) }; } - pub fn rotateByAnimated(self: *MapHandle, first: values.ScreenPoint, second: values.ScreenPoint, animation: ?values.AnimationOptions) status.Error!void { - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus( - c.mln_map_rotate_by_animated(try native(self), values.screenPointToNative(first), values.screenPointToNative(second), animation_ptr), - diagnosticStore(self), - ); - } - - pub fn pitchBy(self: *MapHandle, pitch: f64) status.Error!void { - try status.checkStatus(c.mln_map_pitch_by(try native(self), pitch), diagnosticStore(self)); - } - - pub fn pitchByAnimated(self: *MapHandle, pitch: f64, animation: ?values.AnimationOptions) status.Error!void { - var raw_animation = if (animation) |options| values.animationOptionsToNative(options) else undefined; - const animation_ptr = if (animation != null) &raw_animation else null; - try status.checkStatus(c.mln_map_pitch_by_animated(try native(self), pitch, animation_ptr), diagnosticStore(self)); - } - - pub fn cancelTransitions(self: *MapHandle) status.Error!void { - try status.checkStatus(c.mln_map_cancel_transitions(try native(self)), diagnosticStore(self)); + pub fn updateCamera(self: *MapHandle, update: CameraUpdate) status.Error!completion.Future(completion.CommandCompletion) { + var raw = c.mln_camera_update_default(); + raw.mode = update.mode.toRaw(); + raw.camera = values.cameraOptionsToNative(update.camera); + raw.animation = values.animationOptionsToNative(update.animation); + raw.gesture_phase = update.gesture_phase.toRaw(); + return submitCommand(self, c.mln_map_update_camera, .{ try native(self), &raw }); } - pub fn setGestureInProgress(self: *MapHandle, in_progress: bool) status.Error!void { - try status.checkStatus(c.mln_map_set_gesture_in_progress(try native(self), in_progress), diagnosticStore(self)); + pub fn applyCameraDelta(self: *MapHandle, delta: CameraDelta) status.Error!completion.Future(completion.CommandCompletion) { + var raw = c.mln_camera_delta_default(); + raw.kind = delta.kind.toRaw(); + raw.offset = .{ .x = delta.offset.x, .y = delta.offset.y }; + raw.amount = delta.amount; + if (delta.anchor) |anchor| { + raw.has_anchor = true; + raw.anchor = .{ .x = anchor.x, .y = anchor.y }; + } + raw.animation = values.animationOptionsToNative(delta.animation); + return submitCommand(self, c.mln_map_apply_camera_delta, .{ try native(self), &raw }); } - pub fn isGestureInProgress(self: *MapHandle) status.Error!bool { - var in_progress = false; - try status.checkStatus(c.mln_map_is_gesture_in_progress(try native(self), &in_progress), diagnosticStore(self)); - return in_progress; + pub fn cameraQuery(self: *MapHandle) status.Error!completion.Future(CameraSnapshot) { + return submitQuery(CameraSnapshot, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!CameraSnapshot { + const raw = try completion.value(c.mln_camera_query_result)(result); + return .{ .generation = raw.generation, .camera = values.cameraOptionsFromNative(raw.camera) }; + } + }.copyResult, c.mln_map_camera_query, .{try native(self)}); } - pub fn requestStillImage(self: *MapHandle) status.Error!void { - try status.checkStatus(c.mln_map_request_still_image(try native(self)), diagnosticStore(self)); + pub fn requestStillImage(self: *MapHandle) status.Error!completion.Future(void) { + return submitQuery(void, self, completion.unit, c.mln_map_request_still_image, .{try native(self)}); } - - pub fn setProjectionMode(self: *MapHandle, mode: values.ProjectionMode) status.Error!void { + pub fn setProjectionMode(self: *MapHandle, mode: values.ProjectionMode) status.Error!completion.Future(completion.CommandCompletion) { var raw_mode = values.projectionModeToNative(mode); - try status.checkStatus(c.mln_map_set_projection_mode(try native(self), &raw_mode), diagnosticStore(self)); + return submitCommand(self, c.mln_map_set_projection_mode, .{ try native(self), &raw_mode }); } pub fn getProjectionMode(self: *MapHandle) status.Error!values.ProjectionMode { - var mode = c.mln_projection_mode_default(); - try status.checkStatus(c.mln_map_get_projection_mode(try native(self), &mode), diagnosticStore(self)); - return values.projectionModeFromNative(mode); + return (try self.snapshot()).projection_mode; } - pub fn pixelForLatLng(self: *MapHandle, coordinate: values.LatLng) status.Error!values.ScreenPoint { - var point: c.mln_screen_point = undefined; - try status.checkStatus( - c.mln_map_pixel_for_lat_lng(try native(self), values.latLngToNative(coordinate), &point), - diagnosticStore(self), - ); - return values.screenPointFromNative(point); + pub fn pixelForLatLng(self: *MapHandle, coordinate: values.LatLng) status.Error!completion.Future(values.ScreenPoint) { + return submitQuery(values.ScreenPoint, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!values.ScreenPoint { + return values.screenPointFromNative(try completion.value(c.mln_screen_point)(result)); + } + }.copyResult, c.mln_map_pixel_for_lat_lng, .{ try native(self), values.latLngToNative(coordinate) }); } - pub fn latLngForPixel(self: *MapHandle, point: values.ScreenPoint) status.Error!values.LatLng { - var coordinate: c.mln_lat_lng = undefined; - try status.checkStatus( - c.mln_map_lat_lng_for_pixel(try native(self), values.screenPointToNative(point), &coordinate), - diagnosticStore(self), - ); - return values.latLngFromNative(coordinate); + pub fn latLngForPixel(self: *MapHandle, point: values.ScreenPoint) status.Error!completion.Future(values.LatLng) { + return submitQuery(values.LatLng, self, struct { + fn copyResult(result: *const c.mln_completion_result) status.Error!values.LatLng { + return values.latLngFromNative(try completion.value(c.mln_lat_lng)(result)); + } + }.copyResult, c.mln_map_lat_lng_for_pixel, .{ try native(self), values.screenPointToNative(point) }); } - pub fn pixelsForLatLngs( - self: *MapHandle, - allocator: std.mem.Allocator, - coordinates: []const values.LatLng, - out_points: []values.ScreenPoint, - ) status.Error!void { - if (coordinates.len != out_points.len) return error.InvalidArgument; - if (coordinates.len == 0) { - try status.checkStatus(c.mln_map_pixels_for_lat_lngs(try native(self), null, 0, null), diagnosticStore(self)); - return; - } + pub fn pixelsForLatLngs(self: *MapHandle, allocator: std.mem.Allocator, coordinates: []const values.LatLng) status.Error!completion.Future(ScreenPointList) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_coordinates = try temp.latLngs(coordinates); - const raw_points = try allocator.alloc(c.mln_screen_point, out_points.len); - defer allocator.free(raw_points); - try status.checkStatus( - c.mln_map_pixels_for_lat_lngs(try native(self), raw_coordinates.ptr, raw_coordinates.len, raw_points.ptr), - diagnosticStore(self), - ); - for (raw_points, out_points) |raw_point, *out_point| out_point.* = values.screenPointFromNative(raw_point); + return submitAllocatedQuery(ScreenPointList, self, allocator, struct { + fn copyResult(result: *const c.mln_completion_result, target: *std.mem.Allocator) status.Error!ScreenPointList { + const items = try target.alloc(values.ScreenPoint, result.value_count); + errdefer target.free(items); + if (result.value_count != 0) { + const raw = @as([*]align(1) const c.mln_screen_point, @ptrCast(result.value orelse return error.NativeError))[0..result.value_count]; + for (raw, items) |point, *item| item.* = values.screenPointFromNative(point); + } + return .{ .allocator = target.*, .items = items }; + } + }.copyResult, c.mln_map_pixels_for_lat_lngs, .{ try native(self), if (raw_coordinates.len == 0) null else raw_coordinates.ptr, raw_coordinates.len }); } - pub fn latLngsForPixels( - self: *MapHandle, - allocator: std.mem.Allocator, - points: []const values.ScreenPoint, - out_coordinates: []values.LatLng, - ) status.Error!void { - if (points.len != out_coordinates.len) return error.InvalidArgument; - if (points.len == 0) { - try status.checkStatus(c.mln_map_lat_lngs_for_pixels(try native(self), null, 0, null), diagnosticStore(self)); - return; - } + pub fn latLngsForPixels(self: *MapHandle, allocator: std.mem.Allocator, points: []const values.ScreenPoint) status.Error!completion.Future(LatLngList) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_points = try temp.screenPoints(points); - const raw_coordinates = try allocator.alloc(c.mln_lat_lng, out_coordinates.len); - defer allocator.free(raw_coordinates); - try status.checkStatus( - c.mln_map_lat_lngs_for_pixels(try native(self), raw_points.ptr, raw_points.len, raw_coordinates.ptr), - diagnosticStore(self), - ); - for (raw_coordinates, out_coordinates) |raw_coordinate, *out_coordinate| out_coordinate.* = values.latLngFromNative(raw_coordinate); + return submitAllocatedQuery(LatLngList, self, allocator, struct { + fn copyResult(result: *const c.mln_completion_result, target: *std.mem.Allocator) status.Error!LatLngList { + const items = try target.alloc(values.LatLng, result.value_count); + errdefer target.free(items); + if (result.value_count != 0) { + const raw = @as([*]align(1) const c.mln_lat_lng, @ptrCast(result.value orelse return error.NativeError))[0..result.value_count]; + for (raw, items) |coordinate, *item| item.* = values.latLngFromNative(coordinate); + } + return .{ .allocator = target.*, .items = items }; + } + }.copyResult, c.mln_map_lat_lngs_for_pixels, .{ try native(self), if (raw_points.len == 0) null else raw_points.ptr, raw_points.len }); } - pub fn cameraForLatLngBounds( - self: *MapHandle, - bounds: values.LatLngBounds, - fit_options: ?values.CameraFitOptions, - ) status.Error!values.CameraOptions { + pub fn cameraForLatLngBounds(self: *MapHandle, bounds: values.LatLngBounds, fit_options: ?values.CameraFitOptions) status.Error!completion.Future(values.CameraOptions) { var raw_fit = if (fit_options) |options| values.cameraFitOptionsToNative(options) else undefined; const fit_ptr = if (fit_options != null) &raw_fit else null; - var camera = c.mln_camera_options_default(); - try status.checkStatus( - c.mln_map_camera_for_lat_lng_bounds(try native(self), values.latLngBoundsToNative(bounds), fit_ptr, &camera), - diagnosticStore(self), - ); - return values.cameraOptionsFromNative(camera); + return submitQuery(values.CameraOptions, self, copyCameraOptionsResult, c.mln_map_camera_for_lat_lng_bounds, .{ try native(self), values.latLngBoundsToNative(bounds), fit_ptr }); } - pub fn cameraForLatLngs( - self: *MapHandle, - allocator: std.mem.Allocator, - coordinates: []const values.LatLng, - fit_options: ?values.CameraFitOptions, - ) status.Error!values.CameraOptions { + pub fn cameraForLatLngs(self: *MapHandle, allocator: std.mem.Allocator, coordinates: []const values.LatLng, fit_options: ?values.CameraFitOptions) status.Error!completion.Future(values.CameraOptions) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); const raw_coordinates = try temp.latLngs(coordinates); var raw_fit = if (fit_options) |options| values.cameraFitOptionsToNative(options) else undefined; const fit_ptr = if (fit_options != null) &raw_fit else null; - var camera = c.mln_camera_options_default(); - try status.checkStatus( - c.mln_map_camera_for_lat_lngs(try native(self), if (raw_coordinates.len == 0) null else raw_coordinates.ptr, raw_coordinates.len, fit_ptr, &camera), - diagnosticStore(self), - ); - return values.cameraOptionsFromNative(camera); + return submitQuery(values.CameraOptions, self, copyCameraOptionsResult, c.mln_map_camera_for_lat_lngs, .{ try native(self), if (raw_coordinates.len == 0) null else raw_coordinates.ptr, raw_coordinates.len, fit_ptr }); } - pub fn cameraForGeometry( - self: *MapHandle, - allocator: std.mem.Allocator, - geometry: []const u8, - fit_options: ?values.CameraFitOptions, - ) status.Error!values.CameraOptions { + pub fn cameraForGeometry(self: *MapHandle, allocator: std.mem.Allocator, geometry: []const u8, fit_options: ?values.CameraFitOptions) status.Error!completion.Future(values.CameraOptions) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_fit = if (fit_options) |options| values.cameraFitOptionsToNative(options) else undefined; const fit_ptr = if (fit_options != null) &raw_fit else null; - var camera = c.mln_camera_options_default(); - try status.checkStatus( - c.mln_map_camera_for_geometry(try native(self), try temp.stringView(geometry), fit_ptr, &camera), - diagnosticStore(self), - ); - return values.cameraOptionsFromNative(camera); + return submitQuery(values.CameraOptions, self, copyCameraOptionsResult, c.mln_map_camera_for_geometry, .{ try native(self), try temp.stringView(geometry), fit_ptr }); } /// Computes geographic bounds for a camera from two viewport corners. @@ -2065,11 +1501,9 @@ pub const MapHandle = enum(c.mln_map) { /// that camera in the current viewport. When bearing and pitch are zero, the /// box equals the visible area. Those corners are the northwest and /// southeast of the viewport. Longitudes stay in -180 to 180. - pub fn latLngBoundsForCamera(self: *MapHandle, camera: values.CameraOptions) status.Error!values.LatLngBounds { + pub fn latLngBoundsForCamera(self: *MapHandle, camera: values.CameraOptions) status.Error!completion.Future(values.LatLngBounds) { var raw_camera = values.cameraOptionsToNative(camera); - var bounds: c.mln_lat_lng_bounds = undefined; - try status.checkStatus(c.mln_map_lat_lng_bounds_for_camera(try native(self), &raw_camera, &bounds), diagnosticStore(self)); - return values.latLngBoundsFromNative(bounds); + return submitQuery(values.LatLngBounds, self, copyLatLngBoundsResult, c.mln_map_lat_lng_bounds_for_camera, .{ try native(self), &raw_camera }); } /// Computes geographic bounds for a camera from the four viewport corners. @@ -2078,33 +1512,19 @@ pub const MapHandle = enum(c.mln_map) { /// encompasses the projected viewport. Longitudes unwrap onto the shortest /// path through the center. A viewport that crosses the antimeridian reports /// values outside -180 to 180. - pub fn latLngBoundsForCameraUnwrapped(self: *MapHandle, camera: values.CameraOptions) status.Error!values.LatLngBounds { + pub fn latLngBoundsForCameraUnwrapped(self: *MapHandle, camera: values.CameraOptions) status.Error!completion.Future(values.LatLngBounds) { var raw_camera = values.cameraOptionsToNative(camera); - var bounds: c.mln_lat_lng_bounds = undefined; - try status.checkStatus(c.mln_map_lat_lng_bounds_for_camera_unwrapped(try native(self), &raw_camera, &bounds), diagnosticStore(self)); - return values.latLngBoundsFromNative(bounds); + return submitQuery(values.LatLngBounds, self, copyLatLngBoundsResult, c.mln_map_lat_lng_bounds_for_camera_unwrapped, .{ try native(self), &raw_camera }); } - pub fn getBounds(self: *MapHandle) status.Error!values.BoundOptions { - var raw_options = c.mln_bound_options_default(); - try status.checkStatus(c.mln_map_get_bounds(try native(self), &raw_options), diagnosticStore(self)); - return values.boundOptionsFromNative(raw_options); - } - - pub fn setBounds(self: *MapHandle, options: values.BoundOptions) status.Error!void { + pub fn setBounds(self: *MapHandle, options: values.BoundOptions) status.Error!completion.Future(completion.CommandCompletion) { var raw_options = values.boundOptionsToNative(options); - try status.checkStatus(c.mln_map_set_bounds(try native(self), &raw_options), diagnosticStore(self)); - } - - pub fn getFreeCameraOptions(self: *MapHandle) status.Error!values.FreeCameraOptions { - var raw_options = c.mln_free_camera_options_default(); - try status.checkStatus(c.mln_map_get_free_camera_options(try native(self), &raw_options), diagnosticStore(self)); - return values.freeCameraOptionsFromNative(raw_options); + return submitCommand(self, c.mln_map_set_bounds, .{ try native(self), &raw_options }); } - pub fn setFreeCameraOptions(self: *MapHandle, options: values.FreeCameraOptions) status.Error!void { + pub fn setFreeCameraOptions(self: *MapHandle, options: values.FreeCameraOptions) status.Error!completion.Future(completion.CommandCompletion) { var raw_options = values.freeCameraOptionsToNative(options); - try status.checkStatus(c.mln_map_set_free_camera_options(try native(self), &raw_options), diagnosticStore(self)); + return submitCommand(self, c.mln_map_set_free_camera_options, .{ try native(self), &raw_options }); } /// Selects which map-originated event types this map queues. @@ -2118,27 +1538,55 @@ pub const MapHandle = enum(c.mln_map) { /// /// Narrowing gates later events and keeps queued ones, so a host drains what /// it already caused. - pub fn setEventMask(self: *MapHandle, mask: runtime_module.RuntimeEventMask) status.Error!void { - const map_state = try mapStateForHandle(self); - try status.checkStatus( - c.mln_map_set_event_mask(try native(self), runtime_module.eventMaskToRaw(mask)), - map_state.diagnostic_store, - ); + pub fn setEventMask(self: *MapHandle, mask: runtime_module.RuntimeEventMask) status.Error!completion.Future(completion.CommandCompletion) { + return submitCommand(self, c.mln_map_set_event_mask, .{ try native(self), runtime_module.eventMaskToRaw(mask) }); } - /// Reports which map-originated event types this map queues. A map that has - /// not been narrowed reports `RuntimeEventMask.all`. pub fn eventMask(self: *MapHandle) status.Error!runtime_module.RuntimeEventMask { - const map_state = try mapStateForHandle(self); - var raw: u64 = 0; - try status.checkStatus( - c.mln_map_get_event_mask(try native(self), &raw), - map_state.diagnostic_store, - ); - return runtime_module.eventMaskFromRaw(raw); + return (try self.snapshot()).event_mask; + } + + fn removeStyleObjectCommand( + self: *MapHandle, + allocator: std.mem.Allocator, + object_id: []const u8, + comptime command: *const fn (c.mln_map, c.mln_buffer_view, [*c]const c.mln_completion) callconv(.c) c.mln_status, + ) status.Error!completion.Future(completion.CommandCompletion) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitCommand(self, command, .{ try native(self), try temp.stringView(object_id) }); + } + + fn setLayerStringCommand( + self: *MapHandle, + allocator: std.mem.Allocator, + layer_id: []const u8, + value: []const u8, + comptime command: *const fn (c.mln_map, c.mln_buffer_view, c.mln_buffer_view, [*c]const c.mln_completion) callconv(.c) c.mln_status, + ) status.Error!completion.Future(completion.CommandCompletion) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitCommand(self, command, .{ try native(self), try temp.stringView(layer_id), try temp.stringView(value) }); + } + + fn setLayerNumberCommand( + self: *MapHandle, + allocator: std.mem.Allocator, + layer_id: []const u8, + value: f64, + comptime command: *const fn (c.mln_map, c.mln_buffer_view, f64, [*c]const c.mln_completion) callconv(.c) c.mln_status, + ) status.Error!completion.Future(completion.CommandCompletion) { + var temp = native_temp.TempStorage.init(allocator); + defer temp.deinit(); + return submitCommand(self, command, .{ try native(self), try temp.stringView(layer_id), value }); } pub fn close(self: *MapHandle) status.Error!void { + var teardown = try self.closeAsync(); + teardown.deinit(); + } + + pub fn closeAsync(self: *MapHandle) status.Error!completion.Future(void) { const map_close = beginMapClose(self.*) catch |err| { if (err == error.InvalidState) { if (diagnosticStore(self)) |store| { @@ -2146,14 +1594,19 @@ pub const MapHandle = enum(c.mln_map) { } } return err; - } orelse return; - status.checkStatus(c.mln_map_destroy(map_close.native), map_close.diagnostic_store) catch |err| { + } orelse return completion.completed(void, {}); + const teardown = completion.submit(void, map_close.diagnostic_store, completion.unit, map_close.native, struct { + fn start(raw_map: c.mln_map, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_map_release(raw_map, descriptor); + } + }.start) catch |err| { cancelMapClose(map_close.state); return err; }; runtime_module.unregisterMap(map_close.runtime_registry, map_close.native); const map_state = finishMapClose(self.*) orelse map_close.state; std.heap.smp_allocator.destroy(map_state); + return teardown; } }; @@ -2355,7 +1808,7 @@ fn canonicalTileIdFromNative(tile_id: c.mln_canonical_tile_id) CanonicalTileId { return .{ .z = tile_id.z, .x = tile_id.x, .y = tile_id.y }; } -// The C API invokes this once, on the map owner thread, after it stops +// The C API invokes this once on the runtime worker after it stops // referencing the state: on an explicit removal, when a style load leaves a // style without the source, and when the map is destroyed. fn customGeometryReleaseTrampoline(user_data: ?*anyopaque) callconv(.c) void { @@ -2560,6 +2013,91 @@ fn unlockMapRegistry() void { map_registry_lock.store(false, .seq_cst); } +fn submitCommand(handle: *MapHandle, comptime command: anytype, arguments: anytype) status.Error!completion.Future(completion.CommandCompletion) { + const Arguments = @TypeOf(arguments); + return completion.submit(completion.CommandCompletion, diagnosticStore(handle), completion.command, arguments, struct { + fn start(args: Arguments, descriptor: *const c.mln_completion) c.mln_status { + return @call(.auto, command, args ++ .{descriptor}); + } + }.start); +} + +fn submitQuery( + comptime T: type, + handle: *MapHandle, + comptime copy: *const fn (*const c.mln_completion_result) status.Error!T, + comptime query: anytype, + arguments: anytype, +) status.Error!completion.Future(T) { + const Arguments = @TypeOf(arguments); + return completion.submit(T, diagnosticStore(handle), copy, arguments, struct { + fn start(args: Arguments, descriptor: *const c.mln_completion) c.mln_status { + return @call(.auto, query, args ++ .{descriptor}); + } + }.start); +} + +fn submitAllocatedQuery( + comptime T: type, + handle: *MapHandle, + allocator: std.mem.Allocator, + comptime copy: *const fn (*const c.mln_completion_result, *std.mem.Allocator) status.Error!T, + comptime query: anytype, + arguments: anytype, +) status.Error!completion.Future(T) { + const Arguments = @TypeOf(arguments); + return completion.submitWithCopyContext(T, std.mem.Allocator, diagnosticStore(handle), copy, allocator, arguments, struct { + fn start(args: Arguments, descriptor: *const c.mln_completion) c.mln_status { + return @call(.auto, query, args ++ .{descriptor}); + } + }.start); +} + +fn optionalCompletionValue(comptime T: type, result: *const c.mln_completion_result) status.Error!?T { + if (result.value_count == 0) return null; + return try completion.value(T)(result); +} + +fn copyView(allocator: std.mem.Allocator, view: c.mln_buffer_view) status.Error![]const u8 { + if (view.size == 0) return allocator.dupe(u8, ""); + const data = view.data orelse return error.NativeError; + return allocator.dupe(u8, @as([*]const u8, @ptrCast(data))[0..view.size]); +} + +fn copyOwnedStringResult(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!values.OwnedString { + const view = try completion.value(c.mln_buffer_view)(result); + return .{ .allocator = allocator.*, .value = try copyView(allocator.*, view) }; +} + +fn copyOptionalOwnedStringResult(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!?values.OwnedString { + const view = (try optionalCompletionValue(c.mln_buffer_view, result)) orelse return null; + return .{ .allocator = allocator.*, .value = try copyView(allocator.*, view) }; +} + +fn copyStringListResult(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!values.StringList { + const items = try allocator.alloc([]const u8, result.value_count); + errdefer allocator.free(items); + var initialized: usize = 0; + errdefer for (items[0..initialized]) |item| allocator.free(item); + if (result.value_count != 0) { + const pointer = result.value orelse return error.NativeError; + const views = @as([*]align(1) const c.mln_buffer_view, @ptrCast(pointer))[0..result.value_count]; + for (views, items) |view, *item| { + item.* = try copyView(allocator.*, view); + initialized += 1; + } + } + return .{ .allocator = allocator.*, .items = items }; +} + +fn copyCameraOptionsResult(result: *const c.mln_completion_result) status.Error!values.CameraOptions { + return values.cameraOptionsFromNative(try completion.value(c.mln_camera_options)(result)); +} + +fn copyLatLngBoundsResult(result: *const c.mln_completion_result) status.Error!values.LatLngBounds { + return values.latLngBoundsFromNative(try completion.value(c.mln_lat_lng_bounds)(result)); +} + fn featureStateSelectorToNative( temp: *native_temp.TempStorage, selector: FeatureStateSelector, @@ -2608,6 +2146,7 @@ pub fn registerRenderSession(handle: *MapHandle) status.BindingError!RenderSessi return .{ .native = @intFromEnum(handle.*), .diagnostic_store = map_state.diagnostic_store, + .runtime = map_state.runtime, }; } @@ -2768,24 +2307,29 @@ const test_style_json = ; fn createLoadedMapForTesting(runtime: *RuntimeHandle) !MapHandle { - var map = try MapHandle.create(runtime, .{}); + var map_future = try MapHandle.create(runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); errdefer map.close() catch {}; - try map.setStyleJson(std.testing.allocator, test_style_json); + _ = try map.setStyleJson(std.testing.allocator, test_style_json); try std.testing.expect(try waitForRuntimeEventForTesting(runtime, .map_style_loaded)); return map; } +fn waitForCommandDispositionForTesting(runtime: *RuntimeHandle, future_value: completion.Future(completion.CommandCompletion)) !completion.CommandDisposition { + _ = runtime; + var future = future_value; + defer future.deinit(); + return (try future.wait(null)).disposition; +} + fn waitForRuntimeEventForTesting(runtime: *RuntimeHandle, event_type: runtime_module.RuntimeEventType) !bool { var attempts: usize = 0; while (attempts < 200) : (attempts += 1) { - try runtime.pump(0, null); - // One event per drain, so an event this wait is not looking for stays - // queued rather than being dropped with the batch that carried it. - while (true) { - var batch = try runtime.drainEvents(std.testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) break; - const event = try batch.at(0); + var batch = try runtime.drainEvents(std.testing.allocator); + defer batch.deinit(); + for (0..batch.len()) |index| { + const event = try batch.at(index); if (std.meta.eql(event.event_type, event_type)) return true; } try std.testing.io.sleep(.fromMilliseconds(10), .awake); @@ -2793,24 +2337,6 @@ fn waitForRuntimeEventForTesting(runtime: *RuntimeHandle, event_type: runtime_mo return false; } -// A map whose mask clears style-loaded reaches a loaded style with no event to -// wait for, so the style's own sources report the load instead. -fn waitForStyleSourceForTesting( - runtime: *RuntimeHandle, - map: *MapHandle, - source_id: []const u8, -) !bool { - var attempts: usize = 0; - while (attempts < 200) : (attempts += 1) { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(std.testing.allocator, 0); - batch.deinit(); - if (try map.styleSourceExists(std.testing.allocator, source_id)) return true; - try std.testing.io.sleep(.fromMilliseconds(10), .awake); - } - return false; -} - fn testStyleJsonProvider( context: ?*anyopaque, request: runtime_module.ResourceRequest, @@ -2831,37 +2357,41 @@ fn testStyleJsonProvider( // the tests below watch the live-state count rather than a style load. test "an explicit source removal releases the callback state" { var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); var map = try createLoadedMapForTesting(&runtime); defer map.close() catch @panic("map close failed"); const baseline = liveCustomGeometrySourceCountForTesting(); var state = TestCustomGeometryCallbackState{}; - try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ + _ = try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ .fetch_tile = testFetchCustomGeometryTile, .context = &state, }); try std.testing.expectEqual(baseline + 1, liveCustomGeometrySourceCountForTesting()); - try std.testing.expect(try map.removeStyleSource(std.testing.allocator, "custom")); + const remove_id = try map.removeStyleSource(std.testing.allocator, "custom"); + const disposition = try waitForCommandDispositionForTesting(&runtime, remove_id); + try std.testing.expect(std.meta.eql(disposition, completion.CommandDisposition.committed)); try std.testing.expectEqual(baseline, liveCustomGeometrySourceCountForTesting()); } test "an explicit custom MVT vector source removal releases the callback state" { var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); var map = try createLoadedMapForTesting(&runtime); defer map.close() catch @panic("map close failed"); const baseline = liveCustomMvtVectorSourceCountForTesting(); var state = TestCustomGeometryCallbackState{}; - try map.addCustomMvtVectorSource(std.testing.allocator, "custom-mvt", .{ + _ = try map.addCustomMvtVectorSource(std.testing.allocator, "custom-mvt", .{ .fetch_tile = testFetchCustomGeometryTile, .context = &state, }); try std.testing.expectEqual(baseline + 1, liveCustomMvtVectorSourceCountForTesting()); - try std.testing.expect(try map.removeStyleSource(std.testing.allocator, "custom-mvt")); + const remove_id = try map.removeStyleSource(std.testing.allocator, "custom-mvt"); + const disposition = try waitForCommandDispositionForTesting(&runtime, remove_id); + try std.testing.expect(std.meta.eql(disposition, completion.CommandDisposition.committed)); try std.testing.expectEqual(baseline, liveCustomMvtVectorSourceCountForTesting()); } @@ -2876,31 +2406,34 @@ test "a style load that drops a source releases the callback state unsubscribed" }; var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - try runtime.setResourceProvider(.{ .handler = testStyleJsonProvider }); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); + var provider_future = try runtime.setResourceProvider(.{ .handler = testStyleJsonProvider }); + defer provider_future.deinit(); + _ = try provider_future.wait(null); - var map = try MapHandle.create(&runtime, .{ .event_mask = narrowed }); + var map = try createLoadedMapForTesting(&runtime); defer map.close() catch @panic("map close failed"); - try map.setStyleJson(std.testing.allocator, test_style_json); - try std.testing.expect(try waitForStyleSourceForTesting(&runtime, &map, "point")); + var mask_future = try map.setEventMask(narrowed); + defer mask_future.deinit(); + _ = try mask_future.wait(null); - const baseline = liveCustomGeometrySourceCountForTesting(); var state = TestCustomGeometryCallbackState{}; - try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ + _ = try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ .fetch_tile = testFetchCustomGeometryTile, .release_context = testReleaseCustomGeometryContext, .context = &state, }); - try std.testing.expectEqual(baseline + 1, liveCustomGeometrySourceCountForTesting()); + var add_barrier = try runtime.barrier(); + defer add_barrier.deinit(); + _ = try add_barrier.wait(null); try std.testing.expectEqual(@as(usize, 0), state.release_count); // The mask stays what the host set, because the binding adds nothing to it. try std.testing.expectEqual(narrowed, try map.eventMask()); - try map.setStyleUrl(std.testing.allocator, "custom://style.json"); + _ = try map.setStyleUrl(std.testing.allocator, "custom://style.json"); var released = false; for (0..200) |_| { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(std.testing.allocator, 0); + var batch = try runtime.drainEvents(std.testing.allocator); defer batch.deinit(); for (0..batch.len()) |index| { const event = try batch.at(index); @@ -2909,7 +2442,7 @@ test "a style load that drops a source releases the callback state unsubscribed" runtime_module.RuntimeEventType.map_style_loaded, )); } - if (liveCustomGeometrySourceCountForTesting() == baseline) { + if (state.release_count == 1) { released = true; break; } @@ -2923,28 +2456,30 @@ test "a style load that drops a source releases the callback state unsubscribed" test "closing a map releases its surviving source callback states" { var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); var map = try createLoadedMapForTesting(&runtime); var map_open = true; defer if (map_open) map.close() catch @panic("map close failed"); - const baseline = liveCustomGeometrySourceCountForTesting(); var state = TestCustomGeometryCallbackState{}; - try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ + _ = try map.addCustomGeometrySource(std.testing.allocator, "custom", .{ .fetch_tile = testFetchCustomGeometryTile, + .release_context = testReleaseCustomGeometryContext, .context = &state, }); - try std.testing.expectEqual(baseline + 1, liveCustomGeometrySourceCountForTesting()); try map.close(); map_open = false; - try std.testing.expectEqual(baseline, liveCustomGeometrySourceCountForTesting()); + var close_barrier = try runtime.barrier(); + defer close_barrier.deinit(); + _ = try close_barrier.wait(null); + try std.testing.expectEqual(@as(usize, 1), state.release_count); } // A rejected add releases nothing, so the state belongs to the failing call. test "a rejected custom geometry source add releases its own callback state" { var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); var map = try createLoadedMapForTesting(&runtime); defer map.close() catch @panic("map close failed"); @@ -2964,86 +2499,43 @@ test "a rejected custom geometry source add releases its own callback state" { try std.testing.expectEqual(@as(usize, 0), state.release_count); } -test "a released map id replayed after a new map is reported stale" { +test "a released map id is stale and map snapshots work from another thread" { var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer runtime_module.closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); + + var first_future = try MapHandle.create(&runtime, .{}); + + defer first_future.deinit(); - var first = try MapHandle.create(&runtime, .{}); + var first = try first_future.wait(null); const released = @intFromEnum(first); try first.close(); - // The released slot is the one the next map takes, so the replayed id - // names a retired generation of a slot that is live again. - var second = try MapHandle.create(&runtime, .{}); - defer second.close() catch @panic("map close failed"); - - var width: u32 = 0; - var height: u32 = 0; - var scale_factor: f64 = 0; + var stale_snapshot = std.mem.zeroes(c.mln_map_snapshot); + stale_snapshot.size = @sizeOf(c.mln_map_snapshot); try std.testing.expectError( error.InvalidArgument, - status.checkStatus( - c.mln_map_get_size(released, &width, &height, &scale_factor), - null, - ), - ); - try std.testing.expect( - std.mem.indexOf(u8, std.mem.span(c.mln_thread_last_error_message()), "stale") != null, + status.checkStatus(c.mln_map_snapshot_get(released, &stale_snapshot), null), ); - // The live map is unaffected by the replay. - _ = try second.getSize(); -} - -test "a map id passed to a runtime operation is rejected on its kind" { - var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); + var second_future = try MapHandle.create(&runtime, .{}); - // MapHandle and RuntimeHandle are distinct enums, so this call has no - // expression in the safe API and needs the raw id. - try std.testing.expectError( - error.InvalidArgument, - status.checkStatus(c.mln_runtime_pump(@intFromEnum(map), 0, -1), null), - ); - const message = std.mem.span(c.mln_thread_last_error_message()); - try std.testing.expect(std.mem.indexOf(u8, message, "map") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "runtime") != null); -} + defer second_future.deinit(); -test "a live map id called from another thread reports wrong thread" { - var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); + var second = try second_future.wait(null); + defer second.close() catch @panic("map close failed"); const CrossThread = struct { - live: c.mln_map, - result: status.Error!void = {}, - stale: bool = false, + map: *MapHandle, + result: status.Error!MapSnapshot = undefined, fn run(self: *@This()) void { - var width: u32 = 0; - var height: u32 = 0; - var scale_factor: f64 = 0; - self.result = status.checkStatus( - c.mln_map_get_size(self.live, &width, &height, &scale_factor), - null, - ); - self.stale = std.mem.indexOf( - u8, - std.mem.span(c.mln_thread_last_error_message()), - "stale", - ) != null; + self.result = self.map.snapshot(); } }; - var context = CrossThread{ .live = @intFromEnum(map) }; + var context = CrossThread{ .map = &second }; const thread = try std.Thread.spawn(.{}, CrossThread.run, .{&context}); thread.join(); - - // The id is live, so the owner-thread rule decides rather than identity. - try std.testing.expectError(error.WrongThread, context.result); - try std.testing.expect(!context.stale); + _ = try context.result; } diff --git a/bindings/zig/src/maplibre_native_ffi.zig b/bindings/zig/src/maplibre_native_ffi.zig index bd2af57c6..71d1c836c 100644 --- a/bindings/zig/src/maplibre_native_ffi.zig +++ b/bindings/zig/src/maplibre_native_ffi.zig @@ -1,9 +1,8 @@ -const builtin = @import("builtin"); const c = @import("c.zig").raw; +const completion = @import("completion.zig"); const diagnostics = @import("diagnostics.zig"); const logging = @import("logging.zig"); const map = @import("map.zig"); -const native_temp = @import("native_temp.zig"); const projection = @import("projection.zig"); const render = @import("render.zig"); const runtime = @import("runtime.zig"); @@ -15,16 +14,14 @@ pub const DiagnosticStore = diagnostics.DiagnosticStore; pub const NativeStatusError = status.NativeStatusError; pub const BindingError = status.BindingError; pub const Error = status.Error; +pub const Future = completion.Future; +pub const CommandCompletion = completion.CommandCompletion; pub const RuntimeHandle = runtime.RuntimeHandle; pub const RuntimeOptions = runtime.RuntimeOptions; pub const NetworkStatus = runtime.NetworkStatus; pub const AmbientCacheOperation = runtime.AmbientCacheOperation; pub const OfflineRegionId = runtime.OfflineRegionId; -pub const OfflineOperationId = runtime.OfflineOperationId; -pub const OfflineOperationKind = runtime.OfflineOperationKind; -pub const OfflineOperationResultKind = runtime.OfflineOperationResultKind; -pub const OfflineOperationHandle = runtime.OfflineOperationHandle; pub const OfflineTilePyramidRegionDefinition = runtime.OfflineTilePyramidRegionDefinition; pub const OfflineGeometryRegionDefinition = runtime.OfflineGeometryRegionDefinition; pub const OfflineRegionDefinition = runtime.OfflineRegionDefinition; @@ -54,7 +51,6 @@ pub const ResourceResponse = runtime.ResourceResponse; pub const ResourceProviderHandler = runtime.ResourceProviderHandler; pub const ResourceProvider = runtime.ResourceProvider; pub const ResourceRequestHandle = runtime.ResourceRequestHandle; -pub const WakeSourceHandle = runtime.WakeSourceHandle; pub const EventBatch = runtime.EventBatch; pub const RuntimeEvent = runtime.RuntimeEvent; pub const RuntimeEventPayload = runtime.RuntimeEventPayload; @@ -65,6 +61,7 @@ pub const RuntimeEventSourceId = runtime.RuntimeEventSourceId; pub const RuntimeEventPayloadType = runtime.RuntimeEventPayloadType; pub const CameraChangeMode = runtime.CameraChangeMode; pub const CameraTransitionFinishedPayload = runtime.CameraTransitionFinishedPayload; +pub const CommandDisposition = completion.CommandDisposition; pub const RenderMode = runtime.RenderMode; pub const RenderingStats = runtime.RenderingStats; pub const RenderFramePayload = runtime.RenderFramePayload; @@ -78,7 +75,6 @@ pub const OfflineRegionStatusPayload = runtime.OfflineRegionStatusPayload; pub const ResourceErrorReason = runtime.ResourceErrorReason; pub const OfflineRegionResponseErrorPayload = runtime.OfflineRegionResponseErrorPayload; pub const OfflineRegionTileCountLimitPayload = runtime.OfflineRegionTileCountLimitPayload; -pub const OfflineOperationCompletedPayload = runtime.OfflineOperationCompletedPayload; pub const UnknownPayload = runtime.UnknownPayload; pub const LogSeverity = logging.LogSeverity; @@ -89,27 +85,45 @@ pub const LogHandler = logging.LogHandler; pub const LogCallback = logging.LogCallback; pub const NativePointer = render.NativePointer; +pub const RenderDriver = render.RenderDriver; +pub const RenderSessionAttachOptions = render.RenderSessionAttachOptions; +pub const RenderSessionAttachment = render.RenderSessionAttachment; pub const RenderResult = render.RenderResult; -pub const RenderUpdate = render.RenderUpdate; +pub const FrameDemand = render.FrameDemand; +pub const FrameResult = render.FrameResult; +pub const RenderSessionCapabilities = render.RenderSessionCapabilities; +pub const RenderSessionSnapshot = render.RenderSessionSnapshot; +pub const RenderSessionLifecycle = render.RenderSessionLifecycle; +pub const GpuSync = render.GpuSync; +pub const AbandonResult = render.AbandonResult; +pub const RenderFrameBatch = render.RenderFrameBatch; +pub const AbandonDisposition = render.AbandonDisposition; +pub const AcquiredFrame = render.AcquiredFrame; +pub const OwnedReadback = render.OwnedReadback; pub const RenderBackendSupport = render.RenderBackendSupport; pub const OpenGLContextProviderSupport = render.OpenGLContextProviderSupport; pub const RenderTargetExtent = render.RenderTargetExtent; pub const MetalContextDescriptor = render.MetalContextDescriptor; pub const VulkanContextDescriptor = render.VulkanContextDescriptor; +pub const WebGPUContextDescriptor = render.WebGPUContextDescriptor; pub const OpenGLContextOwnership = render.OpenGLContextOwnership; pub const OpenGLClientApi = render.OpenGLClientApi; pub const WglContextDescriptor = render.WglContextDescriptor; pub const EglContextDescriptor = render.EglContextDescriptor; +pub const WebGLContextDescriptor = render.WebGLContextDescriptor; pub const OpenGLContextDescriptor = render.OpenGLContextDescriptor; pub const MetalOwnedTextureDescriptor = render.MetalOwnedTextureDescriptor; pub const MetalBorrowedTextureDescriptor = render.MetalBorrowedTextureDescriptor; pub const VulkanOwnedTextureDescriptor = render.VulkanOwnedTextureDescriptor; pub const VulkanBorrowedTextureDescriptor = render.VulkanBorrowedTextureDescriptor; +pub const WebGPUOwnedTextureDescriptor = render.WebGPUOwnedTextureDescriptor; +pub const WebGPUBorrowedTextureDescriptor = render.WebGPUBorrowedTextureDescriptor; pub const OpenGLOwnedTextureDescriptor = render.OpenGLOwnedTextureDescriptor; pub const OpenGLBorrowedTextureDescriptor = render.OpenGLBorrowedTextureDescriptor; pub const MetalSurfaceDescriptor = render.MetalSurfaceDescriptor; pub const VulkanSurfaceDescriptor = render.VulkanSurfaceDescriptor; pub const OpenGLSurfaceDescriptor = render.OpenGLSurfaceDescriptor; +pub const WebGPUSurfaceDescriptor = render.WebGPUSurfaceDescriptor; pub const TextureImageInfo = render.TextureImageInfo; pub const FeatureStateSelector = map.FeatureStateSelector; pub const ScreenBox = render.ScreenBox; @@ -121,15 +135,20 @@ pub const QueriedFeatureList = render.QueriedFeatureList; pub const MetalOwnedTextureFrameInfo = render.MetalOwnedTextureFrameInfo; pub const VulkanOwnedTextureFrameInfo = render.VulkanOwnedTextureFrameInfo; pub const OpenGLOwnedTextureFrameInfo = render.OpenGLOwnedTextureFrameInfo; +pub const WebGPUOwnedTextureFrameInfo = render.WebGPUOwnedTextureFrameInfo; pub const RenderSessionHandle = render.RenderSessionHandle; -pub const MetalOwnedTextureFrameHandle = render.MetalOwnedTextureFrameHandle; -pub const VulkanOwnedTextureFrameHandle = render.VulkanOwnedTextureFrameHandle; -pub const OpenGLOwnedTextureFrameHandle = render.OpenGLOwnedTextureFrameHandle; pub const GeoJsonSourceDataHandle = map.GeoJsonSourceDataHandle; pub const MapHandle = map.MapHandle; pub const MapOptions = map.MapOptions; pub const MapMode = map.MapMode; +pub const CameraUpdateMode = map.CameraUpdateMode; +pub const GesturePhase = map.GesturePhase; +pub const CameraUpdate = map.CameraUpdate; +pub const CameraDeltaKind = map.CameraDeltaKind; +pub const CameraDelta = map.CameraDelta; +pub const CameraSnapshot = map.CameraSnapshot; +pub const MapSnapshot = map.MapSnapshot; pub const CanonicalTileId = map.CanonicalTileId; pub const CustomGeometrySourceTileCallback = map.CustomGeometrySourceTileCallback; pub const CustomGeometrySourceReleaseCallback = map.CustomGeometrySourceReleaseCallback; @@ -183,6 +202,8 @@ pub const StringList = values.StringList; pub const StyleSourceType = values.StyleSourceType; pub const StyleTileJsonInfo = values.StyleTileJsonInfo; pub const StyleSourceInfo = values.StyleSourceInfo; +pub const StyleSourceMetadata = map.StyleSourceMetadata; +pub const StyleLayerInfo = map.StyleLayerInfo; pub const OwnedString = values.OwnedString; pub const projectedMetersForLatLng = projection.projectedMetersForLatLng; @@ -197,6 +218,9 @@ pub const supportedOpenGLContextProviders = render.supportedOpenGLContextProvide pub const attachMetalOwnedTexture = render.attachMetalOwnedTexture; pub const attachMetalBorrowedTexture = render.attachMetalBorrowedTexture; pub const attachVulkanOwnedTexture = render.attachVulkanOwnedTexture; +pub const attachWebGPUOwnedTexture = render.attachWebGPUOwnedTexture; +pub const attachWebGPUBorrowedTexture = render.attachWebGPUBorrowedTexture; +pub const attachWebGPUSurface = render.attachWebGPUSurface; pub const attachVulkanBorrowedTexture = render.attachVulkanBorrowedTexture; pub const attachOpenGLOwnedTexture = render.attachOpenGLOwnedTexture; pub const attachOpenGLBorrowedTexture = render.attachOpenGLBorrowedTexture; @@ -217,26 +241,4 @@ pub fn validateAbiVersion(diagnostic_store: ?*DiagnosticStore) Error!void { comptime { _ = c.MLN_STATUS_OK; - if (builtin.is_test) { - @export(&testFailNextOwnedTextureFrameWrapperAllocation, .{ .name = "mln_zig_test_fail_next_owned_texture_frame_wrapper_allocation" }); - @export(&testUseCountingBufferDestroy, .{ .name = "mln_zig_test_use_counting_buffer_destroy" }); - @export(&testRestoreBufferDestroy, .{ .name = "mln_zig_test_restore_buffer_destroy" }); - @export(&testBufferDestroyCount, .{ .name = "mln_zig_test_buffer_destroy_count" }); - } -} - -fn testFailNextOwnedTextureFrameWrapperAllocation() callconv(.c) void { - render.failNextOwnedTextureFrameWrapperAllocationForTesting(); -} - -fn testUseCountingBufferDestroy() callconv(.c) void { - native_temp.useCountingBufferDestroyForTesting(); -} - -fn testRestoreBufferDestroy() callconv(.c) void { - native_temp.restoreBufferDestroyForTesting(); -} - -fn testBufferDestroyCount() callconv(.c) usize { - return native_temp.bufferDestroyCountForTesting(); } diff --git a/bindings/zig/src/native_temp.zig b/bindings/zig/src/native_temp.zig index 039933228..ef8c52db5 100644 --- a/bindings/zig/src/native_temp.zig +++ b/bindings/zig/src/native_temp.zig @@ -5,17 +5,13 @@ const diagnostics = @import("diagnostics.zig"); const status = @import("status.zig"); const values = @import("values.zig"); -const BufferDestroyFn = *const fn (c.mln_buffer) callconv(.c) void; -var buffer_destroy_for_testing: BufferDestroyFn = c.mln_buffer_destroy; -var buffer_destroy_count_for_testing = std.atomic.Value(usize).init(0); - pub fn copyOwnedBuffer( allocator: std.mem.Allocator, buffer: c.mln_buffer, diagnostic_store: ?*diagnostics.DiagnosticStore, ) status.Error!?values.OwnedString { if (buffer == 0) return null; - defer buffer_destroy_for_testing(buffer); + defer c.mln_buffer_destroy(buffer); var view = c.mln_buffer_view{ .data = null, .size = 0 }; try status.checkStatus(c.mln_buffer_get(buffer, &view), diagnostic_store); if (view.size == 0) { @@ -25,24 +21,6 @@ pub fn copyOwnedBuffer( return .{ .allocator = allocator, .value = try allocator.dupe(u8, data[0..view.size]) }; } -fn countingBufferDestroy(buffer: c.mln_buffer) callconv(.c) void { - _ = buffer_destroy_count_for_testing.fetchAdd(1, .seq_cst); - c.mln_buffer_destroy(buffer); -} - -pub fn useCountingBufferDestroyForTesting() void { - buffer_destroy_count_for_testing.store(0, .seq_cst); - buffer_destroy_for_testing = countingBufferDestroy; -} - -pub fn restoreBufferDestroyForTesting() void { - buffer_destroy_for_testing = c.mln_buffer_destroy; -} - -pub fn bufferDestroyCountForTesting() usize { - return buffer_destroy_count_for_testing.load(.seq_cst); -} - pub const TempStorage = struct { arena: std.heap.ArenaAllocator, diagnostic_store: ?*diagnostics.DiagnosticStore = null, diff --git a/bindings/zig/src/projection.zig b/bindings/zig/src/projection.zig index 72e3c2bde..7a02da705 100644 --- a/bindings/zig/src/projection.zig +++ b/bindings/zig/src/projection.zig @@ -1,9 +1,9 @@ const c = @import("c.zig").raw; +const completion = @import("completion.zig"); const diagnostics = @import("diagnostics.zig"); const map_module = @import("map.zig"); const MapHandle = map_module.MapHandle; const native_temp = @import("native_temp.zig"); -const runtime_module = @import("runtime.zig"); const status = @import("status.zig"); const std = @import("std"); const values = @import("values.zig"); @@ -28,34 +28,45 @@ const ProjectionLease = struct { var projection_registry_lock = std.atomic.Value(bool).init(false); var projection_registry: std.AutoHashMapUnmanaged(c.mln_map_projection, *ProjectionState) = .empty; -/// Any-thread standalone projection snapshot with serialized native calls. +/// Standalone projection copied from a map's transform state at creation. +/// +/// Creation is an operation ordered after every earlier map command; every +/// later call is synchronous, callable from any thread, and internally +/// serialized. A projection never observes map changes made after its +/// creation and remains usable after its source map and runtime close. pub const MapProjectionHandle = enum(c.mln_map_projection) { _, - pub fn create(map: *MapHandle) status.Error!MapProjectionHandle { - var projection: c.mln_map_projection = 0; + pub fn create(map: *MapHandle) status.Error!completion.Future(MapProjectionHandle) { const diagnostic_store = map_module.diagnosticStore(map); - try status.checkStatus( - c.mln_map_projection_create(try map_module.native(map), &projection), - diagnostic_store, - ); - errdefer _ = c.mln_map_projection_destroy(projection); - - const projection_state = try std.heap.smp_allocator.create(ProjectionState); - projection_state.* = .{ .diagnostic_store = diagnostic_store }; - errdefer std.heap.smp_allocator.destroy(projection_state); - - return try registerProjectionState(projection, projection_state); + return completion.submitWithCopyContext(MapProjectionHandle, ?*diagnostics.DiagnosticStore, diagnostic_store, struct { + fn copyResult(result: *const c.mln_completion_result, store: *?*diagnostics.DiagnosticStore) status.Error!MapProjectionHandle { + const projection = try completion.value(c.mln_map_projection)(result); + const projection_state = try std.heap.smp_allocator.create(ProjectionState); + projection_state.* = .{ .diagnostic_store = store.* }; + errdefer std.heap.smp_allocator.destroy(projection_state); + return registerProjectionState(projection, projection_state); + } + }.copyResult, diagnostic_store, try map_module.native(map), struct { + fn start(native_map: c.mln_map, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_map_projection_create(native_map, descriptor); + } + }.start); } + /// Copies the projection camera. Synchronous, callable from any thread; the + /// result observes every earlier projection setter. pub fn getCamera(self: *MapProjectionHandle) status.Error!values.CameraOptions { - var camera = c.mln_camera_options_default(); const lease = try projectionLease(self.*); defer lease.release(); + var camera = c.mln_camera_options_default(); try status.checkStatus(c.mln_map_projection_get_camera(lease.native, &camera), lease.diagnostic_store); return values.cameraOptionsFromNative(camera); } + /// Applies a camera update before returning, so a later read or conversion + /// observes it. Synchronous, callable from any thread; the map's camera is + /// unaffected. pub fn setCamera(self: *MapProjectionHandle, camera: values.CameraOptions) status.Error!void { var raw_camera = values.cameraOptionsToNative(camera); const lease = try projectionLease(self.*); @@ -63,6 +74,9 @@ pub const MapProjectionHandle = enum(c.mln_map_projection) { try status.checkStatus(c.mln_map_projection_set_camera(lease.native, &raw_camera), lease.diagnostic_store); } + /// Applies a camera fit for geographic coordinates before returning, so a + /// later read or conversion observes it. Synchronous, callable from any + /// thread. pub fn setVisibleCoordinates( self: *MapProjectionHandle, allocator: std.mem.Allocator, @@ -76,16 +90,14 @@ pub const MapProjectionHandle = enum(c.mln_map_projection) { const lease = try projectionLease(self.*); defer lease.release(); try status.checkStatus( - c.mln_map_projection_set_visible_coordinates( - lease.native, - coordinate_ptr, - raw_coordinates.len, - values.edgeInsetsToNative(padding), - ), + c.mln_map_projection_set_visible_coordinates(lease.native, coordinate_ptr, raw_coordinates.len, values.edgeInsetsToNative(padding)), lease.diagnostic_store, ); } + /// Applies a camera fit for GeoJSON Geometry bytes before returning, so a + /// later read or conversion observes it. Synchronous, callable from any + /// thread. pub fn setVisibleGeometry( self: *MapProjectionHandle, allocator: std.mem.Allocator, @@ -97,40 +109,38 @@ pub const MapProjectionHandle = enum(c.mln_map_projection) { const lease = try projectionLease(self.*); defer lease.release(); try status.checkStatus( - c.mln_map_projection_set_visible_geometry( - lease.native, - try temp.stringView(geometry), - values.edgeInsetsToNative(padding), - ), + c.mln_map_projection_set_visible_geometry(lease.native, try temp.stringView(geometry), values.edgeInsetsToNative(padding)), lease.diagnostic_store, ); } + /// Converts a geographic coordinate to a screen point. Synchronous, + /// callable from any thread; the result observes every earlier projection + /// setter, and never a map change made after creation. pub fn pixelForLatLng(self: *MapProjectionHandle, coordinate: values.LatLng) status.Error!values.ScreenPoint { - var point: c.mln_screen_point = undefined; const lease = try projectionLease(self.*); defer lease.release(); - try status.checkStatus( - c.mln_map_projection_pixel_for_lat_lng(lease.native, values.latLngToNative(coordinate), &point), - lease.diagnostic_store, - ); + var point: c.mln_screen_point = undefined; + try status.checkStatus(c.mln_map_projection_pixel_for_lat_lng(lease.native, values.latLngToNative(coordinate), &point), lease.diagnostic_store); return values.screenPointFromNative(point); } + /// Converts a screen point to a geographic coordinate. Synchronous, + /// callable from any thread; the result observes every earlier projection + /// setter, and never a map change made after creation. pub fn latLngForPixel(self: *MapProjectionHandle, point: values.ScreenPoint) status.Error!values.LatLng { - var coordinate: c.mln_lat_lng = undefined; const lease = try projectionLease(self.*); defer lease.release(); - try status.checkStatus( - c.mln_map_projection_lat_lng_for_pixel(lease.native, values.screenPointToNative(point), &coordinate), - lease.diagnostic_store, - ); + var coordinate: c.mln_lat_lng = undefined; + try status.checkStatus(c.mln_map_projection_lat_lng_for_pixel(lease.native, values.screenPointToNative(point), &coordinate), lease.diagnostic_store); return values.latLngFromNative(coordinate); } + /// Closes the projection before returning. Synchronous, callable from any + /// thread. pub fn close(self: *MapProjectionHandle) status.Error!void { const projection_close = try beginProjectionClose(self.*) orelse return; - status.checkStatus(c.mln_map_projection_destroy(projection_close.native), projection_close.diagnostic_store) catch |err| { + status.checkStatus(c.mln_map_projection_close(projection_close.native), projection_close.diagnostic_store) catch |err| { cancelProjectionClose(projection_close.state); return err; }; diff --git a/bindings/zig/src/render.zig b/bindings/zig/src/render.zig index 0070d7894..5fb570004 100644 --- a/bindings/zig/src/render.zig +++ b/bindings/zig/src/render.zig @@ -1,6 +1,7 @@ const std = @import("std"); const c = @import("c.zig").raw; +const completion = @import("completion.zig"); const diagnostics = @import("diagnostics.zig"); const map_module = @import("map.zig"); const native_temp = @import("native_temp.zig"); @@ -8,59 +9,19 @@ const runtime_module = @import("runtime.zig"); const status = @import("status.zig"); const values = @import("values.zig"); -const MetalOwnedTextureFrameStorage = struct { raw: c.mln_metal_owned_texture_frame }; -const OpenGLOwnedTextureFrameStorage = struct { raw: c.mln_opengl_owned_texture_frame }; -const VulkanOwnedTextureFrameStorage = struct { raw: c.mln_vulkan_owned_texture_frame }; - -const RenderSessionFrameState = struct { - metal_frame: ?MetalOwnedTextureFrameStorage = null, - metal_frame_active: bool = false, - metal_frame_generation: u64 = 0, - opengl_frame: ?OpenGLOwnedTextureFrameStorage = null, - opengl_frame_active: bool = false, - opengl_frame_generation: u64 = 0, - vulkan_frame: ?VulkanOwnedTextureFrameStorage = null, - vulkan_frame_active: bool = false, - vulkan_frame_generation: u64 = 0, -}; - const RenderSessionState = struct { - /// Cleared once a successful detach or close releases the registration. map_handle: ?map_module.MapHandle, - /// The session's own store, never the map's: a session's owner thread need - /// not be the map's, and `DiagnosticStore` is unsynchronized. - diagnostic_store: ?*diagnostics.DiagnosticStore, - frame_state: ?*RenderSessionFrameState, - active_leases: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), - closing: bool = false, -}; - -const OwnedTextureFrameKind = enum { - metal, - vulkan, - opengl, -}; - -const OwnedTextureFrameState = struct { - kind: OwnedTextureFrameKind, - session_native: c.mln_render_session, + runtime: runtime_module.RuntimeHandle, diagnostic_store: ?*diagnostics.DiagnosticStore, - frame_state: ?*RenderSessionFrameState, - generation: u64, active_leases: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), closing: bool = false, }; -const OwnedTextureFrameRegistrySlot = struct { - state: ?*OwnedTextureFrameState, - generation: u64, -}; - const RenderSessionLease = struct { state: *RenderSessionState, native: c.mln_render_session, + runtime: runtime_module.RuntimeHandle, diagnostic_store: ?*diagnostics.DiagnosticStore, - frame_state: *RenderSessionFrameState, fn release(self: RenderSessionLease) void { _ = self.state.active_leases.fetchSub(1, .seq_cst); @@ -71,31 +32,11 @@ const RenderSessionClose = struct { state: *RenderSessionState, native: c.mln_render_session, diagnostic_store: ?*diagnostics.DiagnosticStore, - frame_state: *RenderSessionFrameState, -}; - -const OwnedTextureFrameLease = struct { - state: *OwnedTextureFrameState, - session_native: c.mln_render_session, - diagnostic_store: ?*diagnostics.DiagnosticStore, - frame_state: *RenderSessionFrameState, - generation: u64, - - fn release(self: OwnedTextureFrameLease) void { - _ = self.state.active_leases.fetchSub(1, .seq_cst); - } }; -// An owned texture frame is a binding concept with no C handle behind it, so -// the frame tables below carry their own generations. var render_session_registry_lock = std.atomic.Value(bool).init(false); var render_session_registry: std.AutoHashMapUnmanaged(c.mln_render_session, *RenderSessionState) = .empty; -var owned_texture_frame_registry_lock = std.atomic.Value(bool).init(false); -var owned_texture_frame_registry: std.ArrayList(OwnedTextureFrameRegistrySlot) = .empty; -var owned_texture_frame_free_list: std.ArrayList(usize) = .empty; -var fail_next_owned_texture_frame_wrapper_allocation_for_testing = std.atomic.Value(bool).init(false); - pub const NativePointer = enum(usize) { _, @@ -114,16 +55,30 @@ pub const NativePointer = enum(usize) { } }; -/// Which outcome a successful `RenderSessionHandle.renderUpdate` call reached. +pub const RenderDriver = enum { + core_worker, + caller_graphics_thread, + + fn toRaw(self: RenderDriver) u32 { + return switch (self) { + .core_worker => c.MLN_RENDER_DRIVER_CORE_WORKER, + .caller_graphics_thread => c.MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + }; + } +}; + +pub const RenderSessionAttachOptions = struct { + driver: RenderDriver, + requested_texture_ring_depth: u32 = 1, +}; + pub const RenderResult = union(enum) { - /// The call rendered a frame into the render target. rendered, - /// The call produced no frame. no_update, - /// The map has not applied the session's current size yet. size_pending, - /// The render target had no frame to draw into. target_not_ready, + superseded, + deadline_missed, unknown: u32, fn fromRaw(raw: u32) RenderResult { @@ -132,23 +87,92 @@ pub const RenderResult = union(enum) { c.MLN_RENDER_RESULT_NO_UPDATE => .no_update, c.MLN_RENDER_RESULT_SIZE_PENDING => .size_pending, c.MLN_RENDER_RESULT_TARGET_NOT_READY => .target_not_ready, + c.MLN_RENDER_RESULT_SUPERSEDED => .superseded, + c.MLN_RENDER_RESULT_DEADLINE_MISSED => .deadline_missed, else => .{ .unknown = raw }, }; } }; -/// Outcome of a successful `RenderSessionHandle.renderUpdate` call. -pub const RenderUpdate = struct { - /// Which outcome the call reached. - result: RenderResult, +pub const FrameDemand = struct { + if_needed: bool = true, + present: bool = false, + token: u64 = 0, + coalescing_boundary: u64 = 0, + timeout_ns: u64 = 0, +}; + +pub const FrameResult = struct { + disposition: RenderResult, + token: u64, + map_update_generation: u64, + extent_generation: u64, + frame_generation: u64, /// Whether the map asked for another frame while it rendered this one, as - /// during an ongoing camera transition. True only when `result` is - /// `.rendered`; this is the same signal that a map-render-frame-finished - /// event carries in its `needs_repaint` field, delivered without the - /// event round trip. + /// during an ongoing camera transition. Set only when `disposition` is + /// `.rendered`, and false for every other outcome. needs_repaint: bool, }; +pub const RenderSessionCapabilities = struct { + driver: RenderDriver, + texture_ring_depth: u32, + frame_acquisition: bool, + readback: bool, + consumer_sync: bool, + presentation: bool, +}; +pub const RenderSessionLifecycle = union(enum) { + attaching, + attached, + detaching, + detached, + target_lost, + abandoned, + unknown: u32, +}; + +pub const RenderSessionSnapshot = struct { + state: RenderSessionLifecycle, + driver: RenderDriver, + latest_result: RenderResult, + extent: RenderTargetExtent, + generation: u64, + map_update_generation: u64, + rendered_update_generation: u64, + extent_generation: u64, + frame_generation: u64, + latest_demand_token: u64, + pending_demand_count: u32, + acquired_frame_count: u32, + target_ready: bool, + pending_changes: bool, +}; + +pub const GpuSync = union(enum) { + cpu_complete, + metal_shared_event: struct { object: NativePointer, value: u64 }, + vulkan_timeline_semaphore: struct { object: NativePointer, value: u64 }, + opengl_fence: NativePointer, + webgpu_token: struct { object: NativePointer, value: u64 }, +}; + +pub const AbandonDisposition = union(enum) { + clean, + quarantined, + unknown: u32, +}; + +pub const AbandonResult = struct { + disposition: AbandonDisposition, + quarantined_resource_count: u32, +}; + +pub const RenderSessionAttachment = struct { + session: RenderSessionHandle, + completion: completion.Future(void), +}; + pub const RenderBackendSupport = struct { metal: bool, opengl: bool, @@ -217,18 +241,23 @@ pub const VulkanContextDescriptor = struct { get_instance_proc_addr: ?NativePointer = null, get_device_proc_addr: ?NativePointer = null, }; +pub const WebGPUContextDescriptor = struct { + instance: ?NativePointer = null, + device: NativePointer, + queue: ?NativePointer = null, +}; -/// How a render session's OpenGL context relates to the thread that attached -/// it. +/// How a render session's OpenGL context relates to its driver thread and host +/// graphics state. pub const OpenGLContextOwnership = union(enum) { /// The session shares its thread with host graphics work. Every render /// makes the session context current and restores whatever was current /// before, and the session context joins the share group that the /// descriptor names. shared, - /// The session owns its thread's OpenGL context. It makes its context - /// current once, keeps it current between renders, and joins no share - /// group. Use this for a thread that exists to drive one render session. + /// The session owns its driver thread's OpenGL context. It keeps the + /// context current between renders and joins no share group. The driver may + /// be a native core worker or a dedicated host thread. dedicated, unknown: u32, @@ -288,10 +317,15 @@ pub const EglContextDescriptor = struct { ownership: OpenGLContextOwnership = .shared, get_proc_address: ?NativePointer = null, }; +pub const WebGLContextDescriptor = union(enum) { + existing: i32, + transferred_canvas: []const u8, +}; pub const OpenGLContextDescriptor = union(enum) { wgl: WglContextDescriptor, egl: EglContextDescriptor, + webgl: WebGLContextDescriptor, }; pub const MetalOwnedTextureDescriptor = struct { @@ -325,6 +359,21 @@ pub const VulkanBorrowedTextureDescriptor = struct { final_layout: u32, }; +pub const WebGPUOwnedTextureDescriptor = struct { + extent: RenderTargetExtent = .{}, + context: WebGPUContextDescriptor, +}; + +pub const WebGPUBorrowedTextureDescriptor = struct { + extent: RenderTargetExtent = .{}, + physical_width: u32, + physical_height: u32, + context: WebGPUContextDescriptor, + texture: NativePointer, + texture_view: NativePointer, + format: u32, +}; + pub const OpenGLOwnedTextureDescriptor = struct { extent: RenderTargetExtent = .{}, context: OpenGLContextDescriptor, @@ -358,6 +407,13 @@ pub const OpenGLSurfaceDescriptor = struct { surface: NativePointer, }; +pub const WebGPUSurfaceDescriptor = struct { + extent: RenderTargetExtent = .{}, + context: WebGPUContextDescriptor, + surface: NativePointer, + format: u32, +}; + pub const TextureImageInfo = struct { width: u32, height: u32, @@ -365,8 +421,6 @@ pub const TextureImageInfo = struct { byte_length: usize, }; -pub const FeatureStateSelector = map_module.FeatureStateSelector; - /// Screen-space box in logical map pixels. Corners may be given in any order /// and may extend past the viewport; rendered queries normalize and clip them. pub const ScreenBox = struct { @@ -477,854 +531,655 @@ pub const OpenGLOwnedTextureFrameInfo = struct { format: u32, type: u32, }; +pub const WebGPUOwnedTextureFrameInfo = struct { + generation: u64, + width: u32, + height: u32, + scale_factor: f64, + texture: NativePointer, + texture_view: NativePointer, + device: NativePointer, + format: u32, +}; -pub const RenderSessionHandle = enum(c.mln_render_session) { - _, +pub const RenderFrameBatch = struct { + native: c.mln_render_frame_batch, + diagnostic_store: ?*diagnostics.DiagnosticStore, - /// Resizes this attached render session. Borrowed texture targets are sized - /// by their owner and return `error.Unsupported`; hand over a new texture - /// with the backend's `set*BorrowedTextureTarget` method instead. - /// - /// The session keeps its renderer, along with the tile pyramid, glyph and - /// image atlases, and symbol placement. A scale factor change retires the - /// renderer instead, because shaders are compiled for one pixel ratio. - /// Map-owned feature state survives either way. - pub fn resize(self: *RenderSessionHandle, extent: RenderTargetExtent) status.Error!void { - const lease = try renderSessionLease(self.*); + pub fn count(self: RenderFrameBatch) status.Error!usize { + var result: usize = 0; + try status.checkStatus(c.mln_render_frame_batch_count(self.native, &result), self.diagnostic_store); + return result; + } + + pub fn get(self: RenderFrameBatch, index: usize) status.Error!FrameResult { + var raw: c.mln_render_frame_result = undefined; + raw.size = @sizeOf(c.mln_render_frame_result); + try status.checkStatus(c.mln_render_frame_batch_get(self.native, index, &raw), self.diagnostic_store); + return frameResultFromNative(raw); + } + + pub fn release(self: *RenderFrameBatch) void { + if (self.native == 0) return; + c.mln_render_frame_batch_release(self.native); + self.native = 0; + } +}; + +pub const AcquiredFrame = struct { + native: c.mln_acquired_frame, + session: RenderSessionHandle, + + pub fn result(self: AcquiredFrame) status.Error!FrameResult { + const lease = try renderSessionLease(self.session); defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - try status.checkStatus( - c.mln_render_session_resize(lease.native, extent.width, extent.height, extent.scale_factor), - lease.diagnostic_store, - ); + var raw: c.mln_render_frame_result = undefined; + raw.size = @sizeOf(c.mln_render_frame_result); + try status.checkStatus(c.mln_acquired_frame_get_result(self.native, &raw), lease.diagnostic_store); + return frameResultFromNative(raw); } - /// Presents this attached surface session through a new Metal surface, - /// keeping this session's renderer. The descriptor's extent applies as - /// `resize` applies one. A `context.device` that is neither null nor this - /// session's device returns `error.InvalidArgument` and leaves the session - /// on its current surface; the session assigns the layer its own device and - /// pixel format. - pub fn setMetalSurfaceTarget(self: *RenderSessionHandle, descriptor: MetalSurfaceDescriptor) status.Error!void { - var raw = metalSurfaceDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_metal_surface_set_target, &raw); + pub fn producerSync(self: AcquiredFrame) status.Error!GpuSync { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var raw = gpuSyncToNative(.cpu_complete); + try status.checkStatus(c.mln_acquired_frame_get_producer_sync(self.native, &raw), lease.diagnostic_store); + return gpuSyncFromNative(raw); } - /// Presents this attached surface session through a new Vulkan surface, - /// keeping this session's renderer. The outgoing `VkSurfaceKHR` must still - /// be valid, since this session destroys the swapchain built from it. A - /// host that must release its surface first closes this session instead. - pub fn setVulkanSurfaceTarget(self: *RenderSessionHandle, descriptor: VulkanSurfaceDescriptor) status.Error!void { - var raw = vulkanSurfaceDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_vulkan_surface_set_target, &raw); + pub fn metalTexture(self: AcquiredFrame) status.Error!MetalOwnedTextureFrameInfo { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var raw: c.mln_metal_owned_texture_frame = undefined; + raw.size = @sizeOf(c.mln_metal_owned_texture_frame); + try status.checkStatus(c.mln_acquired_frame_get_metal_texture(self.native, &raw), lease.diagnostic_store); + return .{ .generation = raw.generation, .width = raw.width, .height = raw.height, .scale_factor = raw.scale_factor, .texture = NativePointer.fromPtr(raw.texture orelse return error.ClosedHandle), .device = NativePointer.fromPtr(raw.device orelse return error.ClosedHandle), .pixel_format = raw.pixel_format }; } - /// Presents this attached surface session through a new OpenGL surface, - /// keeping this session's renderer. The new surface is made current on the - /// next render, so the outgoing surface may already be destroyed, and an - /// unusable replacement surfaces as `error.NativeError` from the next - /// `renderUpdate` rather than from this call. - pub fn setOpenGLSurfaceTarget(self: *RenderSessionHandle, descriptor: OpenGLSurfaceDescriptor) status.Error!void { - var raw = openglSurfaceDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_opengl_surface_set_target, &raw); + pub fn vulkanTexture(self: AcquiredFrame) status.Error!VulkanOwnedTextureFrameInfo { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var raw: c.mln_vulkan_owned_texture_frame = undefined; + raw.size = @sizeOf(c.mln_vulkan_owned_texture_frame); + try status.checkStatus(c.mln_acquired_frame_get_vulkan_texture(self.native, &raw), lease.diagnostic_store); + return .{ .generation = raw.generation, .width = raw.width, .height = raw.height, .scale_factor = raw.scale_factor, .image = NativePointer.fromPtr(raw.image orelse return error.ClosedHandle), .image_view = NativePointer.fromPtr(raw.image_view orelse return error.ClosedHandle), .device = NativePointer.fromPtr(raw.device orelse return error.ClosedHandle), .format = raw.format, .layout = raw.layout }; } - /// Renders this attached texture session into a new caller-owned Metal - /// texture, keeping this session's renderer. The new extent applies as - /// `resize` applies one. - /// - /// The replacement must belong to the device this session attached with - /// (`error.InvalidArgument` otherwise) and carry the pixel format it - /// attached with (`error.Unsupported` otherwise); either leaves the session - /// on its current texture. The caller keeps the replacement valid until the - /// next replacement, detach, or close. The outgoing texture is neither read - /// nor released here. - pub fn setMetalBorrowedTextureTarget(self: *RenderSessionHandle, descriptor: MetalBorrowedTextureDescriptor) status.Error!void { - var raw = metalBorrowedTextureDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_metal_borrowed_texture_set_target, &raw); + pub fn openGLTexture(self: AcquiredFrame) status.Error!OpenGLOwnedTextureFrameInfo { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var raw: c.mln_opengl_owned_texture_frame = undefined; + raw.size = @sizeOf(c.mln_opengl_owned_texture_frame); + try status.checkStatus(c.mln_acquired_frame_get_opengl_texture(self.native, &raw), lease.diagnostic_store); + return .{ .generation = raw.generation, .width = raw.width, .height = raw.height, .scale_factor = raw.scale_factor, .texture = raw.texture, .target = raw.target, .internal_format = raw.internal_format, .format = raw.format, .type = raw.type }; } - /// Renders this attached texture session into a new caller-owned Vulkan - /// image. See `setMetalBorrowedTextureTarget`. The replacement must carry - /// the format and both layouts this session attached with. - pub fn setVulkanBorrowedTextureTarget(self: *RenderSessionHandle, descriptor: VulkanBorrowedTextureDescriptor) status.Error!void { - var raw = vulkanBorrowedTextureDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_vulkan_borrowed_texture_set_target, &raw); + pub fn webGPUTexture(self: AcquiredFrame) status.Error!WebGPUOwnedTextureFrameInfo { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var raw: c.mln_webgpu_owned_texture_frame = undefined; + raw.size = @sizeOf(c.mln_webgpu_owned_texture_frame); + try status.checkStatus(c.mln_acquired_frame_get_webgpu_texture(self.native, &raw), lease.diagnostic_store); + return .{ .generation = raw.generation, .width = raw.width, .height = raw.height, .scale_factor = raw.scale_factor, .texture = NativePointer.fromPtr(raw.texture orelse return error.ClosedHandle), .texture_view = NativePointer.fromPtr(raw.texture_view orelse return error.ClosedHandle), .device = NativePointer.fromPtr(raw.device orelse return error.ClosedHandle), .format = raw.format }; } - /// Renders this attached texture session into a new caller-owned OpenGL - /// texture. See `setMetalBorrowedTextureTarget`. The replacement must - /// belong to the context this session attached with or one in its share - /// group, and that context must be current on this thread. - pub fn setOpenGLBorrowedTextureTarget(self: *RenderSessionHandle, descriptor: OpenGLBorrowedTextureDescriptor) status.Error!void { - var raw = openglBorrowedTextureDescriptorToNative(descriptor); - return try setTarget(self.*, c.mln_opengl_borrowed_texture_set_target, &raw); + pub fn release(self: *AcquiredFrame, consumer_completion: GpuSync) status.Error!void { + const lease = try renderSessionLease(self.session); + defer lease.release(); + var sync = gpuSyncToNative(consumer_completion); + try status.checkStatus(c.mln_acquired_frame_release(&self.native, &sync), lease.diagnostic_store); + } +}; + +pub const OwnedReadback = struct { + allocator: std.mem.Allocator, + data: []u8, + info: TextureImageInfo, + + pub fn deinit(self: *OwnedReadback) void { + self.allocator.free(self.data); + self.data = &.{}; } +}; - /// Renders the latest available map render update. The map retains its - /// latest update, so repeated calls re-render it and report `.rendered` - /// again. Every other result names the wake to wait for: `.no_update` and - /// `.size_pending` resolve on a render-update-available event, and - /// `.target_not_ready` resolves when the host changes the render target. - pub fn renderUpdate(self: *RenderSessionHandle) status.Error!RenderUpdate { - const lease = try renderSessionLease(self.*); +pub const RenderSessionHandle = enum(c.mln_render_session) { + _, + + pub fn capabilities(self: RenderSessionHandle) status.Error!RenderSessionCapabilities { + const lease = try renderSessionLease(self); defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - var result: c.mln_render_result = c.MLN_RENDER_RESULT_NO_UPDATE; - var needs_repaint: bool = false; - try status.checkStatus( - c.mln_render_session_render_update(lease.native, &result, &needs_repaint), - lease.diagnostic_store, - ); + var raw: c.mln_render_session_capabilities = undefined; + raw.size = @sizeOf(c.mln_render_session_capabilities); + try status.checkStatus(c.mln_render_session_get_capabilities(lease.native, &raw), lease.diagnostic_store); return .{ - .result = RenderResult.fromRaw(@intCast(result)), - .needs_repaint = needs_repaint, + .driver = try driverFromRaw(raw.driver), + .texture_ring_depth = raw.texture_ring_depth, + .frame_acquisition = (raw.flags & c.MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION) != 0, + .readback = (raw.flags & c.MLN_RENDER_SESSION_CAPABILITY_READBACK) != 0, + .consumer_sync = (raw.flags & c.MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC) != 0, + .presentation = (raw.flags & c.MLN_RENDER_SESSION_CAPABILITY_PRESENTATION) != 0, }; } - /// Detaches the render target from this session. The session stays live and - /// still needs `close`, but releases its map, so the map may be closed while - /// this session is open and every operation needing an attached target - /// returns a native error. Detaching is permanent; attach a new session to - /// render again. - pub fn detach(self: *RenderSessionHandle) status.Error!void { - const lease = try renderSessionLease(self.*); + pub fn snapshot(self: RenderSessionHandle) status.Error!RenderSessionSnapshot { + const lease = try renderSessionLease(self); defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - try status.checkStatus(c.mln_render_session_detach(lease.native), lease.diagnostic_store); - if (takeMapRegistration(lease.state)) |map_handle| { - map_module.unregisterRenderSession(map_handle); - } + var raw: c.mln_render_session_snapshot = undefined; + raw.size = @sizeOf(c.mln_render_session_snapshot); + try status.checkStatus(c.mln_render_session_get_snapshot(lease.native, &raw), lease.diagnostic_store); + return .{ + .state = lifecycleFromRaw(raw.state), + .driver = try driverFromRaw(raw.driver), + .latest_result = RenderResult.fromRaw(raw.latest_result), + .extent = renderTargetExtentFromNative(raw.extent), + .generation = raw.generation, + .map_update_generation = raw.map_update_generation, + .rendered_update_generation = raw.rendered_update_generation, + .extent_generation = raw.extent_generation, + .frame_generation = raw.frame_generation, + .latest_demand_token = raw.latest_demand_token, + .pending_demand_count = raw.pending_demand_count, + .acquired_frame_count = raw.acquired_frame_count, + .target_ready = raw.target_ready, + .pending_changes = raw.pending_changes, + }; + } + + pub fn requestFrame(self: RenderSessionHandle, demand: FrameDemand) status.Error!void { + const lease = try renderSessionLease(self); + defer lease.release(); + var raw = frameDemandToNative(demand); + try status.checkStatus(c.mln_render_session_request_frame(lease.native, &raw), lease.diagnostic_store); } - pub fn reduceMemoryUse(self: *RenderSessionHandle) status.Error!void { - const lease = try renderSessionLease(self.*); + pub fn drainFrameResults(self: RenderSessionHandle) status.Error!RenderFrameBatch { + const lease = try renderSessionLease(self); defer lease.release(); - try status.checkStatus(c.mln_render_session_reduce_memory_use(lease.native), lease.diagnostic_store); + var batch: c.mln_render_frame_batch = 0; + try status.checkStatus(c.mln_render_session_drain_frame_results(lease.native, &batch), lease.diagnostic_store); + return .{ .native = batch, .diagnostic_store = lease.diagnostic_store }; } - pub fn clearData(self: *RenderSessionHandle) status.Error!void { - const lease = try renderSessionLease(self.*); + pub fn acquireFrame(self: RenderSessionHandle) status.Error!AcquiredFrame { + const lease = try renderSessionLease(self); defer lease.release(); - try status.checkStatus(c.mln_render_session_clear_data(lease.native), lease.diagnostic_store); + var frame: c.mln_acquired_frame = 0; + try status.checkStatus(c.mln_render_session_acquire_frame(lease.native, &frame), lease.diagnostic_store); + return .{ .native = frame, .session = self }; } - pub fn dumpDebugLogs(self: *RenderSessionHandle) status.Error!void { - const lease = try renderSessionLease(self.*); + /// This is the only graphics-thread-affine session method. + pub fn serviceDriverWork(self: RenderSessionHandle, max_work: usize) status.Error!usize { + const lease = try renderSessionLease(self); defer lease.release(); - try status.checkStatus(c.mln_render_session_dump_debug_logs(lease.native), lease.diagnostic_store); + var serviced: usize = 0; + try status.checkStatus(c.mln_render_session_service_driver_work(lease.native, max_work, &serviced), lease.diagnostic_store); + return serviced; + } + + pub fn resize(self: RenderSessionHandle, extent: RenderTargetExtent) status.Error!completion.Future(void) { + var raw = renderTargetExtentToNative(extent); + return submitRender(void, self, completion.unit, c.mln_render_session_resize, .{&raw}); + } + + pub fn barrier(self: RenderSessionHandle) status.Error!completion.Future(void) { + return submitRender(void, self, completion.unit, c.mln_render_session_barrier, .{}); } - pub fn queryRenderedFeatures( - self: *RenderSessionHandle, - allocator: std.mem.Allocator, - geometry: RenderedQueryGeometry, - options: ?RenderedFeatureQueryOptions, - ) status.Error!QueriedFeatureList { + pub fn reduceMemoryUse(self: RenderSessionHandle) status.Error!completion.Future(void) { + return submitRender(void, self, completion.unit, c.mln_render_session_reduce_memory_use, .{}); + } + + pub fn clearData(self: RenderSessionHandle) status.Error!completion.Future(void) { + return submitRender(void, self, completion.unit, c.mln_render_session_clear_data, .{}); + } + + pub fn dumpDebugLogs(self: RenderSessionHandle) status.Error!completion.Future(void) { + return submitRender(void, self, completion.unit, c.mln_render_session_dump_debug_logs, .{}); + } + + pub fn queryRenderedFeatures(self: RenderSessionHandle, allocator: std.mem.Allocator, geometry: RenderedQueryGeometry, options: ?RenderedFeatureQueryOptions) status.Error!completion.Future(QueriedFeatureList) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); var raw_geometry = try renderedQueryGeometryToNative(&temp, geometry); - var raw_options = if (options) |query_options| try renderedFeatureQueryOptionsToNative(&temp, query_options) else undefined; - var result: c.mln_queried_feature_list = 0; - const lease = try renderSessionLease(self.*); - defer lease.release(); - try status.checkStatus( - c.mln_render_session_query_rendered_features(lease.native, &raw_geometry, if (options != null) &raw_options else null, &result), - lease.diagnostic_store, - ); - defer c.mln_queried_feature_list_destroy(result); - return try copyQueriedFeatureList(allocator, result, lease.diagnostic_store); + var raw_options = if (options) |value| try renderedFeatureQueryOptionsToNative(&temp, value) else undefined; + return submitAllocatedRender(QueriedFeatureList, self, allocator, copyQueriedFeaturesCompletion, c.mln_render_session_query_rendered_features, .{ &raw_geometry, if (options != null) &raw_options else null }); } - pub fn querySourceFeatures( - self: *RenderSessionHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - options: ?SourceFeatureQueryOptions, - ) status.Error!QueriedFeatureList { + pub fn querySourceFeatures(self: RenderSessionHandle, allocator: std.mem.Allocator, source_id: []const u8, options: ?SourceFeatureQueryOptions) status.Error!completion.Future(QueriedFeatureList) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - const raw_source_id = try temp.stringView(source_id); - var raw_options = if (options) |query_options| try sourceFeatureQueryOptionsToNative(&temp, query_options) else undefined; - var result: c.mln_queried_feature_list = 0; - const lease = try renderSessionLease(self.*); - defer lease.release(); - try status.checkStatus( - c.mln_render_session_query_source_features(lease.native, raw_source_id, if (options != null) &raw_options else null, &result), - lease.diagnostic_store, - ); - defer c.mln_queried_feature_list_destroy(result); - return try copyQueriedFeatureList(allocator, result, lease.diagnostic_store); + var raw_options = if (options) |value| try sourceFeatureQueryOptionsToNative(&temp, value) else undefined; + return submitAllocatedRender(QueriedFeatureList, self, allocator, copyQueriedFeaturesCompletion, c.mln_render_session_query_source_features, .{ try temp.stringView(source_id), if (options != null) &raw_options else null }); } - /// Queries a feature extension from the latest render session state. - /// - /// The returned string contains either a JSON value or a GeoJSON Feature - /// Collection. The `supercluster` extension requires `cluster_id`, `limit`, - /// and `offset` to be nonnegative integer JSON literals. Floating-point or - /// negative representations are treated as absent. An absent cluster ID - /// produces JSON null; absent limit and offset values use ten leaves at - /// offset zero. - pub fn queryFeatureExtension( - self: *RenderSessionHandle, - allocator: std.mem.Allocator, - source_id: []const u8, - feature: []const u8, - extension: []const u8, - extension_field: []const u8, - arguments: ?[]const u8, - ) status.Error!values.OwnedString { + pub fn queryFeatureExtension(self: RenderSessionHandle, allocator: std.mem.Allocator, source_id: []const u8, feature: []const u8, extension: []const u8, extension_field: []const u8, arguments: ?[]const u8) status.Error!completion.Future(values.OwnedString) { var temp = native_temp.TempStorage.init(allocator); defer temp.deinit(); - const raw_source_id = try temp.stringView(source_id); - const raw_feature = try temp.stringView(feature); - const raw_extension = try temp.stringView(extension); - const raw_extension_field = try temp.stringView(extension_field); - var raw_arguments_value = if (arguments) |value| try temp.stringView(value) else undefined; - const raw_arguments = if (arguments != null) &raw_arguments_value else null; - var result: c.mln_buffer = 0; - const lease = try renderSessionLease(self.*); - defer lease.release(); - try status.checkStatus( - c.mln_render_session_query_feature_extensions(lease.native, raw_source_id, raw_feature, raw_extension, raw_extension_field, raw_arguments, &result), - lease.diagnostic_store, - ); - return (try native_temp.copyOwnedBuffer(allocator, result, lease.diagnostic_store)) orelse error.NativeError; + var raw_arguments = if (arguments) |value| try temp.stringView(value) else undefined; + return submitAllocatedRender(values.OwnedString, self, allocator, copyOwnedBufferViewCompletion, c.mln_render_session_query_feature_extensions, .{ + try temp.stringView(source_id), + try temp.stringView(feature), + try temp.stringView(extension), + try temp.stringView(extension_field), + if (arguments != null) &raw_arguments else null, + }); } - pub fn readPremultipliedRgba8Into(self: *RenderSessionHandle, buffer: []u8) status.Error!TextureImageInfo { - var info = c.mln_texture_image_info_default(); - const lease = try renderSessionLease(self.*); - defer lease.release(); - try status.checkStatus( - c.mln_texture_read_premultiplied_rgba8(lease.native, buffer.ptr, buffer.len, &info), - lease.diagnostic_store, - ); - return textureImageInfoFromNative(info); + pub fn readback(self: RenderSessionHandle, allocator: std.mem.Allocator) status.Error!completion.Future(OwnedReadback) { + return submitAllocatedRender(OwnedReadback, self, allocator, copyReadbackCompletion, c.mln_texture_read_premultiplied_rgba8, .{}); } - pub fn acquireMetalOwnedTextureFrame(self: *RenderSessionHandle) status.Error!MetalOwnedTextureFrameHandle { - const lease = try renderSessionLease(self.*); - defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - var frame = c.mln_metal_owned_texture_frame{ - .size = @sizeOf(c.mln_metal_owned_texture_frame), - .generation = 0, - .width = 0, - .height = 0, - .scale_factor = 0, - .frame_id = 0, - .texture = null, - .device = null, - .pixel_format = 0, - }; - try status.checkStatus(c.mln_metal_owned_texture_acquire_frame(lease.native, &frame), lease.diagnostic_store); - const session_frame_state = lease.frame_state; - session_frame_state.metal_frame_generation +%= 1; - session_frame_state.metal_frame = .{ .raw = frame }; - session_frame_state.metal_frame_active = true; - errdefer { - var release_frame = frame; - _ = c.mln_metal_owned_texture_release_frame(lease.native, &release_frame); - session_frame_state.metal_frame_active = false; - session_frame_state.metal_frame = null; - } - return try newOwnedTextureFrameHandle(MetalOwnedTextureFrameHandle, .{ - .kind = .metal, - .session_native = lease.native, - .diagnostic_store = lease.diagnostic_store, - .frame_state = session_frame_state, - .generation = session_frame_state.metal_frame_generation, - }); + pub fn setMetalSurfaceTarget(self: RenderSessionHandle, descriptor: MetalSurfaceDescriptor) status.Error!completion.Future(void) { + var raw = metalSurfaceDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_metal_surface_set_target, .{&raw}); } - pub fn acquireVulkanOwnedTextureFrame(self: *RenderSessionHandle) status.Error!VulkanOwnedTextureFrameHandle { - const lease = try renderSessionLease(self.*); - defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - var frame = c.mln_vulkan_owned_texture_frame{ - .size = @sizeOf(c.mln_vulkan_owned_texture_frame), - .generation = 0, - .width = 0, - .height = 0, - .scale_factor = 0, - .frame_id = 0, - .image = null, - .image_view = null, - .device = null, - .format = 0, - .layout = 0, - }; - try status.checkStatus(c.mln_vulkan_owned_texture_acquire_frame(lease.native, &frame), lease.diagnostic_store); - const session_frame_state = lease.frame_state; - session_frame_state.vulkan_frame_generation +%= 1; - session_frame_state.vulkan_frame = .{ .raw = frame }; - session_frame_state.vulkan_frame_active = true; - errdefer { - var release_frame = frame; - _ = c.mln_vulkan_owned_texture_release_frame(lease.native, &release_frame); - session_frame_state.vulkan_frame_active = false; - session_frame_state.vulkan_frame = null; - } - return try newOwnedTextureFrameHandle(VulkanOwnedTextureFrameHandle, .{ - .kind = .vulkan, - .session_native = lease.native, - .diagnostic_store = lease.diagnostic_store, - .frame_state = session_frame_state, - .generation = session_frame_state.vulkan_frame_generation, - }); + pub fn setVulkanSurfaceTarget(self: RenderSessionHandle, descriptor: VulkanSurfaceDescriptor) status.Error!completion.Future(void) { + var raw = vulkanSurfaceDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_vulkan_surface_set_target, .{&raw}); } - pub fn acquireOpenGLOwnedTextureFrame(self: *RenderSessionHandle) status.Error!OpenGLOwnedTextureFrameHandle { - const lease = try renderSessionLease(self.*); - defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - var frame = c.mln_opengl_owned_texture_frame{ - .size = @sizeOf(c.mln_opengl_owned_texture_frame), - .generation = 0, - .width = 0, - .height = 0, - .scale_factor = 0, - .frame_id = 0, - .texture = 0, - .target = 0, - .internal_format = 0, - .format = 0, - .type = 0, - }; - try status.checkStatus(c.mln_opengl_owned_texture_acquire_frame(lease.native, &frame), lease.diagnostic_store); - const session_frame_state = lease.frame_state; - session_frame_state.opengl_frame_generation +%= 1; - session_frame_state.opengl_frame = .{ .raw = frame }; - session_frame_state.opengl_frame_active = true; - errdefer { - var release_frame = frame; - _ = c.mln_opengl_owned_texture_release_frame(lease.native, &release_frame); - session_frame_state.opengl_frame_active = false; - session_frame_state.opengl_frame = null; - } - return try newOwnedTextureFrameHandle(OpenGLOwnedTextureFrameHandle, .{ - .kind = .opengl, - .session_native = lease.native, - .diagnostic_store = lease.diagnostic_store, - .frame_state = session_frame_state, - .generation = session_frame_state.opengl_frame_generation, - }); + pub fn setOpenGLSurfaceTarget(self: RenderSessionHandle, descriptor: OpenGLSurfaceDescriptor) status.Error!completion.Future(void) { + var raw = openglSurfaceDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_opengl_surface_set_target, .{&raw}); } - pub fn close(self: *RenderSessionHandle) status.Error!void { - const session_close = try beginRenderSessionClose(self.*) orelse return; - status.checkStatus(c.mln_render_session_destroy(session_close.native), session_close.diagnostic_store) catch |err| { - cancelRenderSessionClose(session_close.state); - return err; - }; - if (takeMapRegistration(session_close.state)) |map_handle| { - map_module.unregisterRenderSession(map_handle); - } - std.heap.smp_allocator.destroy(session_close.frame_state); - const session_state = finishRenderSessionClose(self.*) orelse session_close.state; - if (session_state.diagnostic_store) |store| { - store.deinit(); - std.heap.smp_allocator.destroy(store); - } - std.heap.smp_allocator.destroy(session_state); + pub fn setMetalBorrowedTextureTarget(self: RenderSessionHandle, descriptor: MetalBorrowedTextureDescriptor) status.Error!completion.Future(void) { + var raw = metalBorrowedTextureDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_metal_borrowed_texture_set_target, .{&raw}); } -}; -pub const MetalOwnedTextureFrameHandle = enum(u128) { - _, - - /// Returns native Metal objects for this acquired frame. - /// - /// Safety: the returned pointers stay valid only until this frame handle is - /// released, under the C API's backend synchronization rules. - pub fn info(self: *const MetalOwnedTextureFrameHandle) status.BindingError!MetalOwnedTextureFrameInfo { - const lease = try ownedTextureFrameLease(self.*, .metal); - defer lease.release(); - const frame = try metalFrame(lease); - return .{ - .generation = frame.generation, - .width = frame.width, - .height = frame.height, - .scale_factor = frame.scale_factor, - .texture = NativePointer.fromPtr(frame.texture orelse return error.ClosedHandle), - .device = NativePointer.fromPtr(frame.device orelse return error.ClosedHandle), - .pixel_format = frame.pixel_format, - }; + pub fn setVulkanBorrowedTextureTarget(self: RenderSessionHandle, descriptor: VulkanBorrowedTextureDescriptor) status.Error!completion.Future(void) { + var raw = vulkanBorrowedTextureDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_vulkan_borrowed_texture_set_target, .{&raw}); } - pub fn release(self: *MetalOwnedTextureFrameHandle) status.Error!void { - const frame_close = try beginOwnedTextureFrameClose(self.*, .metal) orelse return; - const session_frame_state = frame_close.frame_state; - const session_native = frame_close.session_native; - if (!session_frame_state.metal_frame_active or frame_close.generation != session_frame_state.metal_frame_generation) { - cancelOwnedTextureFrameClose(frame_close.state); - return; - } - var frame = (session_frame_state.metal_frame orelse { - cancelOwnedTextureFrameClose(frame_close.state); - return; - }).raw; - status.checkStatus(c.mln_metal_owned_texture_release_frame(session_native, &frame), frame_close.diagnostic_store) catch |err| { - cancelOwnedTextureFrameClose(frame_close.state); - return err; - }; - session_frame_state.metal_frame_active = false; - session_frame_state.metal_frame = null; - const frame_state = unregisterOwnedTextureFrameState(self.*) orelse return; - std.heap.smp_allocator.destroy(frame_state); + pub fn setOpenGLBorrowedTextureTarget(self: RenderSessionHandle, descriptor: OpenGLBorrowedTextureDescriptor) status.Error!completion.Future(void) { + var raw = openglBorrowedTextureDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_opengl_borrowed_texture_set_target, .{&raw}); } -}; - -pub const VulkanOwnedTextureFrameHandle = enum(u128) { - _, - - /// Returns native Vulkan objects for this acquired frame. - /// - /// Safety: the returned pointers stay valid only until this frame handle is - /// released, under the C API's backend synchronization rules. - pub fn info(self: *const VulkanOwnedTextureFrameHandle) status.BindingError!VulkanOwnedTextureFrameInfo { - const lease = try ownedTextureFrameLease(self.*, .vulkan); - defer lease.release(); - const frame = try vulkanFrame(lease); - return .{ - .generation = frame.generation, - .width = frame.width, - .height = frame.height, - .scale_factor = frame.scale_factor, - .image = NativePointer.fromPtr(frame.image orelse return error.ClosedHandle), - .image_view = NativePointer.fromPtr(frame.image_view orelse return error.ClosedHandle), - .device = NativePointer.fromPtr(frame.device orelse return error.ClosedHandle), - .format = frame.format, - .layout = frame.layout, - }; + pub fn setWebGPUBorrowedTextureTarget(self: RenderSessionHandle, descriptor: WebGPUBorrowedTextureDescriptor) status.Error!completion.Future(void) { + var raw = webgpuBorrowedTextureDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_webgpu_borrowed_texture_set_target, .{&raw}); } - pub fn release(self: *VulkanOwnedTextureFrameHandle) status.Error!void { - const frame_close = try beginOwnedTextureFrameClose(self.*, .vulkan) orelse return; - const session_frame_state = frame_close.frame_state; - const session_native = frame_close.session_native; - if (!session_frame_state.vulkan_frame_active or frame_close.generation != session_frame_state.vulkan_frame_generation) { - cancelOwnedTextureFrameClose(frame_close.state); - return; - } - var frame = (session_frame_state.vulkan_frame orelse { - cancelOwnedTextureFrameClose(frame_close.state); - return; - }).raw; - status.checkStatus(c.mln_vulkan_owned_texture_release_frame(session_native, &frame), frame_close.diagnostic_store) catch |err| { - cancelOwnedTextureFrameClose(frame_close.state); - return err; - }; - session_frame_state.vulkan_frame_active = false; - session_frame_state.vulkan_frame = null; - const frame_state = unregisterOwnedTextureFrameState(self.*) orelse return; - std.heap.smp_allocator.destroy(frame_state); + pub fn setWebGPUSurfaceTarget(self: RenderSessionHandle, descriptor: WebGPUSurfaceDescriptor) status.Error!completion.Future(void) { + var raw = webgpuSurfaceDescriptorToNative(descriptor); + return submitRender(void, self, completion.unit, c.mln_webgpu_surface_set_target, .{&raw}); } -}; -pub const OpenGLOwnedTextureFrameHandle = enum(u128) { - _, + pub fn detach(self: RenderSessionHandle) status.Error!completion.Future(void) { + return submitRender(void, self, completion.unit, c.mln_render_session_detach, .{}); + } - /// Returns native OpenGL object names for this acquired frame. + /// Irreversibly closes control and mailboxes without a graphics call. /// - /// Safety: the returned names stay valid only until this frame handle is - /// released, under the C API's context-sharing and synchronization rules. - pub fn info(self: *const OpenGLOwnedTextureFrameHandle) status.BindingError!OpenGLOwnedTextureFrameInfo { - const lease = try ownedTextureFrameLease(self.*, .opengl); + /// The call waits for the map's in-flight tile work before returning, so no + /// library thread touches the session's target or device afterward and the + /// host may destroy its graphics objects immediately. Do not call it from a + /// MapLibre worker callback. + pub fn abandon(self: RenderSessionHandle) status.Error!AbandonResult { + const lease = try renderSessionLease(self); defer lease.release(); - const frame = try openglFrame(lease); - return .{ - .generation = frame.generation, - .width = frame.width, - .height = frame.height, - .scale_factor = frame.scale_factor, - .texture = frame.texture, - .target = frame.target, - .internal_format = frame.internal_format, - .format = frame.format, - .type = frame.type, - }; + var raw: c.mln_render_abandon_result = undefined; + raw.size = @sizeOf(c.mln_render_abandon_result); + try status.checkStatus(c.mln_render_session_abandon(lease.native, &raw), lease.diagnostic_store); + if (takeMapRegistration(lease.state)) |map_handle| map_module.unregisterRenderSession(map_handle); + return .{ .disposition = abandonDispositionFromRaw(raw.disposition), .quarantined_resource_count = raw.quarantined_resource_count }; } - pub fn release(self: *OpenGLOwnedTextureFrameHandle) status.Error!void { - const frame_close = try beginOwnedTextureFrameClose(self.*, .opengl) orelse return; - const session_frame_state = frame_close.frame_state; - const session_native = frame_close.session_native; - if (!session_frame_state.opengl_frame_active or frame_close.generation != session_frame_state.opengl_frame_generation) { - cancelOwnedTextureFrameClose(frame_close.state); - return; - } - var frame = (session_frame_state.opengl_frame orelse { - cancelOwnedTextureFrameClose(frame_close.state); - return; - }).raw; - status.checkStatus(c.mln_opengl_owned_texture_release_frame(session_native, &frame), frame_close.diagnostic_store) catch |err| { - cancelOwnedTextureFrameClose(frame_close.state); + pub fn destroy(self: *RenderSessionHandle) status.Error!void { + const session_close = try beginRenderSessionClose(self.*) orelse return; + status.checkStatus(c.mln_render_session_destroy(session_close.native), session_close.diagnostic_store) catch |err| { + cancelRenderSessionClose(session_close.state); return err; }; - session_frame_state.opengl_frame_active = false; - session_frame_state.opengl_frame = null; - const frame_state = unregisterOwnedTextureFrameState(self.*) orelse return; - std.heap.smp_allocator.destroy(frame_state); + if (takeMapRegistration(session_close.state)) |map_handle| map_module.unregisterRenderSession(map_handle); + const state = finishRenderSessionClose(self.*) orelse session_close.state; + if (state.diagnostic_store) |store| { + store.deinit(); + std.heap.smp_allocator.destroy(store); + } + std.heap.smp_allocator.destroy(state); + self.* = @enumFromInt(0); } }; -pub fn attachMetalOwnedTexture(map: *map_module.MapHandle, descriptor: MetalOwnedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachMetalOwnedTexture(map: *map_module.MapHandle, descriptor: MetalOwnedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = c.mln_metal_owned_texture_descriptor_default(); raw.extent = renderTargetExtentToNative(descriptor.extent); raw.context = metalContextToNative(descriptor.context); - return try attach(map, c.mln_metal_owned_texture_attach, &raw); + return attach(map, c.mln_metal_owned_texture_attach, &raw, options); } -pub fn attachMetalBorrowedTexture(map: *map_module.MapHandle, descriptor: MetalBorrowedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachMetalBorrowedTexture(map: *map_module.MapHandle, descriptor: MetalBorrowedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = metalBorrowedTextureDescriptorToNative(descriptor); - return try attach(map, c.mln_metal_borrowed_texture_attach, &raw); + return attach(map, c.mln_metal_borrowed_texture_attach, &raw, options); } -pub fn attachVulkanOwnedTexture(map: *map_module.MapHandle, descriptor: VulkanOwnedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachVulkanOwnedTexture(map: *map_module.MapHandle, descriptor: VulkanOwnedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = c.mln_vulkan_owned_texture_descriptor_default(); raw.extent = renderTargetExtentToNative(descriptor.extent); raw.context = vulkanContextToNative(descriptor.context); - return try attach(map, c.mln_vulkan_owned_texture_attach, &raw); + return attach(map, c.mln_vulkan_owned_texture_attach, &raw, options); } -pub fn attachVulkanBorrowedTexture(map: *map_module.MapHandle, descriptor: VulkanBorrowedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachVulkanBorrowedTexture(map: *map_module.MapHandle, descriptor: VulkanBorrowedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = vulkanBorrowedTextureDescriptorToNative(descriptor); - return try attach(map, c.mln_vulkan_borrowed_texture_attach, &raw); + return attach(map, c.mln_vulkan_borrowed_texture_attach, &raw, options); } -pub fn attachOpenGLOwnedTexture(map: *map_module.MapHandle, descriptor: OpenGLOwnedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachOpenGLOwnedTexture(map: *map_module.MapHandle, descriptor: OpenGLOwnedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = c.mln_opengl_owned_texture_descriptor_default(); raw.extent = renderTargetExtentToNative(descriptor.extent); raw.context = openglContextToNative(descriptor.context); - return try attach(map, c.mln_opengl_owned_texture_attach, &raw); + return attach(map, c.mln_opengl_owned_texture_attach, &raw, options); } -pub fn attachOpenGLBorrowedTexture(map: *map_module.MapHandle, descriptor: OpenGLBorrowedTextureDescriptor) status.Error!RenderSessionHandle { +pub fn attachOpenGLBorrowedTexture(map: *map_module.MapHandle, descriptor: OpenGLBorrowedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = openglBorrowedTextureDescriptorToNative(descriptor); - return try attach(map, c.mln_opengl_borrowed_texture_attach, &raw); + return attach(map, c.mln_opengl_borrowed_texture_attach, &raw, options); +} +pub fn attachWebGPUOwnedTexture(map: *map_module.MapHandle, descriptor: WebGPUOwnedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { + var raw = c.mln_webgpu_owned_texture_descriptor_default(); + raw.extent = renderTargetExtentToNative(descriptor.extent); + raw.context = webgpuContextToNative(descriptor.context); + return attach(map, c.mln_webgpu_owned_texture_attach, &raw, options); } -pub fn attachMetalSurface(map: *map_module.MapHandle, descriptor: MetalSurfaceDescriptor) status.Error!RenderSessionHandle { +pub fn attachWebGPUBorrowedTexture(map: *map_module.MapHandle, descriptor: WebGPUBorrowedTextureDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { + var raw = webgpuBorrowedTextureDescriptorToNative(descriptor); + return attach(map, c.mln_webgpu_borrowed_texture_attach, &raw, options); +} + +pub fn attachMetalSurface(map: *map_module.MapHandle, descriptor: MetalSurfaceDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = metalSurfaceDescriptorToNative(descriptor); - return try attach(map, c.mln_metal_surface_attach, &raw); + return attach(map, c.mln_metal_surface_attach, &raw, options); } -pub fn attachVulkanSurface(map: *map_module.MapHandle, descriptor: VulkanSurfaceDescriptor) status.Error!RenderSessionHandle { +pub fn attachVulkanSurface(map: *map_module.MapHandle, descriptor: VulkanSurfaceDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = vulkanSurfaceDescriptorToNative(descriptor); - return try attach(map, c.mln_vulkan_surface_attach, &raw); + return attach(map, c.mln_vulkan_surface_attach, &raw, options); } -pub fn attachOpenGLSurface(map: *map_module.MapHandle, descriptor: OpenGLSurfaceDescriptor) status.Error!RenderSessionHandle { +pub fn attachOpenGLSurface(map: *map_module.MapHandle, descriptor: OpenGLSurfaceDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { var raw = openglSurfaceDescriptorToNative(descriptor); - return try attach(map, c.mln_opengl_surface_attach, &raw); + return attach(map, c.mln_opengl_surface_attach, &raw, options); +} +pub fn attachWebGPUSurface(map: *map_module.MapHandle, descriptor: WebGPUSurfaceDescriptor, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { + var raw = webgpuSurfaceDescriptorToNative(descriptor); + return attach(map, c.mln_webgpu_surface_attach, &raw, options); } -fn attach( - map: *map_module.MapHandle, - comptime attachFn: anytype, - descriptor: anytype, -) status.Error!RenderSessionHandle { - var session: c.mln_render_session = 0; +fn attach(map: *map_module.MapHandle, comptime attachFn: anytype, descriptor: anytype, options: RenderSessionAttachOptions) status.Error!RenderSessionAttachment { const registration = try map_module.registerRenderSession(map); - errdefer map_module.unregisterRenderSession(map.*); - try status.checkStatus( - attachFn(registration.native, descriptor, &session), - registration.diagnostic_store, - ); - errdefer _ = c.mln_render_session_destroy(session); - return try newRenderSession(session, map.*, registration.diagnostic_store); + var raw_options = attachOptionsToNative(options); + const Context = struct { + map: c.mln_map, + descriptor: @TypeOf(descriptor), + options: *const c.mln_render_session_attach_options, + }; + var submission = completion.submitWithOutput(void, c.mln_render_session, registration.diagnostic_store, completion.unit, 0, Context{ + .map = registration.native, + .descriptor = descriptor, + .options = &raw_options, + }, struct { + fn start(context: Context, out_session: *c.mln_render_session, completion_descriptor: *const c.mln_completion) c.mln_status { + return attachFn(context.map, context.descriptor, context.options, out_session, completion_descriptor); + } + }.start) catch |err| { + map_module.unregisterRenderSession(map.*); + return err; + }; + const session = submission.output; + const handle = newRenderSession(session, map.*, registration.runtime, registration.diagnostic_store) catch |err| { + submission.future.deinit(); + var abandoned: c.mln_render_abandon_result = undefined; + abandoned.size = @sizeOf(c.mln_render_abandon_result); + _ = c.mln_render_session_abandon(session, &abandoned); + _ = c.mln_render_session_destroy(session); + map_module.unregisterRenderSession(map.*); + return err; + }; + return .{ .session = handle, .completion = submission.future }; } -/// Shared body for the `set*Target` methods. The C API borrows the caller's -/// native descriptor only for the duration of this call. -fn setTarget( +fn submitRender( + comptime T: type, handle: RenderSessionHandle, - comptime setTargetFn: anytype, - descriptor: anytype, -) status.Error!void { + comptime copy: *const fn (*const c.mln_completion_result) status.Error!T, + comptime startFn: anytype, + arguments: anytype, +) status.Error!completion.Future(T) { const lease = try renderSessionLease(handle); defer lease.release(); - try ensureNoActiveOwnedFrame(lease); - try status.checkStatus(setTargetFn(lease.native, descriptor), lease.diagnostic_store); + const Arguments = @TypeOf(arguments); + const Context = struct { native: c.mln_render_session, arguments: Arguments }; + return completion.submit(T, lease.diagnostic_store, copy, Context{ .native = lease.native, .arguments = arguments }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return @call(.auto, startFn, .{context.native} ++ context.arguments ++ .{descriptor}); + } + }.start); } -fn newRenderSession( - session: c.mln_render_session, - map_handle: map_module.MapHandle, - diagnostic_store: ?*diagnostics.DiagnosticStore, -) std.mem.Allocator.Error!RenderSessionHandle { - const session_frame_state = try std.heap.smp_allocator.create(RenderSessionFrameState); - session_frame_state.* = .{}; - errdefer std.heap.smp_allocator.destroy(session_frame_state); +fn submitAllocatedRender( + comptime T: type, + handle: RenderSessionHandle, + allocator: std.mem.Allocator, + comptime copy: *const fn (*const c.mln_completion_result, *std.mem.Allocator) status.Error!T, + comptime startFn: anytype, + arguments: anytype, +) status.Error!completion.Future(T) { + const lease = try renderSessionLease(handle); + defer lease.release(); + const Arguments = @TypeOf(arguments); + const Context = struct { native: c.mln_render_session, arguments: Arguments }; + return completion.submitWithCopyContext(T, std.mem.Allocator, lease.diagnostic_store, copy, allocator, Context{ .native = lease.native, .arguments = arguments }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return @call(.auto, startFn, .{context.native} ++ context.arguments ++ .{descriptor}); + } + }.start); +} - // Allocated from the map store's allocator so host memory still accounts - // for it. +fn newRenderSession(session: c.mln_render_session, map_handle: map_module.MapHandle, runtime: runtime_module.RuntimeHandle, diagnostic_store: ?*diagnostics.DiagnosticStore) std.mem.Allocator.Error!RenderSessionHandle { const session_store: ?*diagnostics.DiagnosticStore = if (diagnostic_store) |map_store| store: { - const store = try std.heap.smp_allocator.create(diagnostics.DiagnosticStore); - store.* = diagnostics.DiagnosticStore.init(map_store.allocator); - break :store store; + const result = try std.heap.smp_allocator.create(diagnostics.DiagnosticStore); + result.* = diagnostics.DiagnosticStore.init(map_store.allocator); + break :store result; } else null; errdefer if (session_store) |store| { store.deinit(); std.heap.smp_allocator.destroy(store); }; - - const session_state = try std.heap.smp_allocator.create(RenderSessionState); - session_state.* = .{ - .map_handle = map_handle, - .diagnostic_store = session_store, - .frame_state = session_frame_state, - }; - errdefer std.heap.smp_allocator.destroy(session_state); - - return try registerRenderSessionState(session, session_state); -} - -fn registerRenderSessionState( - session: c.mln_render_session, - session_state: *RenderSessionState, -) std.mem.Allocator.Error!RenderSessionHandle { + const state = try std.heap.smp_allocator.create(RenderSessionState); + state.* = .{ .map_handle = map_handle, .runtime = runtime, .diagnostic_store = session_store }; + errdefer std.heap.smp_allocator.destroy(state); lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - try render_session_registry.put(std.heap.smp_allocator, session, session_state); + try render_session_registry.put(std.heap.smp_allocator, session, state); return @enumFromInt(session); } fn renderSessionLease(handle: RenderSessionHandle) status.BindingError!RenderSessionLease { lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - const session_state = render_session_registry.get(@intFromEnum(handle)) orelse return error.ClosedHandle; - if (session_state.closing) return error.ActiveBorrow; - const session_frame_state = session_state.frame_state orelse return error.ClosedHandle; - _ = session_state.active_leases.fetchAdd(1, .seq_cst); - return .{ - .state = session_state, - .native = @intFromEnum(handle), - .diagnostic_store = session_state.diagnostic_store, - .frame_state = session_frame_state, - }; + const state = render_session_registry.get(@intFromEnum(handle)) orelse return error.ClosedHandle; + if (state.closing) return error.ActiveBorrow; + _ = state.active_leases.fetchAdd(1, .seq_cst); + return .{ .state = state, .native = @intFromEnum(handle), .runtime = state.runtime, .diagnostic_store = state.diagnostic_store }; } fn beginRenderSessionClose(handle: RenderSessionHandle) status.BindingError!?RenderSessionClose { lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - const session_state = render_session_registry.get(@intFromEnum(handle)) orelse return null; - if (session_state.closing) return error.ActiveBorrow; - if (session_state.active_leases.load(.seq_cst) != 0) return error.ActiveBorrow; - const session_frame_state = session_state.frame_state orelse return null; - if (session_frame_state.metal_frame_active or session_frame_state.opengl_frame_active or session_frame_state.vulkan_frame_active) return error.ActiveBorrow; - session_state.closing = true; - return .{ - .state = session_state, - .native = @intFromEnum(handle), - .diagnostic_store = session_state.diagnostic_store, - .frame_state = session_frame_state, - }; + const state = render_session_registry.get(@intFromEnum(handle)) orelse return null; + if (state.closing or state.active_leases.load(.seq_cst) != 0) return error.ActiveBorrow; + state.closing = true; + return .{ .state = state, .native = @intFromEnum(handle), .diagnostic_store = state.diagnostic_store }; } -/// Takes this session's map registration exactly once. The caller passes the -/// returned handle to `map_module.unregisterRenderSession` outside the render -/// session registry lock. -fn takeMapRegistration(session_state: *RenderSessionState) ?map_module.MapHandle { +fn takeMapRegistration(state: *RenderSessionState) ?map_module.MapHandle { lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - const map_handle = session_state.map_handle orelse return null; - session_state.map_handle = null; + const map_handle = state.map_handle orelse return null; + state.map_handle = null; return map_handle; } -fn cancelRenderSessionClose(session_state: *RenderSessionState) void { +fn cancelRenderSessionClose(state: *RenderSessionState) void { lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - session_state.closing = false; + state.closing = false; } fn finishRenderSessionClose(handle: RenderSessionHandle) ?*RenderSessionState { lockRenderSessionRegistry(); defer unlockRenderSessionRegistry(); - - const entry = render_session_registry.fetchRemove(@intFromEnum(handle)) orelse return null; - entry.value.frame_state = null; - return entry.value; + return (render_session_registry.fetchRemove(@intFromEnum(handle)) orelse return null).value; } fn lockRenderSessionRegistry() void { - while (render_session_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) { - std.Thread.yield() catch {}; - } + while (render_session_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) std.Thread.yield() catch {}; } fn unlockRenderSessionRegistry() void { render_session_registry_lock.store(false, .seq_cst); } -fn newOwnedTextureFrameHandle(comptime T: type, initial_state: OwnedTextureFrameState) std.mem.Allocator.Error!T { - if (fail_next_owned_texture_frame_wrapper_allocation_for_testing.swap(false, .seq_cst)) return error.OutOfMemory; - const frame_state_handle = try std.heap.smp_allocator.create(OwnedTextureFrameState); - frame_state_handle.* = initial_state; - errdefer std.heap.smp_allocator.destroy(frame_state_handle); - return try registerOwnedTextureFrameState(T, frame_state_handle); -} - -pub fn failNextOwnedTextureFrameWrapperAllocationForTesting() void { - fail_next_owned_texture_frame_wrapper_allocation_for_testing.store(true, .seq_cst); -} - -fn registerOwnedTextureFrameState(comptime T: type, frame_state_handle: *OwnedTextureFrameState) std.mem.Allocator.Error!T { - lockOwnedTextureFrameRegistry(); - defer unlockOwnedTextureFrameRegistry(); - - if (owned_texture_frame_free_list.items.len > 0) { - const slot_index = owned_texture_frame_free_list.pop().?; - owned_texture_frame_registry.items[slot_index].state = frame_state_handle; - owned_texture_frame_registry.items[slot_index].generation = runtime_module.nextHandleGeneration(); - return ownedTextureFrameHandle(T, slot_index + 1, owned_texture_frame_registry.items[slot_index].generation); - } - - const generation = runtime_module.nextHandleGeneration(); - try owned_texture_frame_free_list.ensureTotalCapacity(std.heap.smp_allocator, owned_texture_frame_registry.items.len + 1); - try owned_texture_frame_registry.append(std.heap.smp_allocator, .{ .state = frame_state_handle, .generation = generation }); - return ownedTextureFrameHandle(T, owned_texture_frame_registry.items.len, generation); +fn renderTargetExtentToNative(extent: RenderTargetExtent) c.mln_render_target_extent { + return .{ + .size = @sizeOf(c.mln_render_target_extent), + .width = extent.width, + .height = extent.height, + .scale_factor = extent.scale_factor, + }; } -fn ownedTextureFrameHandle(comptime T: type, index: usize, generation: u64) T { - return @enumFromInt((@as(u128, generation) << 64) | @as(u128, @intCast(index))); +fn lifecycleFromRaw(raw: u32) RenderSessionLifecycle { + return switch (raw) { + c.MLN_RENDER_SESSION_STATE_ATTACHING => .attaching, + c.MLN_RENDER_SESSION_STATE_ATTACHED => .attached, + c.MLN_RENDER_SESSION_STATE_DETACHING => .detaching, + c.MLN_RENDER_SESSION_STATE_DETACHED => .detached, + c.MLN_RENDER_SESSION_STATE_TARGET_LOST => .target_lost, + c.MLN_RENDER_SESSION_STATE_ABANDONED => .abandoned, + else => .{ .unknown = raw }, + }; } -fn ownedTextureFrameHandleIndex(handle_value: u128) ?usize { - const index = handle_value & std.math.maxInt(u64); - if (index == 0 or index > std.math.maxInt(usize)) return null; - return @intCast(index); +fn abandonDispositionFromRaw(raw: u32) AbandonDisposition { + return switch (raw) { + c.MLN_RENDER_ABANDON_DISPOSITION_CLEAN => .clean, + c.MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED => .quarantined, + else => .{ .unknown = raw }, + }; } - -fn ownedTextureFrameHandleGeneration(handle_value: u128) u64 { - return @intCast(handle_value >> 64); +fn renderTargetExtentFromNative(raw: c.mln_render_target_extent) RenderTargetExtent { + return .{ .width = raw.width, .height = raw.height, .scale_factor = raw.scale_factor }; } -fn ownedTextureFrameLease(handle: anytype, kind: OwnedTextureFrameKind) status.BindingError!OwnedTextureFrameLease { - lockOwnedTextureFrameRegistry(); - defer unlockOwnedTextureFrameRegistry(); - - const handle_value = @intFromEnum(handle); - const index = ownedTextureFrameHandleIndex(handle_value) orelse return error.ClosedHandle; - if (index > owned_texture_frame_registry.items.len) return error.ClosedHandle; - const slot = owned_texture_frame_registry.items[index - 1]; - if (slot.generation != ownedTextureFrameHandleGeneration(handle_value)) return error.ClosedHandle; - const frame_state_handle = slot.state orelse return error.ClosedHandle; - if (frame_state_handle.kind != kind) return error.ClosedHandle; - if (frame_state_handle.closing) return error.ActiveBorrow; - if (frame_state_handle.session_native == 0) return error.ClosedHandle; - const session_native: c.mln_render_session = frame_state_handle.session_native; - const session_frame_state = frame_state_handle.frame_state orelse return error.ClosedHandle; - _ = frame_state_handle.active_leases.fetchAdd(1, .seq_cst); - return .{ - .state = frame_state_handle, - .session_native = session_native, - .diagnostic_store = frame_state_handle.diagnostic_store, - .frame_state = session_frame_state, - .generation = frame_state_handle.generation, +fn driverFromRaw(raw: u32) status.Error!RenderDriver { + return switch (raw) { + c.MLN_RENDER_DRIVER_CORE_WORKER => .core_worker, + c.MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD => .caller_graphics_thread, + else => error.NativeError, }; } -fn beginOwnedTextureFrameClose(handle: anytype, kind: OwnedTextureFrameKind) status.BindingError!?OwnedTextureFrameLease { - lockOwnedTextureFrameRegistry(); - defer unlockOwnedTextureFrameRegistry(); - - const handle_value = @intFromEnum(handle); - const index = ownedTextureFrameHandleIndex(handle_value) orelse return null; - if (index > owned_texture_frame_registry.items.len) return null; - const slot = owned_texture_frame_registry.items[index - 1]; - if (slot.generation != ownedTextureFrameHandleGeneration(handle_value)) return null; - const frame_state_handle = slot.state orelse return null; - if (frame_state_handle.kind != kind) return null; - if (frame_state_handle.closing) return error.ActiveBorrow; - if (frame_state_handle.active_leases.load(.seq_cst) != 0) return error.ActiveBorrow; - if (frame_state_handle.session_native == 0) return null; - const session_native: c.mln_render_session = frame_state_handle.session_native; - const session_frame_state = frame_state_handle.frame_state orelse return null; - frame_state_handle.closing = true; - return .{ - .state = frame_state_handle, - .session_native = session_native, - .diagnostic_store = frame_state_handle.diagnostic_store, - .frame_state = session_frame_state, - .generation = frame_state_handle.generation, - }; +fn attachOptionsToNative(options: RenderSessionAttachOptions) c.mln_render_session_attach_options { + var raw = c.mln_render_session_attach_options_default(); + raw.driver = options.driver.toRaw(); + raw.requested_texture_ring_depth = options.requested_texture_ring_depth; + return raw; } - -fn cancelOwnedTextureFrameClose(frame_state_handle: *OwnedTextureFrameState) void { - lockOwnedTextureFrameRegistry(); - defer unlockOwnedTextureFrameRegistry(); - - frame_state_handle.closing = false; +fn frameDemandToNative(demand: FrameDemand) c.mln_frame_demand { + var flags: u32 = 0; + if (demand.if_needed) flags |= c.MLN_FRAME_DEMAND_IF_NEEDED; + if (demand.present) flags |= c.MLN_FRAME_DEMAND_PRESENT; + return .{ .size = @sizeOf(c.mln_frame_demand), .flags = flags, .token = demand.token, .coalescing_boundary = demand.coalescing_boundary, .timeout_ns = demand.timeout_ns }; } -fn unregisterOwnedTextureFrameState(handle: anytype) ?*OwnedTextureFrameState { - lockOwnedTextureFrameRegistry(); - defer unlockOwnedTextureFrameRegistry(); - - const handle_value = @intFromEnum(handle); - const index = ownedTextureFrameHandleIndex(handle_value) orelse return null; - if (index > owned_texture_frame_registry.items.len) return null; - const slot_index = index - 1; - const slot = &owned_texture_frame_registry.items[slot_index]; - if (slot.generation != ownedTextureFrameHandleGeneration(handle_value)) return null; - const frame_state_handle = slot.state orelse return null; - slot.state = null; - slot.generation = runtime_module.nextHandleGeneration(); - frame_state_handle.session_native = 0; - frame_state_handle.frame_state = null; - owned_texture_frame_free_list.appendAssumeCapacity(slot_index); - return frame_state_handle; +fn frameResultFromNative(raw: c.mln_render_frame_result) FrameResult { + return .{ .disposition = RenderResult.fromRaw(raw.disposition), .token = raw.token, .map_update_generation = raw.map_update_generation, .extent_generation = raw.extent_generation, .frame_generation = raw.frame_generation, .needs_repaint = raw.needs_repaint }; } -fn lockOwnedTextureFrameRegistry() void { - while (owned_texture_frame_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) { - std.Thread.yield() catch {}; +fn gpuSyncToNative(sync: GpuSync) c.mln_gpu_sync { + var raw = c.mln_gpu_sync_default(); + switch (sync) { + .cpu_complete => {}, + .metal_shared_event => |value| { + raw.kind = c.MLN_GPU_SYNC_METAL_SHARED_EVENT; + raw.object = value.object.toPtr(); + raw.value = value.value; + }, + .vulkan_timeline_semaphore => |value| { + raw.kind = c.MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE; + raw.object = value.object.toPtr(); + raw.value = value.value; + }, + .opengl_fence => |value| { + raw.kind = c.MLN_GPU_SYNC_OPENGL_FENCE; + raw.object = value.toPtr(); + }, + .webgpu_token => |value| { + raw.kind = c.MLN_GPU_SYNC_WEBGPU_TOKEN; + raw.object = value.object.toPtr(); + raw.value = value.value; + }, } + return raw; } -fn unlockOwnedTextureFrameRegistry() void { - owned_texture_frame_registry_lock.store(false, .seq_cst); -} - -fn ensureNoActiveOwnedFrame(lease: RenderSessionLease) status.BindingError!void { - const session_frame_state = lease.frame_state; - if (session_frame_state.metal_frame_active or session_frame_state.opengl_frame_active or session_frame_state.vulkan_frame_active) return error.ActiveBorrow; +fn gpuSyncFromNative(raw: c.mln_gpu_sync) status.Error!GpuSync { + return switch (raw.kind) { + c.MLN_GPU_SYNC_CPU_COMPLETE => .cpu_complete, + c.MLN_GPU_SYNC_METAL_SHARED_EVENT => .{ .metal_shared_event = .{ .object = NativePointer.fromPtr(raw.object orelse return error.NativeError), .value = raw.value } }, + c.MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE => .{ .vulkan_timeline_semaphore = .{ .object = NativePointer.fromPtr(raw.object orelse return error.NativeError), .value = raw.value } }, + c.MLN_GPU_SYNC_OPENGL_FENCE => .{ .opengl_fence = NativePointer.fromPtr(raw.object orelse return error.NativeError) }, + c.MLN_GPU_SYNC_WEBGPU_TOKEN => .{ .webgpu_token = .{ .object = NativePointer.fromPtr(raw.object orelse return error.NativeError), .value = raw.value } }, + else => error.NativeError, + }; } -fn metalFrame(lease: OwnedTextureFrameLease) status.BindingError!c.mln_metal_owned_texture_frame { - const session_frame_state = lease.frame_state; - if (!session_frame_state.metal_frame_active or lease.generation != session_frame_state.metal_frame_generation) return error.ClosedHandle; - return (session_frame_state.metal_frame orelse return error.ClosedHandle).raw; +fn copyNativeBuffer(allocator: std.mem.Allocator, buffer: c.mln_buffer, diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error![]u8 { + const owned = (try native_temp.copyOwnedBuffer(allocator, buffer, diagnostic_store)) orelse return error.NativeError; + return @constCast(owned.value); } -fn vulkanFrame(lease: OwnedTextureFrameLease) status.BindingError!c.mln_vulkan_owned_texture_frame { - const session_frame_state = lease.frame_state; - if (!session_frame_state.vulkan_frame_active or lease.generation != session_frame_state.vulkan_frame_generation) return error.ClosedHandle; - return (session_frame_state.vulkan_frame orelse return error.ClosedHandle).raw; +fn copyOwnedBufferViewCompletion(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!values.OwnedString { + const view = try completion.value(c.mln_buffer_view)(result); + return .{ .allocator = allocator.*, .value = try copyBufferView(allocator.*, view) }; } -fn openglFrame(lease: OwnedTextureFrameLease) status.BindingError!c.mln_opengl_owned_texture_frame { - const session_frame_state = lease.frame_state; - if (!session_frame_state.opengl_frame_active or lease.generation != session_frame_state.opengl_frame_generation) return error.ClosedHandle; - return (session_frame_state.opengl_frame orelse return error.ClosedHandle).raw; +fn copyQueriedFeaturesCompletion(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!QueriedFeatureList { + if (result.value_count != 0 and result.value == null) return error.NativeError; + const raw = if (result.value_count == 0) + &.{} + else + @as([*]align(1) const c.mln_queried_feature, @ptrCast(result.value.?))[0..result.value_count]; + const items = try allocator.alloc(QueriedFeature, raw.len); + var initialized: usize = 0; + errdefer { + for (items[0..initialized]) |*item| item.deinit(); + allocator.free(items); + } + for (raw, items) |feature, *item| { + item.* = try copyQueriedFeature(allocator.*, feature); + initialized += 1; + } + return .{ .allocator = allocator.*, .items = items }; } -fn renderTargetExtentToNative(extent: RenderTargetExtent) c.mln_render_target_extent { - return .{ - .size = @sizeOf(c.mln_render_target_extent), - .width = extent.width, - .height = extent.height, - .scale_factor = extent.scale_factor, +fn copyReadbackCompletion(result: *const c.mln_completion_result, allocator: *std.mem.Allocator) status.Error!OwnedReadback { + const raw = try completion.value(c.mln_texture_readback_result)(result); + const data = if (raw.data.size == 0) + try allocator.dupe(u8, "") + else blk: { + const pointer = raw.data.data orelse return error.NativeError; + break :blk try allocator.dupe(u8, @as([*]const u8, @ptrCast(pointer))[0..raw.data.size]); }; + return .{ .allocator = allocator.*, .data = data, .info = textureImageInfoFromNative(raw.info) }; } fn textureImageInfoFromNative(info: c.mln_texture_image_info) TextureImageInfo { @@ -1476,6 +1331,14 @@ fn openglSurfaceDescriptorToNative(descriptor: OpenGLSurfaceDescriptor) c.mln_op raw.surface = descriptor.surface.toPtr(); return raw; } +fn webgpuSurfaceDescriptorToNative(descriptor: WebGPUSurfaceDescriptor) c.mln_webgpu_surface_descriptor { + var raw = c.mln_webgpu_surface_descriptor_default(); + raw.extent = renderTargetExtentToNative(descriptor.extent); + raw.context = webgpuContextToNative(descriptor.context); + raw.surface = descriptor.surface.toPtr(); + raw.format = descriptor.format; + return raw; +} fn metalBorrowedTextureDescriptorToNative(descriptor: MetalBorrowedTextureDescriptor) c.mln_metal_borrowed_texture_descriptor { var raw = c.mln_metal_borrowed_texture_descriptor_default(); @@ -1510,6 +1373,17 @@ fn openglBorrowedTextureDescriptorToNative(descriptor: OpenGLBorrowedTextureDesc raw.target = descriptor.target; return raw; } +fn webgpuBorrowedTextureDescriptorToNative(descriptor: WebGPUBorrowedTextureDescriptor) c.mln_webgpu_borrowed_texture_descriptor { + var raw = c.mln_webgpu_borrowed_texture_descriptor_default(); + raw.extent = renderTargetExtentToNative(descriptor.extent); + raw.physical_width = descriptor.physical_width; + raw.physical_height = descriptor.physical_height; + raw.context = webgpuContextToNative(descriptor.context); + raw.texture = descriptor.texture.toPtr(); + raw.texture_view = descriptor.texture_view.toPtr(); + raw.format = descriptor.format; + return raw; +} fn metalContextToNative(context: MetalContextDescriptor) c.mln_metal_context_descriptor { return .{ @@ -1530,6 +1404,14 @@ fn vulkanContextToNative(context: VulkanContextDescriptor) c.mln_vulkan_context_ .get_device_proc_addr = if (context.get_device_proc_addr) |pointer| pointer.toPtr() else null, }; } +fn webgpuContextToNative(context: WebGPUContextDescriptor) c.mln_webgpu_context_descriptor { + return .{ + .size = @sizeOf(c.mln_webgpu_context_descriptor), + .instance = if (context.instance) |value| value.toPtr() else null, + .device = context.device.toPtr(), + .queue = if (context.queue) |value| value.toPtr() else null, + }; +} fn openglContextToNative(context: OpenGLContextDescriptor) c.mln_opengl_context_descriptor { var raw = c.mln_opengl_context_descriptor{ @@ -1561,6 +1443,28 @@ fn openglContextToNative(context: OpenGLContextDescriptor) c.mln_opengl_context_ .get_proc_address = if (egl.get_proc_address) |pointer| pointer.toPtr() else null, }; }, + .webgl => |webgl| { + raw.platform = c.MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + raw.data.webgl = .{ + .size = @sizeOf(c.mln_webgl_context_descriptor), + .kind = switch (webgl) { + .existing => c.MLN_WEBGL_CONTEXT_EXISTING, + .transferred_canvas => c.MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS, + }, + .context = switch (webgl) { + .existing => |value| value, + .transferred_canvas => 0, + }, + .canvas_selector = switch (webgl) { + .existing => .{ .data = null, .size = 0 }, + .transferred_canvas => |value| .{ .data = value.ptr, .size = value.len }, + }, + }; + switch (webgl) { + .existing => {}, + .transferred_canvas => raw.ownership = c.MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED, + } + }, } return raw; } diff --git a/bindings/zig/src/runtime.zig b/bindings/zig/src/runtime.zig index b4945d3e9..6ca2e83b3 100644 --- a/bindings/zig/src/runtime.zig +++ b/bindings/zig/src/runtime.zig @@ -1,6 +1,7 @@ const std = @import("std"); const c = @import("c.zig").raw; +const completion = @import("completion.zig"); const diagnostics = @import("diagnostics.zig"); const native_temp = @import("native_temp.zig"); const status = @import("status.zig"); @@ -14,7 +15,6 @@ const MapRegistration = struct { pub const RuntimeRegistry = struct { maps: std.ArrayList(MapRegistration), next_map_id: u64, - live_offline_operations: usize, }; const ResourceProviderState = struct { @@ -25,11 +25,11 @@ const ResourceProviderState = struct { const RuntimeState = struct { diagnostic_store: ?*diagnostics.DiagnosticStore, registry: ?*RuntimeRegistry, - resource_transform: ?*ResourceTransform, - http_header_transform: ?*HttpHeaderTransform, - resource_provider: ?*ResourceProviderState, active_leases: std.atomic.Value(usize), closing: bool, + event_callback: ?WakeCallback = null, + event_user_data: ?*anyopaque = null, + wake_lock: std.Io.Mutex = .init, }; pub const RuntimeLease = struct { @@ -42,6 +42,9 @@ pub const RuntimeLease = struct { } }; +/// Callback that schedules a later event drain on the host receiver. +pub const WakeCallback = *const fn (?*anyopaque) callconv(.c) void; + pub const RegisteredMap = struct { registry: *RuntimeRegistry, id: values.MapId, @@ -53,43 +56,14 @@ const ResourceRequestState = struct { diagnostic_store: ?*diagnostics.DiagnosticStore, }; -const WakeSourceState = struct { - native: c.mln_wake_source, - diagnostic_store: ?*diagnostics.DiagnosticStore, -}; - -const OfflineOperationState = struct { - runtime: RuntimeHandle, - operation_id: OfflineOperationId, - operation_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, -}; - -const OfflineOperationRegistrySlot = struct { - state: ?*OfflineOperationState, - generation: u64, -}; - -const RequiredOfflineOperation = struct { - operation_id: OfflineOperationId, - operation_kind: OfflineOperationKind, -}; - -// An offline operation is a binding concept with no C handle behind it, so the -// operation table below carries its own generations. +// The registry gives copied Zig handle values stable closed-handle detection +// while native completion state owns callback and result delivery. var runtime_registry_lock = std.atomic.Value(bool).init(false); var runtime_handle_registry: std.AutoHashMapUnmanaged(c.mln_runtime, *RuntimeState) = .empty; var resource_request_registry_lock = std.atomic.Value(bool).init(false); var resource_request_registry: std.AutoHashMapUnmanaged(c.mln_resource_request_handle, *ResourceRequestState) = .empty; -var wake_source_registry_lock = std.atomic.Value(bool).init(false); -var wake_source_registry: std.AutoHashMapUnmanaged(c.mln_wake_source, *WakeSourceState) = .empty; - -var offline_operation_registry_lock = std.atomic.Value(bool).init(false); -var offline_operation_registry: std.ArrayList(OfflineOperationRegistrySlot) = .empty; -var offline_operation_free_list: std.ArrayList(usize) = .empty; - var handle_generation_counter = std.atomic.Value(u64).init(0); var handle_generation_seed = std.atomic.Value(u64).init(0); @@ -158,90 +132,6 @@ pub const AmbientCacheOperation = enum { }; pub const OfflineRegionId = i64; -pub const OfflineOperationId = u64; - -pub const OfflineOperationKind = union(enum) { - ambient_cache, - region_create, - region_get, - regions_list, - regions_merge_database, - region_update_metadata, - region_get_status, - region_set_observed, - region_set_download_state, - region_invalidate, - region_delete, - set_maximum_ambient_cache_size, - unknown: u32, - - pub fn fromRaw(raw: u32) OfflineOperationKind { - return switch (raw) { - c.MLN_OFFLINE_OPERATION_AMBIENT_CACHE => .ambient_cache, - c.MLN_OFFLINE_OPERATION_REGION_CREATE => .region_create, - c.MLN_OFFLINE_OPERATION_REGION_GET => .region_get, - c.MLN_OFFLINE_OPERATION_REGIONS_LIST => .regions_list, - c.MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE => .regions_merge_database, - c.MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA => .region_update_metadata, - c.MLN_OFFLINE_OPERATION_REGION_GET_STATUS => .region_get_status, - c.MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED => .region_set_observed, - c.MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE => .region_set_download_state, - c.MLN_OFFLINE_OPERATION_REGION_INVALIDATE => .region_invalidate, - c.MLN_OFFLINE_OPERATION_REGION_DELETE => .region_delete, - c.MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE => .set_maximum_ambient_cache_size, - else => .{ .unknown = raw }, - }; - } - - pub fn toRaw(self: OfflineOperationKind) u32 { - return switch (self) { - .ambient_cache => c.MLN_OFFLINE_OPERATION_AMBIENT_CACHE, - .region_create => c.MLN_OFFLINE_OPERATION_REGION_CREATE, - .region_get => c.MLN_OFFLINE_OPERATION_REGION_GET, - .regions_list => c.MLN_OFFLINE_OPERATION_REGIONS_LIST, - .regions_merge_database => c.MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE, - .region_update_metadata => c.MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA, - .region_get_status => c.MLN_OFFLINE_OPERATION_REGION_GET_STATUS, - .region_set_observed => c.MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED, - .region_set_download_state => c.MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE, - .region_invalidate => c.MLN_OFFLINE_OPERATION_REGION_INVALIDATE, - .region_delete => c.MLN_OFFLINE_OPERATION_REGION_DELETE, - .set_maximum_ambient_cache_size => c.MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE, - .unknown => |raw| raw, - }; - } -}; - -pub const OfflineOperationResultKind = union(enum) { - none, - region, - optional_region, - region_list, - region_status, - unknown: u32, - - pub fn fromRaw(raw: u32) OfflineOperationResultKind { - return switch (raw) { - c.MLN_OFFLINE_OPERATION_RESULT_NONE => .none, - c.MLN_OFFLINE_OPERATION_RESULT_REGION => .region, - c.MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION => .optional_region, - c.MLN_OFFLINE_OPERATION_RESULT_REGION_LIST => .region_list, - c.MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS => .region_status, - else => .{ .unknown = raw }, - }; - } - - pub fn toRaw(self: OfflineOperationResultKind) u32 { - return switch (self) { - .none => c.MLN_OFFLINE_OPERATION_RESULT_NONE, - .region => c.MLN_OFFLINE_OPERATION_RESULT_REGION, - .optional_region => c.MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION, - .region_list => c.MLN_OFFLINE_OPERATION_RESULT_REGION_LIST, - .region_status => c.MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS, - .unknown => |raw| raw, - }; - } -}; pub const OfflineTilePyramidRegionDefinition = struct { style_url: []const u8, @@ -522,6 +412,21 @@ pub const ResourceProvider = struct { context: ?*anyopaque = null, }; +fn releaseResourceTransform(user_data: ?*anyopaque) callconv(.c) void { + const value: *ResourceTransform = @ptrCast(@alignCast(user_data orelse return)); + std.heap.smp_allocator.destroy(value); +} + +fn releaseHttpHeaderTransform(user_data: ?*anyopaque) callconv(.c) void { + const value: *HttpHeaderTransform = @ptrCast(@alignCast(user_data orelse return)); + std.heap.smp_allocator.destroy(value); +} + +fn releaseResourceProvider(user_data: ?*anyopaque) callconv(.c) void { + const value: *ResourceProviderState = @ptrCast(@alignCast(user_data orelse return)); + std.heap.smp_allocator.destroy(value); +} + pub const ResourceRequestHandle = enum(c.mln_resource_request_handle) { _, @@ -560,38 +465,6 @@ pub const ResourceRequestHandle = enum(c.mln_resource_request_handle) { } }; -/// Releases a runtime owner thread parked in `RuntimeHandle.pump`. -/// -/// Usable from any thread, and still usable after its runtime closes, when -/// signalling does nothing. The diagnostic store the runtime was created with -/// must outlive the source. -pub const WakeSourceHandle = enum(c.mln_wake_source) { - _, - - /// Sets the runtime's wake flag and releases the parked owner thread. A - /// signal raised while the owner thread runs leaves the flag set, so the - /// next `RuntimeHandle.pump` returns without parking. - pub fn signal(self: WakeSourceHandle) status.Error!void { - lockWakeSourceRegistry(); - defer unlockWakeSourceRegistry(); - - const source_state = wakeSourceState(self) orelse return error.ClosedHandle; - if (source_state.native == 0) return error.ClosedHandle; - const native_source = source_state.native; - try status.checkStatus(c.mln_wake_source_signal(native_source), source_state.diagnostic_store); - } - - /// Releases the wake source. Later signals report a closed handle. - pub fn release(self: WakeSourceHandle) void { - lockWakeSourceRegistry(); - defer unlockWakeSourceRegistry(); - - const source_state = unregisterWakeSourceState(self) orelse return; - defer std.heap.smp_allocator.destroy(source_state); - c.mln_wake_source_destroy(source_state.native); - } -}; - /// One drained batch of copied runtime events. /// /// The batch owns its events and remains valid after another drain or after the @@ -599,14 +472,12 @@ pub const WakeSourceHandle = enum(c.mln_wake_source) { pub const EventBatch = struct { allocator: std.mem.Allocator, events: []RuntimeEvent, - remaining_count: usize, /// Releases every event and the batch storage. pub fn deinit(self: *EventBatch) void { for (self.events) |*event| event.deinit(); self.allocator.free(self.events); self.events = &.{}; - self.remaining_count = 0; } /// Number of events this batch reports. @@ -614,11 +485,6 @@ pub const EventBatch = struct { return self.events.len; } - /// Events still queued for this runtime after this batch. - pub fn remaining(self: EventBatch) usize { - return self.remaining_count; - } - /// Reads the event at `index` in queue order. pub fn at(self: EventBatch, index: usize) status.Error!*const RuntimeEvent { if (index >= self.events.len) return error.InvalidArgument; @@ -673,8 +539,8 @@ pub const RuntimeEvent = struct { payload_type: RuntimeEventPayloadType, /// Secondary detail whose meaning `event_type` selects: a raw /// `CameraChangeMode` for camera change events, MapLibre Native's map load - /// error ordinal for `map_loading_failed`, the raw native status for - /// `offline_operation_completed`, and 0 for every other event type. + /// error ordinal for `map_loading_failed`, and 0 for every other event + /// type. code: i32, message: []const u8, payload: RuntimeEventPayload, @@ -716,7 +582,6 @@ pub const RuntimeEventPayload = union(enum) { offline_region_status: OfflineRegionStatusPayload, offline_region_response_error: OfflineRegionResponseErrorPayload, offline_region_tile_count_limit: OfflineRegionTileCountLimitPayload, - offline_operation_completed: OfflineOperationCompletedPayload, camera_transition_finished: CameraTransitionFinishedPayload, unknown: UnknownPayload, @@ -928,16 +793,6 @@ pub const OfflineRegionTileCountLimitPayload = struct { limit: u64, }; -pub const OfflineOperationCompletedPayload = struct { - operation_id: OfflineOperationId, - operation_kind: OfflineOperationKind, - raw_operation_kind: u32, - result_kind: OfflineOperationResultKind, - raw_result_kind: u32, - result_status: i32, - found: bool, -}; - /// Payload for `map_camera_transition_finished` events. /// /// A camera command carrying `AnimationOptions.transition_id` reports this @@ -950,7 +805,6 @@ pub const CameraTransitionFinishedPayload = struct { /// transition. transition_id: u64, }; - /// Payload of a type this binding does not name, kept as the event's fixed /// payload window: the batch's event size less the payload's offset in the /// event. @@ -981,7 +835,6 @@ pub const RuntimeEventType = union(enum) { offline_region_status_changed, offline_region_response_error, offline_region_tile_count_limit_exceeded, - offline_operation_completed, map_camera_transition_finished, unknown: u32, @@ -1008,7 +861,6 @@ pub const RuntimeEventType = union(enum) { c.MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED => .offline_region_status_changed, c.MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR => .offline_region_response_error, c.MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED => .offline_region_tile_count_limit_exceeded, - c.MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED => .offline_operation_completed, c.MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED => .map_camera_transition_finished, else => .{ .unknown = raw }, }; @@ -1045,24 +897,17 @@ pub const RuntimeEventMask = struct { offline_region_status_changed: bool = false, offline_region_response_error: bool = false, offline_region_tile_count_limit_exceeded: bool = false, - offline_operation_completed: bool = false, /// Bits reported by a newer native library that this binding does not name. unknown_bits: u64 = 0, - /// Selects no event type. pub const none = RuntimeEventMask{}; - /// Selects every map-originated event type this binding names. pub const all_map_events = fromRaw(c.MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS); - /// Selects every runtime-originated event type this binding names. pub const all_runtime_events = fromRaw(c.MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS); - /// Selects every event type this binding names. pub const all = fromRaw(c.MLN_RUNTIME_EVENT_MASK_ALL); - /// Returns the mask selecting every type either mask selects. pub fn unionWith(self: RuntimeEventMask, other: RuntimeEventMask) RuntimeEventMask { return fromRaw(self.toRaw() | other.toRaw()); } - /// Reports whether this mask selects `event_type`. pub fn contains(self: RuntimeEventMask, event_type: RuntimeEventType) bool { switch (event_type) { @@ -1140,100 +985,6 @@ pub const RuntimeEventSourceType = union(enum) { } }; -pub const OfflineOperationHandle = enum(u128) { - _, - - fn init( - runtime: *RuntimeHandle, - operation_id: OfflineOperationId, - operation_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, - ) status.Error!OfflineOperationHandle { - if (operation_id == 0) return error.InvalidArgument; - try registerRuntimeOfflineOperation(runtime.*); - errdefer unregisterRuntimeOfflineOperation(runtime.*); - - const operation_state = try std.heap.smp_allocator.create(OfflineOperationState); - operation_state.* = .{ - .runtime = runtime.*, - .operation_id = operation_id, - .operation_kind = operation_kind, - .result_kind = result_kind, - }; - errdefer std.heap.smp_allocator.destroy(operation_state); - return try registerOfflineOperationState(operation_state); - } - - pub fn operationId(self: OfflineOperationHandle) status.BindingError!OfflineOperationId { - lockOfflineOperationRegistry(); - defer unlockOfflineOperationRegistry(); - const operation_state = offlineOperationState(self) orelse return error.ClosedHandle; - return operation_state.operation_id; - } - - fn require( - self: OfflineOperationHandle, - expected_runtime: *RuntimeHandle, - operation_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, - ) status.Error!RequiredOfflineOperation { - lockOfflineOperationRegistry(); - defer unlockOfflineOperationRegistry(); - const operation_state = offlineOperationState(self) orelse return error.ClosedHandle; - if (operation_state.runtime != expected_runtime.*) return error.InvalidState; - if (!std.meta.eql(operation_state.operation_kind, operation_kind) or !std.meta.eql(operation_state.result_kind, result_kind)) { - return error.InvalidState; - } - return .{ .operation_id = operation_state.operation_id, .operation_kind = operation_state.operation_kind }; - } - - fn requireEither( - self: OfflineOperationHandle, - expected_runtime: *RuntimeHandle, - first_kind: OfflineOperationKind, - second_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, - ) status.Error!RequiredOfflineOperation { - lockOfflineOperationRegistry(); - defer unlockOfflineOperationRegistry(); - const operation_state = offlineOperationState(self) orelse return error.ClosedHandle; - if (operation_state.runtime != expected_runtime.*) return error.InvalidState; - if ((!std.meta.eql(operation_state.operation_kind, first_kind) and !std.meta.eql(operation_state.operation_kind, second_kind)) or - !std.meta.eql(operation_state.result_kind, result_kind)) - { - return error.InvalidState; - } - return .{ .operation_id = operation_state.operation_id, .operation_kind = operation_state.operation_kind }; - } - - fn consume(self: OfflineOperationHandle) void { - const operation_state = unregisterOfflineOperationState(self) orelse return; - unregisterRuntimeOfflineOperation(operation_state.runtime); - std.heap.smp_allocator.destroy(operation_state); - } - - pub fn discard(self: OfflineOperationHandle) status.Error!void { - lockOfflineOperationRegistry(); - const operation_state = offlineOperationState(self) orelse { - unlockOfflineOperationRegistry(); - return; - }; - const operation_id = operation_state.operation_id; - var runtime_handle = operation_state.runtime; - unlockOfflineOperationRegistry(); - - const runtime_lease = lease(&runtime_handle) catch |err| { - return err; - }; - defer runtime_lease.release(); - try status.checkStatus( - c.mln_runtime_offline_operation_discard(runtime_lease.native, operation_id), - runtime_lease.diagnostic_store, - ); - self.consume(); - } -}; - pub const RuntimeEventPayloadType = union(enum) { none, render_frame, @@ -1242,7 +993,6 @@ pub const RuntimeEventPayloadType = union(enum) { offline_region_status, offline_region_response_error, offline_region_tile_count_limit, - offline_operation_completed, camera_transition_finished, unknown: u32, @@ -1255,7 +1005,6 @@ pub const RuntimeEventPayloadType = union(enum) { c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS => .offline_region_status, c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR => .offline_region_response_error, c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT => .offline_region_tile_count_limit, - c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED => .offline_operation_completed, c.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED => .camera_transition_finished, else => .{ .unknown = raw }, }; @@ -1285,103 +1034,72 @@ pub const RuntimeHandle = enum(c.mln_runtime) { native_options.cache_path = cache_path.?.ptr; } if (options.event_mask) |value| native_options.event_mask = value.toRaw(); - return createNative(&native_options, diagnostic_store); } - /// Advances this runtime: parks the owner thread when `timeout_ms` allows - /// it, then drains the owner-thread task queues. Drain the resulting - /// runtime events with `drainEvents` afterwards. - /// - /// `timeout_ms` bounds the park: zero drains and returns, a positive value - /// parks for at most that many milliseconds, and null parks until a wake - /// arrives. Timers and ready file descriptors only wake the runtime when - /// they queue owner-thread work, so pass a bounded timeout to cap the wait. - /// - /// `budget_ms` bounds the drain: null drains without a bound, and zero or - /// a positive value stops the drain at the first task boundary after that - /// many milliseconds, measured from the start of the drain. The first - /// queued task always runs, so a bounded pump always makes progress, and - /// tasks left behind set the wake flag so the next pump returns without - /// parking and continues them. The budget bounds the task queues alone: a - /// task runs to completion once started, so one long task can overrun it. - /// - /// A non-zero timeout blocks the calling thread. Call it outside any lock - /// that a thread signalling a wake source takes. An undrained event never - /// parks the owner thread, so a host that pumps and drains in a loop keeps - /// draining. - pub fn pump(self: *RuntimeHandle, timeout_ms: ?u64, budget_ms: ?u64) status.Error!void { + pub fn barrier(self: *RuntimeHandle) status.Error!completion.Future(void) { const runtime_lease = try lease(self); defer runtime_lease.release(); - const native_timeout: i64 = if (timeout_ms) |value| - std.math.cast(i64, value) orelse std.math.maxInt(i64) - else - -1; - const native_budget: i64 = if (budget_ms) |value| - std.math.cast(i64, value) orelse std.math.maxInt(i64) - else - -1; - try status.checkStatus( - c.mln_runtime_pump(runtime_lease.native, native_timeout, native_budget), - runtime_lease.diagnostic_store, - ); + return completion.submit(void, runtime_lease.diagnostic_store, completion.unit, runtime_lease.native, struct { + fn start(native: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_barrier(native, descriptor); + } + }.start); } - /// Acquires a wake source that releases this runtime's parked owner thread. - /// The returned handle is usable from any thread, and the caller releases - /// it. - pub fn wakeSource(self: *RuntimeHandle) status.Error!WakeSourceHandle { + pub fn setEventWakeCallback( + self: *RuntimeHandle, + callback: WakeCallback, + user_data: ?*anyopaque, + ) status.Error!void { const runtime_lease = try lease(self); defer runtime_lease.release(); + std.Io.Threaded.mutexLock(&runtime_lease.state.wake_lock); + defer std.Io.Threaded.mutexUnlock(&runtime_lease.state.wake_lock); + runtime_lease.state.event_callback = callback; + runtime_lease.state.event_user_data = user_data; + } - var native_source: c.mln_wake_source = 0; - try status.checkStatus( - c.mln_runtime_wake_source_acquire(runtime_lease.native, &native_source), - runtime_lease.diagnostic_store, - ); - if (native_source == 0) return error.NativeError; - const source = native_source; - - const source_state = std.heap.smp_allocator.create(WakeSourceState) catch { - c.mln_wake_source_destroy(source); - return error.OutOfMemory; - }; - source_state.* = .{ .native = source, .diagnostic_store = runtime_lease.diagnostic_store }; - return registerWakeSourceState(source, source_state) catch { - std.heap.smp_allocator.destroy(source_state); - c.mln_wake_source_destroy(source); - return error.OutOfMemory; - }; + pub fn clearEventWakeCallback(self: *RuntimeHandle) status.Error!void { + const runtime_lease = try lease(self); + defer runtime_lease.release(); + std.Io.Threaded.mutexLock(&runtime_lease.state.wake_lock); + defer std.Io.Threaded.mutexUnlock(&runtime_lease.state.wake_lock); + runtime_lease.state.event_callback = null; + runtime_lease.state.event_user_data = null; } /// Drains this runtime's queued events into one copied batch. /// - /// `max_events` bounds the drain: zero drains every queued event, and a - /// positive value drains at most that many and leaves the rest for the next - /// drain, which `EventBatch.remaining` reports. - /// /// Narrowing a subscription gates later events and keeps queued ones, so a /// batch still reports the events a host already caused. - pub fn drainEvents(self: *RuntimeHandle, allocator: std.mem.Allocator, max_events: usize) status.Error!EventBatch { + pub fn drainEvents(self: *RuntimeHandle, allocator: std.mem.Allocator) status.Error!EventBatch { const runtime_lease = try lease(self); defer runtime_lease.release(); const runtime_state = runtime_lease.state; - var native_batch = c.mln_runtime_event_batch_default(); + var native_batch: c.mln_event_batch = 0; + try status.checkStatus( + c.mln_runtime_drain_events(runtime_lease.native, &native_batch), + runtime_lease.diagnostic_store, + ); + defer c.mln_event_batch_release(native_batch); + var view: c.mln_runtime_event_batch_view = std.mem.zeroes(c.mln_runtime_event_batch_view); + view.size = @sizeOf(c.mln_runtime_event_batch_view); try status.checkStatus( - c.mln_runtime_drain_events(runtime_lease.native, max_events, &native_batch), + c.mln_event_batch_get(native_batch, &view), runtime_lease.diagnostic_store, ); - const event_size: usize = native_batch.event_size; - const events: []const u8 = if (native_batch.event_count == 0 or native_batch.events == null) + const event_size: usize = view.event_size; + const events: []const u8 = if (view.event_count == 0 or view.events == null) &.{} else - @as([*]const u8, @ptrCast(native_batch.events))[0 .. native_batch.event_count * event_size]; - const messages: []const u8 = if (native_batch.messages_size == 0 or native_batch.messages == null) + @as([*]const u8, @ptrCast(view.events))[0 .. view.event_count * event_size]; + const messages: []const u8 = if (view.messages_size == 0 or view.messages == null) "" else - native_batch.messages[0..native_batch.messages_size]; - const copied = try allocator.alloc(RuntimeEvent, native_batch.event_count); + view.messages[0..view.messages_size]; + const copied = try allocator.alloc(RuntimeEvent, view.event_count); var initialized: usize = 0; errdefer { for (copied[0..initialized]) |*event| event.deinit(); @@ -1391,7 +1109,7 @@ pub const RuntimeHandle = enum(c.mln_runtime) { event.* = try eventViewAt(runtime_state, events, event_size, messages, index).toOwned(allocator); initialized += 1; } - return .{ .allocator = allocator, .events = copied, .remaining_count = native_batch.remaining_count }; + return .{ .allocator = allocator, .events = copied }; } /// Selects which runtime-originated event types this runtime queues. @@ -1422,408 +1140,335 @@ pub const RuntimeHandle = enum(c.mln_runtime) { return RuntimeEventMask.fromRaw(raw); } - fn operationHandle( - self: *RuntimeHandle, - operation_id: c.mln_offline_operation_id, - operation_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, - ) status.Error!OfflineOperationHandle { + pub fn runAmbientCacheOperation(self: *RuntimeHandle, operation: AmbientCacheOperation) status.Error!completion.Future(void) { const runtime_lease = try lease(self); defer runtime_lease.release(); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, operation_kind, result_kind); - } - - fn operationHandleWithRuntime( - self: *RuntimeHandle, - runtime: c.mln_runtime, - operation_id: c.mln_offline_operation_id, - operation_kind: OfflineOperationKind, - result_kind: OfflineOperationResultKind, - ) status.Error!OfflineOperationHandle { - // `runtime` must come from an active RuntimeLease that spans this call. - return OfflineOperationHandle.init(self, operation_id, operation_kind, result_kind) catch |err| { - if (operation_id != 0) _ = c.mln_runtime_offline_operation_discard(runtime, operation_id); - return err; - }; - } - - pub fn startAmbientCacheOperation(self: *RuntimeHandle, operation: AmbientCacheOperation) status.Error!OfflineOperationHandle { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_run_ambient_cache_operation_start(runtime_lease.native, operation.toRaw(), &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .ambient_cache, .none); + const Context = struct { native: c.mln_runtime, operation: u32 }; + return completion.submit(void, runtime_lease.diagnostic_store, completion.unit, Context{ + .native = runtime_lease.native, + .operation = operation.toRaw(), + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_run_ambient_cache_operation(context.native, context.operation, descriptor); + } + }.start); } - /// Starts a change to this runtime's maximum ambient cache size. Lowering - /// it evicts ambient resources; offline regions are unaffected. - pub fn startSetMaximumAmbientCacheSize(self: *RuntimeHandle, size: u64) status.Error!OfflineOperationHandle { + pub fn setMaximumAmbientCacheSize(self: *RuntimeHandle, size: u64) status.Error!completion.Future(void) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_set_maximum_ambient_cache_size_start(runtime_lease.native, size, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .set_maximum_ambient_cache_size, .none); + const Context = struct { native: c.mln_runtime, size: u64 }; + return completion.submit(void, runtime_lease.diagnostic_store, completion.unit, Context{ + .native = runtime_lease.native, + .size = size, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_set_maximum_ambient_cache_size(context.native, context.size, descriptor); + } + }.start); } - pub fn startCreateOfflineRegion( + pub fn createOfflineRegion( self: *RuntimeHandle, allocator: std.mem.Allocator, definition: OfflineRegionDefinition, metadata: []const u8, - ) status.Error!OfflineOperationHandle { + ) status.Error!completion.Future(OwnedOfflineRegion) { const runtime_lease = try lease(self); defer runtime_lease.release(); var temp = native_temp.TempStorage.initWithDiagnostics(allocator, runtime_lease.diagnostic_store); defer temp.deinit(); const native_definition = try temp.offlineRegionDefinition(definition); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_create_start( - runtime_lease.native, - native_definition, - if (metadata.len == 0) null else metadata.ptr, - metadata.len, - &operation_id, - ), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_create, .region); + const Context = struct { + native: c.mln_runtime, + definition: [*c]const c.mln_offline_region_definition, + metadata: ?[*]const u8, + metadata_size: usize, + }; + return completion.submit(OwnedOfflineRegion, runtime_lease.diagnostic_store, copyCompletedOfflineRegion, Context{ + .native = runtime_lease.native, + .definition = native_definition, + .metadata = if (metadata.len == 0) null else metadata.ptr, + .metadata_size = metadata.len, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_region_create( + context.native, + context.definition, + context.metadata, + context.metadata_size, + descriptor, + ); + } + }.start); } - pub fn startGetOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!OfflineOperationHandle { + pub fn getOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!completion.Future(?OwnedOfflineRegion) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_get_start(runtime_lease.native, region_id, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_get, .optional_region); + const Context = struct { native: c.mln_runtime, region_id: i64 }; + return completion.submit(?OwnedOfflineRegion, runtime_lease.diagnostic_store, copyCompletedOptionalOfflineRegion, Context{ + .native = runtime_lease.native, + .region_id = region_id, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_region_get(context.native, context.region_id, descriptor); + } + }.start); } - pub fn startListOfflineRegions(self: *RuntimeHandle) status.Error!OfflineOperationHandle { + pub fn listOfflineRegions(self: *RuntimeHandle) status.Error!completion.Future(OfflineRegionList) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_regions_list_start(runtime_lease.native, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .regions_list, .region_list); + return completion.submit(OfflineRegionList, runtime_lease.diagnostic_store, copyCompletedOfflineRegionList, runtime_lease.native, struct { + fn start(native: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_regions_list(native, descriptor); + } + }.start); } - pub fn startMergeOfflineRegionsDatabase( + pub fn mergeOfflineRegionsDatabase( self: *RuntimeHandle, allocator: std.mem.Allocator, side_database_path: []const u8, - ) status.Error!OfflineOperationHandle { + ) status.Error!completion.Future(OfflineRegionList) { const runtime_lease = try lease(self); defer runtime_lease.release(); const path = try nulTerminated(allocator, side_database_path, runtime_lease.diagnostic_store, "offline merge database path contains embedded NUL"); defer allocator.free(path); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_regions_merge_database_start(runtime_lease.native, path.ptr, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .regions_merge_database, .region_list); + const Context = struct { native: c.mln_runtime, path: [*:0]const u8 }; + return completion.submit(OfflineRegionList, runtime_lease.diagnostic_store, copyCompletedOfflineRegionList, Context{ + .native = runtime_lease.native, + .path = path.ptr, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_regions_merge_database(context.native, context.path, descriptor); + } + }.start); } - pub fn startUpdateOfflineRegionMetadata( + pub fn updateOfflineRegionMetadata( self: *RuntimeHandle, region_id: OfflineRegionId, metadata: []const u8, - ) status.Error!OfflineOperationHandle { + ) status.Error!completion.Future(OwnedOfflineRegion) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_update_metadata_start( - runtime_lease.native, - region_id, - if (metadata.len == 0) null else metadata.ptr, - metadata.len, - &operation_id, - ), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_update_metadata, .region); + const Context = struct { + native: c.mln_runtime, + region_id: i64, + metadata: ?[*]const u8, + metadata_size: usize, + }; + return completion.submit(OwnedOfflineRegion, runtime_lease.diagnostic_store, copyCompletedOfflineRegion, Context{ + .native = runtime_lease.native, + .region_id = region_id, + .metadata = if (metadata.len == 0) null else metadata.ptr, + .metadata_size = metadata.len, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_region_update_metadata( + context.native, + context.region_id, + context.metadata, + context.metadata_size, + descriptor, + ); + } + }.start); } - pub fn startGetOfflineRegionStatus(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!OfflineOperationHandle { + pub fn getOfflineRegionStatus(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!completion.Future(OfflineRegionStatus) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_get_status_start(runtime_lease.native, region_id, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_get_status, .region_status); + const Context = struct { native: c.mln_runtime, region_id: i64 }; + return completion.submit(OfflineRegionStatus, runtime_lease.diagnostic_store, copyCompletedOfflineRegionStatus, Context{ + .native = runtime_lease.native, + .region_id = region_id, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_region_get_status(context.native, context.region_id, descriptor); + } + }.start); } - pub fn startSetOfflineRegionObserved(self: *RuntimeHandle, region_id: OfflineRegionId, observed: bool) status.Error!OfflineOperationHandle { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_set_observed_start(runtime_lease.native, region_id, observed, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_set_observed, .none); + pub fn setOfflineRegionObserved(self: *RuntimeHandle, region_id: OfflineRegionId, observed: bool) status.Error!completion.Future(void) { + return self.offlineRegionUnit(region_id, observed, c.mln_runtime_offline_region_set_observed); } - pub fn startSetOfflineRegionDownloadState( + pub fn setOfflineRegionDownloadState( self: *RuntimeHandle, region_id: OfflineRegionId, download_state: OfflineRegionDownloadState, - ) status.Error!OfflineOperationHandle { + ) status.Error!completion.Future(void) { const runtime_lease = try lease(self); defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_set_download_state_start(runtime_lease.native, region_id, try download_state.toInputRaw(runtime_lease.diagnostic_store), &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_set_download_state, .none); + const Context = struct { native: c.mln_runtime, region_id: i64, state: u32 }; + return completion.submit(void, runtime_lease.diagnostic_store, completion.unit, Context{ + .native = runtime_lease.native, + .region_id = region_id, + .state = try download_state.toInputRaw(runtime_lease.diagnostic_store), + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_offline_region_set_download_state(context.native, context.region_id, context.state, descriptor); + } + }.start); } - pub fn startInvalidateOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!OfflineOperationHandle { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_invalidate_start(runtime_lease.native, region_id, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_invalidate, .none); + pub fn invalidateOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!completion.Future(void) { + return self.offlineRegionUnit(region_id, {}, c.mln_runtime_offline_region_invalidate); } - pub fn startDeleteOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!OfflineOperationHandle { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - var operation_id: c.mln_offline_operation_id = 0; - try status.checkStatus( - c.mln_runtime_offline_region_delete_start(runtime_lease.native, region_id, &operation_id), - runtime_lease.diagnostic_store, - ); - return self.operationHandleWithRuntime(runtime_lease.native, operation_id, .region_delete, .none); + pub fn deleteOfflineRegion(self: *RuntimeHandle, region_id: OfflineRegionId) status.Error!completion.Future(void) { + return self.offlineRegionUnit(region_id, {}, c.mln_runtime_offline_region_delete); } - pub fn takeOfflineRegion( - self: *RuntimeHandle, - allocator: std.mem.Allocator, - operation: OfflineOperationHandle, - ) status.Error!OwnedOfflineRegion { + fn offlineRegionUnit(self: *RuntimeHandle, region_id: OfflineRegionId, argument: anytype, comptime start_fn: anytype) status.Error!completion.Future(void) { const runtime_lease = try lease(self); defer runtime_lease.release(); - const required = try operation.requireEither(self, .region_create, .region_update_metadata, .region); - var snapshot: c.mln_offline_region_snapshot = 0; - const native_status = switch (required.operation_kind) { - .region_create => c.mln_runtime_offline_region_create_take_result(runtime_lease.native, required.operation_id, &snapshot), - .region_update_metadata => c.mln_runtime_offline_region_update_metadata_take_result(runtime_lease.native, required.operation_id, &snapshot), - else => c.MLN_STATUS_INVALID_STATE, - }; - try status.checkStatus(native_status, runtime_lease.diagnostic_store); - operation.consume(); - if (snapshot == 0) return error.NativeError; - const snapshot_handle = snapshot; - defer destroyOfflineRegionSnapshot(snapshot_handle); - return copyOfflineRegionSnapshot(allocator, snapshot_handle); - } - - pub fn takeOptionalOfflineRegion( - self: *RuntimeHandle, - allocator: std.mem.Allocator, - operation: OfflineOperationHandle, - ) status.Error!?OwnedOfflineRegion { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - const required = try operation.require(self, .region_get, .optional_region); - var snapshot: c.mln_offline_region_snapshot = 0; - var found = false; - const native_status = c.mln_runtime_offline_region_get_take_result(runtime_lease.native, required.operation_id, &snapshot, &found); - try status.checkStatus(native_status, runtime_lease.diagnostic_store); - operation.consume(); - if (!found) return null; - if (snapshot == 0) return error.NativeError; - const snapshot_handle = snapshot; - defer destroyOfflineRegionSnapshot(snapshot_handle); - return try copyOfflineRegionSnapshot(allocator, snapshot_handle); - } - - pub fn takeOfflineRegionList( - self: *RuntimeHandle, - allocator: std.mem.Allocator, - operation: OfflineOperationHandle, - ) status.Error!OfflineRegionList { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - const required = try operation.requireEither(self, .regions_list, .regions_merge_database, .region_list); - var list: c.mln_offline_region_list = 0; - const native_status = switch (required.operation_kind) { - .regions_list => c.mln_runtime_offline_regions_list_take_result(runtime_lease.native, required.operation_id, &list), - .regions_merge_database => c.mln_runtime_offline_regions_merge_database_take_result(runtime_lease.native, required.operation_id, &list), - else => c.MLN_STATUS_INVALID_STATE, - }; - try status.checkStatus(native_status, runtime_lease.diagnostic_store); - operation.consume(); - if (list == 0) return error.NativeError; - const list_handle = list; - defer destroyOfflineRegionList(list_handle); - return copyOfflineRegionList(allocator, list_handle); + const Context = struct { native: c.mln_runtime, region_id: i64, argument: @TypeOf(argument) }; + return completion.submit(void, runtime_lease.diagnostic_store, completion.unit, Context{ + .native = runtime_lease.native, + .region_id = region_id, + .argument = argument, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + if (comptime @TypeOf(context.argument) == bool) { + return start_fn(context.native, context.region_id, context.argument, descriptor); + } + return start_fn(context.native, context.region_id, descriptor); + } + }.start); } - pub fn takeOfflineRegionStatus(self: *RuntimeHandle, operation: OfflineOperationHandle) status.Error!OfflineRegionStatus { - const runtime_lease = try lease(self); - defer runtime_lease.release(); - const required = try operation.require(self, .region_get_status, .region_status); - var native_status_value: c.mln_offline_region_status = undefined; - native_status_value.size = @sizeOf(c.mln_offline_region_status); - const native_status = c.mln_runtime_offline_region_get_status_take_result(runtime_lease.native, required.operation_id, &native_status_value); - try status.checkStatus(native_status, runtime_lease.diagnostic_store); - operation.consume(); - return offlineStatusFromNative(native_status_value); - } - - /// Registers, replaces, or clears the runtime-scoped URL transform for - /// network resources; passing null clears it. The binding keeps the handler - /// and context alive until the call that replaces or clears them returns, - /// and releases them when the runtime closes. - pub fn setResourceTransform(self: *RuntimeHandle, transform: ?ResourceTransform) status.Error!void { + /// Registers, replaces, or clears the runtime-scoped URL transform. + /// Native retains each accepted callback root until it is no longer + /// reachable from the runtime. + pub fn setResourceTransform(self: *RuntimeHandle, transform: ?ResourceTransform) status.Error!completion.Future(completion.CommandCompletion) { const runtime_lease = try lease(self); defer runtime_lease.release(); - const runtime_state = runtime_lease.state; if (transform) |value| { const replacement = try std.heap.smp_allocator.create(ResourceTransform); errdefer std.heap.smp_allocator.destroy(replacement); replacement.* = value; - var native_transform = c.mln_resource_transform{ + const native_transform = c.mln_resource_transform{ .size = @sizeOf(c.mln_resource_transform), .callback = resourceTransformTrampoline, .user_data = replacement, + .release_user_data = releaseResourceTransform, }; - try status.checkStatus( - c.mln_runtime_set_resource_transform(runtime_lease.native, &native_transform), - runtime_lease.diagnostic_store, - ); - const previous = runtime_state.resource_transform; - runtime_state.resource_transform = replacement; - if (previous) |old| std.heap.smp_allocator.destroy(old); - return; - } - - try status.checkStatus( - c.mln_runtime_clear_resource_transform(runtime_lease.native), - runtime_lease.diagnostic_store, - ); - if (runtime_state.resource_transform) |old| { - runtime_state.resource_transform = null; - std.heap.smp_allocator.destroy(old); + const Context = struct { native: c.mln_runtime, transform: c.mln_resource_transform }; + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, Context{ + .native = runtime_lease.native, + .transform = native_transform, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_set_resource_transform(context.native, &context.transform, descriptor); + } + }.start); } + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, runtime_lease.native, struct { + fn start(native_runtime: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_clear_resource_transform(native_runtime, descriptor); + } + }.start); } - /// Registers, replaces, or clears the runtime-scoped network resource - /// provider; passing null clears it. The binding keeps the handler and - /// context alive until the call that replaces or clears them returns, and - /// releases them when the runtime closes. Handles the previous provider - /// already took stay valid: complete and release each one as usual. - pub fn setResourceProvider(self: *RuntimeHandle, provider: ?ResourceProvider) status.Error!void { + pub fn setResourceProvider(self: *RuntimeHandle, provider: ?ResourceProvider) status.Error!completion.Future(completion.CommandCompletion) { const runtime_lease = try lease(self); defer runtime_lease.release(); - const runtime_state = runtime_lease.state; if (provider) |value| { const replacement = try std.heap.smp_allocator.create(ResourceProviderState); errdefer std.heap.smp_allocator.destroy(replacement); replacement.* = .{ .provider = value, .diagnostic_store = runtime_lease.diagnostic_store }; - var native_provider = c.mln_resource_provider{ + const native_provider = c.mln_resource_provider{ .size = @sizeOf(c.mln_resource_provider), .callback = resourceProviderTrampoline, .user_data = replacement, + .release_user_data = releaseResourceProvider, }; - try status.checkStatus( - c.mln_runtime_set_resource_provider(runtime_lease.native, &native_provider), - runtime_lease.diagnostic_store, - ); - const previous = runtime_state.resource_provider; - runtime_state.resource_provider = replacement; - if (previous) |old_provider| std.heap.smp_allocator.destroy(old_provider); - return; - } - - try status.checkStatus( - c.mln_runtime_clear_resource_provider(runtime_lease.native), - runtime_lease.diagnostic_store, - ); - if (runtime_state.resource_provider) |old_provider| { - runtime_state.resource_provider = null; - std.heap.smp_allocator.destroy(old_provider); + const Context = struct { native: c.mln_runtime, provider: c.mln_resource_provider }; + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, Context{ + .native = runtime_lease.native, + .provider = native_provider, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_set_resource_provider(context.native, &context.provider, descriptor); + } + }.start); } + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, runtime_lease.native, struct { + fn start(native_runtime: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_clear_resource_provider(native_runtime, descriptor); + } + }.start); } - /// Registers, replaces, or clears headers added to built-in HTTP requests. - pub fn setHttpHeaderTransform(self: *RuntimeHandle, transform: ?HttpHeaderTransform) status.Error!void { + pub fn setHttpHeaderTransform(self: *RuntimeHandle, transform: ?HttpHeaderTransform) status.Error!completion.Future(completion.CommandCompletion) { const runtime_lease = try lease(self); defer runtime_lease.release(); - const runtime_state = runtime_lease.state; if (transform) |value| { const replacement = try std.heap.smp_allocator.create(HttpHeaderTransform); errdefer std.heap.smp_allocator.destroy(replacement); replacement.* = value; - var native_transform = c.mln_http_header_transform{ + const native_transform = c.mln_http_header_transform{ .size = @sizeOf(c.mln_http_header_transform), .callback = httpHeaderTransformTrampoline, .user_data = replacement, + .release_user_data = releaseHttpHeaderTransform, }; - try status.checkStatus( - c.mln_runtime_set_http_header_transform(runtime_lease.native, &native_transform), - runtime_lease.diagnostic_store, - ); - const previous = runtime_state.http_header_transform; - runtime_state.http_header_transform = replacement; - if (previous) |old| std.heap.smp_allocator.destroy(old); - return; - } - try status.checkStatus( - c.mln_runtime_clear_http_header_transform(runtime_lease.native), - runtime_lease.diagnostic_store, - ); - if (runtime_state.http_header_transform) |old| { - runtime_state.http_header_transform = null; - std.heap.smp_allocator.destroy(old); + const Context = struct { native: c.mln_runtime, transform: c.mln_http_header_transform }; + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, Context{ + .native = runtime_lease.native, + .transform = native_transform, + }, struct { + fn start(context: Context, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_set_http_header_transform(context.native, &context.transform, descriptor); + } + }.start); } + return completion.submit(completion.CommandCompletion, runtime_lease.diagnostic_store, completion.command, runtime_lease.native, struct { + fn start(native_runtime: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_clear_http_header_transform(native_runtime, descriptor); + } + }.start); } - pub fn close(self: *RuntimeHandle) status.Error!void { - const runtime_close = try beginRuntimeClose(self.*) orelse return; - status.checkStatus(c.mln_runtime_destroy(runtime_close.native), runtime_close.diagnostic_store) catch |err| { + /// Releases the runtime handle and returns the future for its native + /// teardown. + /// + /// The future completes after every accepted submission, including released + /// maps' teardown, has finished and the runtime's threads and resources are + /// gone, so a host that waits for it may exit the process without racing + /// native teardown. A host that outlives its runtimes deinitializes the + /// future without waiting. Closing an already closed runtime returns a + /// future that has already completed. + pub fn close(self: *RuntimeHandle) status.Error!completion.Future(void) { + const runtime_close = try beginRuntimeClose(self.*) orelse + return completion.completed(void, {}); + const teardown = completion.submit(void, runtime_close.diagnostic_store, completion.unit, runtime_close.native, struct { + fn start(native: c.mln_runtime, descriptor: *const c.mln_completion) c.mln_status { + return c.mln_runtime_release(native, descriptor); + } + }.start) catch |err| { cancelRuntimeClose(runtime_close.state); return err; }; - if (runtime_close.state.resource_transform) |old| { - runtime_close.state.resource_transform = null; - std.heap.smp_allocator.destroy(old); - } - if (runtime_close.state.http_header_transform) |old| { - runtime_close.state.http_header_transform = null; - std.heap.smp_allocator.destroy(old); - } - if (runtime_close.state.resource_provider) |old| { - runtime_close.state.resource_provider = null; - std.heap.smp_allocator.destroy(old); - } runtime_close.registry.maps.deinit(std.heap.smp_allocator); std.heap.smp_allocator.destroy(runtime_close.registry); const runtime_state = finishRuntimeClose(self.*) orelse runtime_close.state; std.heap.smp_allocator.destroy(runtime_state); + return teardown; } }; +/// Test-only teardown that closes a runtime and waits for its native teardown. +pub fn closeRuntimeForTesting(handle: *RuntimeHandle) status.Error!void { + var teardown = try handle.close(); + defer teardown.deinit(); + try teardown.wait(null); +} + pub fn getNetworkStatus(diagnostic_store: ?*diagnostics.DiagnosticStore) status.Error!NetworkStatus { var raw: u32 = 0; try status.checkStatus(c.mln_network_status_get(&raw), diagnostic_store); @@ -1838,29 +1483,44 @@ fn createNative( native_options: *c.mln_runtime_options, diagnostic_store: ?*diagnostics.DiagnosticStore, ) status.Error!RuntimeHandle { - var runtime: c.mln_runtime = 0; - try status.checkStatus(c.mln_runtime_create(native_options, &runtime), diagnostic_store); - errdefer _ = c.mln_runtime_destroy(runtime); - const runtime_registry = try std.heap.smp_allocator.create(RuntimeRegistry); - runtime_registry.* = .{ .maps = .empty, .next_map_id = 1, .live_offline_operations = 0 }; - errdefer std.heap.smp_allocator.destroy(runtime_registry); + runtime_registry.* = .{ .maps = .empty, .next_map_id = 1 }; + errdefer { + runtime_registry.maps.deinit(std.heap.smp_allocator); + std.heap.smp_allocator.destroy(runtime_registry); + } const runtime_state = try std.heap.smp_allocator.create(RuntimeState); runtime_state.* = .{ .diagnostic_store = diagnostic_store, .registry = runtime_registry, - .resource_transform = null, - .http_header_transform = null, - .resource_provider = null, - .active_leases = std.atomic.Value(usize).init(0), + .active_leases = .init(0), .closing = false, }; errdefer std.heap.smp_allocator.destroy(runtime_state); + native_options.event_wake = .{ + .size = @sizeOf(c.mln_wake), + .callback = eventWakeTrampoline, + .user_data = runtime_state, + .release_user_data = eventWakeRelease, + }; + var runtime: c.mln_runtime = 0; + try status.checkStatus(c.mln_runtime_create(native_options, &runtime), diagnostic_store); return try registerRuntimeState(runtime, runtime_state); } +fn eventWakeTrampoline(user_data: ?*anyopaque) callconv(.c) void { + const state: *RuntimeState = @ptrCast(@alignCast(user_data orelse return)); + std.Io.Threaded.mutexLock(&state.wake_lock); + const callback = state.event_callback; + const callback_data = state.event_user_data; + std.Io.Threaded.mutexUnlock(&state.wake_lock); + if (callback) |wake| wake(callback_data); +} + +fn eventWakeRelease(_: ?*anyopaque) callconv(.c) void {} + // Events index by the batch's stride, which a later C API version may widen // past this binding's compiled event size, so the fixed prefix is copied out // rather than read through a pointer cast. @@ -1887,7 +1547,7 @@ fn eventViewAt( .source_id = mapIdForNativeSource(runtime_state, native_event.source_type, native_event.source), .payload_type = RuntimeEventPayloadType.fromRaw(native_event.payload_type), .code = native_event.code, - .message = messages[native_event.message_offset..][0..native_event.message_size], + .message = messages[@intCast(native_event.message_offset)..][0..native_event.message_size], .payload = payloadFromNative(native_event, payloadWindow(events, event_size, offset)), }; } @@ -2086,10 +1746,6 @@ fn beginRuntimeClose(handle: RuntimeHandle) status.Error!?RuntimeClose { try status.setBindingDiagnostic(runtime_state.diagnostic_store, "runtime has live maps"); return error.InvalidState; } - if (runtime_registry.live_offline_operations != 0) { - try status.setBindingDiagnostic(runtime_state.diagnostic_store, "runtime has live offline operations"); - return error.InvalidState; - } const runtime: c.mln_runtime = @intFromEnum(handle); runtime_state.closing = true; return .{ @@ -2117,25 +1773,6 @@ fn finishRuntimeClose(handle: RuntimeHandle) ?*RuntimeState { return runtime_state; } -fn registerRuntimeOfflineOperation(handle: RuntimeHandle) status.BindingError!void { - lockRuntimeRegistry(); - defer unlockRuntimeRegistry(); - - const runtime_state = runtimeStateLocked(handle) orelse return error.ClosedHandle; - if (runtime_state.closing) return error.ActiveBorrow; - const runtime_registry = runtime_state.registry orelse return error.ClosedHandle; - runtime_registry.live_offline_operations += 1; -} - -fn unregisterRuntimeOfflineOperation(handle: RuntimeHandle) void { - lockRuntimeRegistry(); - defer unlockRuntimeRegistry(); - - const runtime_state = runtimeStateLocked(handle) orelse return; - const runtime_registry = runtime_state.registry orelse return; - if (runtime_registry.live_offline_operations > 0) runtime_registry.live_offline_operations -= 1; -} - fn lockRuntimeRegistry() void { while (runtime_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) { std.Thread.yield() catch {}; @@ -2176,101 +1813,6 @@ fn unlockResourceRequestRegistry() void { resource_request_registry_lock.store(false, .seq_cst); } -fn registerWakeSourceState( - native_handle: c.mln_wake_source, - source_state: *WakeSourceState, -) std.mem.Allocator.Error!WakeSourceHandle { - lockWakeSourceRegistry(); - defer unlockWakeSourceRegistry(); - - try wake_source_registry.put(std.heap.smp_allocator, native_handle, source_state); - return @enumFromInt(native_handle); -} - -fn wakeSourceState(handle: WakeSourceHandle) ?*WakeSourceState { - return wake_source_registry.get(@intFromEnum(handle)); -} - -fn unregisterWakeSourceState(handle: WakeSourceHandle) ?*WakeSourceState { - const entry = wake_source_registry.fetchRemove(@intFromEnum(handle)) orelse return null; - return entry.value; -} - -fn lockWakeSourceRegistry() void { - while (wake_source_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) { - std.Thread.yield() catch {}; - } -} - -fn unlockWakeSourceRegistry() void { - wake_source_registry_lock.store(false, .seq_cst); -} - -fn registerOfflineOperationState(operation_state: *OfflineOperationState) std.mem.Allocator.Error!OfflineOperationHandle { - lockOfflineOperationRegistry(); - defer unlockOfflineOperationRegistry(); - - if (offline_operation_free_list.items.len > 0) { - const slot_index = offline_operation_free_list.pop().?; - offline_operation_registry.items[slot_index].state = operation_state; - offline_operation_registry.items[slot_index].generation = nextHandleGeneration(); - return offlineOperationHandle(slot_index + 1, offline_operation_registry.items[slot_index].generation); - } - - const generation = nextHandleGeneration(); - try offline_operation_free_list.ensureTotalCapacity(std.heap.smp_allocator, offline_operation_registry.items.len + 1); - try offline_operation_registry.append(std.heap.smp_allocator, .{ .state = operation_state, .generation = generation }); - return offlineOperationHandle(offline_operation_registry.items.len, generation); -} - -fn offlineOperationHandle(index: usize, generation: u64) OfflineOperationHandle { - return @enumFromInt((@as(u128, generation) << 64) | @as(u128, @intCast(index))); -} - -fn offlineOperationIndex(handle: OfflineOperationHandle) ?usize { - const index = @intFromEnum(handle) & std.math.maxInt(u64); - if (index == 0 or index > std.math.maxInt(usize)) return null; - return @intCast(index); -} - -fn offlineOperationGeneration(handle: OfflineOperationHandle) u64 { - return @intCast(@intFromEnum(handle) >> 64); -} - -fn offlineOperationState(handle: OfflineOperationHandle) ?*OfflineOperationState { - const index = offlineOperationIndex(handle) orelse return null; - if (index > offline_operation_registry.items.len) return null; - const slot = offline_operation_registry.items[index - 1]; - if (slot.generation != offlineOperationGeneration(handle)) return null; - return slot.state; -} - -fn unregisterOfflineOperationState(handle: OfflineOperationHandle) ?*OfflineOperationState { - lockOfflineOperationRegistry(); - defer unlockOfflineOperationRegistry(); - - const index = offlineOperationIndex(handle) orelse return null; - if (index > offline_operation_registry.items.len) return null; - const slot_index = index - 1; - const slot = &offline_operation_registry.items[slot_index]; - if (slot.generation != offlineOperationGeneration(handle)) return null; - const operation_state = slot.state orelse return null; - slot.state = null; - slot.generation = nextHandleGeneration(); - offline_operation_free_list.appendAssumeCapacity(slot_index); - return operation_state; -} - -fn lockOfflineOperationRegistry() void { - while (offline_operation_registry_lock.cmpxchgWeak(false, true, .seq_cst, .seq_cst) != null) { - std.Thread.yield() catch {}; - } -} - -fn unlockOfflineOperationRegistry() void { - offline_operation_registry_lock.store(false, .seq_cst); -} - pub fn nextHandleGeneration() u64 { const seed = handleGenerationSeed(); const counter = handle_generation_counter.fetchAdd(1, .seq_cst) +% 1; @@ -2400,18 +1942,6 @@ fn payloadFromNative(native_event: c.mln_runtime_event, window: []const u8) Runt .limit = payload.limit, } }; }, - c.MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED => blk: { - const payload = native_event.payload.offline_operation_completed; - break :blk .{ .offline_operation_completed = .{ - .operation_id = payload.operation_id, - .operation_kind = OfflineOperationKind.fromRaw(payload.operation_kind), - .raw_operation_kind = payload.operation_kind, - .result_kind = OfflineOperationResultKind.fromRaw(payload.result_kind), - .raw_result_kind = payload.result_kind, - .result_status = payload.result_status, - .found = payload.found, - } }; - }, c.MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED => .{ .camera_transition_finished = .{ .transition_id = native_event.payload.camera_transition_finished.transition_id, @@ -2488,6 +2018,39 @@ fn offlineStatusFromNative(raw: c.mln_offline_region_status) OfflineRegionStatus }; } +fn copyCompletedOfflineRegion(result: *const c.mln_completion_result) status.Error!OwnedOfflineRegion { + const info = try completion.value(c.mln_offline_region_info)(result); + return copyOfflineRegionInfo(std.heap.smp_allocator, info); +} + +fn copyCompletedOptionalOfflineRegion(result: *const c.mln_completion_result) status.Error!?OwnedOfflineRegion { + if (result.value_count == 0) return null; + return @as(?OwnedOfflineRegion, try copyCompletedOfflineRegion(result)); +} + +fn copyCompletedOfflineRegionList(result: *const c.mln_completion_result) status.Error!OfflineRegionList { + if (result.value_count != 0 and result.value == null) return error.NativeError; + const raw = if (result.value_count == 0) + &.{} + else + @as([*]align(1) const c.mln_offline_region_info, @ptrCast(result.value.?))[0..result.value_count]; + const items = try std.heap.smp_allocator.alloc(OwnedOfflineRegion, raw.len); + var initialized: usize = 0; + errdefer { + for (items[0..initialized]) |*item| item.deinit(); + std.heap.smp_allocator.free(items); + } + for (raw, items) |info, *item| { + item.* = try copyOfflineRegionInfo(std.heap.smp_allocator, info); + initialized += 1; + } + return .{ .allocator = std.heap.smp_allocator, .items = items }; +} + +fn copyCompletedOfflineRegionStatus(result: *const c.mln_completion_result) status.Error!OfflineRegionStatus { + return offlineStatusFromNative(try completion.value(c.mln_offline_region_status)(result)); +} + fn copyOfflineRegionSnapshot( allocator: std.mem.Allocator, snapshot: c.mln_offline_region_snapshot, @@ -2640,6 +2203,11 @@ fn countingOfflineRegionListDestroy(list: c.mln_offline_region_list) callconv(.c c.mln_offline_region_list_destroy(list); } +fn markNotification(user_data: ?*anyopaque) callconv(.c) void { + const notified: *std.atomic.Value(bool) = @ptrCast(@alignCast(user_data orelse return)); + notified.store(true, .seq_cst); +} + fn tempPathForTesting(allocator: std.mem.Allocator, sub_path: []const u8, filename: []const u8) ![]u8 { return std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/{s}", .{ sub_path, filename }); } @@ -2656,31 +2224,6 @@ fn offlineTileDefinitionForTesting() OfflineRegionDefinition { .pixel_ratio = 2.0, } }; } - -fn waitForOfflineOperationForTesting(runtime: *RuntimeHandle, operation: OfflineOperationHandle) !void { - const operation_id = try operation.operationId(); - for (0..5000) |_| { - try runtime.pump(0, null); - // One event per drain, so an event this wait is not looking for stays - // queued rather than being dropped with the batch that carried it. - while (true) { - var batch = try runtime.drainEvents(std.testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) break; - const event = try batch.at(0); - const payload = switch (event.payload) { - .offline_operation_completed => |completed| completed, - else => continue, - }; - if (payload.operation_id != operation_id) continue; - try std.testing.expectEqual(@as(i32, c.MLN_STATUS_OK), payload.result_status); - return; - } - try std.testing.io.sleep(.fromMilliseconds(1), .awake); - } - return error.EventNotObserved; -} - test "runtime event raw domains preserve unknown values" { try std.testing.expect(std.meta.eql(RuntimeEventType.fromRaw(0xfeed), RuntimeEventType{ .unknown = 0xfeed })); try std.testing.expect(std.meta.eql(RuntimeEventSourceType.fromRaw(0xbeef), RuntimeEventSourceType{ .unknown = 0xbeef })); @@ -2825,7 +2368,7 @@ test "raw event masks reject bits outside the mask enum" { defer store.deinit(); var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, &store); - defer runtime.close() catch @panic("runtime close failed"); + defer closeRuntimeForTesting(&runtime) catch @panic("runtime close failed"); const native_runtime: c.mln_runtime = @intFromEnum(runtime); // The binding can retain an unnamed bit reported by a newer library, but @@ -2851,91 +2394,3 @@ test "event mask conversion preserves unnamed bits" { try std.testing.expect(mask.contains(.{ .unknown = 63 })); try std.testing.expectEqual(raw, eventMaskToRaw(mask)); } - -test "offline operation take-result failures preserve handle state" { - var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - const operation = try runtime.operationHandle(9_999_999, .region_get_status, .region_status); - try std.testing.expectError(error.InvalidArgument, runtime.takeOfflineRegionStatus(operation)); - try std.testing.expectEqual(@as(OfflineOperationId, 9_999_999), try operation.operationId()); - - operation.consume(); -} - -test "offline operation discard failures preserve handle state" { - var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - const operation = try runtime.operationHandle(9_999_997, .region_get_status, .region_status); - try std.testing.expectError(error.InvalidArgument, operation.discard()); - try std.testing.expectEqual(@as(OfflineOperationId, 9_999_997), try operation.operationId()); - - operation.consume(); -} - -test "runtime close rejects live offline operations" { - var diagnostic_store = diagnostics.DiagnosticStore.init(std.testing.allocator); - defer diagnostic_store.deinit(); - - var runtime = try RuntimeHandle.create(std.testing.allocator, .{}, &diagnostic_store); - var runtime_open = true; - defer if (runtime_open) runtime.close() catch @panic("runtime close failed"); - - const operation = try runtime.operationHandle(9_999_998, .region_get_status, .region_status); - try std.testing.expectError(error.InvalidState, runtime.close()); - try std.testing.expectEqualStrings("runtime has live offline operations", diagnostic_store.get().?.message); - - operation.consume(); - try runtime.close(); - runtime_open = false; -} - -test "offline region snapshot destroy runs when copied output allocation fails" { - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - const cache_path = try tempPathForTesting(std.testing.allocator, tmp.sub_path[0..], "snapshot-copy-failure-cache.db"); - defer std.testing.allocator.free(cache_path); - - var runtime = try RuntimeHandle.create(std.testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); - - const operation = try runtime.startCreateOfflineRegion(std.testing.allocator, offlineTileDefinitionForTesting(), &.{}); - try waitForOfflineOperationForTesting(&runtime, operation); - - var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); - snapshot_destroy_count_for_testing = 0; - offline_region_snapshot_destroy_for_testing = countingOfflineRegionSnapshotDestroy; - defer offline_region_snapshot_destroy_for_testing = c.mln_offline_region_snapshot_destroy; - - try std.testing.expectError(error.OutOfMemory, runtime.takeOfflineRegion(failing_allocator.allocator(), operation)); - try std.testing.expectEqual(@as(usize, 1), snapshot_destroy_count_for_testing); - try std.testing.expectError(error.ClosedHandle, operation.operationId()); -} - -test "offline region list destroy runs when copied output allocation fails" { - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - const cache_path = try tempPathForTesting(std.testing.allocator, tmp.sub_path[0..], "list-copy-failure-cache.db"); - defer std.testing.allocator.free(cache_path); - - var runtime = try RuntimeHandle.create(std.testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); - - const create_operation = try runtime.startCreateOfflineRegion(std.testing.allocator, offlineTileDefinitionForTesting(), &.{}); - try waitForOfflineOperationForTesting(&runtime, create_operation); - var region = try runtime.takeOfflineRegion(std.testing.allocator, create_operation); - defer region.deinit(); - - const list_operation = try runtime.startListOfflineRegions(); - try waitForOfflineOperationForTesting(&runtime, list_operation); - - var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); - list_destroy_count_for_testing = 0; - offline_region_list_destroy_for_testing = countingOfflineRegionListDestroy; - defer offline_region_list_destroy_for_testing = c.mln_offline_region_list_destroy; - - try std.testing.expectError(error.OutOfMemory, runtime.takeOfflineRegionList(failing_allocator.allocator(), list_operation)); - try std.testing.expectEqual(@as(usize, 1), list_destroy_count_for_testing); - try std.testing.expectError(error.ClosedHandle, list_operation.operationId()); -} diff --git a/bindings/zig/src/status.zig b/bindings/zig/src/status.zig index 6314692e5..af57568b3 100644 --- a/bindings/zig/src/status.zig +++ b/bindings/zig/src/status.zig @@ -10,6 +10,11 @@ pub const NativeStatusError = error{ InvalidState, WrongThread, Unsupported, + Cancelled, + Busy, + TargetLost, + NotReady, + NotFound, NativeError, UnknownStatus, }; @@ -73,13 +78,25 @@ fn copyThreadLastErrorMessage( return null; } +/// Converts a raw native status into this binding's error set without touching +/// diagnostics: void for MLN_STATUS_OK, the mapped error otherwise. +pub fn errorFromRawStatus(raw_status: i32) NativeStatusError!void { + if (raw_status == c.MLN_STATUS_OK) return; + return nativeStatusError(raw_status); +} + fn nativeStatusError(raw_status: i32) NativeStatusError { return switch (raw_status) { c.MLN_STATUS_INVALID_ARGUMENT => error.InvalidArgument, c.MLN_STATUS_INVALID_STATE => error.InvalidState, c.MLN_STATUS_WRONG_THREAD => error.WrongThread, c.MLN_STATUS_UNSUPPORTED => error.Unsupported, + c.MLN_STATUS_BUSY => error.Busy, + c.MLN_STATUS_TARGET_LOST => error.TargetLost, + c.MLN_STATUS_NOT_READY => error.NotReady, + c.MLN_STATUS_NOT_FOUND => error.NotFound, c.MLN_STATUS_NATIVE_ERROR => error.NativeError, + c.MLN_STATUS_CANCELLED => error.Cancelled, else => error.UnknownStatus, }; } @@ -89,25 +106,62 @@ test "native status values map to stable Zig errors" { try std.testing.expectError(error.InvalidState, checkStatus(c.MLN_STATUS_INVALID_STATE, null)); try std.testing.expectError(error.WrongThread, checkStatus(c.MLN_STATUS_WRONG_THREAD, null)); try std.testing.expectError(error.Unsupported, checkStatus(c.MLN_STATUS_UNSUPPORTED, null)); + try std.testing.expectError(error.Cancelled, checkStatus(c.MLN_STATUS_CANCELLED, null)); + try std.testing.expectError(error.Busy, checkStatus(c.MLN_STATUS_BUSY, null)); + try std.testing.expectError(error.TargetLost, checkStatus(c.MLN_STATUS_TARGET_LOST, null)); + try std.testing.expectError(error.NotReady, checkStatus(c.MLN_STATUS_NOT_READY, null)); + try std.testing.expectError(error.NotFound, checkStatus(c.MLN_STATUS_NOT_FOUND, null)); try std.testing.expectError(error.NativeError, checkStatus(c.MLN_STATUS_NATIVE_ERROR, null)); try checkStatus(c.MLN_STATUS_OK, null); } +// Signals that a native runtime finished tearing down, so this test's raw C +// calls do not outlive the library state they used. +const TeardownSignal = struct { + finished: std.atomic.Value(bool) = .init(false), + + fn descriptor(self: *TeardownSignal) c.mln_completion { + return .{ + .size = @sizeOf(c.mln_completion), + .callback = callback, + .user_data = self, + .release_user_data = null, + }; + } + + fn callback(user_data: ?*anyopaque, _: [*c]const c.mln_completion_result) callconv(.c) void { + const self: *TeardownSignal = @ptrCast(@alignCast(user_data orelse return)); + self.finished.store(true, .release); + } + + fn wait(self: *TeardownSignal) void { + while (!self.finished.load(.acquire)) std.Thread.yield() catch {}; + } +}; + test "diagnostic store copies thread-local native message" { var store = diagnostics.DiagnosticStore.init(std.testing.allocator); defer store.deinit(); - try std.testing.expectError(error.InvalidArgument, checkStatus(c.mln_runtime_destroy(0), &store)); + var rejected_signal = TeardownSignal{}; + const rejected_descriptor = rejected_signal.descriptor(); + try std.testing.expectError( + error.InvalidArgument, + checkStatus(c.mln_runtime_release(0, &rejected_descriptor), &store), + ); const first = store.get().?; try std.testing.expectEqual(@as(?i32, c.MLN_STATUS_INVALID_ARGUMENT), first.raw_status); try std.testing.expect(first.message.len > 0); const copied = try std.testing.allocator.dupe(u8, first.message); defer std.testing.allocator.free(copied); + const options = c.mln_runtime_options_default(); var runtime: c.mln_runtime = 0; - var options = c.mln_runtime_options_default(); try checkStatus(c.mln_runtime_create(&options, &runtime), null); - defer checkStatus(c.mln_runtime_destroy(runtime), null) catch @panic("runtime destroy failed"); + var signal = TeardownSignal{}; + const descriptor = signal.descriptor(); + try checkStatus(c.mln_runtime_release(runtime, &descriptor), null); + signal.wait(); try std.testing.expectEqualStrings(copied, store.get().?.message); } diff --git a/bindings/zig/src/values.zig b/bindings/zig/src/values.zig index bcf52483c..052d0a22f 100644 --- a/bindings/zig/src/values.zig +++ b/bindings/zig/src/values.zig @@ -44,9 +44,9 @@ pub const CameraOptions = struct { center: ?LatLng = null, center_altitude: ?f64 = null, padding: ?EdgeInsets = null, - /// Input-only screen point the camera pivots around. `jumpTo`, `easeTo`, and - /// `flyTo` honor it; MapLibre leaves it null on every read path, including - /// camera snapshots and the camera-for-bounds helpers. + /// Input-only screen point the camera pivots around. Atomic jump, ease, and + /// fly updates honor it; MapLibre leaves it null on every read path, + /// including camera snapshots and camera-for-bounds helpers. anchor: ?ScreenPoint = null, zoom: ?f64 = null, bearing: ?f64 = null, diff --git a/bindings/zig/tests/camera.zig b/bindings/zig/tests/camera.zig index 1c4e2367b..f48089581 100644 --- a/bindings/zig/tests/camera.zig +++ b/bindings/zig/tests/camera.zig @@ -2,171 +2,119 @@ const std = @import("std"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); +const support = @import("support.zig"); const center = maplibre.LatLng{ .latitude = 37.7749, .longitude = -122.4194 }; -const TransitionTally = struct { - finished_count: usize = 0, - last_transition_id: ?u64 = null, - last_change_mode: ?maplibre.CameraChangeMode = null, - change_followed_finish: bool = false, -}; - -/// Drains the queued runtime events and tallies what the camera events among -/// them reported. -fn drainedCameraEvents(runtime: *maplibre.RuntimeHandle) !TransitionTally { - var tally = TransitionTally{}; - var batch = try runtime.drainEvents(testing.allocator, 0); - defer batch.deinit(); - for (0..batch.len()) |index| { - const event = try batch.at(index); - switch (event.event_type) { - .map_camera_transition_finished => { - try testing.expect(std.meta.eql( - event.payload_type, - maplibre.RuntimeEventPayloadType.camera_transition_finished, - )); - const payload = switch (event.payload) { - .camera_transition_finished => |value| value, - else => return error.UnexpectedPayload, - }; - tally.finished_count += 1; - tally.last_transition_id = payload.transition_id; - }, - .map_camera_did_change => { - tally.last_change_mode = maplibre.CameraChangeMode.fromRaw(event.code); - if (tally.finished_count > 0) tally.change_followed_finish = true; - }, - else => {}, - } - } - return tally; -} - -test "camera jump updates snapshot fields through public binding" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - try map.jumpTo(.{ .center = center, .zoom = 10.0 }); - - const snapshot = try map.getCamera(); - try testing.expect(snapshot.center != null); - try testing.expect(snapshot.zoom != null); +fn orderedCamera(map: *maplibre.MapHandle) !maplibre.CameraSnapshot { + var future = try map.cameraQuery(); + defer future.deinit(); + return future.wait(null); } -test "camera commands accept valid public descriptors" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - const anchor = maplibre.ScreenPoint{ .x = 256, .y = 256 }; - const rotate_start = maplibre.ScreenPoint{ .x = 200, .y = 200 }; - const rotate_end = maplibre.ScreenPoint{ .x = 220, .y = 210 }; - const animation = maplibre.AnimationOptions{ - .duration_ms = 0, - .easing = .{ .x1 = 0.0, .y1 = 0.0, .x2 = 0.25, .y2 = 1.0 }, +fn updateCameraOnThread(map: *maplibre.MapHandle, out_generation: *u64, out_error: *?anyerror) void { + var future = map.updateCamera(.{ + .camera = .{ .center = center, .zoom = 9.0 }, + }) catch |err| { + out_error.* = err; + return; }; - - try map.moveBy(4, -2); - try map.moveByAnimated(1, 1, animation); - try map.scaleBy(1.1, anchor); - try map.scaleBy(0.95, null); - try map.scaleByAnimated(1.05, anchor, animation); - try map.rotateBy(rotate_start, rotate_end); - try map.rotateByAnimated(rotate_start, rotate_end, animation); - try map.pitchBy(3); - try map.pitchByAnimated(-1, animation); - try map.easeTo(.{ .center = center, .zoom = 12.0 }, animation); - try map.flyTo(.{ .center = center, .zoom = 10.0 }, animation); - try map.cancelTransitions(); -} - -test "gesture in progress brackets host-driven camera commands" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - try testing.expect(!try map.isGestureInProgress()); - - try map.setGestureInProgress(true); - try testing.expect(try map.isGestureInProgress()); - try map.moveBy(8, -4); - try testing.expect(try map.isGestureInProgress()); - - try map.setGestureInProgress(false); - try testing.expect(!try map.isGestureInProgress()); + defer future.deinit(); + const result = future.wait(null) catch |err| { + out_error.* = err; + return; + }; + out_generation.* = result.generation; + out_error.* = null; } -test "zero-duration ease reports one transition finish ahead of an immediate camera change" { +test "camera jump updates snapshot fields through public binding" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.easeTo(.{ .center = center, .zoom = 11.0 }, .{ .duration_ms = 0, .transition_id = 7 }); + var update = try map.updateCamera(.{ .camera = .{ .center = center, .zoom = 10.0 } }); + defer update.deinit(); + try testing.expect((try update.wait(null)).generation != 0); - const tally = try drainedCameraEvents(&runtime); - try testing.expectEqual(@as(usize, 1), tally.finished_count); - try testing.expectEqual(@as(?u64, 7), tally.last_transition_id); - try testing.expect(tally.change_followed_finish); - try testing.expect(std.meta.eql(tally.last_change_mode.?, maplibre.CameraChangeMode.immediate)); + const snapshot = try orderedCamera(&map); + try testing.expect(snapshot.camera.center != null); + try testing.expect(snapshot.camera.zoom != null); } -test "a superseded camera transition reports one transition finish with an animated camera change" { +test "camera commands accept valid public descriptors" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.easeTo(.{ .center = center, .zoom = 12.0 }, .{ .duration_ms = 5000, .transition_id = 11 }); - try testing.expectEqual(@as(usize, 0), (try drainedCameraEvents(&runtime)).finished_count); - - try map.easeTo(.{ .center = center, .zoom = 13.0 }, .{ .duration_ms = 5000, .transition_id = 12 }); - - const tally = try drainedCameraEvents(&runtime); - try testing.expectEqual(@as(usize, 1), tally.finished_count); - try testing.expectEqual(@as(?u64, 11), tally.last_transition_id); - try testing.expect(std.meta.eql(tally.last_change_mode.?, maplibre.CameraChangeMode.animated)); + var jump = try map.updateCamera(.{ + .mode = .jump, + .camera = .{ .center = center, .zoom = 10.0 }, + }); + defer jump.deinit(); + const jump_result = try jump.wait(null); + var ease = try map.updateCamera(.{ + .mode = .ease, + .camera = .{ .center = center, .zoom = 12.0 }, + .animation = .{ .duration_ms = 0, .easing = .{ .x1 = 0.0, .y1 = 0.0, .x2 = 0.25, .y2 = 1.0 }, .transition_id = 7 }, + }); + defer ease.deinit(); + const ease_result = try ease.wait(null); + var fly = try map.updateCamera(.{ + .mode = .fly, + .camera = .{ .center = center, .zoom = 11.0 }, + .animation = .{ .duration_ms = 0 }, + }); + defer fly.deinit(); + const fly_result = try fly.wait(null); + try testing.expect(jump_result.generation < ease_result.generation); + try testing.expect(ease_result.generation < fly_result.generation); } -test "cancelling reports one transition finish for the transition that carries an ID" { +test "camera commands are accepted from another thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.easeTo(.{ .center = center, .zoom = 12.0 }, .{ .duration_ms = 5000, .transition_id = 21 }); - try map.cancelTransitions(); - - const cancelled = try drainedCameraEvents(&runtime); - try testing.expectEqual(@as(usize, 1), cancelled.finished_count); - try testing.expectEqual(@as(?u64, 21), cancelled.last_transition_id); - - try map.easeTo(.{ .center = center, .zoom = 14.0 }, .{ .duration_ms = 5000 }); - try map.cancelTransitions(); + var generation: u64 = 0; + var thread_error: ?anyerror = error.Unexpected; + const thread = try std.Thread.spawn(.{}, updateCameraOnThread, .{ &map, &generation, &thread_error }); + thread.join(); + try testing.expect(thread_error == null); + try testing.expect(generation != 0); - try testing.expectEqual(@as(usize, 0), (try drainedCameraEvents(&runtime)).finished_count); + const snapshot = try orderedCamera(&map); + try testing.expectApproxEqAbs(@as(f64, 9.0), snapshot.camera.zoom.?, 0.000001); } test "camera fitting computes camera and visible bounds" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); const bounds = maplibre.LatLngBounds{ .southwest = .{ .latitude = 35.0, .longitude = -125.0 }, .northeast = .{ .latitude = 39.0, .longitude = -120.0 }, }; - const camera = try map.cameraForLatLngBounds(bounds, .{ + var camera_future = try map.cameraForLatLngBounds(bounds, .{ .padding = .{ .top = 8, .left = 12, .bottom = 8, .right = 12 }, .bearing = 5, .pitch = 15, }); + defer camera_future.deinit(); + const camera = try camera_future.wait(null); try testing.expect(camera.center != null); try testing.expect(camera.zoom != null); try testing.expect(camera.padding != null); @@ -174,24 +122,34 @@ test "camera fitting computes camera and visible bounds" { try testing.expect(camera.pitch != null); const coordinates = [_]maplibre.LatLng{ bounds.southwest, bounds.northeast }; - const coordinate_camera = try map.cameraForLatLngs(testing.allocator, coordinates[0..], null); + var coordinate_future = try map.cameraForLatLngs(testing.allocator, coordinates[0..], null); + defer coordinate_future.deinit(); + const coordinate_camera = try coordinate_future.wait(null); try testing.expect(coordinate_camera.center != null); try testing.expect(coordinate_camera.zoom != null); - const geometry_camera = try map.cameraForGeometry(testing.allocator, "{\"type\":\"LineString\",\"coordinates\":[[-125,35],[-120,39]]}", null); + var geometry_future = try map.cameraForGeometry(testing.allocator, "{\"type\":\"LineString\",\"coordinates\":[[-125,35],[-120,39]]}", null); + defer geometry_future.deinit(); + const geometry_camera = try geometry_future.wait(null); try testing.expect(geometry_camera.center != null); try testing.expect(geometry_camera.zoom != null); - const visible_bounds = try map.latLngBoundsForCamera(camera); + var bounds_future = try map.latLngBoundsForCamera(camera); + defer bounds_future.deinit(); + const visible_bounds = try bounds_future.wait(null); try testing.expect(visible_bounds.southwest.latitude <= visible_bounds.northeast.latitude); - const unwrapped_bounds = try map.latLngBoundsForCameraUnwrapped(camera); + var unwrapped_future = try map.latLngBoundsForCameraUnwrapped(camera); + defer unwrapped_future.deinit(); + const unwrapped_bounds = try unwrapped_future.wait(null); try testing.expect(unwrapped_bounds.southwest.latitude <= unwrapped_bounds.northeast.latitude); } test "camera constraints and free camera options round-trip public values" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); const constraints = maplibre.BoundOptions{ @@ -204,67 +162,82 @@ test "camera constraints and free camera options round-trip public values" { .min_pitch = 0.0, .max_pitch = 45.0, }; - try map.setBounds(constraints); - const copied_constraints = try map.getBounds(); + const bounds_id = try map.setBounds(constraints); + const bounded = try support.snapshotAfterCommand(&runtime, &map, bounds_id); + const copied_constraints = bounded.bounds; try testing.expect(copied_constraints.bounds != null); try testing.expectApproxEqAbs(constraints.min_zoom.?, copied_constraints.min_zoom.?, 0.000001); try testing.expectApproxEqAbs(constraints.max_pitch.?, copied_constraints.max_pitch.?, 0.000001); - const free_camera = try map.getFreeCameraOptions(); + const free_camera = bounded.free_camera; try testing.expect(free_camera.position != null); try testing.expect(free_camera.orientation != null); - try map.setFreeCameraOptions(.{ .orientation = free_camera.orientation }); + const free_camera_id = try map.setFreeCameraOptions(.{ .orientation = free_camera.orientation }); + const oriented = try support.snapshotAfterCommand(&runtime, &map, free_camera_id); + try testing.expect(oriented.free_camera.orientation != null); } fn jumpedLongitude(map: *maplibre.MapHandle, longitude: f64) !f64 { - try map.jumpTo(.{ .center = .{ .latitude = 0, .longitude = longitude }, .zoom = 2.0 }); - const snapshot = try map.getCamera(); - return (snapshot.center orelse return error.MissingCameraCenter).longitude; + var future = try map.updateCamera(.{ .camera = .{ .center = .{ .latitude = 0, .longitude = longitude }, .zoom = 2.0 } }); + defer future.deinit(); + _ = try future.wait(null); + const snapshot = try orderedCamera(map); + return (snapshot.camera.center orelse return error.MissingCameraCenter).longitude; } // An unbounded camera center pans across the antimeridian; world bounds clamp // longitude to -180..180. test "camera bounds separate the unbounded constraint from world bounds" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - const pristine = try map.getBounds(); + const pristine = (try map.snapshot()).bounds; try testing.expectEqual(.unbounded, std.meta.activeTag(pristine.bounds.?)); try testing.expectApproxEqAbs(@as(f64, -160.0), try jumpedLongitude(&map, 200.0), 1e-6); - try map.setBounds(.{ .bounds = .{ .bounded = .{ + const world_id = try map.setBounds(.{ .bounds = .{ .bounded = .{ .southwest = .{ .latitude = -90.0, .longitude = -180.0 }, .northeast = .{ .latitude = 90.0, .longitude = 180.0 }, } } }); - const world = try map.getBounds(); + const world = (try support.snapshotAfterCommand(&runtime, &map, world_id)).bounds; switch (world.bounds.?) { .bounded => |bounds| try testing.expectApproxEqAbs(@as(f64, 180.0), bounds.northeast.longitude, 1e-6), .unbounded => return error.TestExpectedBoundedConstraint, } try testing.expectApproxEqAbs(@as(f64, 180.0), try jumpedLongitude(&map, 200.0), 1e-6); - try map.setBounds(.{ .bounds = .unbounded }); - const released = try map.getBounds(); + const released_id = try map.setBounds(.{ .bounds = .unbounded }); + const released = (try support.snapshotAfterCommand(&runtime, &map, released_id)).bounds; try testing.expectEqual(.unbounded, std.meta.activeTag(released.bounds.?)); try testing.expectApproxEqAbs(@as(f64, -160.0), try jumpedLongitude(&map, 200.0), 1e-6); } test "camera public descriptors report invalid native arguments" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try testing.expectError(error.InvalidArgument, map.jumpTo(.{ .center = .{ .latitude = std.math.inf(f64), .longitude = 0 } })); - try testing.expectError(error.InvalidArgument, map.easeTo(.{ .center = center }, .{ .duration_ms = -1 })); - try testing.expectError(error.InvalidArgument, map.flyTo(.{ .center = center }, .{ .easing = .{ .x1 = 2, .y1 = 0, .x2 = 1, .y2 = 1 } })); - try testing.expectError(error.InvalidArgument, map.moveBy(std.math.nan(f64), 0)); - try testing.expectError(error.InvalidArgument, map.scaleBy(0, null)); - try testing.expectError(error.InvalidArgument, map.rotateBy(.{ .x = std.math.inf(f64), .y = 0 }, .{ .x = 0, .y = 0 })); - try testing.expectError(error.InvalidArgument, map.pitchBy(std.math.nan(f64))); + try testing.expectError(error.InvalidArgument, map.updateCamera(.{ + .camera = .{ .center = .{ .latitude = std.math.inf(f64), .longitude = 0 } }, + })); + try testing.expectError(error.InvalidArgument, map.updateCamera(.{ + .mode = .ease, + .camera = .{ .center = center }, + .animation = .{ .duration_ms = -1 }, + })); + try testing.expectError(error.InvalidArgument, map.updateCamera(.{ + .mode = .fly, + .camera = .{ .center = center }, + .animation = .{ .easing = .{ .x1 = 2, .y1 = 0, .x2 = 1, .y2 = 1 } }, + })); const inverted_bounds = maplibre.LatLngBounds{ .southwest = .{ .latitude = 10.0, .longitude = 10.0 }, diff --git a/bindings/zig/tests/diagnostics.zig b/bindings/zig/tests/diagnostics.zig index c771e25b5..4b9dbf87b 100644 --- a/bindings/zig/tests/diagnostics.zig +++ b/bindings/zig/tests/diagnostics.zig @@ -1,15 +1,18 @@ const testing = @import("std").testing; const maplibre = @import("maplibre_native_ffi"); +const support = @import("support.zig"); test "diagnostics capture public lifecycle failures and keep copied messages" { var diagnostics = maplibre.DiagnosticStore.init(testing.allocator); defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); - try testing.expectError(error.InvalidState, runtime.close()); + try testing.expectError(error.InvalidState, support.closeRuntime(&runtime)); const first = diagnostics.get().?; try testing.expectEqual(@as(?i32, null), first.raw_status); try testing.expectEqualStrings("runtime has live maps", first.message); @@ -18,5 +21,5 @@ test "diagnostics capture public lifecycle failures and keep copied messages" { try map.close(); try testing.expectEqualStrings(copied, diagnostics.get().?.message); - try runtime.close(); + try support.closeRuntime(&runtime); } diff --git a/bindings/zig/tests/geojson.zig b/bindings/zig/tests/geojson.zig index e9181ea76..e7b05f43d 100644 --- a/bindings/zig/tests/geojson.zig +++ b/bindings/zig/tests/geojson.zig @@ -17,23 +17,23 @@ fn expectListContains(list: maplibre.StringList, expected: []const u8) !void { test "prepared GeoJSON data adds and updates sources through public binding" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const empty_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, empty_collection, null); defer empty_data.release(); - try map.addGeoJsonSourceData(testing.allocator, "empty", empty_data); - try testing.expect(try map.styleSourceExists(testing.allocator, "empty")); - var source_ids = try map.listStyleSourceIds(testing.allocator); + _ = try map.addGeoJsonSourceData(testing.allocator, "empty", empty_data); + try testing.expect(try support.styleSourceExists(&map, "empty")); + var source_ids = try support.listStyleSourceIds(&map); defer source_ids.deinit(); try expectListContains(source_ids, "empty"); const point_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, point_collection, null); defer point_data.release(); - try map.setGeoJsonSourceData(testing.allocator, "empty", point_data); - try map.setGeoJsonSourceUrl(testing.allocator, "empty", "https://example.com/data.geojson"); - try map.addGeoJsonSourceUrl(testing.allocator, "geo-url", "https://example.com/initial.geojson", .{ + _ = try map.setGeoJsonSourceData(testing.allocator, "empty", point_data); + _ = try map.setGeoJsonSourceUrl(testing.allocator, "empty", "https://example.com/data.geojson"); + _ = try map.addGeoJsonSourceUrl(testing.allocator, "geo-url", "https://example.com/initial.geojson", .{ .min_zoom = 1, .max_zoom = 16, .tolerance = 0.5, @@ -41,21 +41,21 @@ test "prepared GeoJSON data adds and updates sources through public binding" { .tile_size = 256, .line_metrics = true, }); - try testing.expectEqual(maplibre.StyleSourceType.geojson, (try map.getStyleSourceType(testing.allocator, "geo-url")).?); - try testing.expectError(error.InvalidArgument, map.addGeoJsonSourceUrl(testing.allocator, "empty", "https://example.com/again.geojson", null)); + try testing.expectEqual(maplibre.StyleSourceType.geojson, (try support.styleSourceType(&map, "geo-url")).?); + try support.expectCommandError(&runtime, try map.addGeoJsonSourceUrl(testing.allocator, "empty", "https://example.com/again.geojson", null), error.InvalidArgument); } test "prepared GeoJSON data supports nested geometry collections" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const collection = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"LineString\",\"coordinates\":[[-123,37],[-122,38]]},{\"type\":\"Polygon\",\"coordinates\":[[[-123,37],[-123,38],[-122,38],[-123,37]]]}]}"; const data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, collection, null); defer data.release(); - try map.addGeoJsonSourceData(testing.allocator, "collection", data); - try testing.expect(try map.styleSourceExists(testing.allocator, "collection")); + _ = try map.addGeoJsonSourceData(testing.allocator, "collection", data); + try testing.expect(try support.styleSourceExists(&map, "collection")); } test "GeoJSON preparation rejects invalid data and passes explicit-length strings" { @@ -65,24 +65,26 @@ test "GeoJSON preparation rejects invalid data and passes explicit-length string ); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const empty_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, empty_collection, null); defer empty_data.release(); - try testing.expectError(error.InvalidArgument, map.addGeoJsonSourceData(testing.allocator, "", empty_data)); + // An empty source ID submits, then the command reports the rejection. + const empty_id = try map.addGeoJsonSourceData(testing.allocator, "", empty_data); + try support.expectCommandError(&runtime, empty_id, error.InvalidArgument); const embedded_nul_id = "{\"type\":\"Feature\",\"id\":\"bad\\u0000id\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0,0]},\"properties\":{}}"; const nul_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, embedded_nul_id, null); defer nul_data.release(); - try map.addGeoJsonSourceData(testing.allocator, "embedded-nul-id", nul_data); - try testing.expect(try map.styleSourceExists(testing.allocator, "embedded-nul-id")); + _ = try map.addGeoJsonSourceData(testing.allocator, "embedded-nul-id", nul_data); + try testing.expect(try support.styleSourceExists(&map, "embedded-nul-id")); } test "GeoJSON preparation bakes in options and validates clustering" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -96,9 +98,9 @@ test "GeoJSON preparation bakes in options and validates clustering" { }; const clustered = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, features, cluster_options); defer clustered.release(); - try map.addGeoJsonSourceData(testing.allocator, "clustered", clustered); - try testing.expect(try map.styleSourceExists(testing.allocator, "clustered")); - try map.setGeoJsonSourceData(testing.allocator, "clustered", clustered); + _ = try map.addGeoJsonSourceData(testing.allocator, "clustered", clustered); + try testing.expect(try support.styleSourceExists(&map, "clustered")); + _ = try map.setGeoJsonSourceData(testing.allocator, "clustered", clustered); // Cluster validation happens at preparation, before any map is involved. try testing.expectError(error.InvalidArgument, maplibre.GeoJsonSourceDataHandle.create( @@ -113,16 +115,18 @@ test "GeoJSON preparation bakes in options and validates clustering" { )); // A set rejects data prepared with options that differ from the source's, - // cluster aggregation expressions included. + // cluster aggregation expressions included; the command reports it. const unclustered = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, features, null); defer unclustered.release(); - try testing.expectError(error.InvalidArgument, map.setGeoJsonSourceData(testing.allocator, "clustered", unclustered)); + const unclustered_set = try map.setGeoJsonSourceData(testing.allocator, "clustered", unclustered); + try support.expectCommandError(&runtime, unclustered_set, error.InvalidArgument); var reproperty_options = cluster_options; reproperty_options.cluster_properties = "{\"total\":[\"max\",[\"get\",\"rank\"]]}"; const repropertied = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, features, reproperty_options); defer repropertied.release(); - try testing.expectError(error.InvalidArgument, map.setGeoJsonSourceData(testing.allocator, "clustered", repropertied)); + const repropertied_set = try map.setGeoJsonSourceData(testing.allocator, "clustered", repropertied); + try support.expectCommandError(&runtime, repropertied_set, error.InvalidArgument); // Aggregations compare by parsed equality, so equivalent JSON with // different formatting still matches. @@ -130,35 +134,39 @@ test "GeoJSON preparation bakes in options and validates clustering" { reformatted_options.cluster_properties = " { \"total\" : [\"+\", [\"get\", \"rank\"]] } "; const reformatted = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, features, reformatted_options); defer reformatted.release(); - try map.setGeoJsonSourceData(testing.allocator, "clustered", reformatted); + const reformatted_set = try map.setGeoJsonSourceData(testing.allocator, "clustered", reformatted); + try testing.expectEqual( + try support.waitForCommandDisposition(&runtime, reformatted_set), + maplibre.CommandDisposition.committed, + ); } test "prepared GeoJSON data installs on many sources and outlives release" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const shared = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, point_collection, null); - try map.addGeoJsonSourceData(testing.allocator, "shared-a", shared); - try map.addGeoJsonSourceData(testing.allocator, "shared-b", shared); - try map.setGeoJsonSourceData(testing.allocator, "shared-a", shared); + _ = try map.addGeoJsonSourceData(testing.allocator, "shared-a", shared); + _ = try map.addGeoJsonSourceData(testing.allocator, "shared-b", shared); + _ = try map.setGeoJsonSourceData(testing.allocator, "shared-a", shared); - // Sources keep their own reference, so release never invalidates them, - // and a second release is a no-op. + // Submitted installs keep their own lease, so a release right after + // submit never invalidates them, and a second release is a no-op. shared.release(); shared.release(); - try testing.expect(try map.styleSourceExists(testing.allocator, "shared-a")); - try testing.expect(try map.styleSourceExists(testing.allocator, "shared-b")); + try testing.expect(try support.styleSourceExists(&map, "shared-a")); + try testing.expect(try support.styleSourceExists(&map, "shared-b")); - // A released handle is stale for new installs. + // A released handle is stale for new installs, rejected at submit. try testing.expectError(error.InvalidArgument, map.setGeoJsonSourceData(testing.allocator, "shared-a", shared)); try testing.expectError(error.InvalidArgument, map.addGeoJsonSourceData(testing.allocator, "shared-c", shared)); } test "GeoJSON preparation runs on a worker thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -172,23 +180,34 @@ test "GeoJSON preparation runs on a worker thread" { thread.join(); const data = try result; defer data.release(); - try map.addGeoJsonSourceData(testing.allocator, "worker-prepared", data); - try testing.expect(try map.styleSourceExists(testing.allocator, "worker-prepared")); + _ = try map.addGeoJsonSourceData(testing.allocator, "worker-prepared", data); + try testing.expect(try support.styleSourceExists(&map, "worker-prepared")); } test "synchronous tiling override applies at runtime" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, point_collection, null); defer data.release(); - try map.addGeoJsonSourceData(testing.allocator, "tracked", data); + _ = try map.addGeoJsonSourceData(testing.allocator, "tracked", data); - try map.setGeoJsonSourceSynchronousTiling(testing.allocator, "tracked", true); - try map.setGeoJsonSourceData(testing.allocator, "tracked", data); - try map.setGeoJsonSourceSynchronousTiling(testing.allocator, "tracked", false); + const enable_id = try map.setGeoJsonSourceSynchronousTiling(testing.allocator, "tracked", true); + try testing.expectEqual( + try support.waitForCommandDisposition(&runtime, enable_id), + maplibre.CommandDisposition.committed, + ); + const set_id = try map.setGeoJsonSourceData(testing.allocator, "tracked", data); + try testing.expectEqual( + try support.waitForCommandDisposition(&runtime, set_id), + maplibre.CommandDisposition.committed, + ); + _ = try map.setGeoJsonSourceSynchronousTiling(testing.allocator, "tracked", false); - try testing.expectError(error.InvalidArgument, map.setGeoJsonSourceSynchronousTiling(testing.allocator, "missing", true)); + // The override rejects a source ID that names nothing, reported by the + // command since the style is only readable at commit. + const missing_id = try map.setGeoJsonSourceSynchronousTiling(testing.allocator, "missing", true); + try support.expectCommandError(&runtime, missing_id, error.InvalidArgument); } diff --git a/bindings/zig/tests/logging.zig b/bindings/zig/tests/logging.zig index 74d40bdc7..69cf87a69 100644 --- a/bindings/zig/tests/logging.zig +++ b/bindings/zig/tests/logging.zig @@ -2,6 +2,7 @@ const std = @import("std"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); +const support = @import("support.zig"); const LogState = struct { count: usize = 0, @@ -31,12 +32,19 @@ test "log callback receives and consumes native logs" { } var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try testing.expectError(error.NativeError, map.setStyleJson(testing.allocator, "{")); + _ = try map.setStyleJson(testing.allocator, "{"); + var barrier = try runtime.barrier(); + defer barrier.deinit(); + _ = try barrier.wait(null); try testing.expect(state.count > 0); try testing.expect(state.saw_parse_style); try testing.expect(state.saw_message); @@ -58,12 +66,19 @@ test "log callback can be cleared" { try maplibre.clearLogCallback(null); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try testing.expectError(error.NativeError, map.setStyleJson(testing.allocator, "{")); + _ = try map.setStyleJson(testing.allocator, "{"); + var barrier = try runtime.barrier(); + defer barrier.deinit(); + _ = try barrier.wait(null); try testing.expectEqual(@as(usize, 0), state.count); } @@ -79,12 +94,19 @@ test "log callback replacement invokes only the replacement" { try maplibre.setLogCallback(.{ .handler = recordLog, .context = &replacement_state }, null); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try testing.expectError(error.NativeError, map.setStyleJson(testing.allocator, "{")); + _ = try map.setStyleJson(testing.allocator, "{"); + var barrier = try runtime.barrier(); + defer barrier.deinit(); + _ = try barrier.wait(null); try testing.expectEqual(@as(usize, 0), first_state.count); try testing.expect(replacement_state.count > 0); try testing.expect(replacement_state.saw_parse_style); diff --git a/bindings/zig/tests/map_lifecycle.zig b/bindings/zig/tests/map_lifecycle.zig index ba10a0e95..b95a2380b 100644 --- a/bindings/zig/tests/map_lifecycle.zig +++ b/bindings/zig/tests/map_lifecycle.zig @@ -5,7 +5,7 @@ const maplibre = @import("maplibre_native_ffi"); const support = @import("support.zig"); fn requestRepaintOnThread(map: *maplibre.MapHandle, out_error: *?anyerror) void { - map.requestRepaint() catch |err| { + _ = map.requestRepaint() catch |err| { out_error.* = err; return; }; @@ -14,8 +14,10 @@ fn requestRepaintOnThread(map: *maplibre.MapHandle, out_error: *?anyerror) void fn createRuntimeAndMap() !struct { runtime: maplibre.RuntimeHandle, map: maplibre.MapHandle } { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - errdefer runtime.close() catch @panic("runtime close failed"); - const map = try maplibre.MapHandle.create(&runtime, .{}); + errdefer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + const map = try map_future.wait(null); return .{ .runtime = runtime, .map = map }; } @@ -24,54 +26,61 @@ test "runtime and map vertical slice" { defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - try runtime.pump(0, null); try testing.expectEqual(@as(usize, 0), try support.drainEvents(&runtime)); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - try map.setStyleJson(testing.allocator, support.style_json); - try runtime.pump(0, null); + defer map_future.deinit(); + + var map = try map_future.wait(null); + + _ = try map.setStyleJson(testing.allocator, support.style_json); try map.close(); - try runtime.pump(0, null); - var map_after_close = try maplibre.MapHandle.create(&runtime, .{}); + var map_after_close_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_after_close_future.deinit(); + + var map_after_close = try map_after_close_future.wait(null); defer map_after_close.close() catch @panic("map close failed"); - var projection = try maplibre.MapProjectionHandle.create(&map_after_close); + var projection_future = try maplibre.MapProjectionHandle.create(&map_after_close); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); try projection.close(); } test "loaded style document and URL read back what was loaded" { var handles = try createRuntimeAndMap(); - defer handles.runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&handles.runtime) catch @panic("runtime close failed"); defer handles.map.close() catch @panic("map close failed"); - var empty_json = try handles.map.copyLoadedStyleJson(testing.allocator); + var empty_json = try support.loadedStyleJson(&handles.map); defer empty_json.deinit(); try testing.expectEqualStrings("", empty_json.value); - var empty_url = try handles.map.copyStyleUrl(testing.allocator); + var empty_url = try support.styleUrl(&handles.map); defer empty_url.deinit(); try testing.expectEqualStrings("", empty_url.value); // The document reads back byte-for-byte, so it can be reloaded unchanged. - try handles.map.setStyleJson(testing.allocator, support.style_json); - var loaded = try handles.map.copyLoadedStyleJson(testing.allocator); + _ = try handles.map.setStyleJson(testing.allocator, support.style_json); + var loaded = try support.loadedStyleJson(&handles.map); defer loaded.deinit(); try testing.expectEqualStrings(support.style_json, loaded.value); - var cleared_url = try handles.map.copyStyleUrl(testing.allocator); + var cleared_url = try support.styleUrl(&handles.map); defer cleared_url.deinit(); try testing.expectEqualStrings("", cleared_url.value); // The URL is request state, recorded before the load can succeed, while the // document still reports the style that last parsed. - try handles.map.setStyleUrl(testing.allocator, "https://example.com/style.json"); - var requested_url = try handles.map.copyStyleUrl(testing.allocator); + _ = try handles.map.setStyleUrl(testing.allocator, "https://example.com/style.json"); + var requested_url = try support.styleUrl(&handles.map); defer requested_url.deinit(); try testing.expectEqualStrings("https://example.com/style.json", requested_url.value); - var still_loaded = try handles.map.copyLoadedStyleJson(testing.allocator); + var still_loaded = try support.loadedStyleJson(&handles.map); defer still_loaded.deinit(); try testing.expectEqualStrings(support.style_json, still_loaded.value); } @@ -79,15 +88,19 @@ test "loaded style document and URL read back what was loaded" { test "map can close after moving with its runtime" { var handles = try createRuntimeAndMap(); try handles.map.close(); - try handles.runtime.close(); + try support.closeRuntime(&handles.runtime); } test "copied runtime and map handles share closed state" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); var runtime_alias = runtime; - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); var map_alias = map; - var projection = try maplibre.MapProjectionHandle.create(&map); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); var projection_alias = projection; try projection.close(); @@ -98,9 +111,9 @@ test "copied runtime and map handles share closed state" { try map_alias.close(); try testing.expectError(error.ClosedHandle, map_alias.id()); - try runtime.close(); - try runtime_alias.close(); - try testing.expectError(error.ClosedHandle, runtime_alias.pump(0, null)); + try support.closeRuntime(&runtime); + try support.closeRuntime(&runtime_alias); + try testing.expectError(error.ClosedHandle, runtime_alias.drainEvents(testing.allocator)); } test "successful close releases lifecycle handles" { @@ -108,15 +121,20 @@ test "successful close releases lifecycle handles" { defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - var map = try maplibre.MapHandle.create(&runtime, .{}); - var projection = try maplibre.MapProjectionHandle.create(&map); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); - try projection.close(); - try projection.close(); try map.close(); try map.close(); - try runtime.close(); - try runtime.close(); + try support.closeRuntime(&runtime); + try support.closeRuntime(&runtime); + _ = try projection.getCamera(); + try projection.close(); + try projection.close(); } test "failed close remains retryable" { @@ -124,16 +142,18 @@ test "failed close remains retryable" { defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); - try testing.expectError(error.InvalidState, runtime.close()); + try testing.expectError(error.InvalidState, support.closeRuntime(&runtime)); try map.close(); - try runtime.close(); + try support.closeRuntime(&runtime); } test "map options validate through public binding" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); try testing.expectError(error.InvalidArgument, maplibre.MapHandle.create(&runtime, .{ .width = 0 })); try testing.expectError(error.InvalidArgument, maplibre.MapHandle.create(&runtime, .{ .height = 0 })); @@ -142,17 +162,25 @@ test "map options validate through public binding" { test "map creation accepts FastPFOR decoding" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{ .fast_pfor_enabled = true }); - var map = try maplibre.MapHandle.create(&runtime, .{ .fast_pfor_enabled = true }); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); } test "unset map options take the C creation defaults" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); const size = try map.getSize(); @@ -163,41 +191,44 @@ test "unset map options take the C creation defaults" { test "continuous repaint request makes render update available" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.requestRepaint(); + _ = try map.requestRepaint(); try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); } -test "wrong-thread map failures propagate diagnostics" { - var diagnostics = maplibre.DiagnosticStore.init(testing.allocator); - defer diagnostics.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); +test "map commands are accepted from another thread" { + var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - var thread_error: ?anyerror = null; + var thread_error: ?anyerror = error.Unexpected; const thread = try std.Thread.spawn(.{}, requestRepaintOnThread, .{ &map, &thread_error }); thread.join(); - - try testing.expectEqual(error.WrongThread, thread_error.?); - try testing.expect(diagnostics.get().?.message.len > 0); + try testing.expect(thread_error == null); } test "runtime supports multiple maps" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var first_future = try maplibre.MapHandle.create(&runtime, .{}); - var first = try maplibre.MapHandle.create(&runtime, .{}); + defer first_future.deinit(); + + var first = try first_future.wait(null); defer first.close() catch @panic("first map close failed"); - var second = try maplibre.MapHandle.create(&runtime, .{}); + var second_future = try maplibre.MapHandle.create(&runtime, .{}); + defer second_future.deinit(); + var second = try second_future.wait(null); defer second.close() catch @panic("second map close failed"); - - try runtime.pump(0, null); } test "style JSON buffers preserve embedded NUL for native validation" { @@ -206,12 +237,11 @@ test "style JSON buffers preserve embedded NUL for native validation" { try diagnostics.set(-5, "stale native diagnostic"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try testing.expectError(error.InvalidArgument, map.setStyleJson(testing.allocator, "{\x00}")); - const diagnostic = diagnostics.get().?; - try testing.expect(diagnostic.raw_status != null); - try testing.expect(diagnostic.message.len > 0); + try support.expectCommandError(&runtime, try map.setStyleJson(testing.allocator, "{\x00}"), error.InvalidArgument); } diff --git a/bindings/zig/tests/map_tuning.zig b/bindings/zig/tests/map_tuning.zig index 4b0a553ac..e5421f42c 100644 --- a/bindings/zig/tests/map_tuning.zig +++ b/bindings/zig/tests/map_tuning.zig @@ -2,11 +2,16 @@ const std = @import("std"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); +const support = @import("support.zig"); -test "map debug options round trip and diagnostics toggles" { +// The committed event fences the snapshot: a snapshot whose generation is at +// or past the commit's observes the committed value. +test "map debug options fence and round trip through the snapshot" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); const debug = maplibre.DebugOptions{ @@ -14,57 +19,60 @@ test "map debug options round trip and diagnostics toggles" { .collision = true, .depth_buffer = true, }; - try map.setDebugOptions(debug); - - const snapshot = try map.getDebugOptions(); - try testing.expect(snapshot.tile_borders); - try testing.expect(snapshot.collision); - try testing.expect(snapshot.depth_buffer); - try testing.expect(!snapshot.overdraw); - - try testing.expect(!try map.getRenderingStatsViewEnabled()); - try map.setRenderingStatsViewEnabled(true); - try testing.expect(try map.getRenderingStatsViewEnabled()); - - _ = try map.isFullyLoaded(); - try map.dumpDebugLogs(); + const completion = try map.setDebugOptions(debug); + const snapshot = try support.snapshotAfterCommand(&runtime, &map, completion); + try testing.expect(snapshot.debug_options.tile_borders); + try testing.expect(snapshot.debug_options.collision); + try testing.expect(snapshot.debug_options.depth_buffer); + try testing.expect(!snapshot.debug_options.overdraw); + + try testing.expect(!snapshot.rendering_stats_view_enabled); + const stats_id = try map.setRenderingStatsViewEnabled(true); + const stats_snapshot = try support.snapshotAfterCommand(&runtime, &map, stats_id); + try testing.expect(stats_snapshot.rendering_stats_view_enabled); + + _ = try map.dumpDebugLogs(); } -test "map viewport options update selected fields through public descriptors" { +test "map viewport options update selected snapshot fields" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setViewportOptions(.{ + const completion = try map.setViewportOptions(.{ .north_orientation = .right, .constrain_mode = .width_and_height, .viewport_mode = .flipped_y, .frustum_offset = .{ .top = 1.0, .left = 2.0, .bottom = 3.0, .right = 4.0 }, }); - var snapshot = try map.getViewportOptions(); - try testing.expectEqual(maplibre.NorthOrientation.right, snapshot.north_orientation.?); - try testing.expectEqual(maplibre.ConstrainMode.width_and_height, snapshot.constrain_mode.?); - try testing.expectEqual(maplibre.ViewportMode.flipped_y, snapshot.viewport_mode.?); - try testing.expectApproxEqAbs(@as(f64, 1.0), snapshot.frustum_offset.?.top, 0.000001); - try testing.expectApproxEqAbs(@as(f64, 2.0), snapshot.frustum_offset.?.left, 0.000001); - try testing.expectApproxEqAbs(@as(f64, 3.0), snapshot.frustum_offset.?.bottom, 0.000001); - try testing.expectApproxEqAbs(@as(f64, 4.0), snapshot.frustum_offset.?.right, 0.000001); - - try map.setViewportOptions(.{ .north_orientation = .down }); - snapshot = try map.getViewportOptions(); - try testing.expectEqual(maplibre.NorthOrientation.down, snapshot.north_orientation.?); - try testing.expectEqual(maplibre.ConstrainMode.width_and_height, snapshot.constrain_mode.?); + var snapshot = try support.snapshotAfterCommand(&runtime, &map, completion); + try testing.expectEqual(maplibre.NorthOrientation.right, snapshot.viewport.north_orientation.?); + try testing.expectEqual(maplibre.ConstrainMode.width_and_height, snapshot.viewport.constrain_mode.?); + try testing.expectEqual(maplibre.ViewportMode.flipped_y, snapshot.viewport.viewport_mode.?); + try testing.expectApproxEqAbs(@as(f64, 1.0), snapshot.viewport.frustum_offset.?.top, 0.000001); + try testing.expectApproxEqAbs(@as(f64, 2.0), snapshot.viewport.frustum_offset.?.left, 0.000001); + try testing.expectApproxEqAbs(@as(f64, 3.0), snapshot.viewport.frustum_offset.?.bottom, 0.000001); + try testing.expectApproxEqAbs(@as(f64, 4.0), snapshot.viewport.frustum_offset.?.right, 0.000001); + + const narrowed_id = try map.setViewportOptions(.{ .north_orientation = .down }); + snapshot = try support.snapshotAfterCommand(&runtime, &map, narrowed_id); + try testing.expectEqual(maplibre.NorthOrientation.down, snapshot.viewport.north_orientation.?); + try testing.expectEqual(maplibre.ConstrainMode.width_and_height, snapshot.viewport.constrain_mode.?); } -test "map tile options update selected fields through public descriptors" { +test "map tile options update selected snapshot fields" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setTileOptions(.{ + const completion = try map.setTileOptions(.{ .prefetch_zoom_delta = 2, .lod_min_radius = 1.5, .lod_scale = 2.5, @@ -73,24 +81,26 @@ test "map tile options update selected fields through public descriptors" { .lod_mode = .distance, }); - var snapshot = try map.getTileOptions(); - try testing.expectEqual(@as(u32, 2), snapshot.prefetch_zoom_delta.?); - try testing.expectApproxEqAbs(@as(f64, 1.5), snapshot.lod_min_radius.?, 0.000001); - try testing.expectApproxEqAbs(@as(f64, 2.5), snapshot.lod_scale.?, 0.000001); - try testing.expectApproxEqAbs(@as(f64, 0.75), snapshot.lod_pitch_threshold.?, 0.000001); - try testing.expectApproxEqAbs(@as(f64, -1.0), snapshot.lod_zoom_shift.?, 0.000001); - try testing.expectEqual(maplibre.TileLodMode.distance, snapshot.lod_mode.?); - - try map.setTileOptions(.{ .prefetch_zoom_delta = 7 }); - snapshot = try map.getTileOptions(); - try testing.expectEqual(@as(u32, 7), snapshot.prefetch_zoom_delta.?); - try testing.expectEqual(maplibre.TileLodMode.distance, snapshot.lod_mode.?); + var snapshot = try support.snapshotAfterCommand(&runtime, &map, completion); + try testing.expectEqual(@as(u32, 2), snapshot.tile.prefetch_zoom_delta.?); + try testing.expectApproxEqAbs(@as(f64, 1.5), snapshot.tile.lod_min_radius.?, 0.000001); + try testing.expectApproxEqAbs(@as(f64, 2.5), snapshot.tile.lod_scale.?, 0.000001); + try testing.expectApproxEqAbs(@as(f64, 0.75), snapshot.tile.lod_pitch_threshold.?, 0.000001); + try testing.expectApproxEqAbs(@as(f64, -1.0), snapshot.tile.lod_zoom_shift.?, 0.000001); + try testing.expectEqual(maplibre.TileLodMode.distance, snapshot.tile.lod_mode.?); + + const narrowed_id = try map.setTileOptions(.{ .prefetch_zoom_delta = 7 }); + snapshot = try support.snapshotAfterCommand(&runtime, &map, narrowed_id); + try testing.expectEqual(@as(u32, 7), snapshot.tile.prefetch_zoom_delta.?); + try testing.expectEqual(maplibre.TileLodMode.distance, snapshot.tile.lod_mode.?); } test "map tuning public descriptors report invalid native arguments" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); try testing.expectError(error.InvalidArgument, map.setViewportOptions(.{ .frustum_offset = .{ .top = std.math.inf(f64) } })); diff --git a/bindings/zig/tests/metal_support.zig b/bindings/zig/tests/metal_support.zig index b3c3130f4..723378562 100644 --- a/bindings/zig/tests/metal_support.zig +++ b/bindings/zig/tests/metal_support.zig @@ -2,11 +2,11 @@ pub const AutoreleasePool = struct { token: *anyopaque, pub fn init() !AutoreleasePool { - return .{ .token = mln_test_autorelease_pool_push() orelse return error.AutoreleasePoolUnavailable }; + return .{ .token = mln_zig_test_autorelease_pool_push() orelse return error.AutoreleasePoolUnavailable }; } pub fn deinit(self: AutoreleasePool) void { - mln_test_autorelease_pool_pop(self.token); + mln_zig_test_autorelease_pool_pop(self.token); } }; @@ -15,64 +15,43 @@ pub const WindowLayer = extern struct { layer: ?*anyopaque, pub fn deinit(self: *WindowLayer) void { - mln_test_destroy_window_metal_layer(self); + mln_zig_test_destroy_window_metal_layer(self); } -}; - -extern "c" fn mln_test_autorelease_pool_push() ?*anyopaque; -extern "c" fn mln_test_autorelease_pool_pop(pool: *anyopaque) void; -extern "c" fn mln_test_create_metal_layer() ?*anyopaque; -extern "c" fn mln_test_create_metal_texture(device: *anyopaque, width: u32, height: u32) ?*anyopaque; -extern "c" fn mln_test_metal_texture_clear_rgba8(texture: *anyopaque, r: u8, g: u8, b: u8, a: u8) bool; -extern "c" fn mln_test_metal_texture_read_pixel_rgba8(texture: *anyopaque, x: u32, y: u32, out_rgba: [*c]u8) bool; -extern "c" fn mln_test_release_metal_object(object: *anyopaque) void; -extern "c" fn mln_test_create_window_metal_layer(width: u32, height: u32, out_layer: *WindowLayer) bool; -extern "c" fn mln_test_create_counting_window_metal_layer(width: u32, height: u32, out_layer: *WindowLayer) bool; -extern "c" fn mln_test_metal_layer_next_drawable_count(layer: *anyopaque) u32; -extern "c" fn mln_test_destroy_window_metal_layer(window_layer: *WindowLayer) void; -pub fn createLayer() !*anyopaque { - return mln_test_create_metal_layer() orelse return error.MetalLayerUnavailable; -} - -pub fn createTexture(device: *anyopaque, width: u32, height: u32) !*anyopaque { - return mln_test_create_metal_texture(device, width, height) orelse return error.MetalTextureUnavailable; -} - -pub fn clearTextureRGBA8(texture: *anyopaque, rgba: [4]u8) !void { - if (!mln_test_metal_texture_clear_rgba8(texture, rgba[0], rgba[1], rgba[2], rgba[3])) { - return error.MetalTextureClearFailed; + pub fn layerPointer(self: WindowLayer) !*anyopaque { + return self.layer orelse error.MetalLayerUnavailable; } -} -pub fn readTexturePixelRGBA8(texture: *anyopaque, x: u32, y: u32) ![4]u8 { - var rgba: [4]u8 = undefined; - if (!mln_test_metal_texture_read_pixel_rgba8(texture, x, y, rgba[0..].ptr)) { - return error.MetalTextureReadFailed; + pub fn nextDrawableCount(self: WindowLayer) !u32 { + return mln_zig_test_metal_layer_next_drawable_count(try self.layerPointer()); } - return rgba; -} -pub fn releaseObject(object: *anyopaque) void { - mln_test_release_metal_object(object); -} + pub fn hasDevice(self: WindowLayer) !bool { + return mln_zig_test_metal_layer_has_device(try self.layerPointer()); + } -pub fn createWindowLayer(width: u32, height: u32) !WindowLayer { - var window_layer = WindowLayer{ .window = null, .layer = null }; - if (!mln_test_create_window_metal_layer(width, height, &window_layer) or window_layer.layer == null) { - return error.MetalWindowUnavailable; + pub fn drawableSize(self: WindowLayer) !struct { width: u32, height: u32 } { + var width: u32 = 0; + var height: u32 = 0; + if (!mln_zig_test_metal_layer_drawable_size(try self.layerPointer(), &width, &height)) { + return error.MetalLayerUnavailable; + } + return .{ .width = width, .height = height }; } - return window_layer; -} +}; + +extern "c" fn mln_zig_test_autorelease_pool_push() ?*anyopaque; +extern "c" fn mln_zig_test_autorelease_pool_pop(pool: *anyopaque) void; +extern "c" fn mln_zig_test_create_counting_window_metal_layer(width: u32, height: u32, out_layer: *WindowLayer) bool; +extern "c" fn mln_zig_test_metal_layer_next_drawable_count(layer: *anyopaque) u32; +extern "c" fn mln_zig_test_metal_layer_has_device(layer: *anyopaque) bool; +extern "c" fn mln_zig_test_metal_layer_drawable_size(layer: *anyopaque, out_width: *u32, out_height: *u32) bool; +extern "c" fn mln_zig_test_destroy_window_metal_layer(window_layer: *WindowLayer) void; pub fn createCountingWindowLayer(width: u32, height: u32) !WindowLayer { var window_layer = WindowLayer{ .window = null, .layer = null }; - if (!mln_test_create_counting_window_metal_layer(width, height, &window_layer) or window_layer.layer == null) { - return error.MetalWindowUnavailable; + if (!mln_zig_test_create_counting_window_metal_layer(width, height, &window_layer) or window_layer.layer == null) { + return error.MetalLayerUnavailable; } return window_layer; } - -pub fn nextDrawableCount(layer: *anyopaque) u32 { - return mln_test_metal_layer_next_drawable_count(layer); -} diff --git a/bindings/zig/tests/metal_support_apple.m b/bindings/zig/tests/metal_support_apple.m new file mode 100644 index 000000000..4111ec3f2 --- /dev/null +++ b/bindings/zig/tests/metal_support_apple.m @@ -0,0 +1,130 @@ +#import +#import +#import + +#if TARGET_OS_OSX +#import +#endif + +#include +#include + +typedef struct mln_zig_test_window_metal_layer { + void* window; + void* layer; +} mln_zig_test_window_metal_layer; + +@interface MLNZigTestCountingMetalLayer : CAMetalLayer +@property(nonatomic) uint32_t nextDrawableCount; +@end + +@implementation MLNZigTestCountingMetalLayer +- (id)nextDrawable { + _nextDrawableCount += 1; + return [super nextDrawable]; +} +@end + +extern void* objc_autoreleasePoolPush(void); +extern void objc_autoreleasePoolPop(void* pool); + +void* mln_zig_test_autorelease_pool_push(void) { + return objc_autoreleasePoolPush(); +} + +void mln_zig_test_autorelease_pool_pop(void* pool) { + objc_autoreleasePoolPop(pool); +} + +bool mln_zig_test_create_counting_window_metal_layer( + uint32_t width, uint32_t height, mln_zig_test_window_metal_layer* out_layer +) { + if (out_layer == NULL || width == 0 || height == 0) { + return false; + } + +#if TARGET_OS_OSX + NSWindow* window = nil; + @try { + [NSApplication sharedApplication]; + const NSRect frame = NSMakeRect(0.0, 0.0, (CGFloat)width, (CGFloat)height); + window = [[NSWindow alloc] initWithContentRect:frame + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]; + NSView* content_view = [window contentView]; + MLNZigTestCountingMetalLayer* layer = [MLNZigTestCountingMetalLayer layer]; + if (window == nil || content_view == nil || layer == nil) { + [window release]; + return false; + } + [window setReleasedWhenClosed:NO]; + [content_view setWantsLayer:YES]; + [content_view setLayer:layer]; + layer.drawableSize = CGSizeMake(1.0, 1.0); + out_layer->window = window; + out_layer->layer = layer; + return true; + } @catch (NSException* exception) { + (void)exception; + [window release]; + return false; + } +#else + MLNZigTestCountingMetalLayer* layer = + [[MLNZigTestCountingMetalLayer alloc] init]; + if (layer == nil) { + return false; + } + layer.frame = CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height); + layer.drawableSize = CGSizeMake(1.0, 1.0); + out_layer->window = layer; + out_layer->layer = layer; + return true; +#endif +} + +uint32_t mln_zig_test_metal_layer_next_drawable_count(void* layer) { + id object = (id)layer; + if (![object isKindOfClass:[MLNZigTestCountingMetalLayer class]]) { + return 0; + } + return [(MLNZigTestCountingMetalLayer*)object nextDrawableCount]; +} + +bool mln_zig_test_metal_layer_has_device(void* layer) { + id object = (id)layer; + return [object isKindOfClass:[CAMetalLayer class]] && + [(CAMetalLayer*)object device] != nil; +} + +bool mln_zig_test_metal_layer_drawable_size( + void* layer, uint32_t* out_width, uint32_t* out_height +) { + id object = (id)layer; + if ( + ![object isKindOfClass:[CAMetalLayer class]] || out_width == NULL || + out_height == NULL + ) { + return false; + } + const CGSize size = [(CAMetalLayer*)object drawableSize]; + *out_width = (uint32_t)size.width; + *out_height = (uint32_t)size.height; + return true; +} + +void mln_zig_test_destroy_window_metal_layer( + mln_zig_test_window_metal_layer* window_layer +) { + if (window_layer == NULL) { + return; + } +#if TARGET_OS_OSX + [(NSWindow*)window_layer->window release]; +#else + [(id)window_layer->window release]; +#endif + window_layer->window = NULL; + window_layer->layer = NULL; +} diff --git a/bindings/zig/tests/metal_support_ios.m b/bindings/zig/tests/metal_support_ios.m deleted file mode 100644 index 67650d015..000000000 --- a/bindings/zig/tests/metal_support_ios.m +++ /dev/null @@ -1,201 +0,0 @@ -#import -#import -#include -#include -#include - -typedef struct mln_test_window_metal_layer { - void* window; - void* layer; -} mln_test_window_metal_layer; - -@interface MLNTestCountingMetalLayer : CAMetalLayer -@property(nonatomic) uint32_t nextDrawableCount; -@end - -@implementation MLNTestCountingMetalLayer -- (id)nextDrawable { - _nextDrawableCount += 1; - return [super nextDrawable]; -} -@end - -extern void* objc_autoreleasePoolPush(void); -extern void objc_autoreleasePoolPop(void* pool); - -void* mln_test_autorelease_pool_push(void) { - return objc_autoreleasePoolPush(); -} - -void mln_test_autorelease_pool_pop(void* pool) { - objc_autoreleasePoolPop(pool); -} - -void* mln_test_create_metal_layer(void) { - return [[CAMetalLayer layer] retain]; -} - -void* mln_test_create_metal_texture( - void* device, uint32_t width, uint32_t height -) { - if (device == NULL || width == 0 || height == 0) { - return NULL; - } - MTLTextureDescriptor* descriptor = [MTLTextureDescriptor - texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm - width:(NSUInteger)width - height:(NSUInteger)height - mipmapped:NO]; - descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; - return [(id)device newTextureWithDescriptor:descriptor]; -} - -bool mln_test_metal_texture_clear_rgba8( - void* texture, uint8_t r, uint8_t g, uint8_t b, uint8_t a -) { - id metal_texture = (id)texture; - if (metal_texture == nil) { - return false; - } - - id queue = [[metal_texture device] newCommandQueue]; - if (queue == nil) { - return false; - } - - MTLRenderPassDescriptor* descriptor = - [MTLRenderPassDescriptor renderPassDescriptor]; - MTLRenderPassColorAttachmentDescriptor* color_attachment = - [[descriptor colorAttachments] objectAtIndexedSubscript:0]; - [color_attachment setTexture:metal_texture]; - [color_attachment setLoadAction:MTLLoadActionClear]; - [color_attachment setStoreAction:MTLStoreActionStore]; - [color_attachment setClearColor:MTLClearColorMake( - (double)r / 255.0, (double)g / 255.0, - (double)b / 255.0, (double)a / 255.0 - )]; - - id command_buffer = [queue commandBuffer]; - id encoder = - [command_buffer renderCommandEncoderWithDescriptor:descriptor]; - if (encoder == nil) { - [queue release]; - return false; - } - [encoder endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - const bool success = - [command_buffer status] == MTLCommandBufferStatusCompleted; - [queue release]; - return success; -} - -bool mln_test_metal_texture_read_pixel_rgba8( - void* texture, uint32_t x, uint32_t y, uint8_t* out_rgba -) { - id metal_texture = (id)texture; - if ( - metal_texture == nil || out_rgba == NULL || x >= [metal_texture width] || - y >= [metal_texture height] - ) { - return false; - } - - id queue = [[metal_texture device] newCommandQueue]; - if (queue == nil) { - return false; - } - const NSUInteger bytes_per_row = 256; - id buffer = - [[metal_texture device] newBufferWithLength:bytes_per_row - options:MTLResourceStorageModeShared]; - if (buffer == nil) { - [queue release]; - return false; - } - - id command_buffer = [queue commandBuffer]; - id encoder = [command_buffer blitCommandEncoder]; - [encoder copyFromTexture:metal_texture - sourceSlice:0 - sourceLevel:0 - sourceOrigin:MTLOriginMake(x, y, 0) - sourceSize:MTLSizeMake(1, 1, 1) - toBuffer:buffer - destinationOffset:0 - destinationBytesPerRow:bytes_per_row - destinationBytesPerImage:bytes_per_row]; - [encoder endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - - const bool success = - [command_buffer status] == MTLCommandBufferStatusCompleted; - if (success) { - memcpy(out_rgba, [buffer contents], 4); - } - [buffer release]; - [queue release]; - return success; -} - -void mln_test_release_metal_object(void* object) { [(id)object release]; } - -static bool create_window_metal_layer( - uint32_t width, uint32_t height, bool counted, - mln_test_window_metal_layer* out_layer -) { - if (out_layer == NULL || width == 0 || height == 0) { - return false; - } - - CAMetalLayer* layer = - counted ? [MLNTestCountingMetalLayer layer] : [CAMetalLayer layer]; - if (layer == nil) { - return false; - } - - layer.frame = CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height); - layer.drawableSize = CGSizeMake((CGFloat)width, (CGFloat)height); - id device = MTLCreateSystemDefaultDevice(); - if (device == nil) { - return false; - } - layer.device = device; - [layer retain]; - out_layer->window = layer; - out_layer->layer = layer; - return true; -} - -bool mln_test_create_window_metal_layer( - uint32_t width, uint32_t height, mln_test_window_metal_layer* out_layer -) { - return create_window_metal_layer(width, height, false, out_layer); -} - -bool mln_test_create_counting_window_metal_layer( - uint32_t width, uint32_t height, mln_test_window_metal_layer* out_layer -) { - return create_window_metal_layer(width, height, true, out_layer); -} - -uint32_t mln_test_metal_layer_next_drawable_count(void* layer) { - id object = (id)layer; - if (![object isKindOfClass:[MLNTestCountingMetalLayer class]]) { - return 0; - } - return [(MLNTestCountingMetalLayer*)object nextDrawableCount]; -} - -void mln_test_destroy_window_metal_layer( - mln_test_window_metal_layer* window_layer -) { - if (window_layer == NULL) { - return; - } - [(id)window_layer->window release]; - window_layer->window = NULL; - window_layer->layer = NULL; -} diff --git a/bindings/zig/tests/metal_support_macos.m b/bindings/zig/tests/metal_support_macos.m deleted file mode 100644 index 0498b1919..000000000 --- a/bindings/zig/tests/metal_support_macos.m +++ /dev/null @@ -1,215 +0,0 @@ -#import -#import -#import -#include -#include -#include - -typedef struct mln_test_window_metal_layer { - void* window; - void* layer; -} mln_test_window_metal_layer; - -@interface MLNTestCountingMetalLayer : CAMetalLayer -@property(nonatomic) uint32_t nextDrawableCount; -@end - -@implementation MLNTestCountingMetalLayer -- (id)nextDrawable { - _nextDrawableCount += 1; - return [super nextDrawable]; -} -@end - -extern void* objc_autoreleasePoolPush(void); -extern void objc_autoreleasePoolPop(void* pool); - -void* mln_test_autorelease_pool_push(void) { - return objc_autoreleasePoolPush(); -} - -void mln_test_autorelease_pool_pop(void* pool) { - objc_autoreleasePoolPop(pool); -} - -void* mln_test_create_metal_layer(void) { return [CAMetalLayer layer]; } - -void* mln_test_create_metal_texture( - void* device, uint32_t width, uint32_t height -) { - if (device == NULL || width == 0 || height == 0) { - return NULL; - } - MTLTextureDescriptor* descriptor = [MTLTextureDescriptor - texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm - width:(NSUInteger)width - height:(NSUInteger)height - mipmapped:NO]; - descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; - return [(id)device newTextureWithDescriptor:descriptor]; -} - -bool mln_test_metal_texture_clear_rgba8( - void* texture, uint8_t r, uint8_t g, uint8_t b, uint8_t a -) { - id metal_texture = (id)texture; - if (metal_texture == nil) { - return false; - } - - id queue = [[metal_texture device] newCommandQueue]; - if (queue == nil) { - return false; - } - - MTLRenderPassDescriptor* descriptor = - [MTLRenderPassDescriptor renderPassDescriptor]; - MTLRenderPassColorAttachmentDescriptor* color_attachment = - [[descriptor colorAttachments] objectAtIndexedSubscript:0]; - [color_attachment setTexture:metal_texture]; - [color_attachment setLoadAction:MTLLoadActionClear]; - [color_attachment setStoreAction:MTLStoreActionStore]; - [color_attachment setClearColor:MTLClearColorMake( - (double)r / 255.0, (double)g / 255.0, - (double)b / 255.0, (double)a / 255.0 - )]; - - id command_buffer = [queue commandBuffer]; - id encoder = - [command_buffer renderCommandEncoderWithDescriptor:descriptor]; - if (encoder == nil) { - [queue release]; - return false; - } - [encoder endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - const bool success = - [command_buffer status] == MTLCommandBufferStatusCompleted; - [queue release]; - return success; -} - -bool mln_test_metal_texture_read_pixel_rgba8( - void* texture, uint32_t x, uint32_t y, uint8_t* out_rgba -) { - id metal_texture = (id)texture; - if ( - metal_texture == nil || out_rgba == NULL || x >= [metal_texture width] || - y >= [metal_texture height] - ) { - return false; - } - - id queue = [[metal_texture device] newCommandQueue]; - if (queue == nil) { - return false; - } - const NSUInteger bytes_per_row = 256; - id buffer = - [[metal_texture device] newBufferWithLength:bytes_per_row - options:MTLResourceStorageModeShared]; - if (buffer == nil) { - [queue release]; - return false; - } - - id command_buffer = [queue commandBuffer]; - id encoder = [command_buffer blitCommandEncoder]; - [encoder copyFromTexture:metal_texture - sourceSlice:0 - sourceLevel:0 - sourceOrigin:MTLOriginMake(x, y, 0) - sourceSize:MTLSizeMake(1, 1, 1) - toBuffer:buffer - destinationOffset:0 - destinationBytesPerRow:bytes_per_row - destinationBytesPerImage:bytes_per_row]; - [encoder endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - - const bool success = - [command_buffer status] == MTLCommandBufferStatusCompleted; - if (success) { - memcpy(out_rgba, [buffer contents], 4); - } - [buffer release]; - [queue release]; - return success; -} - -void mln_test_release_metal_object(void* object) { [(id)object release]; } - -static bool create_window_metal_layer( - uint32_t width, uint32_t height, bool counted, - mln_test_window_metal_layer* out_layer -) { - if (out_layer == NULL || width == 0 || height == 0) { - return false; - } - - NSWindow* window = nil; - @try { - [NSApplication sharedApplication]; - - const NSRect frame = NSMakeRect(0.0, 0.0, (CGFloat)width, (CGFloat)height); - window = [[NSWindow alloc] initWithContentRect:frame - styleMask:NSWindowStyleMaskBorderless - backing:NSBackingStoreBuffered - defer:YES]; - if (window == nil) { - return false; - } - - [window setReleasedWhenClosed:NO]; - NSView* content_view = [window contentView]; - CAMetalLayer* layer = - counted ? [MLNTestCountingMetalLayer layer] : [CAMetalLayer layer]; - if (content_view == nil || layer == nil) { - [window release]; - return false; - } - - [content_view setWantsLayer:YES]; - [content_view setLayer:layer]; - out_layer->window = window; - out_layer->layer = layer; - return true; - } @catch (NSException* exception) { - (void)exception; - [window release]; - return false; - } -} - -bool mln_test_create_window_metal_layer( - uint32_t width, uint32_t height, mln_test_window_metal_layer* out_layer -) { - return create_window_metal_layer(width, height, false, out_layer); -} - -bool mln_test_create_counting_window_metal_layer( - uint32_t width, uint32_t height, mln_test_window_metal_layer* out_layer -) { - return create_window_metal_layer(width, height, true, out_layer); -} - -uint32_t mln_test_metal_layer_next_drawable_count(void* layer) { - id object = (id)layer; - if (![object isKindOfClass:[MLNTestCountingMetalLayer class]]) { - return 0; - } - return [(MLNTestCountingMetalLayer*)object nextDrawableCount]; -} - -void mln_test_destroy_window_metal_layer( - mln_test_window_metal_layer* window_layer -) { - if (window_layer == NULL) { - return; - } - [(NSWindow*)window_layer->window release]; - window_layer->window = NULL; - window_layer->layer = NULL; -} diff --git a/bindings/zig/tests/projection.zig b/bindings/zig/tests/projection.zig index 2d233791d..ede965fe7 100644 --- a/bindings/zig/tests/projection.zig +++ b/bindings/zig/tests/projection.zig @@ -2,6 +2,7 @@ const std = @import("std"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); +const support = @import("support.zig"); const center = maplibre.LatLng{ .latitude = 37.7749, .longitude = -122.4194 }; @@ -20,6 +21,12 @@ fn expectLatLngApprox(expected: maplibre.LatLng, actual: maplibre.LatLng) !void try testing.expectApproxEqAbs(expected.longitude, actual.longitude, 0.000001); } +fn waitForCameraCommands(map: *maplibre.MapHandle) !void { + var future = try map.cameraQuery(); + defer future.deinit(); + _ = try future.wait(null); +} + fn useAndCloseProjectionOnThread( projection: *maplibre.MapProjectionHandle, out_error: *?anyerror, @@ -37,11 +44,14 @@ fn useAndCloseProjectionOnThread( test "map projection mode updates snapshot fields through public binding" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setProjectionMode(.{ .axonometric = true, .x_skew = 0.25, .y_skew = -0.125 }); + _ = try map.setProjectionMode(.{ .axonometric = true, .x_skew = 0.25, .y_skew = -0.125 }); + try support.waitForBarrier(&runtime); const snapshot = try map.getProjectionMode(); try testing.expectEqual(true, snapshot.axonometric.?); @@ -51,16 +61,23 @@ test "map projection mode updates snapshot fields through public binding" { test "map converts between lat lngs and screen points" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{ .width = viewport_extent, .height = viewport_extent }); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = viewport_extent, .height = viewport_extent }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.jumpTo(.{ .center = center, .zoom = 10.0 }); + _ = try map.updateCamera(.{ .camera = .{ .center = center, .zoom = 10.0 } }); + try waitForCameraCommands(&map); - const point = try map.pixelForLatLng(center); + var point_future = try map.pixelForLatLng(center); + defer point_future.deinit(); + const point = try point_future.wait(null); try expectCenterPoint(point); - const coordinate = try map.latLngForPixel(point); + var coordinate_future = try map.latLngForPixel(point); + defer coordinate_future.deinit(); + const coordinate = try coordinate_future.wait(null); try expectLatLngApprox(center, coordinate); const coordinates = [_]maplibre.LatLng{ @@ -68,26 +85,49 @@ test "map converts between lat lngs and screen points" { .{ .latitude = 0.0, .longitude = 0.0 }, }; var points: [coordinates.len]maplibre.ScreenPoint = undefined; - try map.pixelsForLatLngs(testing.allocator, coordinates[0..], points[0..]); + var points_future = try map.pixelsForLatLngs(testing.allocator, coordinates[0..]); + defer points_future.deinit(); + var owned_points = try points_future.wait(null); + defer owned_points.deinit(); + @memcpy(points[0..], owned_points.items); try expectCenterPoint(points[0]); var roundtrip: [points.len]maplibre.LatLng = undefined; - try map.latLngsForPixels(testing.allocator, points[0..], roundtrip[0..]); + var coordinates_future = try map.latLngsForPixels(testing.allocator, points[0..]); + defer coordinates_future.deinit(); + var owned_coordinates = try coordinates_future.wait(null); + defer owned_coordinates.deinit(); + @memcpy(roundtrip[0..], owned_coordinates.items); try expectLatLngApprox(coordinates[0], roundtrip[0]); try expectLatLngApprox(coordinates[1], roundtrip[1]); - try map.pixelsForLatLngs(testing.allocator, &.{}, &.{}); - try map.latLngsForPixels(testing.allocator, &.{}, &.{}); + var empty_points_future = try map.pixelsForLatLngs(testing.allocator, &.{}); + defer empty_points_future.deinit(); + var empty_points = try empty_points_future.wait(null); + defer empty_points.deinit(); + try testing.expectEqual(@as(usize, 0), empty_points.items.len); + + var empty_coordinates_future = try map.latLngsForPixels(testing.allocator, &.{}); + defer empty_coordinates_future.deinit(); + var empty_coordinates = try empty_coordinates_future.wait(null); + defer empty_coordinates.deinit(); + try testing.expectEqual(@as(usize, 0), empty_coordinates.items.len); } +// Creation is ordered after every earlier map command, so a projection created +// right after a camera command observes that command without an explicit wait. test "standalone projection converts and updates camera" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{ .width = viewport_extent, .height = viewport_extent }); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = viewport_extent, .height = viewport_extent }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.jumpTo(.{ .center = center, .zoom = 10.0 }); - var projection = try maplibre.MapProjectionHandle.create(&map); + _ = try map.updateCamera(.{ .camera = .{ .center = center, .zoom = 10.0 } }); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); defer projection.close() catch @panic("projection close failed"); const point = try projection.pixelForLatLng(center); @@ -96,11 +136,14 @@ test "standalone projection converts and updates camera" { const coordinate = try projection.latLngForPixel(point); try expectLatLngApprox(center, coordinate); + // A setter is applied before it returns, so later conversions observe it. const helper_camera = maplibre.CameraOptions{ .center = .{ .latitude = 0.0, .longitude = 0.0 }, .zoom = 3.0 }; try projection.setCamera(helper_camera); const snapshot = try projection.getCamera(); try expectLatLngApprox(helper_camera.center.?, snapshot.center.?); try testing.expectApproxEqAbs(helper_camera.zoom.?, snapshot.zoom.?, 0.000001); + const recentered = try projection.pixelForLatLng(helper_camera.center.?); + try expectCenterPoint(recentered); var visible = [_]maplibre.LatLng{ .{ .latitude = -10.0, .longitude = -10.0 }, @@ -115,14 +158,54 @@ test "standalone projection converts and updates camera" { const geometry_fitted = try projection.getCamera(); try testing.expect(geometry_fitted.center != null); try testing.expect(geometry_fitted.zoom != null); + + // A later map camera command never reaches the projection. + _ = try map.updateCamera(.{ .camera = .{ .center = center, .zoom = 1.0 } }); + try waitForCameraCommands(&map); + const frozen = try projection.getCamera(); + try testing.expectApproxEqAbs(geometry_fitted.zoom.?, frozen.zoom.?, 0.000001); +} + +fn convertOnThread(projection: *maplibre.MapProjectionHandle, out_point: *maplibre.ScreenPoint, out_error: *?anyerror) void { + out_point.* = projection.pixelForLatLng(center) catch |err| { + out_error.* = err; + return; + }; + out_error.* = null; +} + +test "standalone projection conversions run from another thread" { + var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = viewport_extent, .height = viewport_extent }); + defer map_future.deinit(); + var map = try map_future.wait(null); + defer map.close() catch @panic("map close failed"); + + _ = try map.updateCamera(.{ .camera = .{ .center = center, .zoom = 10.0 } }); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); + defer projection.close() catch @panic("projection close failed"); + + var point = maplibre.ScreenPoint{ .x = 0.0, .y = 0.0 }; + var thread_error: ?anyerror = error.Unexpected; + const thread = try std.Thread.spawn(.{}, convertOnThread, .{ &projection, &point, &thread_error }); + thread.join(); + try testing.expect(thread_error == null); + try expectCenterPoint(point); } test "standalone projection remains usable on another thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - var map = try maplibre.MapHandle.create(&runtime, .{}); - var projection = try maplibre.MapProjectionHandle.create(&map); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + defer projection_future.deinit(); + var projection = try projection_future.wait(null); try map.close(); - try runtime.close(); + try support.closeRuntime(&runtime); var thread_error: ?anyerror = null; const thread = try std.Thread.spawn( @@ -147,15 +230,21 @@ test "projected meters convert to and from lat lng" { test "projection public descriptors report invalid native arguments" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); try testing.expectError(error.InvalidArgument, map.setProjectionMode(.{ .x_skew = std.math.inf(f64) })); try testing.expectError(error.InvalidArgument, map.pixelForLatLng(.{ .latitude = 91.0, .longitude = 0.0 })); try testing.expectError(error.InvalidArgument, map.latLngForPixel(.{ .x = std.math.inf(f64), .y = 0.0 })); - var projection = try maplibre.MapProjectionHandle.create(&map); + var projection_future = try maplibre.MapProjectionHandle.create(&map); + + defer projection_future.deinit(); + + var projection = try projection_future.wait(null); defer projection.close() catch @panic("projection close failed"); try testing.expectError(error.InvalidArgument, projection.setCamera(.{ .center = .{ .latitude = std.math.inf(f64), .longitude = 0.0 } })); diff --git a/bindings/zig/tests/render.zig b/bindings/zig/tests/render.zig index 33b021976..a8ae263ee 100644 --- a/bindings/zig/tests/render.zig +++ b/bindings/zig/tests/render.zig @@ -4,9 +4,7 @@ const build_options = @import("build_options"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); -const metal_support = @import("metal_support.zig"); const support = @import("support.zig"); -const test_hooks = @import("test_hooks.zig"); extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; @@ -21,31 +19,6 @@ const egl = if (supports_egl) @import("egl") else struct {}; const gl = if (supports_wgl or supports_egl) @import("gl") else struct {}; const wgl_test = if (supports_wgl) @import("wgl_test_context") else struct {}; -const cluster_style_json = - \\{ - \\ "version": 8, - \\ "name": "zig-binding-cluster-query-test", - \\ "sources": { - \\ "cluster-source": { - \\ "type": "geojson", - \\ "cluster": true, - \\ "data": { - \\ "type": "FeatureCollection", - \\ "features": [ - \\ {"type":"Feature","geometry":{"type":"Point","coordinates":[0.0,0.0]},"properties":{"name":"one"}}, - \\ {"type":"Feature","geometry":{"type":"Point","coordinates":[0.001,0.001]},"properties":{"name":"two"}}, - \\ {"type":"Feature","geometry":{"type":"Point","coordinates":[0.002,0.002]},"properties":{"name":"three"}} - \\ ] - \\ } - \\ } - \\ }, - \\ "layers": [ - \\ {"id":"background","type":"background","paint":{"background-color":"#ffffff"}}, - \\ {"id":"cluster-circle","type":"circle","source":"cluster-source","filter":["has","point_count"],"paint":{"circle-color":"#2563eb","circle-radius":20}} - \\ ] - \\} -; - test "supported render backend is exposed semantically" { const support_mask = maplibre.supportedRenderBackends(); try testing.expect(support_mask.metal or support_mask.opengl or support_mask.vulkan or support_mask.webgpu); @@ -106,166 +79,6 @@ test "queried features compare copied buffers by content" { try testing.expect(!absent_source.eql(empty_source)); } -fn waitForRenderedFeatureQuery( - runtime: *maplibre.RuntimeHandle, - session: *maplibre.RenderSessionHandle, - geometry: maplibre.RenderedQueryGeometry, - options: maplibre.RenderedFeatureQueryOptions, -) !maplibre.QueriedFeatureList { - for (0..1000) |_| { - var result = try session.queryRenderedFeatures(testing.allocator, geometry, options); - if (result.items.len != 0) return result; - result.deinit(); - try runtime.pump(0, null); - _ = try session.renderUpdate(); - try std.Thread.yield(); - } - return error.RenderedFeatureNotQueryable; -} - -fn waitForSourceFeatureQuery( - runtime: *maplibre.RuntimeHandle, - session: *maplibre.RenderSessionHandle, -) !maplibre.QueriedFeatureList { - for (0..1000) |_| { - var result = try session.querySourceFeatures(testing.allocator, "point", .{ - .filter = "[\"==\",[\"get\",\"kind\"],\"capital\"]", - }); - if (result.items.len != 0) return result; - result.deinit(); - try runtime.pump(0, null); - _ = try session.renderUpdate(); - try std.Thread.yield(); - } - return error.SourceFeatureNotQueryable; -} - -fn skipWhitespace(json: []const u8, start: usize) usize { - var cursor = start; - while (cursor < json.len and std.ascii.isWhitespace(json[cursor])) cursor += 1; - return cursor; -} - -fn jsonStringEnd(json: []const u8, start: usize) ?usize { - var escaped = false; - var cursor = start + 1; - while (cursor < json.len) : (cursor += 1) { - if (escaped) escaped = false else if (json[cursor] == '\\') escaped = true else if (json[cursor] == '"') return cursor + 1; - } - return null; -} - -fn jsonValueEnd(json: []const u8, start: usize) ?usize { - if (start >= json.len) return null; - if (json[start] == '"') return jsonStringEnd(json, start); - if (json[start] != '{' and json[start] != '[') { - var cursor = start; - while (cursor < json.len and !std.ascii.isWhitespace(json[cursor]) and json[cursor] != ',' and json[cursor] != '}' and json[cursor] != ']') cursor += 1; - return cursor; - } - var depth: usize = 0; - var cursor = start; - while (cursor < json.len) { - if (json[cursor] == '"') { - cursor = jsonStringEnd(json, cursor) orelse return null; - continue; - } - if (json[cursor] == '{' or json[cursor] == '[') depth += 1; - if (json[cursor] == '}' or json[cursor] == ']') { - depth -= 1; - if (depth == 0) return cursor + 1; - } - cursor += 1; - } - return null; -} - -fn rawMember(json: []const u8, key: []const u8) ?[]const u8 { - var cursor = skipWhitespace(json, 0); - if (cursor >= json.len or json[cursor] != '{') return null; - cursor += 1; - while (true) { - cursor = skipWhitespace(json, cursor); - if (cursor >= json.len or json[cursor] == '}') return null; - const key_end = jsonStringEnd(json, cursor) orelse return null; - const member_name = json[cursor + 1 .. key_end - 1]; - cursor = skipWhitespace(json, key_end); - if (cursor >= json.len or json[cursor] != ':') return null; - const value_start = skipWhitespace(json, cursor + 1); - const value_end = jsonValueEnd(json, value_start) orelse return null; - if (std.mem.eql(u8, member_name, key)) return json[value_start..value_end]; - cursor = skipWhitespace(json, value_end); - if (cursor >= json.len or json[cursor] != ',') return null; - cursor += 1; - } -} - -fn firstArrayElement(json: []const u8) ?[]const u8 { - var cursor = skipWhitespace(json, 0); - if (cursor >= json.len or json[cursor] != '[') return null; - cursor = skipWhitespace(json, cursor + 1); - if (cursor >= json.len or json[cursor] == ']') return null; - return json[cursor .. jsonValueEnd(json, cursor) orelse return null]; -} - -fn queryFeatureProperty(feature: []const u8, key: []const u8) ?[]const u8 { - const properties = rawMember(feature, "properties") orelse return null; - return rawMember(properties, key); -} - -const TransitionFramePump = struct { - finished_count: usize = 0, - last_transition_id: ?u64 = null, -}; - -fn pumpTransitionFrame( - runtime: *maplibre.RuntimeHandle, - map: *maplibre.MapHandle, - session: *maplibre.RenderSessionHandle, -) !TransitionFramePump { - // Metal hosts drain autoreleased backend objects once per frame-loop - // iteration. - const pool = if (build_options.supports_metal) try metal_support.AutoreleasePool.init() else {}; - defer if (build_options.supports_metal) pool.deinit(); - - try map.requestRepaint(); - try runtime.pump(0, null); - - var result = TransitionFramePump{}; - var render_update_available = false; - { - var batch = try runtime.drainEvents(testing.allocator, 0); - defer batch.deinit(); - for (0..batch.len()) |index| { - const event = try batch.at(index); - if (std.meta.eql(event.event_type, maplibre.RuntimeEventType.map_render_update_available)) { - render_update_available = true; - continue; - } - switch (event.payload) { - .camera_transition_finished => |payload| { - try testing.expect(std.meta.eql(event.event_type, maplibre.RuntimeEventType.map_camera_transition_finished)); - result.finished_count += 1; - result.last_transition_id = payload.transition_id; - }, - else => {}, - } - } - } - if (render_update_available) try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - return result; -} - -fn expectPixelApprox(actual: [4]u8, expected: [4]u8, tolerance: u8) !void { - for (actual, expected) |actual_channel, expected_channel| { - const delta = if (actual_channel > expected_channel) - actual_channel - expected_channel - else - expected_channel - actual_channel; - try testing.expect(delta <= tolerance); - } -} - fn hasNonZeroByte(bytes: []const u8) bool { for (bytes) |byte| { if (byte != 0) return true; @@ -273,132 +86,6 @@ fn hasNonZeroByte(bytes: []const u8) bool { return false; } -const TestImage = struct { - allocator: std.mem.Allocator, - info: maplibre.TextureImageInfo, - data: []u8, - - fn deinit(self: *TestImage) void { - self.allocator.free(self.data); - self.data = &.{}; - self.info = .{ .width = 0, .height = 0, .stride = 0, .byte_length = 0 }; - } -}; - -fn readTestImage(session: *maplibre.RenderSessionHandle, allocator: std.mem.Allocator, byte_length: usize) !TestImage { - const data = try allocator.alloc(u8, byte_length); - errdefer allocator.free(data); - @memset(data, 0); - const info = try session.readPremultipliedRgba8Into(data); - return .{ .allocator = allocator, .info = info, .data = data }; -} - -const RenderSessionThreadCall = enum { - render_update, - resize, - set_target, - detach, - reduce_memory_use, - clear_data, - dump_debug_logs, - close, - acquire_metal_frame, - acquire_opengl_frame, - acquire_vulkan_frame, -}; - -fn callRenderSessionOnThread(session: *maplibre.RenderSessionHandle, call: RenderSessionThreadCall, out_error: *?anyerror) void { - const result = switch (call) { - .render_update => blk: { - _ = session.renderUpdate() catch |err| break :blk err; - break :blk {}; - }, - .resize => session.resize(.{ .width = 16, .height = 16, .scale_factor = 1.0 }), - .set_target => setPlaceholderBorrowedTextureTarget(session), - .detach => session.detach(), - .reduce_memory_use => session.reduceMemoryUse(), - .clear_data => session.clearData(), - .dump_debug_logs => session.dumpDebugLogs(), - .close => session.close(), - .acquire_metal_frame => blk: { - var frame = session.acquireMetalOwnedTextureFrame() catch |err| break :blk err; - frame.release() catch {}; - break :blk {}; - }, - .acquire_opengl_frame => blk: { - var frame = session.acquireOpenGLOwnedTextureFrame() catch |err| break :blk err; - frame.release() catch {}; - break :blk {}; - }, - .acquire_vulkan_frame => blk: { - var frame = session.acquireVulkanOwnedTextureFrame() catch |err| break :blk err; - frame.release() catch {}; - break :blk {}; - }, - }; - if (result) |_| { - out_error.* = null; - } else |err| { - out_error.* = err; - } -} - -fn expectRenderSessionCallWrongThread(session: *maplibre.RenderSessionHandle, call: RenderSessionThreadCall) !void { - var observed: ?anyerror = null; - const thread = try std.Thread.spawn(.{}, callRenderSessionOnThread, .{ session, call, &observed }); - thread.join(); - try testing.expect(observed != null); - try testing.expect(observed.? == error.WrongThread); -} - -fn releaseMetalFrameOnThread(frame: *maplibre.MetalOwnedTextureFrameHandle, out_error: *?anyerror) void { - frame.release() catch |err| { - out_error.* = err; - return; - }; - out_error.* = null; -} - -fn releaseOpenGLFrameOnThread(frame: *maplibre.OpenGLOwnedTextureFrameHandle, out_error: *?anyerror) void { - frame.release() catch |err| { - out_error.* = err; - return; - }; - out_error.* = null; -} - -fn releaseVulkanFrameOnThread(frame: *maplibre.VulkanOwnedTextureFrameHandle, out_error: *?anyerror) void { - frame.release() catch |err| { - out_error.* = err; - return; - }; - out_error.* = null; -} - -fn expectMetalFrameReleaseWrongThread(frame: *maplibre.MetalOwnedTextureFrameHandle) !void { - var observed: ?anyerror = null; - const thread = try std.Thread.spawn(.{}, releaseMetalFrameOnThread, .{ frame, &observed }); - thread.join(); - try testing.expectEqual(error.WrongThread, observed.?); - _ = try frame.info(); -} - -fn expectOpenGLFrameReleaseWrongThread(frame: *maplibre.OpenGLOwnedTextureFrameHandle) !void { - var observed: ?anyerror = null; - const thread = try std.Thread.spawn(.{}, releaseOpenGLFrameOnThread, .{ frame, &observed }); - thread.join(); - try testing.expectEqual(error.WrongThread, observed.?); - _ = try frame.info(); -} - -fn expectVulkanFrameReleaseWrongThread(frame: *maplibre.VulkanOwnedTextureFrameHandle) !void { - var observed: ?anyerror = null; - const thread = try std.Thread.spawn(.{}, releaseVulkanFrameOnThread, .{ frame, &observed }); - thread.join(); - try testing.expectEqual(error.WrongThread, observed.?); - _ = try frame.info(); -} - const TestOwnedTextureDescriptor = struct { extent: maplibre.RenderTargetExtent = .{}, }; @@ -898,121 +585,73 @@ const TestOwnedTextureSession = struct { self.context.deinit(); self.context_active = false; } - try self.session.close(); + try finishOperation(self.session, try self.session.detach()); + try self.session.destroy(); } }; +fn resolveFuture(comptime T: type, session: maplibre.RenderSessionHandle, future_value: maplibre.Future(T)) !T { + var future = future_value; + defer future.deinit(); + for (0..wait_turns) |turn| { + if (try future.poll()) return future.wait(null); + _ = session.serviceDriverWork(64) catch 0; + try waitOneTurn(turn); + } + return error.OperationTimedOut; +} + +fn finishOperation(session: maplibre.RenderSessionHandle, future: maplibre.Future(void)) !void { + _ = try resolveFuture(void, session, future); +} + +fn takeQueryResult(session: maplibre.RenderSessionHandle, future: maplibre.Future(maplibre.OwnedString)) !maplibre.OwnedString { + return resolveFuture(maplibre.OwnedString, session, future); +} + +fn takeQueryFeaturesResult(session: maplibre.RenderSessionHandle, future: maplibre.Future(maplibre.QueriedFeatureList)) !maplibre.QueriedFeatureList { + return resolveFuture(maplibre.QueriedFeatureList, session, future); +} + +fn finishAttachment(attachment: maplibre.RenderSessionAttachment) !maplibre.RenderSessionHandle { + errdefer { + var session = attachment.session; + session.destroy() catch {}; + } + try finishOperation(attachment.session, attachment.completion); + return attachment.session; +} + fn attachTestOwnedTexture(map: *maplibre.MapHandle, descriptor: TestOwnedTextureDescriptor) !TestOwnedTextureSession { if (!supports_test_owned_texture) return error.SkipZigTest; var context = try TestOwnedTextureContext.init(); errdefer context.deinit(); var session = if (build_options.supports_vulkan) - try maplibre.attachVulkanOwnedTexture(map, .{ + try finishAttachment(try maplibre.attachVulkanOwnedTexture(map, .{ .extent = descriptor.extent, .context = context.descriptor(), - }) + }, .{ .driver = .core_worker, .requested_texture_ring_depth = 2 })) else if (build_options.supports_opengl) - try maplibre.attachOpenGLOwnedTexture(map, .{ + try finishAttachment(try maplibre.attachOpenGLOwnedTexture(map, .{ .extent = descriptor.extent, .context = context.descriptor(), - }) + }, .{ .driver = .caller_graphics_thread, .requested_texture_ring_depth = 2 })) else if (build_options.supports_metal) - try maplibre.attachMetalOwnedTexture(map, .{ + try finishAttachment(try maplibre.attachMetalOwnedTexture(map, .{ .extent = descriptor.extent, .context = context.descriptor(), - }) + }, .{ .driver = .core_worker, .requested_texture_ring_depth = 2 })) else unreachable; - errdefer session.close() catch {}; - - return .{ .context = context, .session = session }; -} - -fn expectInvalidOwnedTextureExtent(map: *maplibre.MapHandle, extent: maplibre.RenderTargetExtent) !void { - if (!supports_test_owned_texture) return error.SkipZigTest; - if (build_options.supports_vulkan) { - try testing.expectError(error.InvalidArgument, maplibre.attachVulkanOwnedTexture(map, .{ - .extent = extent, - .context = fakeVulkanContext(), - })); - } else if (build_options.supports_opengl) { - try testing.expectError(error.InvalidArgument, maplibre.attachOpenGLOwnedTexture(map, .{ - .extent = extent, - .context = fakeOpenGLContext(), - })); - } else if (build_options.supports_metal) { - try testing.expectError(error.InvalidArgument, maplibre.attachMetalOwnedTexture(map, .{ - .extent = extent, - .context = .{ .device = fakeNativePointer() }, - })); - } else { - unreachable; - } -} - -/// Hands a session a caller-owned texture target carrying placeholder backend -/// handles, which call sites expect to be rejected before the descriptor is -/// read. -fn setPlaceholderBorrowedTextureTarget(session: *maplibre.RenderSessionHandle) maplibre.Error!void { - const extent = maplibre.RenderTargetExtent{ .width = 16, .height = 16, .scale_factor = 1.0 }; - if (build_options.supports_vulkan) { - return session.setVulkanBorrowedTextureTarget(.{ - .extent = extent, - .physical_width = extent.width, - .physical_height = extent.height, - .context = fakeVulkanContext(), - .image = fakeNativePointer(), - .image_view = fakeNativePointer(), - .format = @as(u32, vk.VK_FORMAT_R8G8B8A8_UNORM), - .initial_layout = @as(u32, vk.VK_IMAGE_LAYOUT_UNDEFINED), - .final_layout = @as(u32, vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), - }); - } else if (build_options.supports_opengl) { - return session.setOpenGLBorrowedTextureTarget(.{ - .extent = extent, - .physical_width = extent.width, - .physical_height = extent.height, - .context = fakeOpenGLContext(), - .texture = 1, - .target = gl_texture_2d, - }); - } else if (build_options.supports_metal) { - return session.setMetalBorrowedTextureTarget(.{ - .extent = extent, - .physical_width = extent.width, - .physical_height = extent.height, - .texture = fakeNativePointer(), - }); - } else { - unreachable; + errdefer { + if (session.detach()) |operation| { + finishOperation(session, operation) catch {}; + } else |_| {} + session.destroy() catch {}; } -} - -fn createMovedMetalSessionWithFrame(device: *anyopaque) !struct { - runtime: maplibre.RuntimeHandle, - map: maplibre.MapHandle, - session: maplibre.RenderSessionHandle, - frame: maplibre.MetalOwnedTextureFrameHandle, -} { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - errdefer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - errdefer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachMetalOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - .context = .{ .device = maplibre.NativePointer.fromPtr(device) }, - }); - errdefer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - const frame = try session.acquireMetalOwnedTextureFrame(); - return .{ .runtime = runtime, .map = map, .session = session, .frame = frame }; + return .{ .context = context, .session = session }; } const VulkanAttachContext = if (build_options.supports_vulkan) struct { @@ -1361,194 +1000,171 @@ fn findVulkanMemoryType(dispatch: *const VulkanDispatch, physical_device: if (bu return error.NoSuitableVulkanMemoryType; } -test "render update without pending update reports no update and keeps session live" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); +var next_frame_token: u64 = 1; - var map = try maplibre.MapHandle.create(&runtime, .{ .width = 64, .height = 64, .mode = .static }); - defer map.close() catch @panic("map close failed"); +// Turns a driver poll waits before giving up. The first `spin_turns` yield, so +// work that lands immediately costs nothing; the rest sleep a millisecond +// each, which puts a wall-clock bound on the wait. Yields alone cannot: they +// measure scheduler turns, and on an idle host a hundred thousand of them +// elapse in tens of milliseconds -- less than a real frame takes, so the wait +// expired while the frame was still on its way. +const spin_turns = 1_000; +const wait_turns = spin_turns + 30_000; - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 16, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; +fn waitOneTurn(turn: usize) !void { + if (turn < spin_turns) return std.Thread.yield(); + try testing.io.sleep(.fromMilliseconds(1), .awake); +} - const update = try owned.session.renderUpdate(); - try testing.expectEqual(@as(maplibre.RenderResult, .no_update), update.result); - // A non-rendered outcome never asks for another frame. - try testing.expect(!update.needs_repaint); - try owned.session.resize(.{ .width = 32, .height = 16, .scale_factor = 1.0 }); +fn awaitRuntimeBarrier(runtime: *maplibre.RuntimeHandle) !void { + var future = try runtime.barrier(); + defer future.deinit(); + _ = try future.wait(null); } -test "owned texture render session lifecycle and readback" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); +fn renderFrame(session: maplibre.RenderSessionHandle, if_needed: bool) !maplibre.FrameResult { + const token = next_frame_token; + next_frame_token += 1; + const capabilities = try session.capabilities(); + try session.requestFrame(.{ + .if_needed = if_needed, + .present = capabilities.presentation, + .token = token, + }); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); + for (0..wait_turns) |turn| { + _ = session.serviceDriverWork(64) catch 0; + var batch = session.drainFrameResults() catch |err| switch (err) { + error.NotReady => { + try waitOneTurn(turn); + continue; + }, + else => return err, + }; + defer batch.release(); + for (0..try batch.count()) |index| { + const result = try batch.get(index); + if (result.token == token) return result; + } + try waitOneTurn(turn); + } + return error.FrameTimedOut; +} - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 16, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; - const session = &owned.session; - - var readback_buffer: [32 * 16 * 4]u8 = undefined; - try testing.expectError(error.InvalidState, session.readPremultipliedRgba8Into(&readback_buffer)); - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - try session.reduceMemoryUse(); - try session.dumpDebugLogs(); - try session.clearData(); - - var too_small_buffer: [4]u8 = undefined; - try testing.expectError(error.InvalidArgument, session.readPremultipliedRgba8Into(&too_small_buffer)); - too_small_buffer[0] = 0xaa; - - var image = try readTestImage(session, testing.allocator, 32 * 16 * 4); - defer image.deinit(); - try testing.expectEqual(@as(u32, 32), image.info.width); - try testing.expectEqual(@as(u32, 16), image.info.height); - try testing.expectEqual(@as(u32, 32 * 4), image.info.stride); - try testing.expectEqual(@as(usize, 32 * 16 * 4), image.info.byte_length); - try testing.expectEqual(image.info.byte_length, image.data.len); - - try session.resize(.{ .width = 64, .height = 64, .scale_factor = 1.0 }); - try session.detach(); - try testing.expectError(error.InvalidState, session.renderUpdate()); - try owned.close(); +fn releaseFrame(frame: *maplibre.AcquiredFrame) !void { + try frame.release(.cpu_complete); } -test "map size follows attach and resize and keeps the creation scale factor" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); +fn expectOwnedFrameExtent( + session: maplibre.RenderSessionHandle, + extent: maplibre.RenderTargetExtent, +) !void { + var frame = try session.acquireFrame(); + if (build_options.supports_vulkan) { + const info = try frame.vulkanTexture(); + try testing.expectEqual(extent.width, info.width); + try testing.expectEqual(extent.height, info.height); + try testing.expectEqual(extent.scale_factor, info.scale_factor); + try testing.expect(@intFromEnum(info.image) != 0); + } else if (build_options.supports_opengl) { + const info = try frame.openGLTexture(); + try testing.expectEqual(extent.width, info.width); + try testing.expectEqual(extent.height, info.height); + try testing.expectEqual(extent.scale_factor, info.scale_factor); + try testing.expect(info.texture != 0); + } else if (build_options.supports_metal) { + const info = try frame.metalTexture(); + try testing.expectEqual(extent.width, info.width); + try testing.expectEqual(extent.height, info.height); + try testing.expectEqual(extent.scale_factor, info.scale_factor); + try testing.expect(@intFromEnum(info.texture) != 0); + } else { + unreachable; + } + try releaseFrame(&frame); +} - var map = try maplibre.MapHandle.create(&runtime, .{ .width = 64, .height = 32, .scale_factor = 2.0 }); - defer map.close() catch @panic("map close failed"); - const created = try map.getSize(); - try testing.expectEqual(@as(u32, 64), created.width); - try testing.expectEqual(@as(u32, 32), created.height); - try testing.expectEqual(@as(f64, 2.0), created.scale_factor); - - // A session enqueues the map size for the map's owner thread rather than - // setting it in place, so the map keeps its previous size until the runtime - // is pumped. The target's scale factor leaves the map's pixel ratio alone. - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 16, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; - const before_pump = try map.getSize(); - try testing.expectEqual(@as(u32, 64), before_pump.width); - try testing.expectEqual(@as(u32, 32), before_pump.height); - - try runtime.pump(0, null); - const attached = try map.getSize(); - try testing.expectEqual(@as(u32, 32), attached.width); - try testing.expectEqual(@as(u32, 16), attached.height); - try testing.expectEqual(@as(f64, 2.0), attached.scale_factor); - - try owned.session.resize(.{ .width = 48, .height = 24, .scale_factor = 1.0 }); - try runtime.pump(0, null); - const resized = try map.getSize(); - try testing.expectEqual(@as(u32, 48), resized.width); - try testing.expectEqual(@as(u32, 24), resized.height); - try testing.expectEqual(@as(f64, 2.0), resized.scale_factor); +fn closeSession(session: *maplibre.RenderSessionHandle) !void { + const operation = try session.detach(); + try finishOperation(session.*, operation); + try session.destroy(); } -test "set target reports unsupported for a session-owned texture" { +test "owned texture session renders acquires resizes and reads back" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; - - // A session-owned texture is sized by its own session, so the session is - // rejected before the placeholder handles are read. - try testing.expectError(error.Unsupported, setPlaceholderBorrowedTextureTarget(&owned.session)); - - // The rejection left the session usable. - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try owned.session.renderUpdate()).result); -} - -test "an ease pumped through rendered frames reports its transition finish once" { + const initial_extent = maplibre.RenderTargetExtent{ .width = 32, .height = 16 }; + var owned = try attachTestOwnedTexture(&map, .{ .extent = initial_extent }); + defer owned.close() catch @panic("render session close failed"); + const capabilities = try owned.session.capabilities(); + try testing.expect(capabilities.frame_acquisition); + try testing.expectEqual(.attached, std.meta.activeTag((try owned.session.snapshot()).state)); + + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); + try testing.expectEqual(.rendered, std.meta.activeTag((try renderFrame(owned.session, false)).disposition)); + try expectOwnedFrameExtent(owned.session, initial_extent); + + if (capabilities.readback) { + var image = try resolveFuture(maplibre.OwnedReadback, owned.session, try owned.session.readback(testing.allocator)); + defer image.deinit(); + try testing.expectEqual(@as(u32, 32), image.info.width); + try testing.expectEqual(@as(u32, 16), image.info.height); + try testing.expectEqual(image.info.byte_length, image.data.len); + try testing.expect(hasNonZeroByte(image.data)); + } + + const resized_extent = maplibre.RenderTargetExtent{ .width = 48, .height = 24 }; + try finishOperation(owned.session, try owned.session.resize(resized_extent)); + _ = try map.resize(48, 24, 1.0); + try awaitRuntimeBarrier(&runtime); + for (0..1000) |_| { + const result = try renderFrame(owned.session, false); + if (std.meta.activeTag(result.disposition) == .rendered) break; + try testing.expectEqual(.size_pending, std.meta.activeTag(result.disposition)); + // The repaint signal is meaningful only on a rendered frame. + try testing.expect(!result.needs_repaint); + } else return error.ResizeDidNotConverge; + try expectOwnedFrameExtent(owned.session, resized_extent); + const snapshot = try owned.session.snapshot(); + try testing.expectEqual(resized_extent.width, snapshot.extent.width); + try testing.expectEqual(resized_extent.height, snapshot.extent.height); +} + +test "texture readback before a frame completes with invalid state" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 16, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 16, .height = 16 } }); + defer owned.close() catch @panic("render session close failed"); - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 16, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - // A pending style frame suppresses the update event that drives the eased - // frame, so consume it before starting the transition. - { - const pool = if (build_options.supports_metal) try metal_support.AutoreleasePool.init() else {}; - defer if (build_options.supports_metal) pool.deinit(); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try owned.session.renderUpdate()).result); - } - - // A camera transition advances when the host requests and renders frames. - try map.easeTo( - .{ .center = .{ .latitude = 37.7749, .longitude = -122.4194 }, .zoom = 4.0 }, - .{ .duration_ms = 50, .transition_id = 31 }, - ); - - var finished_count: usize = 0; - var last_transition_id: ?u64 = null; - for (0..200) |_| { - const frame = try pumpTransitionFrame(&runtime, &map, &owned.session); - finished_count += frame.finished_count; - if (frame.last_transition_id) |transition_id| last_transition_id = transition_id; - if (finished_count > 0) break; - try testing.io.sleep(.fromMilliseconds(1), .awake); - } - - try testing.expectEqual(@as(usize, 1), finished_count); - try testing.expectEqual(@as(?u64, 31), last_transition_id); - - // A completed transform cannot report the same transition again. - var trailing_finished_count: usize = 0; - for (0..8) |_| { - const frame = try pumpTransitionFrame(&runtime, &map, &owned.session); - trailing_finished_count += frame.finished_count; + if ((try owned.session.capabilities()).readback) { + try testing.expectError(error.InvalidState, resolveFuture(maplibre.OwnedReadback, owned.session, try owned.session.readback(testing.allocator))); } - try testing.expectEqual(@as(usize, 0), trailing_finished_count); - - const settled_camera = try map.getCamera(); - try testing.expectApproxEqAbs(@as(f64, 4.0), settled_camera.zoom.?, 0.000001); } -test "map close rejects live render session through public bindings" { +test "live render session blocks map close until detached" { if (!supports_test_owned_texture) return error.SkipZigTest; var diagnostics = maplibre.DiagnosticStore.init(testing.allocator); defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - errdefer runtime.close() catch {}; - - var map = try maplibre.MapHandle.create(&runtime, .{}); + errdefer support.closeRuntime(&runtime) catch {}; + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 32 }); + defer map_future.deinit(); + var map = try map_future.wait(null); errdefer map.close() catch {}; - - var owned = try attachTestOwnedTexture(&map, .{}); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 32, .height = 32 } }); errdefer owned.close() catch {}; try testing.expectError(error.InvalidState, map.close()); @@ -1556,1007 +1172,507 @@ test "map close rejects live render session through public bindings" { try owned.close(); try map.close(); - try runtime.close(); + try support.closeRuntime(&runtime); } -test "map close succeeds after render session detach through public bindings" { +test "still-image map modes complete owned texture renders" { if (!supports_test_owned_texture) return error.SkipZigTest; - var diagnostics = maplibre.DiagnosticStore.init(testing.allocator); - defer diagnostics.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - errdefer runtime.close() catch {}; - - var map = try maplibre.MapHandle.create(&runtime, .{}); - errdefer map.close() catch {}; - - var owned = try attachTestOwnedTexture(&map, .{}); - errdefer owned.close() catch {}; - - try owned.session.detach(); - // Detaching releases the map, so the map closes while the session is open. - try map.close(); + inline for (.{ maplibre.MapMode.static, maplibre.MapMode.tile }) |mode| { + var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 32, .mode = mode }); + defer map_future.deinit(); + var map = try map_future.wait(null); + defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 32, .height = 32 } }); + defer owned.close() catch @panic("render session close failed"); - try owned.close(); - try runtime.close(); + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); + var future = try map.requestStillImage(); + defer future.deinit(); + for (0..1000) |_| { + _ = try renderFrame(owned.session, false); + if (try future.poll()) break; + try std.Thread.yield(); + } else return error.StillImageDidNotComplete; + _ = try future.wait(null); + try expectOwnedFrameExtent(owned.session, .{ .width = 32, .height = 32 }); + } } -test "owned texture frame wrapper allocation failure releases native frame" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); +const feature_state_style_json = + \\{"version":8,"sources":{"point":{"type":"geojson","data":{"type":"FeatureCollection","features":[{"type":"Feature","id":"feature-1","properties":{},"geometry":{"type":"Point","coordinates":[0,0]}}]}}},"layers":[{"id":"circle","type":"circle","source":"point","paint":{"circle-radius":8}}]} +; - var owned = try attachTestOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - }); - defer owned.close() catch {}; - const session = &owned.session; +const cluster_style_json = + \\{"version":8,"sources":{"cluster-source":{"type":"geojson","cluster":true,"data":{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[0.0,0.0]},"properties":{"name":"one"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.001,0.001]},"properties":{"name":"two"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.002,0.002]},"properties":{"name":"three"}}]}}},"layers":[{"id":"cluster-circle","type":"circle","source":"cluster-source","filter":["has","point_count"],"paint":{"circle-radius":20}}]} +; - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); +fn skipJsonWhitespace(json: []const u8, start: usize) usize { + var cursor = start; + while (cursor < json.len and std.ascii.isWhitespace(json[cursor])) cursor += 1; + return cursor; +} - test_hooks.failNextOwnedTextureFrameWrapperAllocation(); - if (build_options.supports_vulkan) { - try testing.expectError(error.OutOfMemory, session.acquireVulkanOwnedTextureFrame()); - var frame = try session.acquireVulkanOwnedTextureFrame(); - try frame.release(); - } else if (build_options.supports_opengl) { - try testing.expectError(error.OutOfMemory, session.acquireOpenGLOwnedTextureFrame()); - var frame = try session.acquireOpenGLOwnedTextureFrame(); - try frame.release(); - } else if (build_options.supports_metal) { - try testing.expectError(error.OutOfMemory, session.acquireMetalOwnedTextureFrame()); - var frame = try session.acquireMetalOwnedTextureFrame(); - try frame.release(); - } else { - unreachable; +fn jsonStringEnd(json: []const u8, start: usize) ?usize { + var escaped = false; + var cursor = start + 1; + while (cursor < json.len) : (cursor += 1) { + if (escaped) escaped = false else if (json[cursor] == '\\') escaped = true else if (json[cursor] == '"') return cursor + 1; } + return null; } -test "still-image map modes drive owned texture rendering" { - if (!supports_test_owned_texture) return error.SkipZigTest; - inline for (.{ maplibre.MapMode.static, maplibre.MapMode.tile }) |mode| { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); +fn jsonValueEnd(json: []const u8, start: usize) ?usize { + if (start >= json.len) return null; + if (json[start] == '"') return jsonStringEnd(json, start); + if (json[start] != '{' and json[start] != '[') { + var cursor = start; + while (cursor < json.len and !std.ascii.isWhitespace(json[cursor]) and json[cursor] != ',' and json[cursor] != '}' and json[cursor] != ']') cursor += 1; + return cursor; + } + var depth: usize = 0; + var cursor = start; + while (cursor < json.len) { + if (json[cursor] == '"') { + cursor = jsonStringEnd(json, cursor) orelse return null; + continue; + } + if (json[cursor] == '{' or json[cursor] == '[') depth += 1; + if (json[cursor] == '}' or json[cursor] == ']') { + depth -= 1; + if (depth == 0) return cursor + 1; + } + cursor += 1; + } + return null; +} - var map = try maplibre.MapHandle.create(&runtime, .{ .mode = mode }); - defer map.close() catch @panic("map close failed"); +fn rawJsonMember(json: []const u8, key: []const u8) ?[]const u8 { + var cursor = skipJsonWhitespace(json, 0); + if (cursor >= json.len or json[cursor] != '{') return null; + cursor += 1; + while (true) { + cursor = skipJsonWhitespace(json, cursor); + if (cursor >= json.len or json[cursor] == '}') return null; + const key_end = jsonStringEnd(json, cursor) orelse return null; + const member_name = json[cursor + 1 .. key_end - 1]; + cursor = skipJsonWhitespace(json, key_end); + if (cursor >= json.len or json[cursor] != ':') return null; + const value_start = skipJsonWhitespace(json, cursor + 1); + const value_end = jsonValueEnd(json, value_start) orelse return null; + if (std.mem.eql(u8, member_name, key)) return json[value_start..value_end]; + cursor = skipJsonWhitespace(json, value_end); + if (cursor >= json.len or json[cursor] != ',') return null; + cursor += 1; + } +} - var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 32, .height = 32 } }); - defer owned.close() catch {}; +fn firstJsonArrayElement(json: []const u8) ?[]const u8 { + var cursor = skipJsonWhitespace(json, 0); + if (cursor >= json.len or json[cursor] != '[') return null; + cursor = skipJsonWhitespace(json, cursor + 1); + if (cursor >= json.len or json[cursor] == ']') return null; + return json[cursor .. jsonValueEnd(json, cursor) orelse return null]; +} - try map.setStyleJson(testing.allocator, support.style_json); - try map.requestStillImage(); - try testing.expectError(error.InvalidState, map.requestStillImage()); - } +fn firstLeafName(collection: []const u8) ?[]const u8 { + const features = rawJsonMember(collection, "features") orelse return null; + const feature = firstJsonArrayElement(features) orelse return null; + const properties = rawJsonMember(feature, "properties") orelse return null; + return rawJsonMember(properties, "name"); } -test "owned texture attachment validates public descriptors" { +test "feature state and rendered queries copy operation results" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 64, .height = 64 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 64, .height = 64 } }); + defer owned.close() catch @panic("render session close failed"); - try expectInvalidOwnedTextureExtent(&map, .{ .width = 0 }); - try expectInvalidOwnedTextureExtent(&map, .{ .height = 0 }); - try expectInvalidOwnedTextureExtent(&map, .{ .scale_factor = 0 }); -} + _ = try map.setStyleJson(testing.allocator, feature_state_style_json); + try awaitRuntimeBarrier(&runtime); + _ = try renderFrame(owned.session, false); -test "owned texture attachment rejects another active session" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + const selector = maplibre.FeatureStateSelector{ .source_id = "point", .feature_id = "feature-1" }; + try testing.expect(std.meta.eql( + try support.waitForCommandDisposition(&runtime, try map.setFeatureState(testing.allocator, selector, "{\"hover\":true,\"count\":3}")), + maplibre.CommandDisposition.committed, + )); + var state_future = try map.getFeatureState(testing.allocator, selector); + defer state_future.deinit(); + var state = try state_future.wait(null); + defer state.deinit(); + try testing.expect(std.mem.indexOf(u8, state.value, "\"hover\":true") != null); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); + for (0..1000) |_| { + var result = try takeQueryFeaturesResult( + owned.session, + try owned.session.queryRenderedFeatures( + testing.allocator, + .{ .box = .{ + .min = .{ .x = 0, .y = 0 }, + .max = .{ .x = 64, .y = 64 }, + } }, + null, + ), + ); + defer result.deinit(); + if (result.items.len != 0 and result.items[0].state != null) { + try testing.expectEqualStrings("point", result.items[0].source_id.?); + try testing.expect(std.mem.indexOf(u8, result.items[0].state.?, "\"hover\":true") != null); + break; + } + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + _ = try renderFrame(owned.session, false); + } else return error.RenderedFeatureNotQueryable; + + var source = try takeQueryFeaturesResult( + owned.session, + try owned.session.querySourceFeatures(testing.allocator, "point", null), + ); + defer source.deinit(); + try testing.expectEqualStrings("point", source.items[0].source_id.?); + try testing.expect(std.mem.indexOf(u8, source.items[0].feature, "\"type\":\"Point\"") != null); + + try testing.expect(std.meta.eql( + try support.waitForCommandDisposition(&runtime, try map.removeFeatureState(testing.allocator, selector)), + maplibre.CommandDisposition.committed, + )); +} + +fn featureStateForTesting(map: *maplibre.MapHandle, selector: maplibre.FeatureStateSelector) !maplibre.OwnedString { + var future = try map.getFeatureState(testing.allocator, selector); + defer future.deinit(); + return future.wait(null); +} - var first = try attachTestOwnedTexture(&map, .{}); - defer first.close() catch {}; - try testing.expectError(error.InvalidState, attachTestOwnedTexture(&map, .{})); +fn commitMapCommand(runtime: *maplibre.RuntimeHandle, future: maplibre.Future(maplibre.CommandCompletion)) !void { + try testing.expect(std.meta.eql( + try support.waitForCommandDisposition(runtime, future), + maplibre.CommandDisposition.committed, + )); } +// Feature state belongs to the map, so it needs no loaded style, ordered reads +// observe every earlier command, and it survives style loads and a renderer +// retirement driven by a scale-factor change. test "map feature state set get and remove" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 64, .height = 64 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 64, .height = 64 } }); - defer owned.close() catch {}; - const session = &owned.session; + defer owned.close() catch @panic("render session close failed"); const selector = maplibre.FeatureStateSelector{ .source_id = "point", .feature_id = "feature-1" }; const feature_state = "{\"hover\":true,\"radius\":18446744073709551615}"; - try map.setFeatureState(testing.allocator, selector, feature_state); - try map.removeFeatureState(testing.allocator, .{ .source_id = "point", .feature_id = "feature-1", .state_key = "hover" }); - var queued = try map.getFeatureState(testing.allocator, selector); + try commitMapCommand(&runtime, try map.setFeatureState(testing.allocator, selector, feature_state)); + try commitMapCommand(&runtime, try map.removeFeatureState(testing.allocator, .{ .source_id = "point", .feature_id = "feature-1", .state_key = "hover" })); + var queued = try featureStateForTesting(&map, selector); defer queued.deinit(); - try testing.expect(rawMember(queued.value, "hover") == null); - try testing.expectEqualStrings("18446744073709551615", rawMember(queued.value, "radius").?); - - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - _ = try session.renderUpdate(); - var before_style = try map.getFeatureState(testing.allocator, selector); - defer before_style.deinit(); - try testing.expect(rawMember(before_style.value, "hover") == null); - try testing.expectEqualStrings("18446744073709551615", rawMember(before_style.value, "radius").?); - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - var snapshot = try map.getFeatureState(testing.allocator, selector); - defer snapshot.deinit(); - try testing.expect(rawMember(snapshot.value, "hover") == null); - try testing.expectEqualStrings("18446744073709551615", rawMember(snapshot.value, "radius").?); - - try map.setFeatureState(testing.allocator, selector, feature_state); - var restored = try map.getFeatureState(testing.allocator, selector); + try testing.expect(rawJsonMember(queued.value, "hover") == null); + try testing.expectEqualStrings("18446744073709551615", rawJsonMember(queued.value, "radius").?); + + // A style load drops style-owned objects, not map-owned feature state. + _ = try map.setStyleJson(testing.allocator, feature_state_style_json); + try awaitRuntimeBarrier(&runtime); + _ = try renderFrame(owned.session, false); + var after_style = try featureStateForTesting(&map, selector); + defer after_style.deinit(); + try testing.expect(rawJsonMember(after_style.value, "hover") == null); + try testing.expectEqualStrings("18446744073709551615", rawJsonMember(after_style.value, "radius").?); + + try commitMapCommand(&runtime, try map.setFeatureState(testing.allocator, selector, feature_state)); + var restored = try featureStateForTesting(&map, selector); defer restored.deinit(); - try testing.expectEqualStrings("true", rawMember(restored.value, "hover").?); - try testing.expectEqualStrings("18446744073709551615", rawMember(restored.value, "radius").?); - - try map.removeFeatureState(testing.allocator, .{ .source_id = "point", .feature_id = "feature-1", .state_key = "hover" }); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - var after_remove = try map.getFeatureState(testing.allocator, selector); - defer after_remove.deinit(); - try testing.expect(rawMember(after_remove.value, "hover") == null); - try testing.expectEqualStrings("18446744073709551615", rawMember(after_remove.value, "radius").?); + try testing.expectEqualStrings("true", rawJsonMember(restored.value, "hover").?); + try testing.expectEqualStrings("18446744073709551615", rawJsonMember(restored.value, "radius").?); try testing.expectError(error.InvalidArgument, map.removeFeatureState(testing.allocator, .{ .source_id = "point", .state_key = "hover" })); - try session.resize(.{ .width = 64, .height = 64, .scale_factor = 2.0 }); - switch ((try session.renderUpdate()).result) { - .rendered => {}, - .size_pending => { - try runtime.pump(0, null); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - }, - else => return error.UnexpectedRenderResult, - } - var after_scale = try map.getFeatureState(testing.allocator, selector); + // A scale-factor change retires the renderer; map-owned state survives. + try finishOperation(owned.session, try owned.session.resize(.{ .width = 64, .height = 64, .scale_factor = 2.0 })); + for (0..100) |_| { + if (std.meta.activeTag((try renderFrame(owned.session, false)).disposition) == .rendered) break; + } else return error.RenderDidNotComplete; + var after_scale = try featureStateForTesting(&map, selector); defer after_scale.deinit(); - try testing.expect(rawMember(after_scale.value, "hover") == null); - try testing.expectEqualStrings("18446744073709551615", rawMember(after_scale.value, "radius").?); + try testing.expectEqualStrings("true", rawJsonMember(after_scale.value, "hover").?); + try testing.expectEqualStrings("18446744073709551615", rawJsonMember(after_scale.value, "radius").?); } -test "render session queries rendered and source features" { +test "cluster feature extensions copy values and feature collections" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var owned = try attachTestOwnedTexture(&map, .{}); - defer owned.close() catch {}; - const session = &owned.session; - - try testing.expectError(error.InvalidState, session.queryRenderedFeatures(testing.allocator, .{ .point = .{ .x = 256, .y = 256 } }, null)); - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - const query_point = try map.pixelForLatLng(.{ .latitude = 37.7749, .longitude = -122.4194 }); - var rendered = try waitForRenderedFeatureQuery(&runtime, session, .{ .box = .{ - .min = .{ .x = query_point.x - 20, .y = query_point.y - 20 }, - .max = .{ .x = query_point.x + 20, .y = query_point.y + 20 }, - } }, .{ - .layer_ids = &.{"point-circle"}, - .filter = "[\"==\",[\"get\",\"kind\"],\"capital\"]", - }); - defer rendered.deinit(); - try testing.expectEqualStrings("point", rendered.items[0].source_id.?); - try testing.expectEqualStrings("\"capital\"", queryFeatureProperty(rendered.items[0].feature, "kind").?); - - var source = try waitForSourceFeatureQuery(&runtime, session); - defer source.deinit(); - try testing.expectEqualStrings("point", source.items[0].source_id.?); - try testing.expectEqualStrings("\"capital\"", queryFeatureProperty(source.items[0].feature, "kind").?); - - var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 }); - try testing.expectError(error.OutOfMemory, session.querySourceFeatures(failing_allocator.allocator(), "point", null)); - - try testing.expectError(error.InvalidArgument, session.queryRenderedFeatures(testing.allocator, .{ .point = .{ .x = std.math.inf(f64), .y = 0.0 } }, null)); - try testing.expectError(error.InvalidArgument, session.querySourceFeatures(testing.allocator, "", null)); -} - -test "render session clips rendered box queries to the viewport" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var owned = try attachTestOwnedTexture(&map, .{}); - defer owned.close() catch {}; - const session = &owned.session; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - const options = maplibre.RenderedFeatureQueryOptions{ .layer_ids = &.{"point-circle"} }; - - // A box that over-covers the viewport must answer like the viewport. - var oversized = try waitForRenderedFeatureQuery(&runtime, session, .{ .box = .{ - .min = .{ .x = -8192, .y = -8192 }, - .max = .{ .x = 8192, .y = 8192 }, - } }, options); - defer oversized.deinit(); - try testing.expectEqualStrings("\"capital\"", queryFeatureProperty(oversized.items[0].feature, "kind").?); - - // Corners in either order describe the same box. - var inverted = try session.queryRenderedFeatures(testing.allocator, .{ .box = .{ - .min = .{ .x = 8192, .y = 8192 }, - .max = .{ .x = -8192, .y = -8192 }, - } }, options); - defer inverted.deinit(); - try testing.expect(inverted.items.len != 0); - - // Clipping keeps a fully off-screen box empty rather than collapsing it - // onto a viewport edge. - var offscreen = try session.queryRenderedFeatures(testing.allocator, .{ .box = .{ - .min = .{ .x = 2048, .y = 2048 }, - .max = .{ .x = 4096, .y = 4096 }, - } }, options); - defer offscreen.deinit(); - try testing.expect(offscreen.items.len == 0); -} - -test "render session queries cluster feature extensions" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 64, .height = 64 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 64, .height = 64 } }); + defer owned.close() catch @panic("render session close failed"); - var owned = try attachTestOwnedTexture(&map, .{}); - defer owned.close() catch {}; - const session = &owned.session; + _ = try map.setStyleJson(testing.allocator, cluster_style_json); + try awaitRuntimeBarrier(&runtime); + _ = try renderFrame(owned.session, false); - try map.jumpTo(.{ .center = .{ .latitude = 0, .longitude = 0 }, .zoom = 0 }); - try map.setStyleJson(testing.allocator, cluster_style_json); - for (0..5) |_| { - if (!try support.waitForEvent(&runtime, .map_render_update_available)) break; - _ = try session.renderUpdate(); + var cluster_result: ?maplibre.QueriedFeatureList = null; + for (0..1000) |_| { + var result = try takeQueryFeaturesResult( + owned.session, + try owned.session.queryRenderedFeatures( + testing.allocator, + .{ .box = .{ + .min = .{ .x = 0, .y = 0 }, + .max = .{ .x = 64, .y = 64 }, + } }, + null, + ), + ); + if (result.items.len != 0) { + cluster_result = result; + break; + } + result.deinit(); + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + _ = try renderFrame(owned.session, false); } - - const query_point = try map.pixelForLatLng(.{ .latitude = 0, .longitude = 0 }); - var clusters = try waitForRenderedFeatureQuery(&runtime, session, .{ .box = .{ - .min = .{ .x = query_point.x - 30, .y = query_point.y - 30 }, - .max = .{ .x = query_point.x + 30, .y = query_point.y + 30 }, - } }, .{ .layer_ids = &.{"cluster-circle"} }); + var clusters = cluster_result orelse return error.ClusterFeatureNotQueryable; defer clusters.deinit(); - const feature = clusters.items[0].feature; - var children = try session.queryFeatureExtension(testing.allocator, "cluster-source", feature, "supercluster", "children", null); + const properties = rawJsonMember(feature, "properties").?; + _ = try std.fmt.parseInt(u64, rawJsonMember(properties, "cluster_id").?, 10); + try testing.expectEqualStrings("3", rawJsonMember(properties, "point_count").?); + + var children = try takeQueryResult( + owned.session, + try owned.session.queryFeatureExtension( + testing.allocator, + "cluster-source", + feature, + "supercluster", + "children", + null, + ), + ); defer children.deinit(); - try testing.expect(firstArrayElement(rawMember(children.value, "features").?) != null); - - var expansion_zoom = try session.queryFeatureExtension(testing.allocator, "cluster-source", feature, "supercluster", "expansion-zoom", null); + try testing.expect(firstJsonArrayElement(rawJsonMember(children.value, "features").?) != null); + + var expansion_zoom = try takeQueryResult( + owned.session, + try owned.session.queryFeatureExtension( + testing.allocator, + "cluster-source", + feature, + "supercluster", + "expansion-zoom", + null, + ), + ); defer expansion_zoom.deinit(); _ = try std.fmt.parseInt(u64, expansion_zoom.value, 10); - // Native reads `limit` and `offset` only as unsigned values, falling back - // to ten leaves at offset zero, so both must move the observed result. - var first = try session.queryFeatureExtension(testing.allocator, "cluster-source", feature, "supercluster", "leaves", "{\"limit\":1,\"offset\":0}"); - defer first.deinit(); - var second = try session.queryFeatureExtension(testing.allocator, "cluster-source", feature, "supercluster", "leaves", "{\"limit\":1,\"offset\":1}"); - defer second.deinit(); - try testing.expect(!std.mem.eql(u8, leafName(first.value).?, leafName(second.value).?)); -} - -fn leafName(collection: []const u8) ?[]const u8 { - const features = rawMember(collection, "features") orelse return null; - const feature = firstArrayElement(features) orelse return null; - const properties = rawMember(feature, "properties") orelse return null; - return rawMember(properties, "name"); -} - -test "GeoJSON source options cluster nearby points and aggregate cluster properties" { - if (!supports_test_owned_texture) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var owned = try attachTestOwnedTexture(&map, .{}); - defer owned.close() catch {}; - const session = &owned.session; - - try map.jumpTo(.{ .center = .{ .latitude = 0, .longitude = 0 }, .zoom = 0 }); - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_style_loaded)); - - const features = "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0,0]},\"properties\":{\"rank\":1}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0.001,0.001]},\"properties\":{\"rank\":2}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0.002,0.002]},\"properties\":{\"rank\":3}}]}"; - const cluster_data = try maplibre.GeoJsonSourceDataHandle.create( - testing.allocator, - features, - .{ - .cluster = true, - .cluster_radius = 50, - .cluster_min_points = 2, - .cluster_max_zoom = 14, - .cluster_properties = "{\"total\":[\"+\",[\"get\",\"rank\"]]}", - }, + var first_leaf = try takeQueryResult( + owned.session, + try owned.session.queryFeatureExtension( + testing.allocator, + "cluster-source", + feature, + "supercluster", + "leaves", + "{\"limit\":1,\"offset\":0}", + ), ); - defer cluster_data.release(); - try map.addGeoJsonSourceData( - testing.allocator, - "cluster-options-source", - cluster_data, + defer first_leaf.deinit(); + var second_leaf = try takeQueryResult( + owned.session, + try owned.session.queryFeatureExtension( + testing.allocator, + "cluster-source", + feature, + "supercluster", + "leaves", + "{\"limit\":1,\"offset\":1}", + ), ); - - try map.addStyleLayerJson(testing.allocator, "{\"id\":\"cluster-options-circle\",\"type\":\"circle\",\"source\":\"cluster-options-source\",\"filter\":[\"has\",\"point_count\"],\"paint\":{\"circle-radius\":20}}", ""); - - for (0..5) |_| { - if (!try support.waitForEvent(&runtime, .map_render_update_available)) break; - _ = try session.renderUpdate(); - } - - // The layer only draws features carrying point_count, so a queried feature - // proves the source options clustered the points. - const query_point = try map.pixelForLatLng(.{ .latitude = 0, .longitude = 0 }); - var clusters = try waitForRenderedFeatureQuery(&runtime, session, .{ .box = .{ - .min = .{ .x = query_point.x - 30, .y = query_point.y - 30 }, - .max = .{ .x = query_point.x + 30, .y = query_point.y + 30 }, - } }, .{ .layer_ids = &.{"cluster-options-circle"} }); - defer clusters.deinit(); - - try testing.expectEqualStrings("true", queryFeatureProperty(clusters.items[0].feature, "cluster").?); - try testing.expectEqualStrings("3", queryFeatureProperty(clusters.items[0].feature, "point_count").?); - try testing.expectEqualStrings("6.0", queryFeatureProperty(clusters.items[0].feature, "total").?); + defer second_leaf.deinit(); + try testing.expect(!std.mem.eql(u8, firstLeafName(first_leaf.value).?, firstLeafName(second_leaf.value).?)); } -test "unsupported backend owned texture attachment reports unsupported" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - const fake_pointer = fakeNativePointer(); - if (build_options.supports_metal) { - try testing.expectError(error.Unsupported, maplibre.attachVulkanOwnedTexture(&map, .{ - .context = .{ - .instance = fake_pointer, - .physical_device = fake_pointer, - .device = fake_pointer, - .graphics_queue = fake_pointer, - .graphics_queue_family_index = 0, - .get_instance_proc_addr = null, - .get_device_proc_addr = null, - }, - })); - } - if (build_options.supports_vulkan) { - try testing.expectError(error.Unsupported, maplibre.attachMetalOwnedTexture(&map, .{ - .context = .{ .device = fake_pointer }, - })); - } - if (!build_options.supports_opengl) { - const context = fakeOpenGLContext(); - try testing.expectError(error.Unsupported, maplibre.attachOpenGLOwnedTexture(&map, .{ - .context = context, - })); - try testing.expectError(error.Unsupported, maplibre.attachOpenGLBorrowedTexture(&map, .{ - .physical_width = 512, - .physical_height = 512, - .context = context, - .texture = 1, - .target = gl_texture_2d, - })); - try testing.expectError(error.Unsupported, maplibre.attachOpenGLSurface(&map, .{ - .context = context, - .surface = fake_pointer, - })); - } -} - -test "OpenGL texture and surface descriptors validate through public bindings" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - const context = fakeOpenGLContext(); - try testing.expectError(error.InvalidArgument, maplibre.attachOpenGLOwnedTexture(&map, .{ - .extent = .{ .width = 0 }, - .context = context, - })); - try testing.expectError(error.InvalidArgument, maplibre.attachOpenGLBorrowedTexture(&map, .{ - .physical_width = 512, - .physical_height = 512, - .context = context, - .texture = 0, - .target = gl_texture_2d, - })); - try testing.expectError(error.InvalidArgument, maplibre.attachOpenGLBorrowedTexture(&map, .{ - .physical_width = 512, - .physical_height = 512, - .context = context, - .texture = 1, - .target = 0, - })); - try testing.expectError(error.InvalidArgument, maplibre.attachOpenGLSurface(&map, .{ - .extent = .{ .width = 0 }, - .context = context, - .surface = fakeNativePointer(), - })); -} - -test "OpenGL owned texture frame scopes public binding access" { - if (!build_options.supports_opengl) return error.SkipZigTest; - - var context = try TestOwnedTextureContext.init(); - defer context.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachOpenGLOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - .context = context.descriptor(), - }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - var image = try readTestImage(&session, testing.allocator, 32 * 32 * 4); - defer image.deinit(); - try testing.expectEqual(@as(u32, 32), image.info.width); - try testing.expectEqual(@as(u32, 32), image.info.height); - try testing.expect(hasNonZeroByte(image.data)); - - var frame = try session.acquireOpenGLOwnedTextureFrame(); - var frame_alias = frame; - const info = try frame.info(); - try testing.expectEqual(@as(u32, 32), info.width); - try testing.expectEqual(@as(u32, 32), info.height); - try testing.expectEqual(@as(u32, gl.TEXTURE_2D), info.target); - try testing.expectEqual(@as(u32, gl.RGBA8), info.internal_format); - try testing.expectEqual(@as(u32, gl.RGBA), info.format); - try testing.expectEqual(@as(u32, gl.UNSIGNED_BYTE), info.type); - try testing.expect(info.texture != 0); - - // The acquired texture belongs to the session context, so this readback - // covers the cross-context handoff. - var host_pixels: [32 * 32 * 4]u8 = undefined; - @memset(&host_pixels, 0); - try context.readRgbaTexture(info.texture, 32, 32, &host_pixels); - try expectPixelApprox(host_pixels[0..4].*, .{ 0xd8, 0xf1, 0xff, 0xff }, 8); - - try expectOpenGLFrameReleaseWrongThread(&frame); - - try testing.expectError(error.ActiveBorrow, setPlaceholderBorrowedTextureTarget(&session)); - try testing.expectError(error.ActiveBorrow, session.renderUpdate()); - try testing.expectError(error.ActiveBorrow, session.detach()); - try testing.expectError(error.ActiveBorrow, session.acquireOpenGLOwnedTextureFrame()); - try testing.expectError(error.ActiveBorrow, session.close()); - - try frame.release(); - try frame_alias.release(); - try testing.expectError(error.ClosedHandle, frame_alias.info()); - - try expectRenderSessionCallWrongThread(&session, .acquire_opengl_frame); - try session.close(); -} - -test "OpenGL borrowed texture renders through public bindings" { - if (!build_options.supports_opengl) return error.SkipZigTest; - - var borrowed = try OpenGLBorrowedTexture.create(128, 128); - defer borrowed.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachOpenGLBorrowedTexture(&map, borrowed.descriptor()); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - const pixels = try testing.allocator.alloc(u8, 128 * 128 * 4); - defer testing.allocator.free(pixels); - @memset(pixels, 0); - try borrowed.readRGBA8(pixels); - try expectPixelApprox(pixels[0..4].*, .{ 0xd8, 0xf1, 0xff, 0xff }, 8); - - try testing.expectError(error.Unsupported, session.acquireOpenGLOwnedTextureFrame()); - var readback_buffer: [128 * 128 * 4]u8 = undefined; - try testing.expectError(error.Unsupported, session.readPremultipliedRgba8Into(&readback_buffer)); -} - -test "OpenGL borrowed texture set target renders into a replacement texture" { - if (!build_options.supports_opengl) return error.SkipZigTest; - - var borrowed = try OpenGLBorrowedTexture.create(128, 128); - defer borrowed.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachOpenGLBorrowedTexture(&map, borrowed.descriptor()); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - // A caller-owned texture is sized by its owner, so resize is rejected and - // the host hands over a texture at the new size instead. - try testing.expectError(error.Unsupported, session.resize(.{ .width = 64, .height = 96, .scale_factor = 1.0 })); - - const replacement = try borrowed.allocateReplacement(64, 96); - try session.setOpenGLBorrowedTextureTarget(borrowed.descriptorFor(replacement, 64, 96)); - borrowed.adopt(replacement, 64, 96); - - // A surface descriptor names a target this session does not have; the - // rejection leaves it on the texture just handed over. - try testing.expectError(error.Unsupported, session.setOpenGLSurfaceTarget(.{ - .extent = .{ .width = 64, .height = 96 }, - .context = borrowed.context.descriptor(), - .surface = fakeNativePointer(), - })); - - // Replacing the target enqueues the new size for the map's owner thread, - // so the map publishes a matching update only once pumped. - const size_pending_update = try session.renderUpdate(); - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), size_pending_update.result); - try testing.expect(!size_pending_update.needs_repaint); - try runtime.pump(0, null); - const resized = try map.getSize(); - try testing.expectEqual(@as(u32, 64), resized.width); - try testing.expectEqual(@as(u32, 96), resized.height); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - const pixels = try testing.allocator.alloc(u8, 64 * 96 * 4); - defer testing.allocator.free(pixels); - @memset(pixels, 0); - try borrowed.readRGBA8(pixels); - try expectPixelApprox(pixels[0..4].*, .{ 0xd8, 0xf1, 0xff, 0xff }, 8); -} - -test "OpenGL surface renders through public bindings" { - if (!build_options.supports_opengl) return error.SkipZigTest; - - var context = try TestOwnedTextureContext.initWithSize(128, 128); - defer context.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachOpenGLSurface(&map, .{ - .extent = .{ .width = 128, .height = 128, .scale_factor = 1.0 }, - .context = context.descriptor(), - .surface = context.surface(), - }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - const pixels = try testing.allocator.alloc(u8, 128 * 128 * 4); - defer testing.allocator.free(pixels); - @memset(pixels, 0); - try context.readSurfaceRGBA8(128, 128, pixels); - // The style's background color reaches the surface through the clear the - // session's exclusive context allows, so a corner away from the circle - // layer carries it exactly. - try testing.expectEqualSlices(u8, &.{ 0xd8, 0xf1, 0xff, 0xff }, pixels[0..4]); - - try testing.expectError(error.Unsupported, session.acquireOpenGLOwnedTextureFrame()); - var readback_buffer: [128 * 128 * 4]u8 = undefined; - try testing.expectError(error.Unsupported, session.readPremultipliedRgba8Into(&readback_buffer)); -} - -test "dedicated OpenGL surface renders through a context it owns" { - if (!supports_egl) return error.SkipZigTest; - - var context = try DedicatedEglSurface.initWithSize(64, 64); - defer context.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachOpenGLSurface(&map, .{ - .extent = .{ .width = 64, .height = 64, .scale_factor = 1.0 }, - .context = context.descriptor(), - .surface = context.surface(), - }); - errdefer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - // The session owns this thread, which is also the map's, so the map reaches - // the style and the extent only while this loop pumps the runtime. - var result: maplibre.RenderResult = .no_update; - for (0..1000) |_| { - try runtime.pump(0, null); - result = (try session.renderUpdate()).result; - if (result == .rendered) break; - try std.Thread.yield(); - } - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), result); - // A dedicated session leaves its context current, so the next render costs - // no EGL call. - try testing.expect(egl.eglGetCurrentContext() != egl.EGL_NO_CONTEXT); - - // Closing the session releases the thread it had taken over. - try session.close(); - try testing.expect(egl.eglGetCurrentContext() == egl.EGL_NO_CONTEXT); -} - -test "Metal owned texture frame handle scopes native pointers" { - if (!build_options.supports_metal) return error.SkipZigTest; - const device = MTLCreateSystemDefaultDevice() orelse return error.MetalDeviceUnavailable; - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachMetalOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - .context = .{ .device = maplibre.NativePointer.fromPtr(device) }, - }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - var image = try readTestImage(&session, testing.allocator, 32 * 32 * 4); - defer image.deinit(); - try testing.expectEqual(@as(u32, 32), image.info.width); - try testing.expectEqual(@as(u32, 32), image.info.height); - try testing.expectEqual(@as(usize, 32 * 32 * 4), image.info.byte_length); - - var frame = try session.acquireMetalOwnedTextureFrame(); - var frame_alias = frame; - const info = try frame.info(); - try testing.expectEqual(@as(u32, 32), info.width); - try testing.expectEqual(@as(u32, 32), info.height); - try testing.expectEqual(@as(u64, 1), info.generation); - try testing.expect(info.texture.toPtr() != info.device.toPtr()); - try expectMetalFrameReleaseWrongThread(&frame); - - try testing.expectError(error.ActiveBorrow, session.resize(.{ .width = 16, .height = 16, .scale_factor = 1.0 })); - try testing.expectError(error.ActiveBorrow, setPlaceholderBorrowedTextureTarget(&session)); - try testing.expectError(error.ActiveBorrow, session.renderUpdate()); - try testing.expectError(error.ActiveBorrow, session.detach()); - try testing.expectError(error.ActiveBorrow, session.acquireMetalOwnedTextureFrame()); - try testing.expectError(error.ActiveBorrow, session.close()); - - try frame.release(); - try frame_alias.release(); - try testing.expectError(error.ClosedHandle, frame_alias.info()); - - // Resizing enqueues the new logical size for the map's owner thread, so - // the map publishes a matching update only once pumped. - try session.resize(.{ .width = 16, .height = 8, .scale_factor = 2.0 }); - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), (try session.renderUpdate()).result); - try runtime.pump(0, null); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - var resized_frame = try session.acquireMetalOwnedTextureFrame(); - const resized_info = try resized_frame.info(); - try testing.expectEqual(@as(u32, 32), resized_info.width); - try testing.expectEqual(@as(u32, 16), resized_info.height); - try testing.expectEqual(@as(f64, 2.0), resized_info.scale_factor); - try testing.expectEqual(@as(u64, 2), resized_info.generation); - try resized_frame.release(); - - try expectRenderSessionCallWrongThread(&session, .acquire_metal_frame); - try session.close(); -} - -test "Metal owned texture frame release follows moved session wrapper" { - if (!build_options.supports_metal) return error.SkipZigTest; - const device = MTLCreateSystemDefaultDevice() orelse return error.MetalDeviceUnavailable; - - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); - - var handles = try createMovedMetalSessionWithFrame(device); - try testing.expectError(error.ActiveBorrow, handles.session.close()); - try handles.frame.release(); - try handles.session.close(); - try handles.map.close(); - try handles.runtime.close(); -} - -test "render session rejects wrong-thread calls through public bindings" { +test "sustained frame demands outlast the texture ring depth" { if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 32 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 32, .height = 32 } }); + defer owned.close() catch @panic("render session close failed"); + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); - var owned = try attachTestOwnedTexture(&map, .{}); - defer owned.close() catch {}; - const session = &owned.session; - - inline for (.{ - RenderSessionThreadCall.render_update, - .resize, - .set_target, - .detach, - .reduce_memory_use, - .clear_data, - .dump_debug_logs, - .close, - }) |call| { - try expectRenderSessionCallWrongThread(session, call); + for (0..64) |_| { + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + try testing.expectEqual(.rendered, std.meta.activeTag((try renderFrame(owned.session, false)).disposition)); } + try testing.expect((try owned.session.snapshot()).frame_generation >= 64); } -test "Metal borrowed texture renders through public bindings" { - if (!build_options.supports_metal) return error.SkipZigTest; - const device = MTLCreateSystemDefaultDevice() orelse return error.MetalDeviceUnavailable; - - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); - - const borrowed = try metal_support.createTexture(device, 128, 128); - defer metal_support.releaseObject(borrowed); - try metal_support.clearTextureRGBA8(borrowed, .{ 255, 0, 255, 255 }); - try expectPixelApprox(try metal_support.readTexturePixelRGBA8(borrowed, 0, 0), .{ 255, 0, 255, 255 }, 0); - +test "a rendered frame during an ease reports needs repaint" { + if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachMetalBorrowedTexture(&map, .{ - .extent = .{ .width = 128, .height = 128 }, - .physical_width = 128, - .physical_height = 128, - .texture = maplibre.NativePointer.fromPtr(borrowed), + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 32, .height = 16 } }); + defer owned.close() catch @panic("render session close failed"); + + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); + // Settle the style's own frames so the transition drives what follows. + try testing.expectEqual(.rendered, std.meta.activeTag((try renderFrame(owned.session, false)).disposition)); + + _ = try map.updateCamera(.{ + .mode = .ease, + .camera = .{ .center = .{ .latitude = 37.7749, .longitude = -122.4194 }, .zoom = 4.0 }, + .animation = .{ .duration_ms = 60_000 }, }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - try expectPixelApprox(try metal_support.readTexturePixelRGBA8(borrowed, 0, 0), .{ 0xd8, 0xf1, 0xff, 0xff }, 8); - - try testing.expectError(error.Unsupported, session.acquireMetalOwnedTextureFrame()); - try testing.expectError(error.Unsupported, session.resize(.{ .width = 64, .height = 64, .scale_factor = 1.0 })); - var readback_buffer: [128 * 128 * 4]u8 = undefined; - try testing.expectError(error.Unsupported, session.readPremultipliedRgba8Into(&readback_buffer)); + try awaitRuntimeBarrier(&runtime); + + // Mid-transition the map asks for the next frame with the one it renders, + // the same signal a map-render-frame-finished event carries. + var observed_repaint = false; + for (0..100) |_| { + const result = try renderFrame(owned.session, false); + if (std.meta.activeTag(result.disposition) == .rendered and result.needs_repaint) { + observed_repaint = true; + break; + } + } + try testing.expect(observed_repaint); } -test "Metal borrowed texture set target renders into a replacement texture" { - if (!build_options.supports_metal) return error.SkipZigTest; - const device = MTLCreateSystemDefaultDevice() orelse return error.MetalDeviceUnavailable; - - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); - - // Allocated up front so the host outlives the session that borrows it. - const borrowed = try metal_support.createTexture(device, 128, 128); - defer metal_support.releaseObject(borrowed); - const replacement = try metal_support.createTexture(device, 64, 96); - defer metal_support.releaseObject(replacement); - try metal_support.clearTextureRGBA8(replacement, .{ 0, 0, 0, 0 }); +const SnapshotThread = struct { + fn run(session: maplibre.RenderSessionHandle, failure: *?anyerror) void { + _ = session.snapshot() catch |err| { + failure.* = err; + return; + }; + } +}; +test "render session controls are usable from another thread" { + if (!supports_test_owned_texture) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 16, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); + var owned = try attachTestOwnedTexture(&map, .{ .extent = .{ .width = 16, .height = 16 } }); + defer owned.close() catch @panic("render session close failed"); - var session = try maplibre.attachMetalBorrowedTexture(&map, .{ - .extent = .{ .width = 128, .height = 128 }, - .physical_width = 128, - .physical_height = 128, - .texture = maplibre.NativePointer.fromPtr(borrowed), - }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - // A caller-owned texture is sized by its owner, so resize is rejected and - // the host hands over a texture at the new size instead. - try testing.expectError(error.Unsupported, session.resize(.{ .width = 64, .height = 96, .scale_factor = 1.0 })); - - try session.setMetalBorrowedTextureTarget(.{ - .extent = .{ .width = 64, .height = 96 }, - .physical_width = 64, - .physical_height = 96, - .texture = maplibre.NativePointer.fromPtr(replacement), - }); - - // A surface descriptor names a target this session does not have; the - // rejection leaves it on the texture just handed over. - try testing.expectError(error.Unsupported, session.setMetalSurfaceTarget(.{ - .extent = .{ .width = 64, .height = 96 }, - .layer = fakeNativePointer(), - })); - - // Replacing the target enqueues the new size for the map's owner thread, - // so the map publishes a matching update only once pumped. - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), (try session.renderUpdate()).result); - try runtime.pump(0, null); - const resized = try map.getSize(); - try testing.expectEqual(@as(u32, 64), resized.width); - try testing.expectEqual(@as(u32, 96), resized.height); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - try expectPixelApprox(try metal_support.readTexturePixelRGBA8(replacement, 0, 0), .{ 0xd8, 0xf1, 0xff, 0xff }, 8); + var failure: ?anyerror = null; + const thread = try std.Thread.spawn(.{}, SnapshotThread.run, .{ owned.session, &failure }); + thread.join(); + try testing.expectEqual(@as(?anyerror, null), failure); } -test "Vulkan owned texture frame handle scopes native pointers" { +test "Vulkan borrowed texture replaces its target" { if (!build_options.supports_vulkan) return error.SkipZigTest; - - var context = try VulkanAttachContext.init(); - defer context.deinit(); - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachVulkanOwnedTexture(&map, .{ - .extent = .{ .width = 32, .height = 32, .scale_factor = 1.0 }, - .context = context.descriptor(), - }); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - var image = try readTestImage(&session, testing.allocator, 32 * 32 * 4); - defer image.deinit(); - try testing.expectEqual(@as(u32, 32), image.info.width); - try testing.expectEqual(@as(u32, 32), image.info.height); - try testing.expectEqual(@as(usize, 32 * 32 * 4), image.info.byte_length); - - var frame = try session.acquireVulkanOwnedTextureFrame(); - var frame_alias = frame; - const info = try frame.info(); - try testing.expectEqual(@as(u32, 32), info.width); - try testing.expectEqual(@as(u32, 32), info.height); - try testing.expectEqual(@as(u64, 1), info.generation); - try testing.expect(info.image.toPtr() != info.device.toPtr()); - try expectVulkanFrameReleaseWrongThread(&frame); - - try testing.expectError(error.ActiveBorrow, session.resize(.{ .width = 16, .height = 16, .scale_factor = 1.0 })); - try testing.expectError(error.ActiveBorrow, setPlaceholderBorrowedTextureTarget(&session)); - try testing.expectError(error.ActiveBorrow, session.renderUpdate()); - try testing.expectError(error.ActiveBorrow, session.detach()); - try testing.expectError(error.ActiveBorrow, session.acquireVulkanOwnedTextureFrame()); - try testing.expectError(error.ActiveBorrow, session.close()); - - try frame.release(); - try frame_alias.release(); - try testing.expectError(error.ClosedHandle, frame_alias.info()); - - // Resizing enqueues the new logical size for the map's owner thread, so - // the map publishes a matching update only once pumped. - try session.resize(.{ .width = 16, .height = 8, .scale_factor = 2.0 }); - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), (try session.renderUpdate()).result); - try runtime.pump(0, null); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - var resized_frame = try session.acquireVulkanOwnedTextureFrame(); - const resized_info = try resized_frame.info(); - try testing.expectEqual(@as(u32, 32), resized_info.width); - try testing.expectEqual(@as(u32, 16), resized_info.height); - try testing.expectEqual(@as(f64, 2.0), resized_info.scale_factor); - try testing.expectEqual(@as(u64, 2), resized_info.generation); - try resized_frame.release(); - - try expectRenderSessionCallWrongThread(&session, .acquire_vulkan_frame); - try session.close(); -} - -test "Vulkan borrowed texture renders through public bindings" { - if (!build_options.supports_vulkan) return error.SkipZigTest; - - var borrowed = try VulkanBorrowedImage.create(128, 128); + var borrowed = try VulkanBorrowedImage.create(32, 16); defer borrowed.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachVulkanBorrowedTexture(&map, borrowed.descriptor()); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - try testing.expectError(error.Unsupported, session.acquireVulkanOwnedTextureFrame()); - try testing.expectError(error.Unsupported, session.resize(.{ .width = 64, .height = 64, .scale_factor = 1.0 })); - var readback_buffer: [128 * 128 * 4]u8 = undefined; - try testing.expectError(error.Unsupported, session.readPremultipliedRgba8Into(&readback_buffer)); + var session = try finishAttachment(try maplibre.attachVulkanBorrowedTexture( + &map, + borrowed.descriptor(), + .{ .driver = .core_worker, .requested_texture_ring_depth = 1 }, + )); + defer closeSession(&session) catch @panic("render session close failed"); + + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); + for (0..1000) |_| { + if (std.meta.activeTag((try renderFrame(session, false)).disposition) == .rendered) break; + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + } else return error.FrameDidNotRender; + + const replacement = try borrowed.allocateReplacement(48, 24); + errdefer borrowed.release(replacement); + try finishOperation( + session, + try session.setVulkanBorrowedTextureTarget(borrowed.descriptorFor(replacement, 48, 24)), + ); + borrowed.adopt(replacement, 48, 24); + _ = try map.resize(48, 24, 1.0); + try awaitRuntimeBarrier(&runtime); + for (0..1000) |_| { + if (std.meta.activeTag((try renderFrame(session, false)).disposition) == .rendered) break; + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + } else return error.FrameDidNotRender; + const snapshot = try session.snapshot(); + try testing.expectEqual(@as(u32, 48), snapshot.extent.width); + try testing.expectEqual(@as(u32, 24), snapshot.extent.height); } -test "Vulkan borrowed texture set target renders into a replacement image" { - if (!build_options.supports_vulkan) return error.SkipZigTest; - - var borrowed = try VulkanBorrowedImage.create(128, 128); - defer borrowed.deinit(); - +test "OpenGL borrowed texture replaces its target" { + if (!build_options.supports_opengl) return error.SkipZigTest; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .width = 32, .height = 16 }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - - var session = try maplibre.attachVulkanBorrowedTexture(&map, borrowed.descriptor()); - defer session.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); - - // A caller-owned image is sized by its owner, so resize is rejected and - // the host hands over an image at the new size instead. - try testing.expectError(error.Unsupported, session.resize(.{ .width = 64, .height = 96, .scale_factor = 1.0 })); - - const replacement = try borrowed.allocateReplacement(64, 96); - try session.setVulkanBorrowedTextureTarget(borrowed.descriptorFor(replacement, 64, 96)); - borrowed.adopt(replacement, 64, 96); - - // A surface descriptor names a target this session does not have; the - // rejection leaves it on the image just handed over. - try testing.expectError(error.Unsupported, session.setVulkanSurfaceTarget(.{ - .extent = .{ .width = 64, .height = 96 }, - .context = borrowed.context.descriptor(), - .surface = fakeNativePointer(), - })); - - // Replacing the target enqueues the new size for the map's owner thread, - // so the map publishes a matching update only once pumped. - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), (try session.renderUpdate()).result); - try runtime.pump(0, null); - const resized = try map.getSize(); - try testing.expectEqual(@as(u32, 64), resized.width); - try testing.expectEqual(@as(u32, 96), resized.height); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try session.renderUpdate()).result); + var borrowed = try OpenGLBorrowedTexture.create(32, 16); + defer borrowed.deinit(); + var session = try finishAttachment(try maplibre.attachOpenGLBorrowedTexture( + &map, + borrowed.descriptor(), + .{ .driver = .caller_graphics_thread, .requested_texture_ring_depth = 1 }, + )); + defer closeSession(&session) catch @panic("render session close failed"); + + _ = try map.setStyleJson(testing.allocator, support.style_json); + try awaitRuntimeBarrier(&runtime); + try testing.expectEqual(.rendered, std.meta.activeTag((try renderFrame(session, false)).disposition)); + var initial_pixels: [32 * 16 * 4]u8 = undefined; + try borrowed.readRGBA8(&initial_pixels); + try testing.expect(hasNonZeroByte(&initial_pixels)); + + const replacement = try borrowed.allocateReplacement(48, 24); + errdefer borrowed.context.destroyTexture(replacement); + try finishOperation( + session, + try session.setOpenGLBorrowedTextureTarget(borrowed.descriptorFor(replacement, 48, 24)), + ); + borrowed.adopt(replacement, 48, 24); + _ = try map.resize(48, 24, 1.0); + try awaitRuntimeBarrier(&runtime); + for (0..1000) |_| { + if (std.meta.activeTag((try renderFrame(session, false)).disposition) == .rendered) break; + _ = try map.requestRepaint(); + try awaitRuntimeBarrier(&runtime); + } else return error.FrameDidNotRender; } diff --git a/bindings/zig/tests/resources.zig b/bindings/zig/tests/resources.zig index 3b93186d6..aebf09a16 100644 --- a/bindings/zig/tests/resources.zig +++ b/bindings/zig/tests/resources.zig @@ -15,63 +15,18 @@ fn sleepOneMillisecond() !void { try testing.io.sleep(.fromMilliseconds(1), .awake); } -fn rawStatusError(raw_status: i32) maplibre.NativeStatusError!void { - return switch (raw_status) { - 0 => {}, - -1 => error.InvalidArgument, - -2 => error.InvalidState, - -3 => error.WrongThread, - -4 => error.Unsupported, - -5 => error.NativeError, - else => error.UnknownStatus, - }; -} - -fn waitForOfflineOperation( - runtime: *maplibre.RuntimeHandle, - operation: maplibre.OfflineOperationHandle, -) !maplibre.OfflineOperationCompletedPayload { - const operation_id = try operation.operationId(); - for (0..5000) |_| { - try runtime.pump(0, null); - // One event per drain, so an event this wait is not looking for stays - // queued for the wait that is. See support.waitForEvent. - while (true) { - var batch = try runtime.drainEvents(testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) break; - const event = try batch.at(0); - const payload = switch (event.payload) { - .offline_operation_completed => |completed| completed, - else => continue, - }; - if (payload.operation_id != operation_id) continue; - try testing.expectEqual(payload.operation_kind.toRaw(), payload.raw_operation_kind); - try testing.expectEqual(payload.result_kind.toRaw(), payload.raw_result_kind); - try rawStatusError(payload.result_status); - return payload; - } - try sleepOneMillisecond(); - } - return error.EventNotObserved; +fn resolve(comptime T: type, future_value: maplibre.Future(T)) !T { + var future = future_value; + defer future.deinit(); + return future.wait(null); } fn runAmbientCacheOperation(runtime: *maplibre.RuntimeHandle, operation: maplibre.AmbientCacheOperation) !void { - const handle = try runtime.startAmbientCacheOperation(operation); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.runAmbientCacheOperation(operation)); } fn setMaximumAmbientCacheSize(runtime: *maplibre.RuntimeHandle, size: u64) !void { - const handle = try runtime.startSetMaximumAmbientCacheSize(size); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.setMaximumAmbientCacheSize(size)); } fn createOfflineRegion( @@ -80,9 +35,7 @@ fn createOfflineRegion( definition: maplibre.OfflineRegionDefinition, metadata: []const u8, ) !maplibre.OwnedOfflineRegion { - const handle = try runtime.startCreateOfflineRegion(allocator, definition, metadata); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOfflineRegion(allocator, handle); + return resolve(maplibre.OwnedOfflineRegion, try runtime.createOfflineRegion(allocator, definition, metadata)); } fn getOfflineRegion( @@ -90,15 +43,13 @@ fn getOfflineRegion( allocator: std.mem.Allocator, region_id: maplibre.OfflineRegionId, ) !?maplibre.OwnedOfflineRegion { - const handle = try runtime.startGetOfflineRegion(region_id); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOptionalOfflineRegion(allocator, handle); + _ = allocator; + return resolve(?maplibre.OwnedOfflineRegion, try runtime.getOfflineRegion(region_id)); } fn listOfflineRegions(runtime: *maplibre.RuntimeHandle, allocator: std.mem.Allocator) !maplibre.OfflineRegionList { - const handle = try runtime.startListOfflineRegions(); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOfflineRegionList(allocator, handle); + _ = allocator; + return resolve(maplibre.OfflineRegionList, try runtime.listOfflineRegions()); } fn mergeOfflineRegionsDatabase( @@ -106,9 +57,7 @@ fn mergeOfflineRegionsDatabase( allocator: std.mem.Allocator, side_database_path: []const u8, ) !maplibre.OfflineRegionList { - const handle = try runtime.startMergeOfflineRegionsDatabase(allocator, side_database_path); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOfflineRegionList(allocator, handle); + return resolve(maplibre.OfflineRegionList, try runtime.mergeOfflineRegionsDatabase(allocator, side_database_path)); } fn updateOfflineRegionMetadata( @@ -117,24 +66,16 @@ fn updateOfflineRegionMetadata( region_id: maplibre.OfflineRegionId, metadata: []const u8, ) !maplibre.OwnedOfflineRegion { - const handle = try runtime.startUpdateOfflineRegionMetadata(region_id, metadata); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOfflineRegion(allocator, handle); + _ = allocator; + return resolve(maplibre.OwnedOfflineRegion, try runtime.updateOfflineRegionMetadata(region_id, metadata)); } fn getOfflineRegionStatus(runtime: *maplibre.RuntimeHandle, region_id: maplibre.OfflineRegionId) !maplibre.OfflineRegionStatus { - const handle = try runtime.startGetOfflineRegionStatus(region_id); - _ = try waitForOfflineOperation(runtime, handle); - return runtime.takeOfflineRegionStatus(handle); + return resolve(maplibre.OfflineRegionStatus, try runtime.getOfflineRegionStatus(region_id)); } fn setOfflineRegionObserved(runtime: *maplibre.RuntimeHandle, region_id: maplibre.OfflineRegionId, observed: bool) !void { - const handle = try runtime.startSetOfflineRegionObserved(region_id, observed); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.setOfflineRegionObserved(region_id, observed)); } fn setOfflineRegionDownloadState( @@ -142,30 +83,15 @@ fn setOfflineRegionDownloadState( region_id: maplibre.OfflineRegionId, download_state: maplibre.OfflineRegionDownloadState, ) !void { - const handle = try runtime.startSetOfflineRegionDownloadState(region_id, download_state); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.setOfflineRegionDownloadState(region_id, download_state)); } fn invalidateOfflineRegion(runtime: *maplibre.RuntimeHandle, region_id: maplibre.OfflineRegionId) !void { - const handle = try runtime.startInvalidateOfflineRegion(region_id); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.invalidateOfflineRegion(region_id)); } fn deleteOfflineRegion(runtime: *maplibre.RuntimeHandle, region_id: maplibre.OfflineRegionId) !void { - const handle = try runtime.startDeleteOfflineRegion(region_id); - _ = waitForOfflineOperation(runtime, handle) catch |err| { - handle.discard() catch {}; - return err; - }; - try handle.discard(); + _ = try resolve(void, try runtime.deleteOfflineRegion(region_id)); } fn waitForStyleLoaded(runtime: *maplibre.RuntimeHandle) !void { @@ -231,7 +157,7 @@ test "network status APIs wrap process-global MapLibre status" { test "ambient cache operations validate cache configuration" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); try runAmbientCacheOperation(&runtime, .pack_database); - try runtime.close(); + try support.closeRuntime(&runtime); var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -239,7 +165,7 @@ test "ambient cache operations validate cache configuration" { defer testing.allocator.free(cache_path); var cached_runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer cached_runtime.close() catch @panic("cached runtime close failed"); + defer support.closeRuntime(&cached_runtime) catch @panic("cached runtime close failed"); try runAmbientCacheOperation(&cached_runtime, .reset_database); try runAmbientCacheOperation(&cached_runtime, .pack_database); try runAmbientCacheOperation(&cached_runtime, .invalidate); @@ -251,12 +177,16 @@ test "file URL style loads through public binding" { defer fixture.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, fixture.style_url); + _ = try map.setStyleUrl(testing.allocator, fixture.style_url); try waitForStyleLoaded(&runtime); } @@ -270,12 +200,16 @@ test "asset URL style loads through public binding runtime asset path" { defer fixture.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .asset_path = fixture.dir_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "asset://style.json"); + _ = try map.setStyleUrl(testing.allocator, "asset://style.json"); try waitForStyleLoaded(&runtime); } @@ -289,12 +223,16 @@ test "missing file URL reports map loading failure through public events" { defer testing.allocator.free(missing_url); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, missing_url); + _ = try map.setStyleUrl(testing.allocator, missing_url); try testing.expect(try support.waitForEvent(&runtime, .map_loading_failed)); } @@ -335,7 +273,7 @@ fn setResourceTransformOnThread( transform: maplibre.ResourceTransform, out_error: *?anyerror, ) void { - runtime.setResourceTransform(transform) catch |err| { + _ = runtime.setResourceTransform(transform) catch |err| { out_error.* = err; return; }; @@ -375,14 +313,18 @@ test "resource transform can be cleared after map creation" { defer maplibre.setNetworkStatus(.online, null) catch @panic("network status restore failed"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = TransformState{ .replacement_url = "unsupported://rewritten-style.json" }; - try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); + _ = try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try runtime.setResourceTransform(null); + _ = try runtime.setResourceTransform(null); var address = try std.Io.net.IpAddress.parse("127.0.0.1", 0); var server = try address.listen(testing.io, .{ .reuse_address = true }); @@ -396,7 +338,7 @@ test "resource transform can be cleared after map creation" { const style_url = try std.fmt.allocPrint(testing.allocator, "http://127.0.0.1:{d}/style.json", .{server.socket.address.getPort()}); defer testing.allocator.free(style_url); - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); try waitForStyleLoaded(&runtime); server_thread.join(); server_thread_joined = true; @@ -422,12 +364,16 @@ test "http URL style loads through native network provider" { defer testing.allocator.free(style_url); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); try waitForStyleLoaded(&runtime); server_thread.join(); server_thread_joined = true; @@ -475,11 +421,13 @@ test "http style can load from ambient cache after online load" { { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); try setMaximumAmbientCacheSize(&runtime, 1024 * 1024); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); try waitForStyleLoaded(&runtime); try runAmbientCacheOperation(&runtime, .pack_database); } @@ -491,10 +439,12 @@ test "http style can load from ambient cache after online load" { try maplibre.setNetworkStatus(.offline, null); var cached_runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer cached_runtime.close() catch @panic("cached runtime close failed"); - var cached_map = try maplibre.MapHandle.create(&cached_runtime, .{}); + defer support.closeRuntime(&cached_runtime) catch @panic("cached runtime close failed"); + var cached_map_future = try maplibre.MapHandle.create(&cached_runtime, .{}); + defer cached_map_future.deinit(); + var cached_map = try cached_map_future.wait(null); defer cached_map.close() catch @panic("cached map close failed"); - try cached_map.setStyleUrl(testing.allocator, style_url); + _ = try cached_map.setStyleUrl(testing.allocator, style_url); try waitForStyleLoaded(&cached_runtime); } @@ -516,13 +466,17 @@ test "resource provider pass-through delegates to native HTTP" { var state = PassThroughProviderState{}; var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - try runtime.setResourceProvider(.{ .handler = passThroughStyleProvider, .context = &state }); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + _ = try runtime.setResourceProvider(.{ .handler = passThroughStyleProvider, .context = &state }); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); try waitForStyleLoaded(&runtime); server_thread.join(); server_thread_joined = true; @@ -537,7 +491,7 @@ test "resource transform rewrites network style URL" { defer maplibre.setNetworkStatus(.online, null) catch @panic("network status restore failed"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); const original_url = "http://example.invalid/original-style.json"; var state = TransformState{ @@ -546,16 +500,19 @@ test "resource transform rewrites network style URL" { var replacement_state = TransformState{ .replacement_url = "unsupported://unexpected-replacement.json", }; - try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); + _ = try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &replacement_state }); + _ = try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &replacement_state }); - try map.setStyleUrl(testing.allocator, original_url); + _ = try map.setStyleUrl(testing.allocator, original_url); for (0..1000) |_| { - try runtime.pump(0, null); _ = try support.drainEvents(&runtime); if (replacement_state.calls.load(.seq_cst) > 0) break; try sleepOneMillisecond(); @@ -565,44 +522,39 @@ test "resource transform rewrites network style URL" { try testing.expect(replacement_state.calls.load(.seq_cst) > 0); } -test "failed resource transform replacement keeps previous callback" { +test "resource transform replacement is accepted from another thread" { try maplibre.setNetworkStatus(.online, null); defer maplibre.setNetworkStatus(.online, null) catch @panic("network status restore failed"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var state = TransformState{ - .replacement_url = "unsupported://active-transform.json", - }; - var replacement_state = TransformState{ - .replacement_url = "unsupported://failed-replacement.json", - }; - try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); - - var replacement_error: ?anyerror = null; - const failed_replacement = maplibre.ResourceTransform{ .handler = rewriteStyleUrl, .context = &replacement_state }; - const thread = try std.Thread.spawn(.{}, setResourceTransformOnThread, .{ - &runtime, - failed_replacement, - &replacement_error, - }); + var state = TransformState{ .replacement_url = "unsupported://active-transform.json" }; + var replacement_state = TransformState{ .replacement_url = "unsupported://replacement.json" }; + _ = try runtime.setResourceTransform(.{ .handler = rewriteStyleUrl, .context = &state }); + + var replacement_error: ?anyerror = error.Unexpected; + const replacement = maplibre.ResourceTransform{ .handler = rewriteStyleUrl, .context = &replacement_state }; + const thread = try std.Thread.spawn(.{}, setResourceTransformOnThread, .{ &runtime, replacement, &replacement_error }); thread.join(); - try testing.expectEqual(error.WrongThread, replacement_error.?); + try testing.expect(replacement_error == null); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "http://example.invalid/original-style.json"); + _ = try map.setStyleUrl(testing.allocator, "http://example.invalid/original-style.json"); for (0..1000) |_| { - try runtime.pump(0, null); _ = try support.drainEvents(&runtime); - if (state.calls.load(.seq_cst) > 0) break; + if (replacement_state.calls.load(.seq_cst) > 0) break; try sleepOneMillisecond(); } - try testing.expect(state.calls.load(.seq_cst) > 0); - try testing.expectEqual(@as(usize, 0), replacement_state.calls.load(.seq_cst)); + try testing.expectEqual(@as(usize, 0), state.calls.load(.seq_cst)); + try testing.expect(replacement_state.calls.load(.seq_cst) > 0); } const ProviderState = struct { @@ -721,9 +673,8 @@ fn pmtilesRangeProvider( return .pass_through; } -fn waitForPmtilesRangeRequest(runtime: *maplibre.RuntimeHandle, state: *PmtilesRangeProviderState) !void { +fn waitForPmtilesRangeRequest(_: *maplibre.RuntimeHandle, state: *PmtilesRangeProviderState) !void { for (0..1000) |_| { - try runtime.pump(0, null); if (state.recorded_pmtiles_request.load(.seq_cst)) return; try sleepOneMillisecond(); } @@ -732,30 +683,38 @@ fn waitForPmtilesRangeRequest(runtime: *maplibre.RuntimeHandle, state: *PmtilesR test "resource provider observes PMTiles range metadata" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = PmtilesRangeProviderState{}; - try runtime.setResourceProvider(.{ .handler = pmtilesRangeProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = pmtilesRangeProvider, .context = &state }); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, pmtiles_style_url); + _ = try map.setStyleUrl(testing.allocator, pmtiles_style_url); try waitForPmtilesRangeRequest(&runtime, &state); try state.expectObservedRequest(); } test "custom URL style loads through resource provider" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = ProviderState{}; - try runtime.setResourceProvider(.{ .handler = customStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = customStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://style.json"); try waitForStyleLoaded(&runtime); try testing.expect(state.calls.load(.seq_cst) > 0); try testing.expectEqual(@as(usize, 1), state.completions.load(.seq_cst)); @@ -799,15 +758,19 @@ fn aliasStyleProvider( test "resource provider sees scheme alias and its resolved URL" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AliasProviderState{}; - try runtime.setResourceProvider(.{ .handler = aliasStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = aliasStyleProvider, .context = &state }); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "maplibre://maps/style"); + _ = try map.setStyleUrl(testing.allocator, "maplibre://maps/style"); try waitForStyleLoaded(&runtime); const len = state.resolved_url_len.load(.seq_cst); @@ -838,7 +801,7 @@ fn countingProvider( // Requests a style whose scheme no file source serves, so the failure event // proves the request reached the network file source. fn loadProbeStyle(runtime: *maplibre.RuntimeHandle, map: *maplibre.MapHandle, style_url: []const u8) !void { - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); var event = try support.waitForOwnedEvent(runtime, .map_loading_failed); defer event.deinit(); try testing.expect(std.mem.indexOf(u8, event.message, "\"jar\"") != null); @@ -849,31 +812,35 @@ test "resource provider can be replaced and cleared after map creation" { defer maplibre.setNetworkStatus(.online, null) catch @panic("network status restore failed"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); var installed = CountingProviderState{}; - try runtime.setResourceProvider(.{ .handler = countingProvider, .context = &installed }); + _ = try runtime.setResourceProvider(.{ .handler = countingProvider, .context = &installed }); try loadProbeStyle(&runtime, &map, "jar:file:/packaged/first.json"); try testing.expect(installed.calls.load(.seq_cst) > 0); var replacement = CountingProviderState{}; - try runtime.setResourceProvider(.{ .handler = countingProvider, .context = &replacement }); + _ = try runtime.setResourceProvider(.{ .handler = countingProvider, .context = &replacement }); const installed_calls = installed.calls.load(.seq_cst); try loadProbeStyle(&runtime, &map, "jar:file:/packaged/second.json"); try testing.expect(replacement.calls.load(.seq_cst) > 0); try testing.expectEqual(installed_calls, installed.calls.load(.seq_cst)); - try runtime.setResourceProvider(null); + _ = try runtime.setResourceProvider(null); const replacement_calls = replacement.calls.load(.seq_cst); try loadProbeStyle(&runtime, &map, "jar:file:/packaged/third.json"); try testing.expectEqual(installed_calls, installed.calls.load(.seq_cst)); try testing.expectEqual(replacement_calls, replacement.calls.load(.seq_cst)); // Clearing an already cleared provider stays a successful no-op. - try runtime.setResourceProvider(null); + _ = try runtime.setResourceProvider(null); } const offline_style_url = "http://example.com/offline-style.json"; @@ -919,7 +886,7 @@ test "offline tile-pyramid regions copy definitions and metadata" { { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var created = try createOfflineRegion(&runtime, testing.allocator, offlineTileDefinition(), metadata[0..]); defer created.deinit(); @@ -941,7 +908,7 @@ test "offline tile-pyramid regions copy definitions and metadata" { { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var reloaded = (try getOfflineRegion(&runtime, testing.allocator, region_id)) orelse return error.RegionReloadFailed; defer reloaded.deinit(); @@ -966,7 +933,7 @@ test "offline region definitions reject invalid public values" { defer testing.allocator.free(cache_path); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var invalid_zoom = offlineTileDefinition(); invalid_zoom.tile_pyramid.min_zoom = 8.0; @@ -991,13 +958,13 @@ test "offline region style URL rejects embedded NUL with binding diagnostic" { try diagnostics.set(-5, "stale native diagnostic"); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var definition = offlineTileDefinition(); definition.tile_pyramid.style_url = "asset://offline\x00style.json"; try testing.expectError( error.InvalidString, - runtime.startCreateOfflineRegion(testing.allocator, definition, &.{}), + runtime.createOfflineRegion(testing.allocator, definition, &.{}), ); const diagnostic = diagnostics.get().?; @@ -1030,13 +997,13 @@ test "offline database merge returns copied region list" { const metadata = [_]u8{ 5, 4, 3 }; { var side_runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = side_cache_path }, null); - defer side_runtime.close() catch @panic("side runtime close failed"); + defer support.closeRuntime(&side_runtime) catch @panic("side runtime close failed"); var created = try createOfflineRegion(&side_runtime, testing.allocator, offlineTileDefinition(), metadata[0..]); defer created.deinit(); } var main_runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = main_cache_path }, null); - defer main_runtime.close() catch @panic("main runtime close failed"); + defer support.closeRuntime(&main_runtime) catch @panic("main runtime close failed"); var merged = try mergeOfflineRegionsDatabase(&main_runtime, testing.allocator, side_cache_path); defer merged.deinit(); try testing.expectEqual(@as(usize, 1), merged.items.len); @@ -1054,7 +1021,7 @@ test "offline geometry regions expose copied geometry values" { { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var created = try createOfflineRegion(&runtime, testing.allocator, .{ .geometry = .{ .style_url = offline_style_url, @@ -1076,7 +1043,7 @@ test "offline geometry regions expose copied geometry values" { { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var reloaded = (try getOfflineRegion(&runtime, testing.allocator, region_id)) orelse return error.RegionReloadFailed; defer reloaded.deinit(); @@ -1154,9 +1121,8 @@ fn delayedStyleProvider( return .handle; } -fn waitForProviderHandle(runtime: *maplibre.RuntimeHandle, state: *AsyncProviderState) !maplibre.ResourceRequestHandle { +fn waitForProviderHandle(_: *maplibre.RuntimeHandle, state: *AsyncProviderState) !maplibre.ResourceRequestHandle { for (0..1000) |_| { - try runtime.pump(0, null); if (state.takeHandle()) |handle| return handle; try sleepOneMillisecond(); } @@ -1196,15 +1162,19 @@ fn probeCancellationUntilClosed(handle: maplibre.ResourceRequestHandle, state: * test "resource provider can complete style request later" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const handle = try waitForProviderHandle(&runtime, &state); defer handle.release(); @@ -1222,20 +1192,24 @@ test "resource provider can complete style request later" { test "released resource request handle copies stay closed after later requests" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const stale_handle = try waitForProviderHandle(&runtime, &state); stale_handle.release(); try testing.expectError(error.ClosedHandle, stale_handle.cancelled()); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const live_handle = try waitForProviderHandle(&runtime, &state); defer live_handle.release(); try testing.expectError(error.ClosedHandle, stale_handle.complete(.{ .bytes = support.style_json })); @@ -1246,15 +1220,19 @@ test "released resource request handle copies stay closed after later requests" test "resource request release is synchronized with cancellation checks" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const handle = try waitForProviderHandle(&runtime, &state); var probe = CancellationProbeState{}; @@ -1276,18 +1254,22 @@ test "resource request release is synchronized with cancellation checks" { test "resource request handles stay usable across many handled requests" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); for (0..16) |request_index| { const style_url = try std.fmt.allocPrint(testing.allocator, "custom://delayed-style-{d}.json", .{request_index}); defer testing.allocator.free(style_url); - try map.setStyleUrl(testing.allocator, style_url); + _ = try map.setStyleUrl(testing.allocator, style_url); const handle = try waitForProviderHandle(&runtime, &state); try state.expectObservedRequest(); @@ -1300,15 +1282,19 @@ test "resource request handles stay usable across many handled requests" { test "resource provider can complete request from another thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const handle = try waitForProviderHandle(&runtime, &state); defer handle.release(); @@ -1340,22 +1326,26 @@ fn errorStyleProvider( test "resource provider error response fails style load" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + _ = try runtime.setResourceProvider(.{ .handler = errorStyleProvider }); - try runtime.setResourceProvider(.{ .handler = errorStyleProvider }); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleUrl(testing.allocator, "custom://error-style.json"); + _ = try map.setStyleUrl(testing.allocator, "custom://error-style.json"); try testing.expect(try support.waitForEvent(&runtime, .map_loading_failed)); } test "offline region download errors are runtime events" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - try runtime.setResourceProvider(.{ .handler = errorStyleProvider }); + _ = try runtime.setResourceProvider(.{ .handler = errorStyleProvider }); var definition = offlineTileDefinition(); definition.tile_pyramid.style_url = "custom://error-style.json"; @@ -1381,10 +1371,9 @@ test "offline region download errors are runtime events" { try testing.expect(event.message.len > 0); } -fn waitForRequestCancellation(runtime: *maplibre.RuntimeHandle, handle: maplibre.ResourceRequestHandle) !void { +fn waitForRequestCancellation(_: *maplibre.RuntimeHandle, handle: maplibre.ResourceRequestHandle) !void { for (0..5000) |_| { if (try handle.cancelled()) return; - try runtime.pump(0, null); try sleepOneMillisecond(); } return error.RequestNotCancelled; @@ -1395,13 +1384,17 @@ test "resource provider observes cancellation before late completion" { defer diagnostics.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var state = AsyncProviderState{}; - try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + _ = try runtime.setResourceProvider(.{ .handler = delayedStyleProvider, .context = &state }); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); - try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); + var map = try map_future.wait(null); + _ = try map.setStyleUrl(testing.allocator, "custom://delayed-style.json"); const handle = try waitForProviderHandle(&runtime, &state); defer handle.release(); @@ -1421,7 +1414,7 @@ test "offline region download control emits copied status events" { defer testing.allocator.free(cache_path); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{ .cache_path = cache_path }, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); const metadata = [_]u8{9}; var created = try createOfflineRegion(&runtime, testing.allocator, offlineTileDefinition(), metadata[0..]); @@ -1438,8 +1431,7 @@ test "offline region download control emits copied status events" { var observed = false; for (0..5000) |_| { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(testing.allocator, 0); + var batch = try runtime.drainEvents(testing.allocator); defer batch.deinit(); for (0..batch.len()) |index| { const event = try batch.at(index); diff --git a/bindings/zig/tests/runtime.zig b/bindings/zig/tests/runtime.zig index 95c350ea4..8fe9cc8da 100644 --- a/bindings/zig/tests/runtime.zig +++ b/bindings/zig/tests/runtime.zig @@ -4,16 +4,8 @@ const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); const support = @import("support.zig"); -fn runRuntimeOnThread(runtime: *maplibre.RuntimeHandle, out_error: *?anyerror) void { - runtime.pump(0, null) catch |err| { - out_error.* = err; - return; - }; - out_error.* = null; -} - fn drainRuntimeOnThread(runtime: *maplibre.RuntimeHandle, out_error: *?anyerror) void { - var batch = runtime.drainEvents(testing.allocator, 0) catch |err| { + var batch = runtime.drainEvents(testing.allocator) catch |err| { out_error.* = err; return; }; @@ -30,7 +22,7 @@ fn setRuntimeEventMaskOnThread(runtime: *maplibre.RuntimeHandle, out_error: *?an } fn setMapEventMaskOnThread(map: *maplibre.MapHandle, out_error: *?anyerror) void { - map.setEventMask(maplibre.RuntimeEventMask.all) catch |err| { + _ = map.setEventMask(maplibre.RuntimeEventMask.all) catch |err| { out_error.* = err; return; }; @@ -38,7 +30,7 @@ fn setMapEventMaskOnThread(map: *maplibre.MapHandle, out_error: *?anyerror) void } fn closeRuntimeOnThread(runtime: *maplibre.RuntimeHandle, out_error: *?anyerror) void { - runtime.close() catch |err| { + support.closeRuntime(runtime) catch |err| { out_error.* = err; return; }; @@ -50,7 +42,7 @@ fn createRuntimeOnThread(out_error: *?anyerror) void { out_error.* = err; return; }; - runtime.close() catch |err| { + support.closeRuntime(&runtime) catch |err| { out_error.* = err; return; }; @@ -75,16 +67,14 @@ fn expectOnlySelectedTypes( rejected: maplibre.RuntimeEventType, ) !void { // Narrowing gates later events and keeps queued ones, so start empty. - try runtime.pump(0, null); _ = try support.drainEvents(runtime); - try map.setStyleJson(testing.allocator, support.style_json); - try map.requestRepaint(); + _ = try map.setStyleJson(testing.allocator, support.style_json); + _ = try map.requestRepaint(); var saw_style_loaded = false; for (0..1000) |_| { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(testing.allocator, 0); + var batch = try runtime.drainEvents(testing.allocator); defer batch.deinit(); for (0..batch.len()) |index| { const event = try batch.at(index); @@ -99,11 +89,12 @@ fn expectOnlySelectedTypes( try testing.expect(saw_style_loaded); } -test "runtime rejects second runtime on same owner and permits distinct owner" { +test "runtimes can be created on the current thread or another thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - try testing.expectError(error.InvalidState, maplibre.RuntimeHandle.create(testing.allocator, .{}, null)); + var second = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); + defer support.closeRuntime(&second) catch @panic("runtime close failed"); var thread_error: ?anyerror = error.InvalidState; const thread = try std.Thread.spawn(.{}, createRuntimeOnThread, .{&thread_error}); @@ -111,57 +102,40 @@ test "runtime rejects second runtime on same owner and permits distinct owner" { try testing.expect(thread_error == null); } -test "wrong-thread runtime failures propagate diagnostics" { - var diagnostics = maplibre.DiagnosticStore.init(testing.allocator); - defer diagnostics.deinit(); - - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, &diagnostics); - var runtime_open = true; - defer if (runtime_open) runtime.close() catch @panic("runtime close failed"); - - var run_once_error: ?anyerror = null; - const run_once_thread = try std.Thread.spawn(.{}, runRuntimeOnThread, .{ &runtime, &run_once_error }); - run_once_thread.join(); - try testing.expectEqual(error.WrongThread, run_once_error.?); - try testing.expect(diagnostics.get().?.message.len > 0); +test "runtime drain and close are callable from another thread" { + var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); var drain_error: ?anyerror = null; const drain_thread = try std.Thread.spawn(.{}, drainRuntimeOnThread, .{ &runtime, &drain_error }); drain_thread.join(); - try testing.expectEqual(error.WrongThread, drain_error.?); - try testing.expect(diagnostics.get().?.message.len > 0); + try testing.expect(drain_error == null); var close_error: ?anyerror = null; const close_thread = try std.Thread.spawn(.{}, closeRuntimeOnThread, .{ &runtime, &close_error }); close_thread.join(); - try testing.expectEqual(error.WrongThread, close_error.?); - try testing.expect(diagnostics.get().?.message.len > 0); - - try runtime.pump(0, null); - try runtime.close(); - runtime_open = false; + try testing.expect(close_error == null); } -test "event mask setters report wrong thread" { +test "event mask setters accept calls from another thread" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); var runtime_mask_error: ?anyerror = null; const runtime_mask_thread = try std.Thread.spawn(.{}, setRuntimeEventMaskOnThread, .{ &runtime, &runtime_mask_error }); runtime_mask_thread.join(); - try testing.expectEqual(error.WrongThread, runtime_mask_error.?); + try testing.expect(runtime_mask_error == null); var map_mask_error: ?anyerror = null; const map_mask_thread = try std.Thread.spawn(.{}, setMapEventMaskOnThread, .{ &map, &map_mask_error }); map_mask_thread.join(); - try testing.expectEqual(error.WrongThread, map_mask_error.?); - - // The owner thread still installs both masks. - try runtime.setEventMask(maplibre.RuntimeEventMask.all); - try map.setEventMask(maplibre.RuntimeEventMask.all); + try testing.expect(map_mask_error == null); } test "runtime option strings reject embedded NUL before C calls" { @@ -181,21 +155,22 @@ test "runtime option strings reject embedded NUL before C calls" { test "one drain reports the events a style load queued together" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); - try map.setStyleJson(testing.allocator, support.style_json); + _ = try map.setStyleJson(testing.allocator, support.style_json); var largest_batch: usize = 0; var saw_style_loaded = false; for (0..1000) |_| { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(testing.allocator, 0); + var batch = try runtime.drainEvents(testing.allocator); defer batch.deinit(); - // An unbounded drain takes the whole queue. - try testing.expectEqual(@as(usize, 0), batch.remaining()); largest_batch = @max(largest_batch, batch.len()); for (0..batch.len()) |index| { const event = try batch.at(index); @@ -209,66 +184,27 @@ test "one drain reports the events a style load queued together" { try testing.expect(saw_style_loaded); try testing.expect(largest_batch > 1); } - -test "a bounded drain reports one event at a time in queue order" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - // A malformed inline style is reported twice: as the error here and as a - // queued loading-failed event carrying the same text. - try testing.expectError(error.NativeError, map.setStyleJson(testing.allocator, "{")); - try map.requestRepaint(); - - var message_length: usize = 0; - var count: usize = 0; - var saw_remaining = false; - var last = maplibre.RuntimeEventType.map_idle; - while (true) { - var batch = try runtime.drainEvents(testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) { - try testing.expectEqual(@as(usize, 0), batch.remaining()); - break; - } - try testing.expectEqual(@as(usize, 1), batch.len()); - // The bound leaves the rest of the queue for the next drain. - if (batch.remaining() > 0) saw_remaining = true; - const event = try batch.at(0); - if (std.meta.eql(event.event_type, maplibre.RuntimeEventType.map_loading_failed)) { - message_length = event.message.len; - } - last = event.event_type; - count += 1; - } - try testing.expect(count > 1); - try testing.expect(saw_remaining); - // The loading failure carries native's own text. - try testing.expect(message_length > 0); - // The repaint queued its invalidation behind the load failure. - try testing.expect(std.meta.eql(last, maplibre.RuntimeEventType.map_render_update_available)); -} - test "a drained batch outlives its runtime" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); var runtime_open = true; - defer if (runtime_open) runtime.close() catch @panic("runtime close failed"); + defer if (runtime_open) support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); var map_open = true; defer if (map_open) map.close() catch @panic("map close failed"); const map_id = try map.id(); - try map.setStyleUrl(testing.allocator, "unsupported://style.json"); + _ = try map.setStyleUrl(testing.allocator, "unsupported://style.json"); var kept: maplibre.EventBatch = undefined; var kept_index: usize = 0; var found = false; for (0..1000) |_| { - try runtime.pump(0, null); - var batch = try runtime.drainEvents(testing.allocator, 0); + var batch = try runtime.drainEvents(testing.allocator); for (0..batch.len()) |index| { const event = try batch.at(index); if (!std.meta.eql(event.event_type, maplibre.RuntimeEventType.map_loading_failed)) continue; @@ -291,7 +227,7 @@ test "a drained batch outlives its runtime" { try map.close(); map_open = false; - try runtime.close(); + try support.closeRuntime(&runtime); runtime_open = false; const after_close = try kept.at(kept_index); @@ -299,26 +235,43 @@ test "a drained batch outlives its runtime" { try testing.expect(after_close.message.len > 0); } -test "closing a map discards its queued runtime events" { +test "closing a map leaves its queued runtime events unchanged" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - try testing.expectError(error.NativeError, map.setStyleJson(testing.allocator, "{")); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); + _ = try map.setStyleJson(testing.allocator, "{"); + try support.waitForBarrier(&runtime); try map.close(); - try testing.expectEqual(@as(usize, 0), try support.drainEvents(&runtime)); + var batch = try runtime.drainEvents(testing.allocator); + defer batch.deinit(); + try testing.expect(batch.len() > 0); + var found_source = false; + for (0..batch.len()) |index| { + const event = try batch.at(index); + if (event.source_type == .map and event.source != .none) found_source = true; + } + try testing.expect(found_source); } test "event masks round-trip through both handles" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); var runtime_mask = try runtime.eventMask(); - runtime_mask.offline_operation_completed = false; + runtime_mask.offline_region_status_changed = false; try runtime.setEventMask(runtime_mask); const read_runtime_mask = try runtime.eventMask(); try testing.expectEqual(runtime_mask, read_runtime_mask); @@ -327,10 +280,11 @@ test "event masks round-trip through both handles" { var map_mask = try map.eventMask(); map_mask.map_tile_action = false; - try map.setEventMask(map_mask); + _ = try map.setEventMask(map_mask); + try support.waitForBarrier(&runtime); const read_map_mask = try map.eventMask(); try testing.expectEqual(map_mask, read_map_mask); - try testing.expect(read_map_mask.offline_operation_completed); + try testing.expect(read_map_mask.offline_region_status_changed); } // A newer native library can report an event type this binding does not name, @@ -342,150 +296,45 @@ test "mask membership rejects an unknown type no mask bit can hold" { test "a narrowed map mask drops the type it clears and keeps the rest" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + var map_future = try maplibre.MapHandle.create(&runtime, .{}); + + defer map_future.deinit(); + + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); const narrowed = maskWithout("map_render_update_available"); - try map.setEventMask(narrowed); + _ = try map.setEventMask(narrowed); + try support.waitForBarrier(&runtime); try testing.expectEqual(narrowed, try map.eventMask()); try expectOnlySelectedTypes(&runtime, &map, .map_render_update_available); // Restoring the bit lets the map's only invalidation report arrive again. - try map.setEventMask(maplibre.RuntimeEventMask.all); - try map.requestRepaint(); + _ = try map.setEventMask(maplibre.RuntimeEventMask.all); + try support.waitForBarrier(&runtime); + _ = try map.requestRepaint(); try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); } test "masks passed as create options narrow both handles" { - const narrowed_runtime_mask = maskWithout("offline_operation_completed"); + const narrowed_runtime_mask = maskWithout("offline_region_status_changed"); var runtime = try maplibre.RuntimeHandle.create( testing.allocator, .{ .event_mask = narrowed_runtime_mask }, null, ); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); try testing.expectEqual(narrowed_runtime_mask, try runtime.eventMask()); const narrowed_map_mask = maskWithout("map_render_update_available"); - var map = try maplibre.MapHandle.create(&runtime, .{ .event_mask = narrowed_map_mask }); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .event_mask = narrowed_map_mask }); + defer map_future.deinit(); + var map = try map_future.wait(null); defer map.close() catch @panic("map close failed"); try testing.expectEqual(narrowed_map_mask, try map.eventMask()); try expectOnlySelectedTypes(&runtime, &map, .map_render_update_available); } - -// Pumps until the runtime is idle, so a park that follows is released by the -// signal the test raises. -fn quiesce(runtime: *maplibre.RuntimeHandle) !void { - for (0..100) |_| { - try runtime.pump(0, null); - if ((try support.drainEvents(runtime)) == 0) return; - } - return error.RuntimeKeptProducingEvents; -} - -fn elapsedMilliseconds(started: std.Io.Timestamp) u64 { - const elapsed = started.durationTo(std.Io.Clock.awake.now(testing.io)); - return @intCast(@divTrunc(elapsed.toNanoseconds(), std.time.ns_per_ms)); -} - -fn signalWakeSourceOnThread(source: maplibre.WakeSourceHandle, out_error: *?anyerror) void { - testing.io.sleep(.fromMilliseconds(20), .awake) catch |err| { - out_error.* = err; - return; - }; - source.signal() catch |err| { - out_error.* = err; - return; - }; - out_error.* = null; -} - -test "a parked owner thread wakes for native work and for a wake source" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - var runtime_open = true; - defer if (runtime_open) runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - var map_open = true; - defer if (map_open) map.close() catch @panic("map close failed"); - try quiesce(&runtime); - - // A malformed style is reported from native's own threads, so the failure - // reaches the parked owner thread. - try map.setStyleUrl(testing.allocator, "unsupported://style.json"); - var loading_failed = false; - const load_started = std.Io.Clock.awake.now(testing.io); - for (0..20) |_| { - try runtime.pump(10_000, null); - if (elapsedMilliseconds(load_started) > 5_000) return error.ParkTimedOut; - var batch = try runtime.drainEvents(testing.allocator, 0); - defer batch.deinit(); - for (0..batch.len()) |index| { - const event = try batch.at(index); - if (std.meta.eql(event.event_type, maplibre.RuntimeEventType.map_loading_failed)) { - loading_failed = true; - } - } - if (loading_failed) break; - } - try testing.expect(loading_failed); - - // The park this signal releases has no other work to end it. - const source = try runtime.wakeSource(); - try quiesce(&runtime); - var thread_error: ?anyerror = error.Unexpected; - const thread = try std.Thread.spawn(.{}, signalWakeSourceOnThread, .{ source, &thread_error }); - const park_started = std.Io.Clock.awake.now(testing.io); - try runtime.pump(10_000, null); - try testing.expect(elapsedMilliseconds(park_started) < 5_000); - thread.join(); - try testing.expect(thread_error == null); - - // A wake source stays usable after its runtime closes. - try map.close(); - map_open = false; - try runtime.close(); - runtime_open = false; - try source.signal(); - source.release(); - try testing.expectError(error.ClosedHandle, source.signal()); -} - -test "a pump clears the wake flag it returns on" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - const source = try runtime.wakeSource(); - defer source.release(); - try quiesce(&runtime); - - try source.signal(); - const signalled_started = std.Io.Clock.awake.now(testing.io); - try runtime.pump(10_000, null); - try testing.expect(elapsedMilliseconds(signalled_started) < 5_000); - - // The pump above cleared the wake flag, so this one waits its full timeout. - const idle_started = std.Io.Clock.awake.now(testing.io); - try runtime.pump(200, null); - try testing.expect(elapsedMilliseconds(idle_started) >= 100); -} - -test "an idle map drains empty batches" { - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - try quiesce(&runtime); - var batch = try runtime.drainEvents(testing.allocator, 0); - defer batch.deinit(); - try testing.expectEqual(@as(usize, 0), batch.len()); - try testing.expectEqual(@as(usize, 0), batch.remaining()); - // An empty batch has no event to report at any index. - try testing.expectError(error.InvalidArgument, batch.at(0)); -} diff --git a/bindings/zig/tests/style_sources.zig b/bindings/zig/tests/style_sources.zig index b04ab3279..e83bf0abc 100644 --- a/bindings/zig/tests/style_sources.zig +++ b/bindings/zig/tests/style_sources.zig @@ -6,15 +6,15 @@ const support = @import("support.zig"); test "style source JSON buffers expose type info and copied attribution" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try map.addStyleSourceJson(testing.allocator, "empty-json", "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\",\"features\":[]}}"); + _ = try map.addStyleSourceJson(testing.allocator, "empty-json", "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\",\"features\":[]}}"); - try testing.expect(try map.styleSourceExists(testing.allocator, "empty-json")); - try testing.expectEqual(maplibre.StyleSourceType.geojson, (try map.getStyleSourceType(testing.allocator, "empty-json")).?); - var info = (try map.getStyleSourceInfo(testing.allocator, "empty-json")).?; + try testing.expect(try support.styleSourceExists(&map, "empty-json")); + try testing.expectEqual(maplibre.StyleSourceType.geojson, (try support.styleSourceType(&map, "empty-json")).?); + var info = (try support.styleSourceInfo(&map, "empty-json")).?; defer info.deinit(); try testing.expectEqual(maplibre.StyleSourceType.geojson, info.source_type); try testing.expectEqual(@as(usize, "empty-json".len), info.id_size); @@ -22,39 +22,42 @@ test "style source JSON buffers expose type info and copied attribution" { try testing.expect(info.url == null); try testing.expect(info.tile_json == null); - try map.addStyleSourceJson(testing.allocator, "vector-meta", "{\"type\":\"vector\",\"tiles\":[\"https://example.com/{z}/{x}/{y}.pbf\"],\"attribution\":\"Example attribution\"}"); + _ = try map.addStyleSourceJson(testing.allocator, "vector-meta", "{\"type\":\"vector\",\"tiles\":[\"https://example.com/{z}/{x}/{y}.pbf\"],\"attribution\":\"Example attribution\"}"); - var vector_info = (try map.getStyleSourceInfo(testing.allocator, "vector-meta")).?; + var vector_info = (try support.styleSourceInfo(&map, "vector-meta")).?; defer vector_info.deinit(); try testing.expectEqual(maplibre.StyleSourceType.vector, vector_info.source_type); try testing.expectEqualStrings("Example attribution", vector_info.attribution.?); - var attribution = (try map.copyStyleSourceAttribution(testing.allocator, "vector-meta")).?; + var attribution = (try support.styleSourceAttribution(&map, "vector-meta")).?; defer attribution.deinit(); try testing.expectEqualStrings("Example attribution", attribution.value); } test "style source removal reports state and copies missing results" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const empty_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, "{\"type\":\"FeatureCollection\",\"features\":[]}", null); defer empty_data.release(); - try map.addGeoJsonSourceData(testing.allocator, "remove-me", empty_data); - try testing.expect(try map.styleSourceExists(testing.allocator, "remove-me")); - try testing.expect(try map.removeStyleSource(testing.allocator, "remove-me")); - try testing.expect(!try map.styleSourceExists(testing.allocator, "remove-me")); - try testing.expect(!try map.removeStyleSource(testing.allocator, "remove-me")); - try testing.expect((try map.getStyleSourceInfo(testing.allocator, "remove-me")) == null); - try testing.expect((try map.copyStyleSourceAttribution(testing.allocator, "remove-me")) == null); - try testing.expect((try map.copyStyleSourceUrl(testing.allocator, "remove-me")) == null); + _ = try map.addGeoJsonSourceData(testing.allocator, "remove-me", empty_data); + try testing.expect(try support.styleSourceExists(&map, "remove-me")); + try testing.expect(try support.removeStyleSource(&runtime, &map, "remove-me")); + try testing.expect(!try support.styleSourceExists(&map, "remove-me")); + + // A removal of a missing ID is accepted, then fails with NOT_FOUND. + try support.expectCommandError(&runtime, try map.removeStyleSource(testing.allocator, "remove-me"), error.NotFound); + + try testing.expect((try support.styleSourceInfo(&map, "remove-me")) == null); + try testing.expect((try support.styleSourceAttribution(&map, "remove-me")) == null); + try testing.expect((try support.styleSourceUrl(&map, "remove-me")) == null); } test "tile source helpers expose copied reconstructible source information" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -62,7 +65,7 @@ test "tile source helpers expose copied reconstructible source information" { "https://a.example.com/vector/{z}/{x}/{y}.mvt", "https://b.example.com/vector/{z}/{x}/{y}.mvt", }; - try map.addVectorSourceTiles(testing.allocator, "vector-helper", vector_tiles[0..], .{ + _ = try map.addVectorSourceTiles(testing.allocator, "vector-helper", vector_tiles[0..], .{ .min_zoom = 1.0, .max_zoom = 14.0, .attribution = "Helper attribution", @@ -73,8 +76,8 @@ test "tile source helpers expose copied reconstructible source information" { }, .vector_encoding = .mlt, }); - try testing.expectEqual(maplibre.StyleSourceType.vector, (try map.getStyleSourceType(testing.allocator, "vector-helper")).?); - var vector_info = (try map.getStyleSourceInfo(testing.allocator, "vector-helper")).?; + try testing.expectEqual(maplibre.StyleSourceType.vector, (try support.styleSourceType(&map, "vector-helper")).?); + var vector_info = (try support.styleSourceInfo(&map, "vector-helper")).?; defer vector_info.deinit(); try testing.expectEqualStrings("Helper attribution", vector_info.attribution.?); try testing.expect(vector_info.url == null); @@ -90,60 +93,52 @@ test "tile source helpers expose copied reconstructible source information" { try testing.expectEqual(@as(f64, -45.0), vector_tile_json.bounds.?.southwest.latitude); try testing.expectEqual(@as(f64, 120.0), vector_tile_json.bounds.?.northeast.longitude); - try map.addVectorSourceUrl(testing.allocator, "vector-url-helper", "https://example.com/vector.json", null); - try testing.expectEqual(maplibre.StyleSourceType.vector, (try map.getStyleSourceType(testing.allocator, "vector-url-helper")).?); - var vector_url_info = (try map.getStyleSourceInfo(testing.allocator, "vector-url-helper")).?; + _ = try map.addVectorSourceUrl(testing.allocator, "vector-url-helper", "https://example.com/vector.json", null); + try testing.expectEqual(maplibre.StyleSourceType.vector, (try support.styleSourceType(&map, "vector-url-helper")).?); + var vector_url_info = (try support.styleSourceInfo(&map, "vector-url-helper")).?; defer vector_url_info.deinit(); try testing.expectEqualStrings("https://example.com/vector.json", vector_url_info.url.?); try testing.expect(vector_url_info.tile_json == null); - var copied_url = (try map.copyStyleSourceUrl(testing.allocator, "vector-url-helper")).?; + var copied_url = (try support.styleSourceUrl(&map, "vector-url-helper")).?; defer copied_url.deinit(); try testing.expectEqualStrings("https://example.com/vector.json", copied_url.value); const raster_tiles = [_][]const u8{"https://example.com/raster/{z}/{x}/{y}.png"}; - try map.addRasterSourceTiles(testing.allocator, "raster-helper", raster_tiles[0..], .{ .tile_size = 256 }); - try testing.expectEqual(maplibre.StyleSourceType.raster, (try map.getStyleSourceType(testing.allocator, "raster-helper")).?); - var raster_info = (try map.getStyleSourceInfo(testing.allocator, "raster-helper")).?; + _ = try map.addRasterSourceTiles(testing.allocator, "raster-helper", raster_tiles[0..], .{ .tile_size = 256 }); + try testing.expectEqual(maplibre.StyleSourceType.raster, (try support.styleSourceType(&map, "raster-helper")).?); + var raster_info = (try support.styleSourceInfo(&map, "raster-helper")).?; defer raster_info.deinit(); try testing.expectEqual(@as(?u32, 256), raster_info.tile_size); try testing.expect(raster_info.vector_encoding == null); try testing.expect(raster_info.raster_encoding == null); - try map.addRasterSourceUrl(testing.allocator, "raster-url-helper", "https://example.com/raster.json", .{ .tile_size = 256 }); + _ = try map.addRasterSourceUrl(testing.allocator, "raster-url-helper", "https://example.com/raster.json", .{ .tile_size = 256 }); const dem_tiles = [_][]const u8{"https://example.com/dem/{z}/{x}/{y}.png"}; - try map.addRasterDemSourceTiles(testing.allocator, "dem", dem_tiles[0..], .{ + _ = try map.addRasterDemSourceTiles(testing.allocator, "dem", dem_tiles[0..], .{ .min_zoom = 0.0, .max_zoom = 14.0, .tile_size = 256, .raster_encoding = .terrarium, }); - try map.addRasterDemSourceUrl(testing.allocator, "dem-url", "https://example.com/dem.json", .{ .tile_size = 256, .raster_encoding = .mapbox }); - try testing.expectEqual(maplibre.StyleSourceType.raster_dem, (try map.getStyleSourceType(testing.allocator, "dem")).?); - var dem_info = (try map.getStyleSourceInfo(testing.allocator, "dem")).?; + _ = try map.addRasterDemSourceUrl(testing.allocator, "dem-url", "https://example.com/dem.json", .{ .tile_size = 256, .raster_encoding = .mapbox }); + try testing.expectEqual(maplibre.StyleSourceType.raster_dem, (try support.styleSourceType(&map, "dem")).?); + var dem_info = (try support.styleSourceInfo(&map, "dem")).?; defer dem_info.deinit(); try testing.expectEqual(@as(?u32, 256), dem_info.tile_size); try testing.expectEqual(maplibre.StyleRasterDemEncoding.terrarium, dem_info.raster_encoding.?); - try map.addHillshadeLayer(testing.allocator, "dem-hillshade", "dem", "point-circle"); - try map.addColorReliefLayer(testing.allocator, "dem-relief", "dem", ""); - var layer_type = (try map.getStyleLayerType(testing.allocator, "dem-hillshade")).?; - defer layer_type.deinit(); - try testing.expectEqualStrings("hillshade", layer_type.value); - var relief_type = (try map.getStyleLayerType(testing.allocator, "dem-relief")).?; - defer relief_type.deinit(); - try testing.expectEqualStrings("color-relief", relief_type.value); - - try map.setLayerProperty(testing.allocator, "dem-relief", "color-relief-color", "[\"interpolate\",[\"linear\"],[\"elevation\"],0,\"black\",1000,\"white\"]"); - try testing.expectError( - error.InvalidArgument, - map.setLayerProperty(testing.allocator, "dem-relief", "color-relief-color", "[\"interpolate\",[\"linear\"],[\"zoom\"],0,\"black\",1,\"white\"]"), - ); - - try testing.expectError(error.InvalidArgument, map.addHillshadeLayer(testing.allocator, "bad-hillshade", "point", "")); + _ = try map.addHillshadeLayer(testing.allocator, "dem-hillshade", "dem", "point-circle"); + _ = try map.addColorReliefLayer(testing.allocator, "dem-relief", "dem", ""); + try testing.expectEqualStrings("hillshade", (try support.styleLayerType(&map, "dem-hillshade")).?); + try testing.expectEqualStrings("color-relief", (try support.styleLayerType(&map, "dem-relief")).?); + + _ = try map.setLayerProperty(testing.allocator, "dem-relief", "color-relief-color", "[\"interpolate\",[\"linear\"],[\"elevation\"],0,\"black\",1000,\"white\"]"); + try support.expectCommandError(&runtime, try map.setLayerProperty(testing.allocator, "dem-relief", "color-relief-color", "[\"interpolate\",[\"linear\"],[\"zoom\"],0,\"black\",1,\"white\"]"), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.addHillshadeLayer(testing.allocator, "bad-hillshade", "point", ""), error.InvalidArgument); try testing.expectError(error.InvalidArgument, map.addRasterSourceTiles(testing.allocator, "bad-raster", raster_tiles[0..], .{ .raster_encoding = .mapbox })); - try testing.expect(try map.removeStyleSource(testing.allocator, "vector-helper")); + try testing.expect(try support.removeStyleSource(&runtime, &map, "vector-helper")); try map.close(); try testing.expectEqualStrings(vector_tiles[0], vector_info.tile_json.?.tile_urls[0]); try testing.expectEqualStrings("https://example.com/vector.json", vector_url_info.url.?); @@ -151,7 +146,7 @@ test "tile source helpers expose copied reconstructible source information" { test "image source helpers add update and copy coordinates" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -161,24 +156,24 @@ test "image source helpers add update and copy coordinates" { .{ .latitude = 37.0, .longitude = -122.0 }, .{ .latitude = 37.0, .longitude = -123.0 }, }; - try map.addImageSourceUrl(testing.allocator, "image-url-source", coordinates, "https://example.com/image.png"); - try testing.expectEqual(maplibre.StyleSourceType.image, (try map.getStyleSourceType(testing.allocator, "image-url-source")).?); + _ = try map.addImageSourceUrl(testing.allocator, "image-url-source", coordinates, "https://example.com/image.png"); + try testing.expectEqual(maplibre.StyleSourceType.image, (try support.styleSourceType(&map, "image-url-source")).?); - const copied = (try map.getImageSourceCoordinates(testing.allocator, "image-url-source")).?; + const copied = (try support.imageSourceCoordinates(&map, "image-url-source")).?; try testing.expectApproxEqAbs(coordinates[0].latitude, copied[0].latitude, 0.000001); try testing.expectApproxEqAbs(coordinates[0].longitude, copied[0].longitude, 0.000001); var image_pixels = [_]u8{ 1, 2, 3, 4 }; - try map.addImageSourceImage(testing.allocator, "image-inline-source", coordinates, .{ + _ = try map.addImageSourceImage(testing.allocator, "image-inline-source", coordinates, .{ .width = 1, .height = 1, .stride = 4, .pixels = image_pixels[0..], }); image_pixels[0] = 9; - try map.setImageSourceUrl(testing.allocator, "image-inline-source", "https://example.com/replacement.png"); + _ = try map.setImageSourceUrl(testing.allocator, "image-inline-source", "https://example.com/replacement.png"); image_pixels[0] = 5; - try map.setImageSourceImage(testing.allocator, "image-inline-source", .{ + _ = try map.setImageSourceImage(testing.allocator, "image-inline-source", .{ .width = 1, .height = 1, .stride = 4, @@ -191,29 +186,26 @@ test "image source helpers add update and copy coordinates" { .{ .latitude = 36.0, .longitude = -121.0 }, .{ .latitude = 36.0, .longitude = -124.0 }, }; - try map.setImageSourceCoordinates(testing.allocator, "image-inline-source", updated_coordinates); - const updated = (try map.getImageSourceCoordinates(testing.allocator, "image-inline-source")).?; + _ = try map.setImageSourceCoordinates(testing.allocator, "image-inline-source", updated_coordinates); + const updated = (try support.imageSourceCoordinates(&map, "image-inline-source")).?; try testing.expectApproxEqAbs(updated_coordinates[0].latitude, updated[0].latitude, 0.000001); try testing.expectApproxEqAbs(updated_coordinates[0].longitude, updated[0].longitude, 0.000001); - try testing.expect((try map.getImageSourceCoordinates(testing.allocator, "missing-image-source")) == null); - try testing.expectError(error.InvalidArgument, map.addImageSourceUrl(testing.allocator, "image-url-source", coordinates, "https://example.com/duplicate.png")); - try testing.expectError(error.InvalidArgument, map.setImageSourceUrl(testing.allocator, "point", "https://example.com/not-image.png")); + try testing.expect((try support.imageSourceCoordinates(&map, "missing-image-source")) == null); + try support.expectCommandError(&runtime, try map.addImageSourceUrl(testing.allocator, "image-url-source", coordinates, "https://example.com/duplicate.png"), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.setImageSourceUrl(testing.allocator, "point", "https://example.com/not-image.png"), error.InvalidArgument); } test "style source JSON buffers reject invalid source data and pass explicit-length IDs" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try testing.expectError( - error.InvalidArgument, - map.addStyleSourceJson(testing.allocator, "invalid-json-source", "{\"type\":\"definitely-not-a-source-type\"}"), - ); - try map.addStyleSourceJson(testing.allocator, "nul\x00source", "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\",\"features\":[]}}"); - try testing.expect(try map.styleSourceExists(testing.allocator, "nul\x00source")); - var info = (try map.getStyleSourceInfo(testing.allocator, "nul\x00source")).?; + try support.expectCommandError(&runtime, try map.addStyleSourceJson(testing.allocator, "invalid-json-source", "{\"type\":\"definitely-not-a-source-type\"}"), error.InvalidArgument); + _ = try map.addStyleSourceJson(testing.allocator, "nul\x00source", "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\",\"features\":[]}}"); + try testing.expect(try support.styleSourceExists(&map, "nul\x00source")); + var info = (try support.styleSourceInfo(&map, "nul\x00source")).?; defer info.deinit(); try testing.expectEqual(@as(usize, "nul\x00source".len), info.id_size); } @@ -244,12 +236,12 @@ fn releaseCustomGeometryContext(context: ?*anyopaque) void { test "custom geometry source helpers add sources and accept tile updates" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); var state = CustomGeometryState{}; - try map.addCustomGeometrySource(testing.allocator, "custom", .{ + _ = try map.addCustomGeometrySource(testing.allocator, "custom", .{ .fetch_tile = fetchCustomGeometryTile, .cancel_tile = cancelCustomGeometryTile, .context = &state, @@ -262,46 +254,44 @@ test "custom geometry source helpers add sources and accept tile updates" { .wrap = true, }); - try testing.expect(try map.styleSourceExists(testing.allocator, "custom")); - try testing.expectEqual(maplibre.StyleSourceType.custom_vector, (try map.getStyleSourceType(testing.allocator, "custom")).?); + try testing.expect(try support.styleSourceExists(&map, "custom")); + try testing.expectEqual(maplibre.StyleSourceType.custom_vector, (try support.styleSourceType(&map, "custom")).?); const tile_id = maplibre.CanonicalTileId{ .z = 0, .x = 0, .y = 0 }; - try map.setCustomGeometrySourceTileData(testing.allocator, "custom", tile_id, "{\"type\":\"FeatureCollection\",\"features\":[]}"); - try map.invalidateCustomGeometrySourceTile(testing.allocator, "custom", tile_id); - try map.invalidateCustomGeometrySourceRegion(testing.allocator, "custom", .{ + _ = try map.setCustomGeometrySourceTileData(testing.allocator, "custom", tile_id, "{\"type\":\"FeatureCollection\",\"features\":[]}"); + _ = try map.invalidateCustomGeometrySourceTile(testing.allocator, "custom", tile_id); + _ = try map.invalidateCustomGeometrySourceRegion(testing.allocator, "custom", .{ .southwest = .{ .latitude = -1.0, .longitude = -1.0 }, .northeast = .{ .latitude = 1.0, .longitude = 1.0 }, }); - try testing.expectError(error.InvalidArgument, map.addCustomGeometrySource(testing.allocator, "custom", .{ + const duplicate_custom = try map.addCustomGeometrySource(testing.allocator, "custom", .{ .fetch_tile = fetchCustomGeometryTile, .context = &state, - })); + }); + try support.expectCommandError(&runtime, duplicate_custom, error.InvalidArgument); try testing.expectError(error.InvalidArgument, map.addCustomGeometrySource(testing.allocator, "bad-zoom", .{ .fetch_tile = fetchCustomGeometryTile, .context = &state, .max_zoom = 33.0, })); - try testing.expectError( - error.InvalidArgument, - map.setCustomGeometrySourceTileData( - testing.allocator, - "custom", - .{ .z = 1, .x = 2, .y = 0 }, - "{\"type\":\"FeatureCollection\",\"features\":[]}", - ), - ); - try testing.expectError(error.InvalidArgument, map.invalidateCustomGeometrySourceTile(testing.allocator, "point", tile_id)); + try support.expectCommandError(&runtime, try map.setCustomGeometrySourceTileData( + testing.allocator, + "custom", + .{ .z = 1, .x = 2, .y = 0 }, + "{\"type\":\"FeatureCollection\",\"features\":[]}", + ), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.invalidateCustomGeometrySourceTile(testing.allocator, "point", tile_id), error.InvalidArgument); } test "custom MVT vector source helpers add sources and accept tile updates" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); var state = CustomGeometryState{}; - try map.addCustomMvtVectorSource(testing.allocator, "custom-mvt", .{ + _ = try map.addCustomMvtVectorSource(testing.allocator, "custom-mvt", .{ .fetch_tile = fetchCustomGeometryTile, .cancel_tile = cancelCustomGeometryTile, .context = &state, @@ -309,55 +299,56 @@ test "custom MVT vector source helpers add sources and accept tile updates" { .max_zoom = 14.0, }); - try testing.expect(try map.styleSourceExists(testing.allocator, "custom-mvt")); - try testing.expectEqual(maplibre.StyleSourceType.custom_mvt_vector, (try map.getStyleSourceType(testing.allocator, "custom-mvt")).?); + try testing.expect(try support.styleSourceExists(&map, "custom-mvt")); + try testing.expectEqual(maplibre.StyleSourceType.custom_mvt_vector, (try support.styleSourceType(&map, "custom-mvt")).?); const tile_id = maplibre.CanonicalTileId{ .z = 0, .x = 0, .y = 0 }; - try map.setCustomMvtVectorSourceTileData(testing.allocator, "custom-mvt", tile_id, ""); - try map.setCustomMvtVectorSourceTileError(testing.allocator, "custom-mvt", tile_id, "missing"); - try map.invalidateCustomMvtVectorSourceTile(testing.allocator, "custom-mvt", tile_id); + _ = try map.setCustomMvtVectorSourceTileData(testing.allocator, "custom-mvt", tile_id, ""); + _ = try map.setCustomMvtVectorSourceTileError(testing.allocator, "custom-mvt", tile_id, "missing"); + _ = try map.invalidateCustomMvtVectorSourceTile(testing.allocator, "custom-mvt", tile_id); - try testing.expectError(error.InvalidArgument, map.addCustomMvtVectorSource(testing.allocator, "custom-mvt", .{ + const duplicate_mvt = try map.addCustomMvtVectorSource(testing.allocator, "custom-mvt", .{ .fetch_tile = fetchCustomGeometryTile, .context = &state, - })); + }); + try support.expectCommandError(&runtime, duplicate_mvt, error.InvalidArgument); try testing.expectError(error.InvalidArgument, map.addCustomMvtVectorSource(testing.allocator, "bad-zoom", .{ .fetch_tile = fetchCustomGeometryTile, .context = &state, .max_zoom = 33.0, })); - try testing.expectError(error.InvalidArgument, map.setCustomMvtVectorSourceTileData( + try support.expectCommandError(&runtime, try map.setCustomMvtVectorSourceTileData( testing.allocator, "custom", tile_id, "", - )); - try testing.expectError(error.InvalidArgument, map.invalidateCustomMvtVectorSourceTile(testing.allocator, "point", tile_id)); + ), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.invalidateCustomMvtVectorSourceTile(testing.allocator, "point", tile_id), error.InvalidArgument); } // A host owns the context its callbacks read, and the release callback is the // only report that the map stopped referencing it. test "a custom geometry source releases its context once per lifetime end" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); var map_open = true; defer if (map_open) map.close() catch @panic("map close failed"); var removed = CustomGeometryState{}; - try map.addCustomGeometrySource(testing.allocator, "removed", .{ + _ = try map.addCustomGeometrySource(testing.allocator, "removed", .{ .fetch_tile = fetchCustomGeometryTile, .release_context = releaseCustomGeometryContext, .context = &removed, }); var retained = CustomGeometryState{}; - try map.addCustomGeometrySource(testing.allocator, "retained", .{ + _ = try map.addCustomGeometrySource(testing.allocator, "retained", .{ .fetch_tile = fetchCustomGeometryTile, .release_context = releaseCustomGeometryContext, .context = &retained, }); - try testing.expect(try map.removeStyleSource(testing.allocator, "removed")); + try testing.expect(try support.removeStyleSource(&runtime, &map, "removed")); try testing.expectEqual(@as(usize, 1), removed.release_count); try testing.expectEqual(@as(usize, 0), retained.release_count); @@ -365,6 +356,7 @@ test "a custom geometry source releases its context once per lifetime end" { // releases that context. try map.close(); map_open = false; + try support.waitForBarrier(&runtime); try testing.expectEqual(@as(usize, 1), removed.release_count); try testing.expectEqual(@as(usize, 1), retained.release_count); } diff --git a/bindings/zig/tests/style_values.zig b/bindings/zig/tests/style_values.zig index 7e3ada166..cbcefd550 100644 --- a/bindings/zig/tests/style_values.zig +++ b/bindings/zig/tests/style_values.zig @@ -20,15 +20,15 @@ fn listIndexOf(list: maplibre.StringList, expected: []const u8) !usize { test "style ID lists are copied into owned Zig output" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - var source_ids = try map.listStyleSourceIds(testing.allocator); + var source_ids = try support.listStyleSourceIds(&map); defer source_ids.deinit(); try expectListContains(source_ids, "point"); - var layer_ids = try map.listStyleLayerIds(testing.allocator); + var layer_ids = try support.listStyleLayerIds(&map); defer layer_ids.deinit(); try expectListContains(layer_ids, "background"); try expectListContains(layer_ids, "point-circle"); @@ -36,39 +36,41 @@ test "style ID lists are copied into owned Zig output" { test "style layer JSON helpers manage lifecycle and order" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); const empty_data = try maplibre.GeoJsonSourceDataHandle.create(testing.allocator, "{\"type\":\"FeatureCollection\",\"features\":[]}", null); defer empty_data.release(); - try map.addGeoJsonSourceData(testing.allocator, "empty-layer-source", empty_data); - try map.addStyleLayerJson(testing.allocator, "{\"id\":\"empty-circle\",\"type\":\"circle\",\"source\":\"empty-layer-source\"}", "point-circle"); - try testing.expect(try map.styleLayerExists("empty-circle")); + _ = try map.addGeoJsonSourceData(testing.allocator, "empty-layer-source", empty_data); + _ = try map.addStyleLayerJson(testing.allocator, "{\"id\":\"empty-circle\",\"type\":\"circle\",\"source\":\"empty-layer-source\"}", "point-circle"); + try testing.expect(try support.styleLayerExists(&map, "empty-circle")); - var before_move = try map.listStyleLayerIds(testing.allocator); + var before_move = try support.listStyleLayerIds(&map); defer before_move.deinit(); try testing.expect((try listIndexOf(before_move, "empty-circle")) < (try listIndexOf(before_move, "point-circle"))); - var layer_type = (try map.getStyleLayerType(testing.allocator, "empty-circle")).?; - defer layer_type.deinit(); - try testing.expectEqualStrings("circle", layer_type.value); + try testing.expectEqualStrings("circle", (try support.styleLayerType(&map, "empty-circle")).?); - var layer_json = (try map.getStyleLayerJson(testing.allocator, "empty-circle")).?; + var layer_json = (try support.styleLayerJson(&map, "empty-circle")).?; defer layer_json.deinit(); try testing.expect(std.mem.indexOf(u8, layer_json.value, "\"id\":\"empty-circle\"") != null); - try map.moveStyleLayer("empty-circle", ""); - try testing.expectError(error.InvalidState, map.removeStyleSource(testing.allocator, "empty-layer-source")); - try testing.expect(try map.removeStyleLayer("empty-circle")); - try testing.expect(!try map.styleLayerExists("empty-circle")); - try testing.expect(try map.removeStyleSource(testing.allocator, "empty-layer-source")); - try testing.expect((try map.getStyleLayerJson(testing.allocator, "empty-circle")) == null); + _ = try map.moveStyleLayer("empty-circle", ""); + // A source a layer still uses fails its removal with INVALID_STATE. + try testing.expectError(error.InvalidState, support.removeStyleSource(&runtime, &map, "empty-layer-source")); + try testing.expect(try support.removeStyleLayer(&runtime, &map, "empty-circle")); + try testing.expect(!try support.styleLayerExists(&map, "empty-circle")); + try testing.expect(try support.removeStyleSource(&runtime, &map, "empty-layer-source")); + // A removal of a missing layer is accepted, then fails with NOT_FOUND. + try testing.expect(!try support.removeStyleLayer(&runtime, &map, "empty-circle")); + try testing.expect((try support.styleLayerJson(&map, "empty-circle")) == null); + try testing.expect((try support.styleLayerInfo(&map, "empty-circle")) == null); } test "nine-patch style images round-trip stretch, content, and text fit" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -84,14 +86,14 @@ test "nine-patch style images round-trip stretch, content, and text fit" { .{ .from = 0.0, .to = 1.0 }, .{ .from = 1.0, .to = 2.0 }, }; - try map.setStyleImage(testing.allocator, "patch", image, .{ + _ = try map.setStyleImage(testing.allocator, "patch", image, .{ .stretch_x = stretch_x[0..], .stretch_y = stretch_y[0..], .content = .{ .left = 0.5, .top = 0.5, .right = 1.5, .bottom = 1.5 }, .text_fit_height = .proportional, }); - const info = (try map.getStyleImageInfo(testing.allocator, "patch")).?; + const info = (try support.styleImageInfo(&map, "patch")).?; try testing.expectEqual(@as(usize, 1), info.stretch_x_count); try testing.expectEqual(@as(usize, 2), info.stretch_y_count); try testing.expectEqual(@as(f32, 1.5), info.content.?.right); @@ -99,14 +101,14 @@ test "nine-patch style images round-trip stretch, content, and text fit" { try testing.expect(info.text_fit_width == null); try testing.expectEqual(maplibre.StyleImageTextFit.proportional, info.text_fit_height.?); - var stretches = (try map.copyStyleImageStretches(testing.allocator, "patch")).?; + var stretches = (try support.styleImageStretches(&map, "patch")).?; defer stretches.deinit(); try testing.expectEqual(@as(usize, 1), stretches.stretch_x.len); try testing.expectEqual(@as(f32, 1.0), stretches.stretch_x[0].to); try testing.expectEqual(@as(usize, 2), stretches.stretch_y.len); try testing.expectEqual(@as(f32, 2.0), stretches.stretch_y[1].to); - try testing.expect((try map.copyStyleImageStretches(testing.allocator, "missing")) == null); + try testing.expect((try support.styleImageStretches(&map, "missing")) == null); // A backwards interval is rejected by C. const backwards = [_]maplibre.ImageStretch{.{ .from = 2.0, .to = 1.0 }}; @@ -118,126 +120,126 @@ test "nine-patch style images round-trip stretch, content, and text fit" { test "layer base accessors round-trip source, zoom range, and visibility" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); { - var empty = try map.copyLayerSourceLayer(testing.allocator, "point-circle"); + var empty = try support.layerSourceLayer(&map, "point-circle"); defer empty.deinit(); try testing.expectEqualStrings("", empty.value); } - try map.setLayerSourceLayer(testing.allocator, "point-circle", "roads"); + _ = try map.setLayerSourceLayer(testing.allocator, "point-circle", "roads"); { - var source_layer = try map.copyLayerSourceLayer(testing.allocator, "point-circle"); + var source_layer = try support.layerSourceLayer(&map, "point-circle"); defer source_layer.deinit(); try testing.expectEqualStrings("roads", source_layer.value); } { - var source_id = try map.copyLayerSourceId(testing.allocator, "point-circle"); + var source_id = try support.layerSourceId(&map, "point-circle"); defer source_id.deinit(); try testing.expectEqualStrings("point", source_id.value); } - // A layer type that takes no source is rejected rather than silently ignored. - try testing.expectError( - error.InvalidArgument, - map.setLayerSourceLayer(testing.allocator, "background", "roads"), - ); + // Semantic rejection is reported asynchronously after command acceptance. + try support.expectCommandError(&runtime, try map.setLayerSourceLayer(testing.allocator, "background", "roads"), error.InvalidArgument); { - var background_source = try map.copyLayerSourceId(testing.allocator, "background"); + var background_source = try support.layerSourceId(&map, "background"); defer background_source.deinit(); try testing.expectEqualStrings("", background_source.value); } - // An unset zoom range crosses the boundary as infinities. - try testing.expectEqual( - -std.math.inf(f64), - try map.getLayerMinZoom(testing.allocator, "point-circle"), - ); - try testing.expectEqual( - std.math.inf(f64), - try map.getLayerMaxZoom(testing.allocator, "point-circle"), - ); - try map.setLayerMinZoom(testing.allocator, "point-circle", 4.0); - try map.setLayerMaxZoom(testing.allocator, "point-circle", 12.5); - try testing.expectEqual(@as(f64, 4.0), try map.getLayerMinZoom(testing.allocator, "point-circle")); - try testing.expectEqual(@as(f64, 12.5), try map.getLayerMaxZoom(testing.allocator, "point-circle")); - - try testing.expectEqual( - maplibre.StyleLayerVisibility.visible, - try map.getLayerVisibility(testing.allocator, "point-circle"), - ); - try map.setLayerVisibility(testing.allocator, "point-circle", .none); - try testing.expectEqual( - maplibre.StyleLayerVisibility.none, - try map.getLayerVisibility(testing.allocator, "point-circle"), - ); + // The layer-info aggregate carries the type, zoom range, visibility, and + // string sizes for the copy operations. An unset zoom range crosses the + // boundary as infinities. + const unset = (try support.styleLayerInfo(&map, "point-circle")).?; + try testing.expectEqualStrings("circle", unset.layer_type); + try testing.expectEqual(-std.math.inf(f64), unset.min_zoom); + try testing.expectEqual(std.math.inf(f64), unset.max_zoom); + try testing.expectEqual(maplibre.StyleLayerVisibility.visible, unset.visibility); + // The reported sizes fit the strings the copy operations return. + try testing.expectEqual(@as(?usize, "point".len), unset.source_id_size); + try testing.expectEqual(@as(?usize, "roads".len), unset.source_layer_size); + { + var copied_source_id = try support.layerSourceId(&map, "point-circle"); + defer copied_source_id.deinit(); + try testing.expectEqual(unset.source_id_size.?, copied_source_id.value.len); + } - // An unknown raw visibility passes through to C, which rejects it. - try testing.expectError( - error.InvalidArgument, - map.setLayerVisibility(testing.allocator, "point-circle", .{ .unknown = 900 }), - ); - try testing.expectError( - error.InvalidArgument, - map.getLayerMinZoom(testing.allocator, "missing"), - ); + _ = try map.setLayerMinZoom(testing.allocator, "point-circle", 4.0); + _ = try map.setLayerMaxZoom(testing.allocator, "point-circle", 12.5); + _ = try map.setLayerVisibility(testing.allocator, "point-circle", .none); + const tuned = (try support.styleLayerInfo(&map, "point-circle")).?; + try testing.expectEqual(@as(f64, 4.0), tuned.min_zoom); + try testing.expectEqual(@as(f64, 12.5), tuned.max_zoom); + try testing.expectEqual(maplibre.StyleLayerVisibility.none, tuned.visibility); + + // A layer that names no source reports absent sizes. + const background = (try support.styleLayerInfo(&map, "background")).?; + try testing.expectEqualStrings("background", background.layer_type); + try testing.expectEqual(@as(?usize, null), background.source_layer_size); + + // An unknown raw visibility is accepted into the ordered queue, then fails. + const rejected_visibility = + try map.setLayerVisibility(testing.allocator, "point-circle", .{ .unknown = 900 }); + try support.expectCommandError(&runtime, rejected_visibility, error.InvalidArgument); + // A missing layer reports not-found through the info getter's found flag. + try testing.expect((try support.styleLayerInfo(&map, "missing")) == null); } test "layer properties accept semantic JSON values and return owned snapshots" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "18"); + _ = try map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "18"); - var snapshot = (try map.getLayerProperty(testing.allocator, "point-circle", "circle-radius")).?; + var snapshot = (try support.layerProperty(&map, "point-circle", "circle-radius")).?; defer snapshot.deinit(); try testing.expectEqualStrings("18.0", snapshot.value); } test "layer filters accept nested semantic JSON arrays and return owned snapshots" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try map.setLayerFilter(testing.allocator, "point-circle", "[\"==\",[\"get\",\"visible\"],true]"); + _ = try map.setLayerFilter(testing.allocator, "point-circle", "[\"==\",[\"get\",\"visible\"],true]"); - var snapshot = (try map.getLayerFilter(testing.allocator, "point-circle")).?; + var snapshot = (try support.layerFilter(&map, "point-circle")).?; defer snapshot.deinit(); try testing.expectEqualStrings("[\"==\",[\"get\",\"visible\"],true]", snapshot.value); - try map.setLayerFilter(testing.allocator, "point-circle", null); - try testing.expect((try map.getLayerFilter(testing.allocator, "point-circle")) == null); + _ = try map.setLayerFilter(testing.allocator, "point-circle", null); + try testing.expect((try support.layerFilter(&map, "point-circle")) == null); } test "style light accepts full JSON and property updates" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try map.setStyleLightJson(testing.allocator, "{\"color\":\"blue\",\"intensity\":0.3,\"position\":[1,2,3]}"); + _ = try map.setStyleLightJson(testing.allocator, "{\"color\":\"blue\",\"intensity\":0.3,\"position\":[1,2,3]}"); - var snapshot = (try map.getStyleLightProperty(testing.allocator, "intensity")).?; + var snapshot = (try support.styleLightProperty(&map, "intensity")).?; defer snapshot.deinit(); try testing.expectEqualStrings("0.30000001192092896", snapshot.value); - try map.setStyleLightProperty(testing.allocator, "intensity", "0.75"); - var updated = (try map.getStyleLightProperty(testing.allocator, "intensity")).?; + _ = try map.setStyleLightProperty(testing.allocator, "intensity", "0.75"); + var updated = (try support.styleLightProperty(&map, "intensity")).?; defer updated.deinit(); try testing.expectEqualStrings("0.75", updated.value); - try testing.expect((try map.getStyleLightProperty(testing.allocator, "unknown-light-property")) == null); - try testing.expectError(error.InvalidArgument, map.setStyleLightProperty(testing.allocator, "intensity", "false")); + try testing.expect((try support.styleLightProperty(&map, "unknown-light-property")) == null); + try support.expectCommandError(&runtime, try map.setStyleLightProperty(testing.allocator, "intensity", "false"), error.InvalidArgument); } test "runtime style images copy premultiplied RGBA8 pixels" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); @@ -246,7 +248,7 @@ test "runtime style images copy premultiplied RGBA8 pixels" { 0, 0, 0, 0, 70, 80, 90, 255, 100, 110, 120, 255, 0, 0, 0, 0, }; - try map.setStyleImage(testing.allocator, "runtime-icon", .{ + _ = try map.setStyleImage(testing.allocator, "runtime-icon", .{ .width = 2, .height = 2, .stride = 12, @@ -254,8 +256,8 @@ test "runtime style images copy premultiplied RGBA8 pixels" { }, .{ .pixel_ratio = 2.0, .sdf = true }); pixels[0] = 200; - try testing.expect(try map.styleImageExists(testing.allocator, "runtime-icon")); - const info = (try map.getStyleImageInfo(testing.allocator, "runtime-icon")).?; + try testing.expect(try support.styleImageExists(&map, "runtime-icon")); + const info = (try support.styleImageInfo(&map, "runtime-icon")).?; try testing.expectEqual(@as(u32, 2), info.width); try testing.expectEqual(@as(u32, 2), info.height); try testing.expectEqual(@as(u32, 8), info.stride); @@ -263,77 +265,70 @@ test "runtime style images copy premultiplied RGBA8 pixels" { try testing.expectApproxEqAbs(@as(f32, 2.0), info.pixel_ratio, 0.000001); try testing.expect(info.sdf); - var copied = (try map.copyStyleImagePremultipliedRgba8(testing.allocator, "runtime-icon")).?; + var copied = (try support.styleImagePixels(&map, "runtime-icon")).?; defer copied.deinit(); try testing.expectEqualSlices(u8, &[_]u8{ 10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255, - }, copied.pixels); + }, copied.value); var replacement_pixels = [_]u8{ 1, 2, 3, 4 }; - try map.setStyleImage(testing.allocator, "runtime-icon", .{ .width = 1, .height = 1, .stride = 4, .pixels = replacement_pixels[0..] }, null); - const replacement_info = (try map.getStyleImageInfo(testing.allocator, "runtime-icon")).?; + _ = try map.setStyleImage(testing.allocator, "runtime-icon", .{ .width = 1, .height = 1, .stride = 4, .pixels = replacement_pixels[0..] }, null); + const replacement_info = (try support.styleImageInfo(&map, "runtime-icon")).?; try testing.expectEqual(@as(u32, 1), replacement_info.width); try testing.expectEqual(@as(u32, 1), replacement_info.height); try testing.expectApproxEqAbs(@as(f32, 1.0), replacement_info.pixel_ratio, 0.000001); try testing.expect(!replacement_info.sdf); - try testing.expect(try map.removeStyleImage(testing.allocator, "runtime-icon")); - try testing.expect(!try map.styleImageExists(testing.allocator, "runtime-icon")); - try testing.expect(!try map.removeStyleImage(testing.allocator, "runtime-icon")); + try testing.expect(try support.removeStyleImage(&runtime, &map, "runtime-icon")); + try testing.expect(!try support.styleImageExists(&map, "runtime-icon")); + // A removal of a missing image is accepted, then fails with NOT_FOUND. + try testing.expect(!try support.removeStyleImage(&runtime, &map, "runtime-icon")); } test "location indicator helpers set focused properties" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try map.addLocationIndicatorLayer(testing.allocator, "location", "point-circle"); - var layer_type = (try map.getStyleLayerType(testing.allocator, "location")).?; - defer layer_type.deinit(); - try testing.expectEqualStrings("location-indicator", layer_type.value); + _ = try map.addLocationIndicatorLayer(testing.allocator, "location", "point-circle"); + try testing.expectEqualStrings("location-indicator", (try support.styleLayerType(&map, "location")).?); - try map.setLocationIndicatorLocation(testing.allocator, "location", .{ .latitude = 37.7749, .longitude = -122.4194 }, 12.0); - var location = (try map.getLayerProperty(testing.allocator, "location", "location")).?; + _ = try map.setLocationIndicatorLocation(testing.allocator, "location", .{ .latitude = 37.7749, .longitude = -122.4194 }, 12.0); + var location = (try support.layerProperty(&map, "location", "location")).?; defer location.deinit(); try testing.expectEqualStrings("[37.7749,-122.4194,12.0]", location.value); - try map.setLocationIndicatorBearing(testing.allocator, "location", 45.0); - var bearing = (try map.getLayerProperty(testing.allocator, "location", "bearing")).?; + _ = try map.setLocationIndicatorBearing(testing.allocator, "location", 45.0); + var bearing = (try support.layerProperty(&map, "location", "bearing")).?; defer bearing.deinit(); try testing.expectEqualStrings("45.0", bearing.value); - try map.setLocationIndicatorAccuracyRadius(testing.allocator, "location", 33.0); - var radius = (try map.getLayerProperty(testing.allocator, "location", "accuracy-radius")).?; + _ = try map.setLocationIndicatorAccuracyRadius(testing.allocator, "location", 33.0); + var radius = (try support.layerProperty(&map, "location", "accuracy-radius")).?; defer radius.deinit(); try testing.expectEqualStrings("33.0", radius.value); - try map.setLocationIndicatorImageName(testing.allocator, "location", .top, "top-icon"); - var top_image = (try map.getLayerProperty(testing.allocator, "location", "top-image")).?; + _ = try map.setLocationIndicatorImageName(testing.allocator, "location", .top, "top-icon"); + var top_image = (try support.layerProperty(&map, "location", "top-image")).?; defer top_image.deinit(); try testing.expect(!std.mem.eql(u8, top_image.value, "null")); - try map.setLocationIndicatorImageName(testing.allocator, "location", .bearing, "bearing-icon"); - try map.setLocationIndicatorImageName(testing.allocator, "location", .shadow, "shadow-icon"); + _ = try map.setLocationIndicatorImageName(testing.allocator, "location", .bearing, "bearing-icon"); + _ = try map.setLocationIndicatorImageName(testing.allocator, "location", .shadow, "shadow-icon"); - try testing.expectError(error.InvalidArgument, map.setLocationIndicatorAccuracyRadius(testing.allocator, "location", -1.0)); - try testing.expectError(error.InvalidArgument, map.setLocationIndicatorBearing(testing.allocator, "point-circle", 1.0)); + try support.expectCommandError(&runtime, try map.setLocationIndicatorAccuracyRadius(testing.allocator, "location", -1.0), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.setLocationIndicatorBearing(testing.allocator, "point-circle", 1.0), error.InvalidArgument); } test "style JSON buffers reject invalid values" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); - try testing.expectError( - error.InvalidArgument, - map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "1e999"), - ); - try testing.expectError( - error.InvalidArgument, - map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "\"not a radius\""), - ); + try support.expectCommandError(&runtime, try map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "1e999"), error.InvalidArgument); + try support.expectCommandError(&runtime, try map.setLayerProperty(testing.allocator, "point-circle", "circle-radius", "\"not a radius\""), error.InvalidArgument); } const transition_style_json = @@ -342,17 +337,17 @@ const transition_style_json = test "style transition options round trip through the C API" { var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); var map = try support.createLoadedMap(&runtime); defer map.close() catch @panic("map close failed"); // The style parser fills in a 300ms duration when a style declares none. - const parsed = try map.getStyleTransitionOptions(); + const parsed = try support.styleTransitionOptions(&map); try testing.expectEqual(@as(?f64, 300.0), parsed.duration_ms); try testing.expectEqual(@as(?f64, null), parsed.delay_ms); - try map.setStyleJson(testing.allocator, transition_style_json); - const declared = try map.getStyleTransitionOptions(); + _ = try map.setStyleJson(testing.allocator, transition_style_json); + const declared = try support.styleTransitionOptions(&map); try testing.expectEqual(@as(?f64, 750.0), declared.duration_ms); try testing.expectEqual(@as(?f64, 100.0), declared.delay_ms); try testing.expectEqual(@as(?bool, true), declared.enable_placement_transitions); @@ -363,22 +358,19 @@ test "style transition options round trip through the C API" { .duration_ms = 0.0, .enable_placement_transitions = false, }; - try map.setStyleTransitionOptions(options); - try testing.expectEqual(options, try map.getStyleTransitionOptions()); + _ = try map.setStyleTransitionOptions(options); + try testing.expectEqual(options, try support.styleTransitionOptions(&map)); // Omitting the flag leaves the cross-fade on rather than clearing it. - try map.setStyleTransitionOptions(.{ .duration_ms = 250.0 }); + _ = try map.setStyleTransitionOptions(.{ .duration_ms = 250.0 }); try testing.expectEqual( @as(?bool, true), - (try map.getStyleTransitionOptions()).enable_placement_transitions, + (try support.styleTransitionOptions(&map)).enable_placement_transitions, ); // Loading a style replaces the override with what that style declares. - try map.setStyleJson(testing.allocator, transition_style_json); - try testing.expectEqual(declared, try map.getStyleTransitionOptions()); + _ = try map.setStyleJson(testing.allocator, transition_style_json); + try testing.expectEqual(declared, try support.styleTransitionOptions(&map)); - try testing.expectError( - error.InvalidArgument, - map.setStyleTransitionOptions(.{ .delay_ms = -1.0 }), - ); + try support.expectCommandError(&runtime, try map.setStyleTransitionOptions(.{ .delay_ms = -1.0 }), error.InvalidArgument); } diff --git a/bindings/zig/tests/support.zig b/bindings/zig/tests/support.zig index 81a123d89..b834977d4 100644 --- a/bindings/zig/tests/support.zig +++ b/bindings/zig/tests/support.zig @@ -25,20 +25,23 @@ pub const style_json = \\} ; -/// Pumps the runtime and waits for an event of `event_type`, reporting whether +/// Closes a runtime and waits for its native teardown, so a test leaves no +/// native thread or resource behind. +pub fn closeRuntime(runtime: *maplibre.RuntimeHandle) !void { + var teardown = try runtime.close(); + defer teardown.deinit(); + try teardown.wait(null); +} + +/// Waits for an autonomously produced event of `event_type`, reporting whether /// it arrived. /// -/// A wait that stops at the event it looks for takes one event per drain, so the -/// events a later wait needs stay queued rather than being dropped with the -/// batch that carried them. pub fn waitForEvent(runtime: *maplibre.RuntimeHandle, event_type: maplibre.RuntimeEventType) !bool { for (0..1000) |_| { - try runtime.pump(0, null); - while (true) { - var batch = try runtime.drainEvents(testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) break; - const event = try batch.at(0); + var batch = try runtime.drainEvents(testing.allocator); + defer batch.deinit(); + for (0..batch.len()) |index| { + const event = try batch.at(index); if (std.meta.eql(event.event_type, event_type)) return true; } try sleepOneMillisecond(); @@ -47,52 +50,270 @@ pub fn waitForEvent(runtime: *maplibre.RuntimeHandle, event_type: maplibre.Runti } /// Waits for an event of `event_type` and copies it out of its batch. The caller -/// deinits the returned event. See `waitForEvent` for what a bounded drain keeps -/// queued. +/// deinits the returned event. pub fn waitForOwnedEvent( runtime: *maplibre.RuntimeHandle, event_type: maplibre.RuntimeEventType, ) !maplibre.RuntimeEvent { for (0..5000) |_| { - try runtime.pump(0, null); - while (true) { - var batch = try runtime.drainEvents(testing.allocator, 1); - defer batch.deinit(); - if (batch.len() == 0) break; - const event = try batch.at(0); + var batch = try runtime.drainEvents(testing.allocator); + defer batch.deinit(); + for (0..batch.len()) |index| { + const event = try batch.at(index); if (std.meta.eql(event.event_type, event_type)) return try event.clone(testing.allocator); } try sleepOneMillisecond(); } return error.EventNotObserved; } +fn resolve(comptime T: type, future_value: maplibre.Future(T)) !T { + var future = future_value; + defer future.deinit(); + return future.wait(null); +} + +pub fn waitForCommandCompletion( + runtime: *maplibre.RuntimeHandle, + future: maplibre.Future(maplibre.CommandCompletion), +) !maplibre.CommandCompletion { + _ = runtime; + return resolve(maplibre.CommandCompletion, future); +} + +pub fn waitForCommandDisposition( + runtime: *maplibre.RuntimeHandle, + future: maplibre.Future(maplibre.CommandCompletion), +) !maplibre.CommandDisposition { + return (try waitForCommandCompletion(runtime, future)).disposition; +} + +/// Verifies that an accepted command completed with a native failure. +pub fn expectCommandError( + runtime: *maplibre.RuntimeHandle, + future_value: maplibre.Future(maplibre.CommandCompletion), + expected: anyerror, +) !void { + _ = runtime; + var future = future_value; + defer future.deinit(); + const completion = try future.wait(null); + try testing.expectEqual(maplibre.CommandDisposition.failed, completion.disposition); + try testing.expectError(expected, completion.statusError()); + try testing.expect((try future.diagnostic()).len != 0); +} + +/// Awaits `completion`'s commit and returns a map snapshot that observes it: +/// the committed event reports the published generation, and a snapshot at or +/// past that generation carries the committed state. +pub fn snapshotAfterCommand( + runtime: *maplibre.RuntimeHandle, + map: *maplibre.MapHandle, + future: maplibre.Future(maplibre.CommandCompletion), +) !maplibre.MapSnapshot { + const finished = try waitForCommandCompletion(runtime, future); + try testing.expect(std.meta.eql(finished.disposition, maplibre.CommandDisposition.committed)); + try testing.expect(finished.generation != 0); + const snapshot = try map.snapshot(); + try testing.expect(snapshot.generation >= finished.generation); + return snapshot; +} /// Drains every queued event, reporting how many the batch carried. pub fn drainEvents(runtime: *maplibre.RuntimeHandle) !usize { - var batch = try runtime.drainEvents(testing.allocator, 0); + var batch = try runtime.drainEvents(testing.allocator); defer batch.deinit(); return batch.len(); } /// Creates a map with `style_json` loaded. pub fn createLoadedMap(runtime: *maplibre.RuntimeHandle) !maplibre.MapHandle { - var map = try maplibre.MapHandle.create(runtime, .{}); + var map_future = try maplibre.MapHandle.create(runtime, .{}); + defer map_future.deinit(); + var map = try map_future.wait(null); errdefer map.close() catch {}; - try map.setStyleJson(testing.allocator, style_json); + _ = try resolve(maplibre.CommandCompletion, try map.setStyleJson(testing.allocator, style_json)); try testing.expect(try waitForEvent(runtime, .map_style_loaded)); return map; } +pub fn waitForBarrier(runtime: *maplibre.RuntimeHandle) !void { + _ = try resolve(void, try runtime.barrier()); +} -fn sleepOneMillisecond() !void { - try testing.io.sleep(.fromMilliseconds(1), .awake); +/// Copies fixed source metadata, reporting null when no source has the ID. +pub fn styleSourceMetadata(map: *maplibre.MapHandle, source_id: []const u8) !?maplibre.StyleSourceMetadata { + return resolve(?maplibre.StyleSourceMetadata, try map.getStyleSourceInfo(testing.allocator, source_id)); +} + +/// Existence via the info getter's found flag. +pub fn styleSourceExists(map: *maplibre.MapHandle, source_id: []const u8) !bool { + return (try styleSourceMetadata(map, source_id)) != null; +} + +pub fn styleSourceType(map: *maplibre.MapHandle, source_id: []const u8) !?maplibre.StyleSourceType { + const metadata = (try styleSourceMetadata(map, source_id)) orelse return null; + return metadata.source_type; +} + +/// Waits for a removal command: true when it commits, false when the ID names +/// nothing, and the command's reported failure otherwise. +fn awaitRemoval(runtime: *maplibre.RuntimeHandle, future: maplibre.Future(maplibre.CommandCompletion)) !bool { + _ = runtime; + const completion_result = try resolve(maplibre.CommandCompletion, future); + return switch (completion_result.disposition) { + .committed => true, + .failed => { + completion_result.statusError() catch |err| { + if (err == error.NotFound) return false; + return err; + }; + return error.UnexpectedCommandDisposition; + }, + else => error.UnexpectedCommandDisposition, + }; +} + +pub fn removeStyleSource(runtime: *maplibre.RuntimeHandle, map: *maplibre.MapHandle, source_id: []const u8) !bool { + return awaitRemoval(runtime, try map.removeStyleSource(testing.allocator, source_id)); +} + +pub fn removeStyleLayer(runtime: *maplibre.RuntimeHandle, map: *maplibre.MapHandle, layer_id: []const u8) !bool { + return awaitRemoval(runtime, try map.removeStyleLayer(testing.allocator, layer_id)); +} + +pub fn removeStyleImage(runtime: *maplibre.RuntimeHandle, map: *maplibre.MapHandle, image_id: []const u8) !bool { + return awaitRemoval(runtime, try map.removeStyleImage(testing.allocator, image_id)); +} + +/// Copies fixed layer metadata, reporting null when no layer has the ID. +pub fn styleLayerInfo(map: *maplibre.MapHandle, layer_id: []const u8) !?maplibre.StyleLayerInfo { + return resolve(?maplibre.StyleLayerInfo, try map.getStyleLayerInfo(testing.allocator, layer_id)); +} + +/// Existence via the info getter's found flag. +pub fn styleLayerExists(map: *maplibre.MapHandle, layer_id: []const u8) !bool { + return (try styleLayerInfo(map, layer_id)) != null; +} + +pub fn loadedStyleJson(map: *maplibre.MapHandle) !maplibre.OwnedString { + return resolve(maplibre.OwnedString, try map.loadedStyleJson(testing.allocator)); +} + +pub fn styleUrl(map: *maplibre.MapHandle) !maplibre.OwnedString { + return resolve(maplibre.OwnedString, try map.styleUrl(testing.allocator)); +} + +pub fn listStyleSourceIds(map: *maplibre.MapHandle) !maplibre.StringList { + return resolve(maplibre.StringList, try map.listStyleSourceIds(testing.allocator)); } -pub fn typeNameContains(comptime T: type, comptime needle: []const u8) bool { - if (std.mem.indexOf(u8, @typeName(T), needle) != null) return true; - const info = @typeInfo(T); - if (info != .@"struct") return false; - inline for (info.@"struct".fields) |field| { - if (std.mem.indexOf(u8, @typeName(field.type), needle) != null) return true; +pub fn listStyleLayerIds(map: *maplibre.MapHandle) !maplibre.StringList { + return resolve(maplibre.StringList, try map.listStyleLayerIds(testing.allocator)); +} + +pub fn styleSourceAttribution(map: *maplibre.MapHandle, source_id: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.copyStyleSourceAttribution(testing.allocator, source_id)); +} + +pub fn styleSourceUrl(map: *maplibre.MapHandle, source_id: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.copyStyleSourceUrl(testing.allocator, source_id)); +} + +pub fn styleSourceInfo(map: *maplibre.MapHandle, source_id: []const u8) !?maplibre.StyleSourceInfo { + const metadata = (try styleSourceMetadata(map, source_id)) orelse return null; + var attribution: ?[]const u8 = null; + errdefer if (attribution) |value| testing.allocator.free(value); + if (metadata.has_attribution) { + var owned = (try styleSourceAttribution(map, source_id)) orelse return error.NativeError; + attribution = owned.value; + owned.value = ""; } - return false; + var url: ?[]const u8 = null; + errdefer if (url) |value| testing.allocator.free(value); + if (metadata.has_url) { + var owned = (try styleSourceUrl(map, source_id)) orelse return error.NativeError; + url = owned.value; + owned.value = ""; + } + var tile_urls: ?maplibre.StringList = null; + errdefer if (tile_urls) |*list| list.deinit(); + if (metadata.has_tile_json) { + tile_urls = try resolve(maplibre.StringList, try map.getStyleSourceTileUrls(testing.allocator, source_id)); + } + return .{ + .allocator = testing.allocator, + .source_type = metadata.source_type, + .id_size = metadata.id_size, + .is_volatile = metadata.is_volatile, + .attribution = attribution, + .url = url, + .tile_json = if (tile_urls) |list| .{ + .tile_urls = list.items, + .min_zoom = metadata.min_zoom, + .max_zoom = metadata.max_zoom, + .scheme = metadata.scheme, + .bounds = metadata.bounds, + } else null, + .tile_size = metadata.tile_size, + .vector_encoding = metadata.vector_encoding, + .raster_encoding = metadata.raster_encoding, + }; +} + +/// The layer's style-spec type name, a static string the process owns. +pub fn styleLayerType(map: *maplibre.MapHandle, layer_id: []const u8) !?[]const u8 { + const info = (try styleLayerInfo(map, layer_id)) orelse return null; + return info.layer_type; +} + +pub fn styleLayerJson(map: *maplibre.MapHandle, layer_id: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.getStyleLayerJson(testing.allocator, layer_id)); +} + +pub fn styleImageInfo(map: *maplibre.MapHandle, image_id: []const u8) !?maplibre.StyleImageInfo { + return resolve(?maplibre.StyleImageInfo, try map.getStyleImageInfo(testing.allocator, image_id)); +} + +pub fn styleImageStretches(map: *maplibre.MapHandle, image_id: []const u8) !?maplibre.OwnedImageStretches { + return resolve(?maplibre.OwnedImageStretches, try map.copyStyleImageStretches(testing.allocator, image_id)); +} + +pub fn imageSourceCoordinates(map: *maplibre.MapHandle, source_id: []const u8) !?[4]maplibre.LatLng { + return resolve(?[4]maplibre.LatLng, try map.getImageSourceCoordinates(testing.allocator, source_id)); +} + +pub fn layerSourceLayer(map: *maplibre.MapHandle, layer_id: []const u8) !maplibre.OwnedString { + return resolve(maplibre.OwnedString, try map.copyLayerSourceLayer(testing.allocator, layer_id)); +} + +pub fn layerSourceId(map: *maplibre.MapHandle, layer_id: []const u8) !maplibre.OwnedString { + return resolve(maplibre.OwnedString, try map.copyLayerSourceId(testing.allocator, layer_id)); +} + +pub fn layerProperty(map: *maplibre.MapHandle, layer_id: []const u8, property_name: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.getLayerProperty(testing.allocator, layer_id, property_name)); +} + +pub fn layerFilter(map: *maplibre.MapHandle, layer_id: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.getLayerFilter(testing.allocator, layer_id)); +} + +pub fn styleLightProperty(map: *maplibre.MapHandle, property_name: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.getStyleLightProperty(testing.allocator, property_name)); +} + +/// Existence via the info getter's found flag. +pub fn styleImageExists(map: *maplibre.MapHandle, image_id: []const u8) !bool { + return (try styleImageInfo(map, image_id)) != null; +} + +pub fn styleImagePixels(map: *maplibre.MapHandle, image_id: []const u8) !?maplibre.OwnedString { + return resolve(?maplibre.OwnedString, try map.copyStyleImagePremultipliedRgba8(testing.allocator, image_id)); +} + +pub fn styleTransitionOptions(map: *maplibre.MapHandle) !maplibre.StyleTransitionOptions { + return resolve(maplibre.StyleTransitionOptions, try map.getStyleTransitionOptions()); +} + +fn sleepOneMillisecond() !void { + try testing.io.sleep(.fromMilliseconds(1), .awake); } diff --git a/bindings/zig/tests/surface.zig b/bindings/zig/tests/surface.zig index e029f7f99..08e5e7248 100644 --- a/bindings/zig/tests/surface.zig +++ b/bindings/zig/tests/surface.zig @@ -1,177 +1,207 @@ +const builtin = @import("builtin"); const std = @import("std"); const build_options = @import("build_options"); const testing = std.testing; const maplibre = @import("maplibre_native_ffi"); -const metal_support = @import("metal_support.zig"); const support = @import("support.zig"); -fn nativePointer(ptr: *anyopaque) maplibre.NativePointer { - return maplibre.NativePointer.fromPtr(ptr); -} - -test "Metal surface renders to window-attached layer through public binding" { - if (!build_options.supports_metal) return error.SkipZigTest; - - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); - - var window_layer = try metal_support.createWindowLayer(64, 64); - defer window_layer.deinit(); +const supports_metal_surface = build_options.supports_metal and switch (builtin.os.tag) { + .macos, .ios, .tvos => true, + else => false, +}; +const metal = if (supports_metal_surface) @import("metal_support.zig") else struct {}; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); +const spin_turns = 1_000; +const wait_turns = spin_turns + 30_000; - var surface = try maplibre.attachMetalSurface(&map, .{ - .extent = .{ .width = 64, .height = 64 }, - .layer = nativePointer(window_layer.layer.?), - }); - defer surface.close() catch {}; +fn waitOneTurn(turn: usize) !void { + if (turn < spin_turns) return std.Thread.yield(); + try testing.io.sleep(.fromMilliseconds(1), .awake); +} - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try surface.renderUpdate()).result); +fn waitForVoid( + session: maplibre.RenderSessionHandle, + future_value: maplibre.Future(void), + service_driver: bool, +) !void { + var future = future_value; + defer future.deinit(); + for (0..wait_turns) |turn| { + if (try future.poll()) return future.wait(null); + if (service_driver) _ = try session.serviceDriverWork(64); + try waitOneTurn(turn); + } + return error.OperationTimedOut; } -test "Metal surface render acquires one drawable per frame through public binding" { - if (!build_options.supports_metal) return error.SkipZigTest; +fn finishAttachment( + attachment: maplibre.RenderSessionAttachment, + service_driver: bool, +) !maplibre.RenderSessionHandle { + errdefer { + var session = attachment.session; + session.destroy() catch {}; + } + try waitForVoid(attachment.session, attachment.completion, service_driver); + return attachment.session; +} - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); +fn closeSession(session: *maplibre.RenderSessionHandle, service_driver: bool) !void { + try waitForVoid(session.*, try session.detach(), service_driver); + try session.destroy(); +} - var window_layer = try metal_support.createCountingWindowLayer(64, 64); - defer window_layer.deinit(); +fn renderFrame( + session: maplibre.RenderSessionHandle, + token: u64, + service_driver: bool, +) !maplibre.FrameResult { + try session.requestFrame(.{ .if_needed = false, .present = true, .token = token }); + for (0..wait_turns) |turn| { + if (service_driver) _ = try session.serviceDriverWork(64); + var batch = session.drainFrameResults() catch |err| switch (err) { + error.NotReady => { + try waitOneTurn(turn); + continue; + }, + else => return err, + }; + defer batch.release(); + for (0..try batch.count()) |index| { + const result = try batch.get(index); + if (result.token == token) return result; + } + try waitOneTurn(turn); + } + return error.FrameTimedOut; +} - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); +fn expectRenderedFrame( + session: maplibre.RenderSessionHandle, + first_token: u64, + service_driver: bool, +) !maplibre.FrameResult { + for (first_token..first_token + 1_000) |token| { + const result = try renderFrame(session, token, service_driver); + switch (result.disposition) { + .rendered => return result, + .size_pending, .target_not_ready => {}, + else => return error.UnexpectedFrameDisposition, + } + } + return error.FrameDidNotRender; +} - var surface = try maplibre.attachMetalSurface(&map, .{ - .extent = .{ .width = 64, .height = 64 }, - .layer = nativePointer(window_layer.layer.?), +fn createMap(runtime: *maplibre.RuntimeHandle, width: u32, height: u32) !maplibre.MapHandle { + var future = try maplibre.MapHandle.create(runtime, .{ + .width = width, + .height = height, }); - defer surface.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(u32, 0), metal_support.nextDrawableCount(window_layer.layer.?)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try surface.renderUpdate()).result); - try testing.expectEqual(@as(u32, 1), metal_support.nextDrawableCount(window_layer.layer.?)); + defer future.deinit(); + return future.wait(null); } -test "Metal surface set target presents through a replacement layer" { - if (!build_options.supports_metal) return error.SkipZigTest; +fn descriptor(layer: metal.WindowLayer, extent: maplibre.RenderTargetExtent) !maplibre.MetalSurfaceDescriptor { + return .{ + .extent = extent, + .layer = maplibre.NativePointer.fromPtr(try layer.layerPointer()), + }; +} - const pool = try metal_support.AutoreleasePool.init(); +test "Metal surface core worker presents and replaces its target" { + if (!supports_metal_surface) return error.SkipZigTest; + const pool = try metal.AutoreleasePool.init(); defer pool.deinit(); - var window_layer = try metal_support.createCountingWindowLayer(64, 64); - defer window_layer.deinit(); - var replacement_layer = try metal_support.createCountingWindowLayer(48, 32); + var initial_layer = try metal.createCountingWindowLayer(32, 16); + defer initial_layer.deinit(); + var replacement_layer = try metal.createCountingWindowLayer(48, 24); defer replacement_layer.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map = try createMap(&runtime, 32, 16); defer map.close() catch @panic("map close failed"); - var surface = try maplibre.attachMetalSurface(&map, .{ - .extent = .{ .width = 64, .height = 64 }, - .layer = nativePointer(window_layer.layer.?), - }); - defer surface.close() catch {}; - - try map.setStyleJson(testing.allocator, support.style_json); - try testing.expect(try support.waitForEvent(&runtime, .map_render_update_available)); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try surface.renderUpdate()).result); - try testing.expectEqual(@as(u32, 1), metal_support.nextDrawableCount(window_layer.layer.?)); - - // The session presents through whatever replacement surface it is handed. - try surface.setMetalSurfaceTarget(.{ - .extent = .{ .width = 48, .height = 32 }, - .layer = nativePointer(replacement_layer.layer.?), - }); - - // A texture descriptor names a target this session does not have; the - // rejection leaves it presenting through the new layer. - try testing.expectError(error.Unsupported, surface.setMetalBorrowedTextureTarget(.{ - .extent = .{ .width = 48, .height = 32 }, - .physical_width = 48, - .physical_height = 32, - .texture = nativePointer(@ptrFromInt(1)), - })); - - // Replacing the surface enqueues the new size for the map's owner thread, - // so the map publishes a matching update only once pumped. - try testing.expectEqual(@as(maplibre.RenderResult, .size_pending), (try surface.renderUpdate()).result); - try runtime.pump(0, null); - const resized = try map.getSize(); - try testing.expectEqual(@as(u32, 48), resized.width); - try testing.expectEqual(@as(u32, 32), resized.height); - try testing.expectEqual(@as(maplibre.RenderResult, .rendered), (try surface.renderUpdate()).result); - try testing.expectEqual(@as(u32, 1), metal_support.nextDrawableCount(replacement_layer.layer.?)); - try testing.expectEqual(@as(u32, 1), metal_support.nextDrawableCount(window_layer.layer.?)); + var session = try finishAttachment( + try maplibre.attachMetalSurface( + &map, + try descriptor(initial_layer, .{ .width = 32, .height = 16, .scale_factor = 2 }), + .{ .driver = .core_worker }, + ), + false, + ); + defer closeSession(&session, false) catch @panic("render session close failed"); + + const capabilities = try session.capabilities(); + try testing.expectEqual(.core_worker, capabilities.driver); + try testing.expect(capabilities.presentation); + try testing.expect(!capabilities.frame_acquisition); + try testing.expect(!capabilities.readback); + try testing.expect(try initial_layer.hasDevice()); + const initial_drawable_size = try initial_layer.drawableSize(); + try testing.expectEqual(@as(u32, 64), initial_drawable_size.width); + try testing.expectEqual(@as(u32, 32), initial_drawable_size.height); + + try testing.expectEqual( + .committed, + try support.waitForCommandDisposition(&runtime, try map.setStyleJson(testing.allocator, support.style_json)), + ); + try support.waitForBarrier(&runtime); + const initial_frame = try expectRenderedFrame(session, 1_000, false); + try testing.expect(initial_frame.frame_generation != 0); + const initial_drawable_count = try initial_layer.nextDrawableCount(); + try testing.expect(initial_drawable_count != 0); + + try waitForVoid( + session, + try session.setMetalSurfaceTarget(try descriptor(replacement_layer, .{ .width = 48, .height = 24 })), + false, + ); + try testing.expectEqual( + .committed, + try support.waitForCommandDisposition(&runtime, try map.resize(48, 24, 1)), + ); + try support.waitForBarrier(&runtime); + _ = try expectRenderedFrame(session, 2_000, false); + + try testing.expectEqual(initial_drawable_count, try initial_layer.nextDrawableCount()); + try testing.expect((try replacement_layer.nextDrawableCount()) != 0); + const snapshot = try session.snapshot(); + try testing.expectEqual(.attached, std.meta.activeTag(snapshot.state)); + try testing.expectEqual(@as(u32, 48), snapshot.extent.width); + try testing.expectEqual(@as(u32, 24), snapshot.extent.height); } -test "surface public descriptors report invalid native arguments" { - if (!build_options.supports_metal and !build_options.supports_vulkan) return error.SkipZigTest; - var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); - defer map.close() catch @panic("map close failed"); - - if (build_options.supports_metal) { - const pool = try metal_support.AutoreleasePool.init(); - defer pool.deinit(); - const layer = try metal_support.createLayer(); - try testing.expectError(error.InvalidArgument, maplibre.attachMetalSurface(&map, .{ .extent = .{ .width = 0 }, .layer = nativePointer(layer) })); - try testing.expectError(error.InvalidArgument, maplibre.attachMetalSurface(&map, .{ .extent = .{ .height = 0 }, .layer = nativePointer(layer) })); - try testing.expectError(error.InvalidArgument, maplibre.attachMetalSurface(&map, .{ .extent = .{ .scale_factor = 0 }, .layer = nativePointer(layer) })); - } else if (build_options.supports_vulkan) { - const fake = nativePointer(@ptrFromInt(1)); - const descriptor = maplibre.VulkanSurfaceDescriptor{ - .context = .{ - .instance = fake, - .physical_device = fake, - .device = fake, - .graphics_queue = fake, - .graphics_queue_family_index = 0, - .get_instance_proc_addr = null, - .get_device_proc_addr = null, - }, - .surface = fake, - }; - try testing.expectError(error.InvalidArgument, maplibre.attachVulkanSurface(&map, .{ .extent = .{ .width = 0 }, .context = descriptor.context, .surface = descriptor.surface })); - try testing.expectError(error.InvalidArgument, maplibre.attachVulkanSurface(&map, .{ .extent = .{ .height = 0 }, .context = descriptor.context, .surface = descriptor.surface })); - try testing.expectError(error.InvalidArgument, maplibre.attachVulkanSurface(&map, .{ .extent = .{ .scale_factor = 0 }, .context = descriptor.context, .surface = descriptor.surface })); - } -} +test "Metal surface caller driver presents when the host services it" { + if (!supports_metal_surface) return error.SkipZigTest; + const pool = try metal.AutoreleasePool.init(); + defer pool.deinit(); -test "unsupported public surface backends report unsupported" { - if (!build_options.supports_metal and !build_options.supports_vulkan) return error.SkipZigTest; + var layer = try metal.createCountingWindowLayer(24, 12); + defer layer.deinit(); var runtime = try maplibre.RuntimeHandle.create(testing.allocator, .{}, null); - defer runtime.close() catch @panic("runtime close failed"); - var map = try maplibre.MapHandle.create(&runtime, .{}); + defer support.closeRuntime(&runtime) catch @panic("runtime close failed"); + var map = try createMap(&runtime, 24, 12); defer map.close() catch @panic("map close failed"); - if (build_options.supports_metal) { - const fake = nativePointer(@ptrFromInt(1)); - try testing.expectError(error.Unsupported, maplibre.attachVulkanSurface(&map, .{ - .context = .{ - .instance = fake, - .physical_device = fake, - .device = fake, - .graphics_queue = fake, - .graphics_queue_family_index = 0, - .get_instance_proc_addr = null, - .get_device_proc_addr = null, - }, - .surface = fake, - })); - } else if (build_options.supports_vulkan) { - try testing.expectError(error.Unsupported, maplibre.attachMetalSurface(&map, .{ .layer = nativePointer(@ptrFromInt(1)) })); - } + var session = try finishAttachment( + try maplibre.attachMetalSurface( + &map, + try descriptor(layer, .{ .width = 24, .height = 12 }), + .{ .driver = .caller_graphics_thread }, + ), + true, + ); + defer closeSession(&session, true) catch @panic("render session close failed"); + + try testing.expectEqual(.caller_graphics_thread, (try session.capabilities()).driver); + try testing.expectEqual( + .committed, + try support.waitForCommandDisposition(&runtime, try map.setStyleJson(testing.allocator, support.style_json)), + ); + try support.waitForBarrier(&runtime); + _ = try expectRenderedFrame(session, 3_000, true); + try testing.expect((try layer.nextDrawableCount()) != 0); } diff --git a/bindings/zig/tests/test_hooks.zig b/bindings/zig/tests/test_hooks.zig deleted file mode 100644 index 42c1c24ca..000000000 --- a/bindings/zig/tests/test_hooks.zig +++ /dev/null @@ -1,20 +0,0 @@ -extern fn mln_zig_test_fail_next_owned_texture_frame_wrapper_allocation() void; -extern fn mln_zig_test_use_counting_buffer_destroy() void; -extern fn mln_zig_test_restore_buffer_destroy() void; -extern fn mln_zig_test_buffer_destroy_count() usize; - -pub fn failNextOwnedTextureFrameWrapperAllocation() void { - mln_zig_test_fail_next_owned_texture_frame_wrapper_allocation(); -} - -pub fn useCountingBufferDestroy() void { - mln_zig_test_use_counting_buffer_destroy(); -} - -pub fn restoreBufferDestroy() void { - mln_zig_test_restore_buffer_destroy(); -} - -pub fn bufferDestroyCount() usize { - return mln_zig_test_buffer_destroy_count(); -} diff --git a/build.zig b/build.zig index 2f35303ba..b8a97a047 100644 --- a/build.zig +++ b/build.zig @@ -14,6 +14,7 @@ const BuildOptions = struct { dependency_library_dirs: []const std.Build.LazyPath, render_backend: RenderBackend, system_root: ?std.Build.LazyPath, + target_libc: ?std.Build.LazyPath, }; pub const RenderBackend = enum { @@ -160,6 +161,14 @@ pub fn maybeSystemRootPath(b: *std.Build) ?std.Build.LazyPath { return b.option(std.Build.LazyPath, "system-root", "Target platform SDK or sysroot"); } +fn takeTargetLibCPath(b: *std.Build) ?std.Build.LazyPath { + const path = b.libc_file orelse return null; + // `--libc` is a global Zig build option. Keep it off host tools such as + // zigglgen, and attach it only to target executables. + b.libc_file = null; + return lazyPath(path); +} + pub fn addIncludePaths(module: *std.Build.Module, include_dirs: []const std.Build.LazyPath) void { for (include_dirs) |include_dir| { module.addIncludePath(include_dir); @@ -555,6 +564,7 @@ fn addTestCompile(b: *std.Build, options: BuildOptions, root_source_file: std.Bu }), .use_lld = if (isAppleMobile(options.target)) false else null, }); + tests.setLibCFile(options.target_libc); if (isAppleMobile(options.target)) { tests.root_module.addCSourceFile(.{ .file = b.path("src/zig_test_support/ios_simulator_dyld_stub.m") }); } @@ -589,14 +599,11 @@ fn addBindingTests(b: *std.Build, options: BuildOptions, maplibre_native_ffi: *s tests.root_module.addImport("wgl_test_context", wgl_test_context); } } - if (options.render_backend == .metal) { - if (isAppleMobile(options.target)) { - tests.root_module.addCSourceFile(.{ .file = b.path("bindings/zig/tests/metal_support_ios.m") }); - tests.root_module.linkSystemLibrary("objc", .{}); - tests.root_module.linkFramework("Foundation", .{}); - } + if (options.render_backend == .metal and options.target.result.os.tag.isDarwin()) { + tests.root_module.addCSourceFile(.{ .file = b.path("bindings/zig/tests/metal_support_apple.m") }); + tests.root_module.linkSystemLibrary("objc", .{}); + tests.root_module.linkFramework("Foundation", .{}); if (options.target.result.os.tag == .macos) { - tests.root_module.addCSourceFile(.{ .file = b.path("bindings/zig/tests/metal_support_macos.m") }); tests.root_module.linkFramework("AppKit", .{}); } } @@ -628,13 +635,20 @@ pub fn addTestRunStep( fn addAndroidTestRunStep( b: *std.Build, tests: []const *std.Build.Step.Compile, + target: std.Build.ResolvedTarget, native_install_dir: std.Build.LazyPath, android_runner: std.Build.LazyPath, ) *std.Build.Step.Run { + const abi = switch (target.result.cpu.arch) { + .aarch64 => "arm64-v8a", + .x86_64 => "x86_64", + else => @panic("Android test runner supports ARM64 and x64 targets only"), + }; const run_tests = b.addSystemCommand(&.{ "bash", android_runner.getPath(b), "180", + abi, installPath(b, native_install_dir, "lib/libmaplibre-native-c.so").getPath(b), "--", }); @@ -646,6 +660,7 @@ fn addAndroidTestRunStep( pub fn build(b: *std.Build) void { const native_install_dir = maybeNativeInstallDirPath(b); + const target_libc = takeTargetLibCPath(b); const target = if (native_install_dir) |install_dir| nativeTarget(b, install_dir) else @@ -679,6 +694,7 @@ pub fn build(b: *std.Build) void { .dependency_library_dirs = dependency_library_dirs, .render_backend = backend, .system_root = system_root, + .target_libc = target_libc, }; const maplibre_native_ffi = addMaplibreNativeModule(b, options); @@ -707,6 +723,7 @@ pub fn build(b: *std.Build) void { const run_tests = addAndroidTestRunStep( b, &test_compiles, + options.target, options.native_install_dir, b.path("scripts/run-android-emulator-test.sh"), ); diff --git a/ci/workflow.toml b/ci/workflow.toml index 94453f76c..d3bef09f5 100644 --- a/ci/workflow.toml +++ b/ci/workflow.toml @@ -14,7 +14,6 @@ commands = [ platforms = ["linux"] commands = [ { task = "//bindings/rust:test:download", include = ["linux-x64-vulkan"], preset = false }, - { task = "//bindings/dart:test:download", include = ["linux-x64-vulkan"], preset = false }, ] # Zig is unavailable on Windows ARM64; see the `core:zig` note in bindings/zig/mise.toml. @@ -64,11 +63,9 @@ commands = [{ task = "//examples/c-map:build" }] platforms = ["linux", "macos", "windows"] commands = [{ task = "//bindings/python:test" }] -# TODO(#412): Re-enable //bindings/dart:test once thread-affine ownership is -# safe across isolate migration; test processes otherwise wedge until timeout. [[suites]] platforms = ["linux", "macos", "windows"] -commands = [{ task = "//bindings/dart:build" }] +commands = [{ task = "//bindings/dart:test" }] # Run Rust browser tests against the Emscripten build. [[suites]] diff --git a/cmake/mln_ffi_c_api.cmake b/cmake/mln_ffi_c_api.cmake index 5923f79fa..de4e87c57 100644 --- a/cmake/mln_ffi_c_api.cmake +++ b/cmake/mln_ffi_c_api.cmake @@ -92,22 +92,30 @@ function(mln_ffi_configure_c_api_implementation target) ${PROJECT_SOURCE_DIR}/src/c_api/android.cpp ${PROJECT_SOURCE_DIR}/src/c_api/buffer.cpp ${PROJECT_SOURCE_DIR}/src/c_api/callback_adapter.cpp + ${PROJECT_SOURCE_DIR}/src/c_api/camera.cpp ${PROJECT_SOURCE_DIR}/src/c_api/diagnostics.cpp ${PROJECT_SOURCE_DIR}/src/c_api/logging.cpp ${PROJECT_SOURCE_DIR}/src/c_api/map.cpp + ${PROJECT_SOURCE_DIR}/src/c_api/projection.cpp + ${PROJECT_SOURCE_DIR}/src/c_api/query.cpp + ${PROJECT_SOURCE_DIR}/src/c_api/style.cpp ${PROJECT_SOURCE_DIR}/src/c_api/network.cpp ${PROJECT_SOURCE_DIR}/src/c_api/render_session.cpp ${PROJECT_SOURCE_DIR}/src/c_api/runtime.cpp ${PROJECT_SOURCE_DIR}/src/c_api/surface.cpp ${PROJECT_SOURCE_DIR}/src/c_api/texture.cpp ${PROJECT_SOURCE_DIR}/src/c_api/version.cpp + ${PROJECT_SOURCE_DIR}/src/completion/completion.cpp ${PROJECT_SOURCE_DIR}/src/diagnostics/diagnostics.cpp ${PROJECT_SOURCE_DIR}/src/geojson/geojson.cpp ${PROJECT_SOURCE_DIR}/src/geojson/geojson_source_data.cpp ${PROJECT_SOURCE_DIR}/src/handles/handle_table.cpp + ${PROJECT_SOURCE_DIR}/src/execution/runtime_executor.cpp ${PROJECT_SOURCE_DIR}/src/logging/logging.cpp + ${PROJECT_SOURCE_DIR}/src/operation/operation.cpp ${PROJECT_SOURCE_DIR}/src/map/feature_state.cpp ${PROJECT_SOURCE_DIR}/src/map/map.cpp + ${PROJECT_SOURCE_DIR}/src/map/style.cpp ${PROJECT_SOURCE_DIR}/src/render/render_session_common.cpp ${PROJECT_SOURCE_DIR}/src/render/surface_session.cpp ${PROJECT_SOURCE_DIR}/src/render/texture_session.cpp @@ -118,6 +126,7 @@ function(mln_ffi_configure_c_api_implementation target) ${PROJECT_SOURCE_DIR}/src/resources/resource_loader.cpp ${PROJECT_SOURCE_DIR}/src/style/style_value.cpp ${PROJECT_SOURCE_DIR}/src/runtime/runtime.cpp) + list(APPEND MLN_FFI_C_API_SOURCES ${PROJECT_SOURCE_DIR}/src/wake/wake.cpp) # Every Apple target needs the pool, not just the Metal backend: MoltenVK and # the platform frameworks hand back autoreleased objects under Vulkan and diff --git a/cmake/mln_ffi_emscripten.cmake b/cmake/mln_ffi_emscripten.cmake index d7382d529..cf1794984 100644 --- a/cmake/mln_ffi_emscripten.cmake +++ b/cmake/mln_ffi_emscripten.cmake @@ -9,17 +9,18 @@ if(NOT EMSCRIPTEN) return() endif() -set(MLN_FFI_EMSCRIPTEN_PTHREAD_POOL_SIZE "16" +# The shared map teardown lane is reserved before map worker pools start. +set(MLN_FFI_EMSCRIPTEN_PTHREAD_POOL_SIZE "17" CACHE STRING "Emscripten pre-spawned pthread pool size") set(MLN_FFI_EMSCRIPTEN_INITIAL_MEMORY "512MB" CACHE STRING "Initial WASM linear memory") set(MLN_FFI_EMSCRIPTEN_STACK_SIZE "1MB" CACHE STRING "WASM stack size for native rendering code") -# MapLibre runs its tile work on threads, and a browser host drives the map from -# whichever pthread owns the runtime, so pthreads are not optional here. They -# require the page to be cross-origin isolated (COOP/COEP), which is a -# deployment constraint for anything embedding a browser build. +# MapLibre runs tile work on threads, and every runtime owns a worker pthread, +# so pthreads are not optional here. They require the page to be cross-origin +# isolated (COOP/COEP), which is a deployment constraint for anything embedding +# a browser build. add_compile_options(-pthread) # WebGPU is asynchronous in a browser and MapLibre calls it synchronously, so diff --git a/cmake/mln_ffi_tests.cmake b/cmake/mln_ffi_tests.cmake index faa28bbfa..7be527281 100644 --- a/cmake/mln_ffi_tests.cmake +++ b/cmake/mln_ffi_tests.cmake @@ -69,6 +69,13 @@ function(mln_ffi_configure_browser_c_api_test) endfunction() function(mln_ffi_add_c_api_test) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + add_test( + NAME execution-conventions + COMMAND + ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/scripts/check-execution-conventions.py + ${PROJECT_SOURCE_DIR}/include/maplibre_native_c) get_target_property(test_supported mln_ffi_platform_dependencies MLN_FFI_TEST_SUPPORTED) if(NOT test_supported) @@ -129,8 +136,17 @@ function(mln_ffi_add_c_api_test) # here; CONFIGURE_DEPENDS reruns the glob when the directory changes. file(GLOB test_abi_sources CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/src/c_api/tests/*_abi.c) - set(test_sources ${PROJECT_SOURCE_DIR}/src/c_api/tests/main.c - ${PROJECT_SOURCE_DIR}/src/c_api/tests/test_support.c ${test_abi_sources}) + set(test_sources + ${PROJECT_SOURCE_DIR}/src/c_api/tests/main.c + ${PROJECT_SOURCE_DIR}/src/c_api/tests/test_support.c + ${PROJECT_SOURCE_DIR}/src/c_api/tests/test_support.cpp ${test_abi_sources}) + if(MLN_FFI_RENDER_BACKEND STREQUAL "metal") + list(APPEND test_sources + ${PROJECT_SOURCE_DIR}/src/c_api/tests/metal_surface_test_support.mm) + set_source_files_properties( + ${PROJECT_SOURCE_DIR}/src/c_api/tests/metal_surface_test_support.mm + PROPERTIES COMPILE_OPTIONS -fobjc-arc) + endif() # Each *_abi.c reaches the runner through one run__tests() call in # main.c. main.c carries no preprocessor guards, so a plain text match is @@ -160,15 +176,40 @@ function(mln_ffi_add_c_api_test) add_executable(mln_ffi_c_api_tests ${test_sources}) set_target_properties( mln_ffi_c_api_tests - PROPERTIES C_STANDARD 23 C_STANDARD_REQUIRED YES C_EXTENSIONS OFF) + PROPERTIES + C_STANDARD + 23 + C_STANDARD_REQUIRED + YES + C_EXTENSIONS + OFF + CXX_STANDARD + 23 + CXX_STANDARD_REQUIRED + YES + CXX_EXTENSIONS + OFF) + if(TARGET maplibre_native_c_static) + set(MLN_FFI_TEST_C_API_TARGET maplibre_native_c_static) + else() + set(MLN_FFI_TEST_C_API_TARGET maplibre_native_c) + endif() target_link_libraries( mln_ffi_c_api_tests PRIVATE - maplibre_native_c unity::framework MLN_FFI::RenderDependencies + ${MLN_FFI_TEST_C_API_TARGET} unity::framework MLN_FFI::RenderDependencies ${dependency_test_libraries}) target_include_directories( mln_ffi_c_api_tests - PRIVATE ${PROJECT_SOURCE_DIR}/src/c_api/tests ${dependency_include_dirs}) + PRIVATE + ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/c_api/tests + ${dependency_include_dirs}) + target_include_directories( + mln_ffi_c_api_tests + SYSTEM + PRIVATE + ${MLN_FFI_SOURCE_DIR}/include + ${MLN_FFI_SOURCE_DIR}/vendor/maplibre-native-base/include) # Enforce the registration contract at compile time. A test that no RUN_TEST # references is an unused static function, and dropping `static` to dodge that diff --git a/docs/snippets/c/attach-opengl-surface.c b/docs/snippets/c/attach-opengl-surface.c deleted file mode 100644 index 7068e83a3..000000000 --- a/docs/snippets/c/attach-opengl-surface.c +++ /dev/null @@ -1,30 +0,0 @@ -// Attaches an EGL surface the host window toolkit already created. - -#include - -mln_render_session attach_surface( - mln_map map, void* egl_display, void* egl_config, void* egl_share_context, - void* egl_surface, uint32_t logical_width, uint32_t logical_height, - double scale_factor -) { - mln_opengl_surface_descriptor descriptor = - mln_opengl_surface_descriptor_default(); - - // Logical pixels, the same units the camera and hit-testing use. - descriptor.extent.width = logical_width; - descriptor.extent.height = logical_height; - descriptor.extent.scale_factor = scale_factor; - - descriptor.context.platform = MLN_OPENGL_CONTEXT_PLATFORM_EGL; - descriptor.context.data.egl.display = egl_display; - descriptor.context.data.egl.config = egl_config; - descriptor.context.data.egl.share_context = egl_share_context; - - descriptor.surface = egl_surface; - - mln_render_session session = MLN_HANDLE_NULL; - if (mln_opengl_surface_attach(map, &descriptor, &session) != MLN_STATUS_OK) { - return MLN_HANDLE_NULL; - } - return session; -} diff --git a/docs/snippets/c/camera-fit-bounds.c b/docs/snippets/c/camera-fit-bounds.c index d083a955d..70108f571 100644 --- a/docs/snippets/c/camera-fit-bounds.c +++ b/docs/snippets/c/camera-fit-bounds.c @@ -2,7 +2,27 @@ #include -mln_status fly_to_bounds(mln_map map, mln_lat_lng_bounds bounds) { +typedef struct fly_to_bounds_state { + mln_map map; + mln_completion fly_completion; +} fly_to_bounds_state; + +static void fitted(void* user_data, const mln_completion_result* result) { + fly_to_bounds_state* state = user_data; + if (result->status != MLN_STATUS_OK || result->value_count != 1) return; + + // #region fit + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_FLY; + update.camera = *(const mln_camera_options*)result->value; + mln_map_update_camera(state->map, &update, &state->fly_completion); + // #endregion fit +} + +mln_status fly_to_bounds( + mln_map map, mln_lat_lng_bounds bounds, fly_to_bounds_state* state, + const mln_completion* fly_completion +) { // #region padding mln_camera_fit_options fit = mln_camera_fit_options_default(); fit.fields = MLN_CAMERA_FIT_OPTION_PADDING; @@ -10,14 +30,12 @@ mln_status fly_to_bounds(mln_map map, mln_lat_lng_bounds bounds) { (mln_edge_insets){.top = 24, .left = 24, .bottom = 24, .right = 24}; // #endregion padding - // #region fit - mln_camera_options fitted = mln_camera_options_default(); - const mln_status status = - mln_map_camera_for_lat_lng_bounds(map, bounds, &fit, &fitted); - if (status != MLN_STATUS_OK) { - return status; - } - - return mln_map_fly_to(map, &fitted, NULL); - // #endregion fit + state->map = map; + state->fly_completion = *fly_completion; + const mln_completion fit_completion = { + .size = sizeof(mln_completion), + .callback = fitted, + .user_data = state, + }; + return mln_map_camera_for_lat_lng_bounds(map, bounds, &fit, &fit_completion); } diff --git a/docs/snippets/c/camera-gesture.c b/docs/snippets/c/camera-gesture.c index c970fa8ed..3e3376b1b 100644 --- a/docs/snippets/c/camera-gesture.c +++ b/docs/snippets/c/camera-gesture.c @@ -1,36 +1,58 @@ -// Applying drag and pinch input within one gesture. +// Applying relative drag and pinch input within one gesture. #include -void begin_gesture(mln_map map) { +static mln_status submit_gesture_phase( + mln_map map, uint32_t phase, const mln_completion* completion +) { + mln_camera_update update = mln_camera_update_default(); + update.gesture_phase = phase; + return mln_map_update_camera(map, &update, completion); +} + +void begin_gesture(mln_map map, const mln_completion* completion) { // #region bracket - mln_map_cancel_transitions(map); - mln_map_set_gesture_in_progress(map, true); + submit_gesture_phase(map, MLN_GESTURE_PHASE_BEGIN, completion); // #endregion bracket } -void drag_by(mln_map map, double delta_x, double delta_y) { +void drag_by( + mln_map map, mln_screen_point offset, const mln_completion* completion +) { // #region drag - // Screen-space deltas in logical pixels, measured since the last call rather - // than from the start of the gesture. - mln_map_move_by(map, delta_x, delta_y); + mln_camera_delta delta = mln_camera_delta_default(); + delta.offset = offset; + mln_map_apply_camera_delta(map, &delta, completion); // #endregion drag } -void pinch_by(mln_map map, double scale, mln_screen_point focus) { +void pinch_by( + mln_map map, double scale, mln_screen_point focus, + const mln_completion* completion +) { // #region pinch - // The focus point stays fixed on screen while the map scales around it. - mln_map_scale_by(map, scale, &focus); + mln_camera_delta delta = mln_camera_delta_default(); + delta.kind = MLN_CAMERA_DELTA_SCALE; + delta.amount = scale; + delta.has_anchor = true; + delta.anchor = focus; + mln_map_apply_camera_delta(map, &delta, completion); // #endregion pinch } -void end_gesture(mln_map map, double residual_scale, mln_screen_point focus) { +void end_gesture( + mln_map map, double residual_scale, mln_screen_point focus, + const mln_completion* inertia_completion, const mln_completion* end_completion +) { // #region release - mln_map_set_gesture_in_progress(map, false); - - mln_animation_options animation = mln_animation_options_default(); - animation.fields |= MLN_ANIMATION_OPTION_DURATION; - animation.duration_ms = 250.0; - mln_map_scale_by_animated(map, residual_scale, &focus, &animation); + mln_camera_delta delta = mln_camera_delta_default(); + delta.kind = MLN_CAMERA_DELTA_SCALE; + delta.amount = residual_scale; + delta.has_anchor = true; + delta.anchor = focus; + delta.animation.fields = MLN_ANIMATION_OPTION_DURATION; + delta.animation.duration_ms = 250.0; + mln_map_apply_camera_delta(map, &delta, inertia_completion); + submit_gesture_phase(map, MLN_GESTURE_PHASE_END, end_completion); // #endregion release } diff --git a/docs/snippets/c/camera.c b/docs/snippets/c/camera.c index 8a34fefd6..e8f466bfd 100644 --- a/docs/snippets/c/camera.c +++ b/docs/snippets/c/camera.c @@ -18,34 +18,35 @@ static mln_camera_options downtown(void) { } // #region jump -mln_status jump_downtown(mln_map map) { - const mln_camera_options camera = downtown(); - return mln_map_jump_to(map, &camera); +mln_status jump_downtown(mln_map map, const mln_completion* completion) { + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_JUMP; + update.camera = downtown(); + return mln_map_update_camera(map, &update, completion); } // #endregion jump -mln_status ease_downtown(mln_map map, uint64_t transition_id) { - const mln_camera_options camera = downtown(); - +mln_status ease_downtown( + mln_map map, uint64_t transition_id, const mln_completion* completion +) { // #region ease - mln_animation_options animation = mln_animation_options_default(); - animation.fields = MLN_ANIMATION_OPTION_DURATION | - MLN_ANIMATION_OPTION_EASING | - MLN_ANIMATION_OPTION_TRANSITION_ID; - animation.duration_ms = 800.0; - // Control points of a cubic bezier that runs from (0, 0) to (1, 1). - animation.easing = + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_EASE; + update.camera = downtown(); + update.animation.fields = MLN_ANIMATION_OPTION_DURATION | + MLN_ANIMATION_OPTION_EASING | + MLN_ANIMATION_OPTION_TRANSITION_ID; + update.animation.duration_ms = 800.0; + update.animation.easing = (mln_unit_bezier){.x1 = 0.25, .y1 = 0.1, .x2 = 0.25, .y2 = 1.0}; - animation.transition_id = transition_id; - return mln_map_ease_to(map, &camera, &animation); + update.animation.transition_id = transition_id; + return mln_map_update_camera(map, &update, completion); // #endregion ease } -// A map reports a finished transition only while its subscription selects this -// type. -mln_status select_camera_events(mln_map map) { +mln_status select_camera_events(mln_map map, const mln_completion* completion) { return mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED + map, MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED, completion ); } @@ -56,8 +57,7 @@ bool transition_finished( if (event->type != MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED) { return false; } - const mln_runtime_event_camera_transition_finished* finished = - &event->payload.camera_transition_finished; - return finished->transition_id == transition_id; + return event->payload.camera_transition_finished.transition_id == + transition_id; // #endregion finished } diff --git a/docs/snippets/c/cluster-leaves.c b/docs/snippets/c/cluster-leaves.c index 7738a8336..dd3c40516 100644 --- a/docs/snippets/c/cluster-leaves.c +++ b/docs/snippets/c/cluster-leaves.c @@ -7,28 +7,32 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -static void read_leaves(mln_buffer result) { +static void read_leaves(void* user_data, const mln_completion_result* result) { + (void)user_data; // #region leaves - mln_buffer_view json = {0}; - if (mln_buffer_get(result, &json) != MLN_STATUS_OK) return; - // Parse json.data[0..json.size] as a GeoJSON FeatureCollection. Each feature - // is one point that the cluster contains. + if (result->status != MLN_STATUS_OK || result->value_count != 1) return; + const mln_buffer_view json = *(const mln_buffer_view*)result->value; + (void)json; + // Parse json.data[0..json.size] before returning. Each feature is one point + // that the cluster contains. // #endregion leaves } -void list_cluster_leaves(mln_render_session session, mln_buffer_view cluster) { +mln_status list_cluster_leaves( + mln_render_session session, mln_buffer_view cluster +) { // #region arguments const mln_buffer_view arguments = view("{\"limit\":10,\"offset\":0}"); // #endregion arguments // #region query - mln_buffer result = MLN_HANDLE_NULL; - const mln_status queried = mln_render_session_query_feature_extensions( + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = read_leaves, + }; + return mln_render_session_query_feature_extensions( session, view("places"), cluster, view("supercluster"), view("leaves"), - &arguments, &result + &arguments, &completion ); - if (queried == MLN_STATUS_OK) read_leaves(result); - - mln_buffer_destroy(result); // #endregion query } diff --git a/docs/snippets/c/create-map.c b/docs/snippets/c/create-map.c index 2ee1cd262..4ba1585ca 100644 --- a/docs/snippets/c/create-map.c +++ b/docs/snippets/c/create-map.c @@ -1,36 +1,39 @@ -// Creating a runtime and a map on one thread, choosing the options that are -// fixed for the map's life, and releasing the two in order. +// Creating a runtime and a map, choosing the options that are fixed for the +// map's life, and closing the two in order. #include mln_status open_runtime(const char* cache_path, mln_runtime* out_runtime) { // #region runtime mln_runtime_options options = mln_runtime_options_default(); - - // A filesystem path keeps cached tiles across runs of the host. The default, - // ":memory:", discards them when the runtime is destroyed. options.cache_path = cache_path; - return mln_runtime_create(&options, out_runtime); // #endregion runtime } +static void map_created(void* user_data, const mln_completion_result* result) { + mln_map* out_map = user_data; + if (result->status == MLN_STATUS_OK && result->value_count == 1) { + *out_map = *(const mln_map*)result->value; + } +} + mln_status open_map( mln_runtime runtime, uint32_t width, uint32_t height, double scale_factor, mln_map* out_map ) { // #region map mln_map_options options = mln_map_options_default(); - - // Replaced by the render target's extent at the first attach. Until then it - // is the viewport that camera and projection queries answer against. - options.width = width; - options.height = height; - - // Fixed for the map's life. It selects sprites, glyphs, and raster tiles. - options.scale_factor = scale_factor; - - return mln_map_create(runtime, &options, out_map); + options.initial_extent = (mln_logical_extent){ + .width = width, .height = height, .scale_factor = scale_factor + }; + *out_map = MLN_HANDLE_NULL; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = map_created, + .user_data = out_map, + }; + return mln_map_create(runtime, &options, &completion); // #endregion map } @@ -39,18 +42,46 @@ mln_status open_static_map( ) { // #region mode mln_map_options options = mln_map_options_default(); - options.width = width; - options.height = height; - options.scale_factor = 1.0; + options.initial_extent = + (mln_logical_extent){.width = width, .height = height, .scale_factor = 1.0}; options.map_mode = MLN_MAP_MODE_STATIC; - - return mln_map_create(runtime, &options, out_map); + *out_map = MLN_HANDLE_NULL; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = map_created, + .user_data = out_map, + }; + return mln_map_create(runtime, &options, &completion); // #endregion mode } +static void runtime_torn_down( + void* user_data, const mln_completion_result* result +) { + (void)result; + // Signal the host's own shutdown gate here; after this callback returns, no + // library thread runs, so the process may exit. + (void)user_data; +} + +static void map_torn_down( + void* user_data, const mln_completion_result* result +) { + (void)user_data; + (void)result; +} + void close_map(mln_runtime runtime, mln_map map) { // #region release - mln_map_destroy(map); - mln_runtime_destroy(runtime); + const mln_completion map_teardown = { + .size = sizeof(mln_completion), + .callback = map_torn_down, + }; + (void)mln_map_release(map, &map_teardown); + const mln_completion teardown = { + .size = sizeof(mln_completion), + .callback = runtime_torn_down, + }; + (void)mln_runtime_release(runtime, &teardown); // #endregion release } diff --git a/docs/snippets/c/feature-state-read.c b/docs/snippets/c/feature-state-read.c index a95439f45..066c9036d 100644 --- a/docs/snippets/c/feature-state-read.c +++ b/docs/snippets/c/feature-state-read.c @@ -8,21 +8,26 @@ extern bool host_json_selected(mln_buffer_view json); // #endregion member -bool read_selected(mln_map map, const mln_feature_state_selector* selector) { - // #region get - mln_buffer result = MLN_HANDLE_NULL; - const mln_status got = mln_map_get_feature_state(map, selector, &result); - if (got != MLN_STATUS_OK) return false; - // #endregion get - +static void read_selected( + void* user_data, const mln_completion_result* result +) { + bool* selected = user_data; // #region read - mln_buffer_view json = {0}; - bool selected = false; - if (mln_buffer_get(result, &json) == MLN_STATUS_OK) { - selected = host_json_selected(json); - } - - mln_buffer_destroy(result); + if (result->status != MLN_STATUS_OK || result->value_count != 1) return; + const mln_buffer_view json = *(const mln_buffer_view*)result->value; + *selected = host_json_selected(json); // #endregion read - return selected; +} + +mln_status start_read_selected( + mln_map map, const mln_feature_state_selector* selector, bool* selected +) { + // #region get + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = read_selected, + .user_data = selected, + }; + return mln_map_get_feature_state(map, selector, &completion); + // #endregion get } diff --git a/docs/snippets/c/feature-state.c b/docs/snippets/c/feature-state.c index c1b18e7a1..0e9b9a102 100644 --- a/docs/snippets/c/feature-state.c +++ b/docs/snippets/c/feature-state.c @@ -19,7 +19,10 @@ static mln_feature_state_selector select_poi(const char* feature_id) { return selector; } -mln_status set_selected(mln_map map, const char* feature_id, bool selected) { +mln_status set_selected( + mln_map map, const char* feature_id, bool selected, + const mln_completion* completion +) { const mln_feature_state_selector selector = select_poi(feature_id); // #region value @@ -28,17 +31,19 @@ mln_status set_selected(mln_map map, const char* feature_id, bool selected) { // #endregion value // #region set - // The call parses or copies the bytes before returning. - return mln_map_set_feature_state(map, &selector, state); + // The start call parses or copies the bytes before returning. + return mln_map_set_feature_state(map, &selector, state, completion); // #endregion set } -mln_status clear_selected(mln_map map, const char* feature_id) { +mln_status clear_selected( + mln_map map, const char* feature_id, const mln_completion* completion +) { // #region remove mln_feature_state_selector selector = select_poi(feature_id); selector.fields |= MLN_FEATURE_STATE_SELECTOR_STATE_KEY; selector.state_key = view("selected"); - return mln_map_remove_feature_state(map, &selector); + return mln_map_remove_feature_state(map, &selector, completion); // #endregion remove } diff --git a/docs/snippets/c/frame-loop.c b/docs/snippets/c/frame-loop.c index 4e72d6452..115503b38 100644 --- a/docs/snippets/c/frame-loop.c +++ b/docs/snippets/c/frame-loop.c @@ -1,51 +1,136 @@ -// One turn of a display-paced host loop: pump the runtime, drain its events, -// and draw a published render update. Call it from the frame callback. +// One turn of a display-paced host loop: respond to direct wake callbacks, +// service caller-driver work, and submit a frame demand when the map +// invalidates. #include +#include -// Call this before the map loads a style. Narrowing a subscription keeps the -// events already queued. -mln_status select_frame_events(mln_map map) { +typedef struct frame_receiver { + mln_runtime runtime; + mln_map map; + atomic_bool events_ready; + atomic_bool driver_ready; + atomic_bool frames_ready; + void (*schedule)(void* user_data); + void* schedule_user_data; + uint64_t next_frame_token; +} frame_receiver; + +static void mark_ready(frame_receiver* receiver, atomic_bool* flag) { + atomic_store_explicit(flag, true, memory_order_release); + receiver->schedule(receiver->schedule_user_data); +} + +void wake_events(void* user_data) { + frame_receiver* receiver = user_data; + mark_ready(receiver, &receiver->events_ready); +} + +void wake_driver(void* user_data) { + frame_receiver* receiver = user_data; + mark_ready(receiver, &receiver->driver_ready); +} + +void wake_frames(void* user_data) { + frame_receiver* receiver = user_data; + mark_ready(receiver, &receiver->frames_ready); +} + +mln_status select_frame_events(mln_map map, const mln_completion* completion) { return mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED + map, + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED, + completion ); } +// #region wants static bool wants_a_frame(const mln_runtime_event* event, mln_map map) { if (event->source != map) return false; if (event->type == MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE) return true; if (event->type != MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED) return false; return event->payload.render_frame.needs_repaint; } +// #endregion wants -void run_one_frame( - mln_runtime runtime, mln_map map, mln_render_session session, bool* pending -) { - // #region pump - mln_runtime_pump(runtime, 0, 8); +static void drain_runtime_events(frame_receiver* receiver, bool* pending) { + mln_event_batch batch = MLN_HANDLE_NULL; + if (mln_runtime_drain_events(receiver->runtime, &batch) != MLN_STATUS_OK) + return; + mln_runtime_event_batch_view view = { + .size = sizeof(mln_runtime_event_batch_view) + }; + if (mln_event_batch_get(batch, &view) == MLN_STATUS_OK) { + for (size_t index = 0; index < view.event_count; index++) { + const char* bytes = (const char*)view.events + index * view.event_size; + if (wants_a_frame((const mln_runtime_event*)bytes, receiver->map)) { + *pending = true; + } + } + } + mln_event_batch_release(batch); +} - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) return; +static void drain_frame_results(mln_render_session session, bool* pending) { + // #region results + mln_render_frame_batch batch = MLN_HANDLE_NULL; + if (mln_render_session_drain_frame_results(session, &batch) != MLN_STATUS_OK) + return; + size_t count = 0; + if (mln_render_frame_batch_count(batch, &count) == MLN_STATUS_OK) { + for (size_t index = 0; index < count; index++) { + mln_render_frame_result result = { + .size = sizeof(mln_render_frame_result) + }; + if ( + mln_render_frame_batch_get(batch, index, &result) == MLN_STATUS_OK && + result.disposition != MLN_RENDER_RESULT_RENDERED + ) { + *pending = true; + } + } + } + mln_render_frame_batch_release(batch); + // #endregion results +} - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - if (wants_a_frame((const mln_runtime_event*)bytes, map)) *pending = true; +void run_one_frame( + frame_receiver* receiver, mln_render_session session, bool* pending +) { + if ( + atomic_exchange_explicit( + &receiver->events_ready, false, memory_order_acq_rel + ) + ) { + drain_runtime_events(receiver, pending); + } + // #region service + if ( + atomic_exchange_explicit( + &receiver->driver_ready, false, memory_order_acq_rel + ) + ) { + size_t serviced = 0; + (void)mln_render_session_service_driver_work(session, 64, &serviced); + } + // #endregion service + if ( + atomic_exchange_explicit( + &receiver->frames_ready, false, memory_order_acq_rel + ) + ) { + drain_frame_results(session, pending); } - // #endregion pump // #region render if (!*pending) return; - - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; - const mln_status status = - mln_render_session_render_update(session, &result, &needs_repaint); - // A rendered frame clears the request unless the map asked for another - // frame while rendering it. Any other result keeps the frame pending for - // the next turn. - if (status == MLN_STATUS_OK && result == MLN_RENDER_RESULT_RENDERED) { - *pending = needs_repaint; + *pending = false; + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = MLN_FRAME_DEMAND_IF_NEEDED | MLN_FRAME_DEMAND_PRESENT; + demand.token = ++receiver->next_frame_token; + if (mln_render_session_request_frame(session, &demand) != MLN_STATUS_OK) { + *pending = true; } // #endregion render } diff --git a/docs/snippets/c/geojson-dataset.c b/docs/snippets/c/geojson-dataset.c index ccb865e0d..61a94d3cc 100644 --- a/docs/snippets/c/geojson-dataset.c +++ b/docs/snippets/c/geojson-dataset.c @@ -9,7 +9,10 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -mln_status show_earthquakes(mln_map map, const char* geojson_url) { +mln_status show_earthquakes( + mln_map map, const char* geojson_url, const mln_completion* source_completion, + const mln_completion* layer_completion +) { // #region options // Each value applies when its field bit is set. mln_geojson_source_options options = mln_geojson_source_options_default(); @@ -18,10 +21,9 @@ mln_status show_earthquakes(mln_map map, const char* geojson_url) { options.cluster = true; options.cluster_radius = 60; // pixels // #endregion options - // #region source mln_status status = mln_map_add_geojson_source_url( - map, view("earthquakes"), view(geojson_url), &options + map, view("earthquakes"), view(geojson_url), &options, source_completion ); if (status != MLN_STATUS_OK) return status; // #endregion source @@ -33,11 +35,15 @@ mln_status show_earthquakes(mln_map map, const char* geojson_url) { // #endregion layer // #region add-layer - return mln_map_add_style_layer_json(map, view(layer), view("")); + return mln_map_add_style_layer_json( + map, view(layer), view(""), layer_completion + ); // #endregion add-layer } -mln_status show_one_point(mln_map map, mln_lat_lng position) { +mln_status show_one_point( + mln_map map, mln_lat_lng position, const mln_completion* completion +) { // #region inline-data char data[256]; const int length = snprintf( @@ -57,7 +63,8 @@ mln_status show_one_point(mln_map map, mln_lat_lng position) { mln_geojson_source_data_create(view(data), NULL, &prepared); if (status != MLN_STATUS_OK) return status; - status = mln_map_add_geojson_source_data(map, view("pins"), prepared); + status = + mln_map_add_geojson_source_data(map, view("pins"), prepared, completion); mln_geojson_source_data_destroy(prepared); return status; // #endregion inline-data diff --git a/docs/snippets/c/geojson-live-updates.c b/docs/snippets/c/geojson-live-updates.c index f16df905e..c326efb21 100644 --- a/docs/snippets/c/geojson-live-updates.c +++ b/docs/snippets/c/geojson-live-updates.c @@ -10,7 +10,7 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -mln_status add_vehicle_source(mln_map map) { +mln_status add_vehicle_source(mln_map map, const mln_completion* completion) { // #region add mln_geojson_source_data empty = MLN_HANDLE_NULL; mln_status status = mln_geojson_source_data_create( @@ -18,7 +18,8 @@ mln_status add_vehicle_source(mln_map map) { ); if (status != MLN_STATUS_OK) return status; - status = mln_map_add_geojson_source_data(map, view("vehicles"), empty); + status = + mln_map_add_geojson_source_data(map, view("vehicles"), empty, completion); mln_geojson_source_data_destroy(empty); return status; // #endregion add @@ -26,7 +27,7 @@ mln_status add_vehicle_source(mln_map map) { mln_status publish_vehicles( mln_map map, const mln_lat_lng* positions, size_t count, char* json, - size_t json_capacity + size_t json_capacity, const mln_completion* completion ) { if (count > MAX_VEHICLES) count = MAX_VEHICLES; @@ -65,17 +66,21 @@ mln_status publish_vehicles( // #endregion prepare // #region publish - // The install is cheap and runs on the map owner thread. - status = mln_map_set_geojson_source_data(map, view("vehicles"), prepared); + // The install is a cheap command submitted to the map owner. + status = mln_map_set_geojson_source_data( + map, view("vehicles"), prepared, completion + ); mln_geojson_source_data_destroy(prepared); return status; // #endregion publish } -mln_status track_position_closely(mln_map map, bool tracking) { +mln_status track_position_closely( + mln_map map, bool tracking, const mln_completion* completion +) { // #region synchronous-tiling return mln_map_set_geojson_source_synchronous_tiling( - map, view("vehicles"), tracking + map, view("vehicles"), tracking, completion ); // #endregion synchronous-tiling } diff --git a/docs/snippets/c/handle-events.c b/docs/snippets/c/handle-events.c index 2ed8bdbe6..a7980fc25 100644 --- a/docs/snippets/c/handle-events.c +++ b/docs/snippets/c/handle-events.c @@ -11,12 +11,14 @@ typedef struct map_observer { } map_observer; // #region subscribe -mln_status select_map_events(mln_map map) { +mln_status select_map_events(mln_map map, const mln_completion* completion) { return mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED | - MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED | - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED + map, + MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED | + MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED | + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED, + completion ); } // #endregion subscribe @@ -32,11 +34,18 @@ static bool asks_for_a_repaint(const mln_runtime_event* event) { void drain_events(mln_runtime runtime, map_observer* observer) { // #region drain - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) return; + mln_event_batch batch = MLN_HANDLE_NULL; + if (mln_runtime_drain_events(runtime, &batch) != MLN_STATUS_OK) return; + mln_runtime_event_batch_view view = { + .size = sizeof(mln_runtime_event_batch_view) + }; + if (mln_event_batch_get(batch, &view) != MLN_STATUS_OK) { + mln_event_batch_release(batch); + return; + } - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; + for (size_t index = 0; index < view.event_count; index++) { + const char* bytes = (const char*)view.events + index * view.event_size; const mln_runtime_event* event = (const mln_runtime_event*)bytes; // #region match @@ -62,5 +71,6 @@ void drain_events(mln_runtime runtime, map_observer* observer) { break; } } + mln_event_batch_release(batch); // #endregion drain } diff --git a/docs/snippets/c/http-header-transform.c b/docs/snippets/c/http-header-transform.c index 5be5b79ef..24c5de85d 100644 --- a/docs/snippets/c/http-header-transform.c +++ b/docs/snippets/c/http-header-transform.c @@ -29,12 +29,14 @@ static mln_status add_authorization( } // #region install -void install_header_transform(mln_runtime runtime, char* token) { +mln_status install_header_transform( + mln_runtime runtime, char* token, const mln_completion* completion +) { mln_http_header_transform transform = { .size = sizeof(transform), .callback = add_authorization, .user_data = token, }; - mln_runtime_set_http_header_transform(runtime, &transform); + return mln_runtime_set_http_header_transform(runtime, &transform, completion); } // #endregion install diff --git a/docs/snippets/c/load-style.c b/docs/snippets/c/load-style.c index 1ffd5cfb1..497602972 100644 --- a/docs/snippets/c/load-style.c +++ b/docs/snippets/c/load-style.c @@ -3,18 +3,19 @@ #include #include -mln_status load_style_from_url(mln_map map, const char* style_url) { +mln_status load_style_from_url( + mln_map map, const char* style_url, const mln_completion* completion +) { // #region url - // Returns once the map accepts the request. The style, its sources, and its - // tiles arrive across the pumps that follow. - return mln_map_set_style_url(map, style_url); + return mln_map_set_style_url(map, style_url, completion); // #endregion url } -mln_status load_style_from_text(mln_map map, const char* style_json) { +mln_status load_style_from_text( + mln_map map, const char* style_json, const mln_completion* completion +) { // #region json - // MapLibre parses or copies these UTF-8 bytes before the call returns. const mln_buffer_view json = {.data = style_json, .size = strlen(style_json)}; - return mln_map_set_style_json(map, json); + return mln_map_set_style_json(map, json, completion); // #endregion json } diff --git a/docs/snippets/c/log-callback.c b/docs/snippets/c/log-callback.c index 7b5c3cfc0..dc9a2a987 100644 --- a/docs/snippets/c/log-callback.c +++ b/docs/snippets/c/log-callback.c @@ -42,7 +42,7 @@ static uint32_t forward_to_host( mln_status capture_logs(void) { // #region install // The process-global callback remains valid for every runtime's lifetime. - return mln_log_set_callback(forward_to_host, NULL); + return mln_log_set_callback(forward_to_host, NULL, NULL); // #endregion install } diff --git a/docs/snippets/c/minimal-lifecycle.c b/docs/snippets/c/minimal-lifecycle.c deleted file mode 100644 index 7facd8ae7..000000000 --- a/docs/snippets/c/minimal-lifecycle.c +++ /dev/null @@ -1,73 +0,0 @@ -// Creates a runtime and a map, loads a style, pumps until the style resolves, -// and tears everything down. One thread does all of it: the thread that creates -// the runtime owns the runtime and every map under it. - -#include -#include -#include - -int main(void) { - mln_runtime_options runtime_options = mln_runtime_options_default(); - runtime_options.cache_path = ":memory:"; - - mln_runtime runtime = MLN_HANDLE_NULL; - if (mln_runtime_create(&runtime_options, &runtime) != MLN_STATUS_OK) { - return 1; - } - - mln_map_options map_options = mln_map_options_default(); - map_options.width = 512; - map_options.height = 512; - map_options.scale_factor = 1.0; - - mln_map map = MLN_HANDLE_NULL; - if (mln_map_create(runtime, &map_options, &map) != MLN_STATUS_OK) { - mln_runtime_destroy(runtime); - return 1; - } - - // A map queues an event only while its subscription selects the type. - mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED | - MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED - ); - - // This call only accepts the request; the outcome arrives later as an event. - const mln_status requested = - mln_map_set_style_url(map, "https://tiles.openfreemap.org/styles/bright"); - bool loaded = false; - bool settled = requested != MLN_STATUS_OK; - - const time_t deadline = time(NULL) + 30; - while (!settled && time(NULL) < deadline) { - // A positive timeout parks the thread until work arrives or it expires. - mln_runtime_pump(runtime, 100, -1); - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - mln_runtime_drain_events(runtime, 0, &batch); - - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - const mln_runtime_event* event = (const mln_runtime_event*)bytes; - if (event->source != map) continue; - if (event->type == MLN_RUNTIME_EVENT_MAP_STYLE_LOADED) { - loaded = true; - settled = true; - } else if (event->type == MLN_RUNTIME_EVENT_MAP_LOADING_FAILED) { - // A message lives in the batch's arena, at the event's own offset. - if (event->message_size > 0) { - fprintf( - stderr, "style failed: %.*s\n", (int)event->message_size, - batch.messages + event->message_offset - ); - } - settled = true; - } - } - } - - // mln_runtime_destroy returns MLN_STATUS_INVALID_STATE while a map is live. - mln_map_destroy(map); - mln_runtime_destroy(runtime); - return loaded ? 0 : 1; -} diff --git a/docs/snippets/c/offline-region.c b/docs/snippets/c/offline-region.c index 9228d8d57..9172277ed 100644 --- a/docs/snippets/c/offline-region.c +++ b/docs/snippets/c/offline-region.c @@ -5,52 +5,46 @@ #include #include -// Pumps until the completion event for this operation arrives, and returns the -// operation's own result status. -static mln_status await_operation( - mln_runtime runtime, mln_offline_operation_id operation_id +typedef struct offline_download { + mln_runtime runtime; + mln_offline_region_id region_id; + mln_completion observe_completion; + mln_completion download_completion; +} offline_download; + +static void region_created( + void* user_data, const mln_completion_result* result ) { - for (;;) { - mln_runtime_pump(runtime, 100, -1); + offline_download* download = user_data; + // #region result + if (result->status != MLN_STATUS_OK || result->value_count != 1) return; + const mln_offline_region_info* info = result->value; + // Copy definition and metadata here if the host keeps them after return. + // #endregion result - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { - return MLN_STATUS_NATIVE_ERROR; - } + // #region region-id + download->region_id = info->id; + // #endregion region-id - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - const mln_runtime_event* event = (const mln_runtime_event*)bytes; - if (event->type != MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED) { - continue; - } - const mln_runtime_event_offline_operation_completed* completed = - &event->payload.offline_operation_completed; - if (completed->operation_id == operation_id) { - return (mln_status)completed->result_status; - } - } - } -} + (void)mln_runtime_offline_region_set_observed( + download->runtime, info->id, true, &download->observe_completion + ); -// An operation with no result still holds runtime state until a discard. -static mln_status finish_operation( - mln_runtime runtime, mln_offline_operation_id operation_id -) { - const mln_status status = await_operation(runtime, operation_id); - mln_runtime_offline_operation_discard(runtime, operation_id); - return status; + // #region download + (void)mln_runtime_offline_region_set_download_state( + download->runtime, info->id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, + &download->download_completion + ); + // #endregion download } -// Returns the new region's ID, or zero. -mln_offline_region_id download_region( - mln_runtime runtime, mln_lat_lng_bounds bounds, const char* metadata +mln_status download_region( + mln_runtime runtime, mln_lat_lng_bounds bounds, const char* metadata, + offline_download* download, const mln_completion* observe_completion, + const mln_completion* download_completion ) { - // The wait below ends on the completion event, so the subscription has to - // select that type. mln_runtime_set_event_mask( - runtime, MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED | - MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED + runtime, MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED ); // #region define @@ -73,62 +67,24 @@ mln_offline_region_id download_region( }; // #endregion tag + download->runtime = runtime; + download->region_id = 0; + download->observe_completion = *observe_completion; + download->download_completion = *download_completion; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = region_created, + .user_data = download, + }; + // #region create - mln_offline_operation_id create_id = 0; - if ( - mln_runtime_offline_region_create_start( - runtime, &definition, (const uint8_t*)metadata, strlen(metadata), - &create_id - ) != MLN_STATUS_OK - ) { - return 0; - } + return mln_runtime_offline_region_create( + runtime, &definition, (const uint8_t*)metadata, strlen(metadata), + &completion + ); // #endregion create - - // #region result - mln_offline_region_snapshot region = MLN_HANDLE_NULL; - if (await_operation(runtime, create_id) == MLN_STATUS_OK) { - mln_runtime_offline_region_create_take_result(runtime, create_id, ®ion); - } - if (region == MLN_HANDLE_NULL) { - mln_runtime_offline_operation_discard(runtime, create_id); - return 0; - } - // #endregion result - - // #region region-id - // Copy metadata before destroying the snapshot; info.id remains valid. - mln_offline_region_info info = {.size = sizeof(info)}; - mln_offline_region_snapshot_get(region, &info); - mln_offline_region_snapshot_destroy(region); - // #endregion region-id - - mln_offline_operation_id observe_id = 0; - if ( - mln_runtime_offline_region_set_observed_start( - runtime, info.id, true, &observe_id - ) != MLN_STATUS_OK || - finish_operation(runtime, observe_id) != MLN_STATUS_OK - ) { - return 0; - } - - // #region download - mln_offline_operation_id download_id = 0; - if ( - mln_runtime_offline_region_set_download_state_start( - runtime, info.id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, &download_id - ) != MLN_STATUS_OK || - finish_operation(runtime, download_id) != MLN_STATUS_OK - ) { - return 0; - } - // #endregion download - - return info.id; } -// Reads one progress event, and reports whether the region is fully stored. bool region_progress( const mln_runtime_event* event, mln_offline_region_id region_id, double* out_fraction @@ -141,7 +97,6 @@ bool region_progress( &event->payload.offline_region_status; if (progress->region_id != region_id) return false; - // Treat the fraction as an estimate until the required count is precise. *out_fraction = progress->status.required_resource_count == 0 ? 0.0 : (double)progress->status.completed_resource_count / diff --git a/docs/snippets/c/offline-storage.c b/docs/snippets/c/offline-storage.c index c73cc41ad..7d92f5e8a 100644 --- a/docs/snippets/c/offline-storage.c +++ b/docs/snippets/c/offline-storage.c @@ -2,107 +2,68 @@ // ones that the host no longer needs. #include -#include #include -// Pumps until the completion event for this operation arrives, and returns the -// operation's own result status. -static mln_status await_operation( - mln_runtime runtime, mln_offline_operation_id operation_id -) { - for (;;) { - mln_runtime_pump(runtime, 100, -1); - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { - return MLN_STATUS_NATIVE_ERROR; - } - - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - const mln_runtime_event* event = (const mln_runtime_event*)bytes; - if (event->type != MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED) { - continue; - } - const mln_runtime_event_offline_operation_completed* completed = - &event->payload.offline_operation_completed; - if (completed->operation_id == operation_id) { - return (mln_status)completed->result_status; - } - } - } -} +typedef struct offline_cleanup { + mln_runtime runtime; + const char* keep_metadata; + size_t deleted; +} offline_cleanup; -// An operation with no result still holds runtime state until a discard. -static mln_status finish_operation( - mln_runtime runtime, mln_offline_operation_id operation_id +static void region_deleted( + void* user_data, const mln_completion_result* result ) { - const mln_status status = await_operation(runtime, operation_id); - mln_runtime_offline_operation_discard(runtime, operation_id); - return status; + offline_cleanup* cleanup = user_data; + if (result->status == MLN_STATUS_OK) cleanup->deleted++; } -// Deletes every stored region whose metadata differs from keep_metadata, and -// returns how many it deleted. -size_t delete_other_regions(mln_runtime runtime, const char* keep_metadata) { - // The wait below ends on the completion event, so the subscription has to - // select that type. - mln_runtime_set_event_mask( - runtime, MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED - ); - - // #region list - mln_offline_operation_id list_id = 0; - if ( - mln_runtime_offline_regions_list_start(runtime, &list_id) != MLN_STATUS_OK - ) { - return 0; - } - // #endregion list - +static void regions_listed( + void* user_data, const mln_completion_result* result +) { + offline_cleanup* cleanup = user_data; // #region result - mln_offline_region_list list = MLN_HANDLE_NULL; - if (await_operation(runtime, list_id) == MLN_STATUS_OK) { - mln_runtime_offline_regions_list_take_result(runtime, list_id, &list); - } - if (list == MLN_HANDLE_NULL) { - mln_runtime_offline_operation_discard(runtime, list_id); - return 0; - } + if (result->status != MLN_STATUS_OK) return; + const mln_offline_region_info* regions = result->value; // #endregion result - size_t deleted = 0; - // #region entries - size_t count = 0; - mln_offline_region_list_count(list, &count); - - for (size_t index = 0; index < count; index++) { - // Copy the definition and metadata before destroying the list. - mln_offline_region_info info = {.size = sizeof(info)}; - if (mln_offline_region_list_get(list, index, &info) != MLN_STATUS_OK) { - continue; - } + for (size_t index = 0; index < result->value_count; index++) { + const mln_offline_region_info* info = ®ions[index]; + // Copy the definition and metadata here if the host keeps them. // #endregion entries if ( - info.metadata_size == strlen(keep_metadata) && - memcmp(info.metadata, keep_metadata, info.metadata_size) == 0 + info->metadata_size == strlen(cleanup->keep_metadata) && + memcmp(info->metadata, cleanup->keep_metadata, info->metadata_size) == 0 ) { continue; } // #region delete - mln_offline_operation_id delete_id = 0; - if ( - mln_runtime_offline_region_delete_start(runtime, info.id, &delete_id) == - MLN_STATUS_OK && - finish_operation(runtime, delete_id) == MLN_STATUS_OK - ) { - deleted++; - } + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = region_deleted, + .user_data = cleanup, + }; + (void)mln_runtime_offline_region_delete( + cleanup->runtime, info->id, &completion + ); // #endregion delete } +} + +mln_status delete_other_regions( + mln_runtime runtime, const char* keep_metadata, offline_cleanup* cleanup +) { + cleanup->runtime = runtime; + cleanup->keep_metadata = keep_metadata; + cleanup->deleted = 0; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = regions_listed, + .user_data = cleanup, + }; - mln_offline_region_list_destroy(list); - return deleted; + // #region list + return mln_runtime_offline_regions_list(runtime, &completion); + // #endregion list } diff --git a/docs/snippets/c/pump-thread.c b/docs/snippets/c/pump-thread.c deleted file mode 100644 index 6840df81c..000000000 --- a/docs/snippets/c/pump-thread.c +++ /dev/null @@ -1,70 +0,0 @@ -// A host-owned pump thread. The runtime and map use this owner thread. The -// display thread owns the render session and draws its updates. - -#include -#include - -enum { park_timeout_ms = 100 }; - -// #region channel -typedef struct pump_channel { - atomic_bool render_pending; - atomic_bool stop_requested; - mln_wake_source wake; -} pump_channel; -// #endregion channel - -// #region signal -void pump_channel_wake(pump_channel* channel) { - mln_wake_source_signal(channel->wake); -} -// #endregion signal - -static bool wants_a_frame(const mln_runtime_event* event, mln_map map) { - if (event->source != map) return false; - if (event->type == MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE) return true; - if (event->type != MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED) return false; - return event->payload.render_frame.needs_repaint; -} - -static void drain_events( - mln_runtime runtime, mln_map map, pump_channel* channel -) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) return; - - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - if (wants_a_frame((const mln_runtime_event*)bytes, map)) { - atomic_store(&channel->render_pending, true); - } - } -} - -void pump_until_stopped( - mln_runtime runtime, mln_map map, pump_channel* channel -) { - // #region acquire - channel->wake = MLN_HANDLE_NULL; - if ( - mln_runtime_wake_source_acquire(runtime, &channel->wake) != MLN_STATUS_OK - ) { - return; - } - - // Only a selected event wakes this thread. - mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED - ); - // #endregion acquire - - // #region loop - while (!atomic_load(&channel->stop_requested)) { - mln_runtime_pump(runtime, park_timeout_ms, -1); - drain_events(runtime, map, channel); - } - // #endregion loop - - mln_wake_source_destroy(channel->wake); -} diff --git a/docs/snippets/c/query-rendered-features.c b/docs/snippets/c/query-rendered-features.c index 51f09bb72..3d53a3066 100644 --- a/docs/snippets/c/query-rendered-features.c +++ b/docs/snippets/c/query-rendered-features.c @@ -8,14 +8,16 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -static void read_query_result(mln_queried_feature_list result) { +static void read_query_result( + void* user_data, const mln_completion_result* result +) { + (void)user_data; // #region read - size_t count = 0; - if (mln_queried_feature_list_count(result, &count) != MLN_STATUS_OK) return; - if (count == 0) return; - mln_queried_feature hit = mln_queried_feature_default(); - if (mln_queried_feature_list_get(result, 0, &hit) != MLN_STATUS_OK) return; - // Copy hit.feature and any identifier or state view you keep. + if (result->status != MLN_STATUS_OK || result->value_count == 0) return; + const mln_queried_feature* features = result->value; + const mln_queried_feature hit = features[0]; + (void)hit; + // Copy hit.feature and any identifier or state view you keep before return. // #endregion read } @@ -42,14 +44,12 @@ mln_status features_at_screen_point( // #endregion layers // #region query - mln_queried_feature_list result = MLN_HANDLE_NULL; - const mln_status queried = mln_render_session_query_rendered_features( - session, &geometry, &options, &result + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = read_query_result, + }; + return mln_render_session_query_rendered_features( + session, &geometry, &options, &completion ); - if (queried != MLN_STATUS_OK) return queried; - - read_query_result(result); - mln_queried_feature_list_destroy(result); - return MLN_STATUS_OK; // #endregion query } diff --git a/docs/snippets/c/query-source-features.c b/docs/snippets/c/query-source-features.c index 300ebb6c7..c5d1b59de 100644 --- a/docs/snippets/c/query-source-features.c +++ b/docs/snippets/c/query-source-features.c @@ -8,18 +8,20 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -static void read_features(mln_queried_feature_list result) { +static void read_features( + void* user_data, const mln_completion_result* result +) { + (void)user_data; // #region read - size_t count = 0; - if (mln_queried_feature_list_count(result, &count) != MLN_STATUS_OK) return; - if (count == 0) return; - mln_queried_feature hit = mln_queried_feature_default(); - if (mln_queried_feature_list_get(result, 0, &hit) != MLN_STATUS_OK) return; - // Copy hit.feature and any identifier or state view you keep. + if (result->status != MLN_STATUS_OK || result->value_count == 0) return; + const mln_queried_feature* features = result->value; + const mln_queried_feature hit = features[0]; + (void)hit; + // Copy hit.feature and any identifier or state view you keep before return. // #endregion read } -void list_source_features(mln_render_session session) { +mln_status list_source_features(mln_render_session session) { // #region options const mln_buffer_view source_layers[] = {view("poi")}; mln_source_feature_query_options options = @@ -30,13 +32,12 @@ void list_source_features(mln_render_session session) { // #endregion options // #region query - mln_queried_feature_list result = MLN_HANDLE_NULL; - const mln_status queried = mln_render_session_query_source_features( - session, view("places"), &options, &result + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = read_features, + }; + return mln_render_session_query_source_features( + session, view("places"), &options, &completion ); - if (queried != MLN_STATUS_OK) return; - - read_features(result); - mln_queried_feature_list_destroy(result); // #endregion query } diff --git a/docs/snippets/c/render-target.c b/docs/snippets/c/render-target.c index 2ebff7d72..cada8c257 100644 --- a/docs/snippets/c/render-target.c +++ b/docs/snippets/c/render-target.c @@ -1,11 +1,19 @@ // Attaching each kind of OpenGL render target to a live map, replacing the -// target of an attached session, and releasing the two in order. +// target of an attached session, and closing the two in order. #include -mln_render_session attach_to_window( +static mln_render_session_attach_options caller_driver(void) { + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; + return options; +} + +mln_status attach_to_window( mln_map map, const mln_opengl_context_descriptor* context, void* egl_surface, - uint32_t width, uint32_t height, double scale_factor + uint32_t width, uint32_t height, double scale_factor, + mln_render_session* out_session, const mln_completion* completion ) { // #region surface mln_opengl_surface_descriptor descriptor = @@ -15,18 +23,17 @@ mln_render_session attach_to_window( descriptor.extent.scale_factor = scale_factor; descriptor.context = *context; descriptor.surface = egl_surface; - - mln_render_session session = MLN_HANDLE_NULL; - const mln_status status = - mln_opengl_surface_attach(map, &descriptor, &session); + const mln_render_session_attach_options options = caller_driver(); + return mln_opengl_surface_attach( + map, &descriptor, &options, out_session, completion + ); // #endregion surface - - return status == MLN_STATUS_OK ? session : MLN_HANDLE_NULL; } -mln_render_session attach_to_own_texture( +mln_status attach_to_own_texture( mln_map map, const mln_opengl_context_descriptor* context, uint32_t width, - uint32_t height, double scale_factor + uint32_t height, double scale_factor, mln_render_session* out_session, + const mln_completion* completion ) { // #region owned mln_opengl_owned_texture_descriptor descriptor = @@ -35,19 +42,18 @@ mln_render_session attach_to_own_texture( descriptor.extent.height = height; descriptor.extent.scale_factor = scale_factor; descriptor.context = *context; - - mln_render_session session = MLN_HANDLE_NULL; - const mln_status status = - mln_opengl_owned_texture_attach(map, &descriptor, &session); + const mln_render_session_attach_options options = caller_driver(); + return mln_opengl_owned_texture_attach( + map, &descriptor, &options, out_session, completion + ); // #endregion owned - - return status == MLN_STATUS_OK ? session : MLN_HANDLE_NULL; } -mln_render_session attach_to_host_texture( +mln_status attach_to_host_texture( mln_map map, const mln_opengl_context_descriptor* context, uint32_t texture, uint32_t texture_target, uint32_t logical_width, uint32_t logical_height, - double scale_factor + double scale_factor, mln_render_session* out_session, + const mln_completion* completion ) { // #region borrowed mln_opengl_borrowed_texture_descriptor descriptor = @@ -55,39 +61,34 @@ mln_render_session attach_to_host_texture( descriptor.extent.width = logical_width; descriptor.extent.height = logical_height; descriptor.extent.scale_factor = scale_factor; - - // These must equal the texture's level-0 dimensions; the session cannot - // verify them on ES 3.0. descriptor.physical_width = (uint32_t)(logical_width * scale_factor); descriptor.physical_height = (uint32_t)(logical_height * scale_factor); - descriptor.context = *context; descriptor.texture = texture; - descriptor.target = texture_target; // GL_TEXTURE_2D - - mln_render_session session = MLN_HANDLE_NULL; - const mln_status status = - mln_opengl_borrowed_texture_attach(map, &descriptor, &session); + descriptor.target = texture_target; + const mln_render_session_attach_options options = caller_driver(); + return mln_opengl_borrowed_texture_attach( + map, &descriptor, &options, out_session, completion + ); // #endregion borrowed - - return status == MLN_STATUS_OK ? session : MLN_HANDLE_NULL; } mln_status resize_session( mln_render_session session, uint32_t width, uint32_t height, - double scale_factor + double scale_factor, const mln_completion* completion ) { // #region resize - // Surface and session-owned texture targets resize in place. A borrowed - // texture takes its size from the host's texture and reports an unsupported - // status here. - return mln_render_session_resize(session, width, height, scale_factor); + const mln_render_target_extent extent = { + .width = width, .height = height, .scale_factor = scale_factor + }; + return mln_render_session_resize(session, &extent, completion); // #endregion resize } mln_status resize_window_target( mln_render_session session, const mln_opengl_context_descriptor* context, - void* egl_surface, uint32_t width, uint32_t height, double scale_factor + void* egl_surface, uint32_t width, uint32_t height, double scale_factor, + const mln_completion* completion ) { // #region set-target mln_opengl_surface_descriptor descriptor = @@ -97,14 +98,42 @@ mln_status resize_window_target( descriptor.extent.scale_factor = scale_factor; descriptor.context = *context; descriptor.surface = egl_surface; - - return mln_opengl_surface_set_target(session, &descriptor); + return mln_opengl_surface_set_target(session, &descriptor, completion); // #endregion set-target } -void release_map(mln_map map, mln_render_session session) { +typedef struct teardown_state { + mln_map map; + mln_render_session session; +} teardown_state; + +static void map_released(void* user_data, const mln_completion_result* result) { + (void)user_data; + (void)result; +} + +static void detached(void* user_data, const mln_completion_result* result) { + teardown_state* state = user_data; + if (result->status != MLN_STATUS_OK) return; + mln_render_session_destroy(state->session); + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = map_released, + }; + (void)mln_map_release(state->map, &completion); +} + +mln_status release_map( + mln_map map, mln_render_session session, teardown_state* state +) { // #region teardown - mln_render_session_destroy(session); - mln_map_destroy(map); + state->map = map; + state->session = session; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = detached, + .user_data = state, + }; + return mln_render_session_detach(session, &completion); // #endregion teardown } diff --git a/docs/snippets/c/resource-provider.c b/docs/snippets/c/resource-provider.c index 8b6a63d69..735d78b6d 100644 --- a/docs/snippets/c/resource-provider.c +++ b/docs/snippets/c/resource-provider.c @@ -44,12 +44,14 @@ static uint32_t serve_bundled_asset( } // #region install -void install_provider(mln_runtime runtime, asset_store* store) { +mln_status install_provider( + mln_runtime runtime, asset_store* store, const mln_completion* completion +) { const mln_resource_provider provider = { .size = sizeof(provider), .callback = serve_bundled_asset, .user_data = store, }; - mln_runtime_set_resource_provider(runtime, &provider); + return mln_runtime_set_resource_provider(runtime, &provider, completion); } // #endregion install diff --git a/docs/snippets/c/resource-transform.c b/docs/snippets/c/resource-transform.c index 2432c9ae2..6adea039d 100644 --- a/docs/snippets/c/resource-transform.c +++ b/docs/snippets/c/resource-transform.c @@ -37,12 +37,14 @@ static mln_status add_api_key( } // #region install -void install_transform(mln_runtime runtime, char* api_key) { +mln_status install_transform( + mln_runtime runtime, char* api_key, const mln_completion* completion +) { mln_resource_transform transform = { .size = sizeof(transform), .callback = add_api_key, .user_data = api_key, }; - mln_runtime_set_resource_transform(runtime, &transform); + return mln_runtime_set_resource_transform(runtime, &transform, completion); } // #endregion install diff --git a/docs/snippets/c/runtime-style.c b/docs/snippets/c/runtime-style.c deleted file mode 100644 index 7afaf8f58..000000000 --- a/docs/snippets/c/runtime-style.c +++ /dev/null @@ -1,28 +0,0 @@ -// Adding a source and a layer to a style that has already loaded. Layers and -// property values are style-spec JSON, passed as UTF-8 bytes. - -#include -#include - -static mln_buffer_view view(const char* text) { - return (mln_buffer_view){.data = text, .size = strlen(text)}; -} - -void add_cities_layer(mln_map map, const char* geojson_url) { - // Null options takes the defaults. - mln_map_add_geojson_source_url(map, view("cities"), view(geojson_url), NULL); - - const char layer[] = - "{\"id\":\"cities\",\"type\":\"circle\",\"source\":\"cities\"}"; - - // An empty before-layer ID puts the layer on top. - mln_map_add_style_layer_json(map, view(layer), view("")); - - mln_map_set_layer_property( - map, view("cities"), view("circle-radius"), view("6.0") - ); -} - -void refresh_cities(mln_map map, const char* new_url) { - mln_map_set_geojson_source_url(map, view("cities"), view(new_url)); -} diff --git a/docs/snippets/c/still-image.c b/docs/snippets/c/still-image.c index 8b1bbb7c1..71985b591 100644 --- a/docs/snippets/c/still-image.c +++ b/docs/snippets/c/still-image.c @@ -2,153 +2,217 @@ // static mode and the session owns its texture. #include +#include #include -#include +#include + +typedef struct still_image_job { + mln_runtime runtime; + mln_map map; + mln_render_session session; + const char* style_url; + void* egl_display; + void* egl_config; + uint32_t width; + uint32_t height; + atomic_bool frames_ready; + atomic_bool completion_ready; + bool frame_pending; + bool rendered; + bool reading; + mln_status status; + uint8_t* pixels; + mln_texture_image_info image_info; + void (*schedule)(void* user_data); + void (*finished)(void* user_data, const struct still_image_job* job); + void* user_data; +} still_image_job; + +static void schedule_job(still_image_job* job) { + job->schedule(job->user_data); +} -typedef enum still_image_state { - STILL_IMAGE_PENDING, - STILL_IMAGE_FINISHED, - STILL_IMAGE_FAILED, -} still_image_state; +static void completion_finished( + void* user_data, const mln_completion_result* result +) { + still_image_job* job = user_data; + job->status = result->status; + atomic_store_explicit(&job->completion_ready, true, memory_order_release); + schedule_job(job); +} -static mln_render_session attach_owned_texture( - mln_map map, const mln_opengl_context_descriptor* context, uint32_t width, - uint32_t height +static void still_image_finished( + void* user_data, const mln_completion_result* result ) { + (void)user_data; + (void)result; +} + +static void frames_ready(void* user_data) { + still_image_job* job = user_data; + atomic_store_explicit(&job->frames_ready, true, memory_order_release); + schedule_job(job); +} + +static mln_status attach_owned_texture(still_image_job* job) { // #region attach mln_opengl_owned_texture_descriptor descriptor = mln_opengl_owned_texture_descriptor_default(); - descriptor.extent.width = width; - descriptor.extent.height = height; + descriptor.extent.width = job->width; + descriptor.extent.height = job->height; descriptor.extent.scale_factor = 1.0; - descriptor.context = *context; - - mln_render_session session = MLN_HANDLE_NULL; - const mln_status status = - mln_opengl_owned_texture_attach(map, &descriptor, &session); + descriptor.context.platform = MLN_OPENGL_CONTEXT_PLATFORM_EGL; + descriptor.context.ownership = MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED; + descriptor.context.data.egl.display = job->egl_display; + descriptor.context.data.egl.config = job->egl_config; + descriptor.context.data.egl.client_api = MLN_OPENGL_CLIENT_API_GLES; + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = MLN_RENDER_DRIVER_CORE_WORKER; + options.requested_texture_ring_depth = 1; + options.frame_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = frames_ready, + .user_data = job, + }; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = completion_finished, + .user_data = job, + }; + return mln_opengl_owned_texture_attach( + job->map, &descriptor, &options, &job->session, &completion + ); // #endregion attach - - return status == MLN_STATUS_OK ? session : MLN_HANDLE_NULL; } -static still_image_state drain_still_image_events( - mln_runtime runtime, mln_map map -) { - still_image_state state = STILL_IMAGE_PENDING; - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { - return STILL_IMAGE_FAILED; +static void map_created(void* user_data, const mln_completion_result* result) { + still_image_job* job = user_data; + if (result->status != MLN_STATUS_OK || result->value_count != 1) { + job->status = result->status; + job->finished(job->user_data, job); + return; } + job->map = *(const mln_map*)result->value; + job->status = attach_owned_texture(job); + if (job->status != MLN_STATUS_OK) job->finished(job->user_data, job); +} - for (size_t index = 0; index < batch.event_count; index++) { - const char* bytes = (const char*)batch.events + index * batch.event_size; - const mln_runtime_event* event = (const mln_runtime_event*)bytes; - if (event->source != map) continue; - if (event->type == MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED) { - state = STILL_IMAGE_FINISHED; - } else if ( - event->type == MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED || - event->type == MLN_RUNTIME_EVENT_MAP_LOADING_FAILED - ) { - return STILL_IMAGE_FAILED; +mln_status start_still_image(still_image_job* job) { + // #region create + mln_map_options options = mln_map_options_default(); + options.initial_extent = (mln_logical_extent){ + .width = job->width, .height = job->height, .scale_factor = 1.0 + }; + options.map_mode = MLN_MAP_MODE_STATIC; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = map_created, + .user_data = job, + }; + return mln_map_create(job->runtime, &options, &completion); + // #endregion create +} + +static void readback_finished( + void* user_data, const mln_completion_result* result +) { + // #region read + still_image_job* job = user_data; + job->status = result->status; + if (result->status == MLN_STATUS_OK && result->value_count == 1) { + const mln_texture_readback_result* readback = result->value; + job->pixels = malloc(readback->data.size); + if (job->pixels == NULL) { + job->status = MLN_STATUS_NATIVE_ERROR; + } else { + memcpy(job->pixels, readback->data.data, readback->data.size); + job->image_info = readback->info; } } + job->finished(job->user_data, job); + // #endregion read +} - return state; +static void request_frame(still_image_job* job) { + mln_frame_demand demand = mln_frame_demand_default(); + demand.token = 1; + job->status = mln_render_session_request_frame(job->session, &demand); + job->frame_pending = job->status == MLN_STATUS_OK; } -static bool await_still_image( - mln_runtime runtime, mln_map map, mln_render_session session -) { - bool finished = false; - bool rendered = false; - const time_t deadline = time(NULL) + 30; +void advance_still_image(still_image_job* job) { + if ( + atomic_load_explicit(&job->completion_ready, memory_order_acquire) && + job->status != MLN_STATUS_OK + ) { + job->finished(job->user_data, job); + return; + } + if ( + atomic_exchange_explicit( + &job->completion_ready, false, memory_order_acq_rel + ) && + job->status == MLN_STATUS_OK && !job->frame_pending && !job->rendered + ) { + const mln_completion command = { + .size = sizeof(mln_completion), + .callback = still_image_finished, + .user_data = job, + }; + job->status = mln_map_set_style_url(job->map, job->style_url, &command); + if (job->status == MLN_STATUS_OK) { + atomic_store_explicit( + &job->completion_ready, false, memory_order_release + ); + const mln_completion still = { + .size = sizeof(mln_completion), + .callback = completion_finished, + .user_data = job, + }; + job->status = mln_map_request_still_image(job->map, &still); + } + if (job->status == MLN_STATUS_OK) request_frame(job); + } // #region await - while (!(finished && rendered) && time(NULL) < deadline) { - mln_runtime_pump(runtime, 10, -1); - const still_image_state state = drain_still_image_events(runtime, map); - if (state == STILL_IMAGE_FAILED) return false; - if (state == STILL_IMAGE_FINISHED) finished = true; - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; + if ( + atomic_exchange_explicit(&job->frames_ready, false, memory_order_acq_rel) + ) { + mln_render_frame_batch batch = MLN_HANDLE_NULL; if ( - mln_render_session_render_update(session, &result, &needs_repaint) == + mln_render_session_drain_frame_results(job->session, &batch) == MLN_STATUS_OK ) { - rendered = rendered || result == MLN_RENDER_RESULT_RENDERED; + size_t count = 0; + (void)mln_render_frame_batch_count(batch, &count); + for (size_t index = 0; index < count; ++index) { + mln_render_frame_result frame = {.size = sizeof(frame)}; + if ( + mln_render_frame_batch_get(batch, index, &frame) == MLN_STATUS_OK && + frame.token == 1 + ) { + job->frame_pending = false; + job->rendered = frame.disposition == MLN_RENDER_RESULT_RENDERED; + } + } + mln_render_frame_batch_release(batch); } } - // #endregion await - - return finished && rendered; -} - -static uint8_t* read_pixels( - mln_render_session session, mln_texture_image_info* out_info -) { - // #region read - // A null buffer with a capacity of 0 is a size probe that fills info. - mln_texture_image_info info = mln_texture_image_info_default(); - mln_texture_read_premultiplied_rgba8(session, NULL, 0, &info); - - uint8_t* pixels = malloc(info.byte_length); - if (pixels == NULL) return NULL; - - const mln_status status = mln_texture_read_premultiplied_rgba8( - session, pixels, info.byte_length, &info - ); - // #endregion read - - if (status != MLN_STATUS_OK) { - free(pixels); - return NULL; - } - - *out_info = info; - return pixels; -} - -// Returns premultiplied RGBA8 pixels that the host frees, and fills out_info -// with their width, height, and row stride. -uint8_t* render_still_image( - mln_runtime runtime, const char* style_url, uint32_t width, uint32_t height, - const mln_opengl_context_descriptor* context, mln_texture_image_info* out_info -) { - // #region create - mln_map_options options = mln_map_options_default(); - options.width = width; - options.height = height; - options.scale_factor = 1.0; - options.map_mode = MLN_MAP_MODE_STATIC; - - mln_map map = MLN_HANDLE_NULL; - if (mln_map_create(runtime, &options, &map) != MLN_STATUS_OK) return NULL; - - // A render session draws the map's latest update whatever the subscription - // selects. - mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FINISHED | - MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FAILED | - MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED - ); - // #endregion create - - uint8_t* pixels = NULL; - const mln_render_session session = - attach_owned_texture(map, context, width, height); - + if (!job->rendered && !job->frame_pending) request_frame(job); if ( - session != MLN_HANDLE_NULL && - mln_map_set_style_url(map, style_url) == MLN_STATUS_OK && - mln_map_request_still_image(map) == MLN_STATUS_OK && - await_still_image(runtime, map, session) + job->rendered && + atomic_load_explicit(&job->completion_ready, memory_order_acquire) && + !job->reading ) { - pixels = read_pixels(session, out_info); + job->reading = true; + const mln_completion readback = { + .size = sizeof(mln_completion), + .callback = readback_finished, + .user_data = job, + }; + job->status = mln_texture_read_premultiplied_rgba8(job->session, &readback); + if (job->status != MLN_STATUS_OK) job->finished(job->user_data, job); } - - if (session != MLN_HANDLE_NULL) mln_render_session_destroy(session); - mln_map_destroy(map); - return pixels; + // #endregion await } diff --git a/docs/snippets/c/style-by-data.c b/docs/snippets/c/style-by-data.c index 9e01dc7f7..917ed24f3 100644 --- a/docs/snippets/c/style-by-data.c +++ b/docs/snippets/c/style-by-data.c @@ -10,7 +10,10 @@ static mln_buffer_view view(const char* text) { } // #endregion node -mln_status size_and_filter_by_magnitude(mln_map map, const char* layer_id) { +mln_status size_and_filter_by_magnitude( + mln_map map, const char* layer_id, const mln_completion* property_completion, + const mln_completion* filter_completion +) { // #region expression const char radius[] = "[\"interpolate\",[\"linear\"],[\"get\",\"mag\"],1,4,6,24]"; @@ -19,10 +22,9 @@ mln_status size_and_filter_by_magnitude(mln_map map, const char* layer_id) { // #region property const mln_buffer_view radius_json = view(radius); // #endregion property - // #region set const mln_status status = mln_map_set_layer_property( - map, view(layer_id), view("circle-radius"), radius_json + map, view(layer_id), view("circle-radius"), radius_json, property_completion ); if (status != MLN_STATUS_OK) return status; // #endregion set @@ -30,6 +32,8 @@ mln_status size_and_filter_by_magnitude(mln_map map, const char* layer_id) { // #region filter // [">=", ["get", "mag"], 2.5] const mln_buffer_view filter = view("[\">=\",[\"get\",\"mag\"],2.5]"); - return mln_map_set_layer_filter(map, view(layer_id), &filter); + return mln_map_set_layer_filter( + map, view(layer_id), &filter, filter_completion + ); // #endregion filter } diff --git a/docs/snippets/c/tile-source-raster-dem.c b/docs/snippets/c/tile-source-raster-dem.c index bacd6936c..e413d9321 100644 --- a/docs/snippets/c/tile-source-raster-dem.c +++ b/docs/snippets/c/tile-source-raster-dem.c @@ -7,7 +7,10 @@ static mln_buffer_view view(const char* text) { return (mln_buffer_view){.data = text, .size = strlen(text)}; } -mln_status add_hillshading(mln_map map) { +mln_status add_hillshading( + mln_map map, const mln_completion* source_completion, + const mln_completion* layer_completion +) { // #region options mln_style_tile_source_options options = mln_style_tile_source_options_default(); @@ -16,13 +19,13 @@ mln_status add_hillshading(mln_map map) { options.raster_encoding = MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM; options.max_zoom = 12; // #endregion options - // #region source const mln_buffer_view tiles[] = { view("https://tiles.example.com/terrain/{z}/{x}/{y}.png"), }; const mln_status status = mln_map_add_raster_dem_source_tiles( - map, view("terrain"), tiles, sizeof(tiles) / sizeof(tiles[0]), &options + map, view("terrain"), tiles, sizeof(tiles) / sizeof(tiles[0]), &options, + source_completion ); // #endregion source if (status != MLN_STATUS_OK) { @@ -32,7 +35,7 @@ mln_status add_hillshading(mln_map map) { // #region layer // An empty before-layer ID puts the layer on top of the style. return mln_map_add_hillshade_layer( - map, view("hillshading"), view("terrain"), view("") + map, view("hillshading"), view("terrain"), view(""), layer_completion ); // #endregion layer } diff --git a/docs/snippets/c/tile-source-templates.c b/docs/snippets/c/tile-source-templates.c index 909f78654..e198fba54 100644 --- a/docs/snippets/c/tile-source-templates.c +++ b/docs/snippets/c/tile-source-templates.c @@ -9,13 +9,16 @@ static mln_buffer_view view(const char* text) { } // An empty before-layer ID puts the layer on top of the style. -static mln_status add_layer(mln_map map) { +static mln_status add_layer(mln_map map, const mln_completion* completion) { const char layer[] = "{\"id\":\"ortho\",\"type\":\"raster\",\"source\":\"ortho\"}"; - return mln_map_add_style_layer_json(map, view(layer), view("")); + return mln_map_add_style_layer_json(map, view(layer), view(""), completion); } -mln_status add_orthophotos(mln_map map) { +mln_status add_orthophotos( + mln_map map, const mln_completion* source_completion, + const mln_completion* layer_completion +) { // #region tiles const mln_buffer_view tiles[] = { view("https://a.tiles.example.com/ortho/{z}/{x}/{y}.png"), @@ -42,14 +45,14 @@ mln_status add_orthophotos(mln_map map) { }; options.attribution = view("Imagery: Example Survey"); // #endregion bounds - // #region source const mln_status status = mln_map_add_raster_source_tiles( - map, view("ortho"), tiles, sizeof(tiles) / sizeof(tiles[0]), &options + map, view("ortho"), tiles, sizeof(tiles) / sizeof(tiles[0]), &options, + source_completion ); if (status != MLN_STATUS_OK) { return status; } - return add_layer(map); + return add_layer(map, layer_completion); // #endregion source } diff --git a/docs/snippets/c/tile-source-tilejson.c b/docs/snippets/c/tile-source-tilejson.c index 9638543a0..5b561bb11 100644 --- a/docs/snippets/c/tile-source-tilejson.c +++ b/docs/snippets/c/tile-source-tilejson.c @@ -8,24 +8,27 @@ static mln_buffer_view view(const char* text) { } // An empty before-layer ID puts the layer on top of the style. -static mln_status add_layer(mln_map map) { +static mln_status add_layer(mln_map map, const mln_completion* completion) { // #region layer const char layer[] = "{\"id\":\"roads\",\"type\":\"line\",\"source\":\"basemap\"," "\"source-layer\":\"transportation\"}"; // #endregion layer - return mln_map_add_style_layer_json(map, view(layer), view("")); + return mln_map_add_style_layer_json(map, view(layer), view(""), completion); } -mln_status add_basemap(mln_map map) { +mln_status add_basemap( + mln_map map, const mln_completion* source_completion, + const mln_completion* layer_completion +) { // #region source const mln_status status = mln_map_add_vector_source_url( map, view("basemap"), view("https://tiles.example.com/planet/tiles.json"), - NULL + NULL, source_completion ); if (status != MLN_STATUS_OK) { return status; } - return add_layer(map); + return add_layer(map, layer_completion); // #endregion source } diff --git a/docs/src/content/docs/concepts.md b/docs/src/content/docs/concepts.md index a9ce6aec2..6a7b3e40d 100644 --- a/docs/src/content/docs/concepts.md +++ b/docs/src/content/docs/concepts.md @@ -12,21 +12,29 @@ Events and bindings connect those objects to host code. ## Runtime -The runtime owns scheduler state and event storage for one owner thread. The -host creates the runtime on the thread that will pump it. Runtime work and -events flow through that thread. +The runtime owns one native scheduler thread and its event storage. Runtime +creation starts that thread, which keeps MapLibre Native's run loop active until +native teardown finishes after runtime release. -Each owner thread has at most one live runtime. Pumping advances MapLibre Native -and collects completed work. +Any host thread can submit runtime and map work. Submissions wake the native run +loop, so progress never depends on a display callback or a host pump. One +runtime may own multiple maps; their commands, queries, barriers, and release +work share one ordered submission stream. -The host sets the pace. A display-paced host pumps once per frame. A host with a -dedicated pump thread parks that thread until the runtime has work. Other host -threads wake it through a wake source. +Use a runtime barrier when later work must wait for every preceding submission +to reach a terminal disposition. The runtime's direct event wake callback tells +the host when its event queue is ready to drain. ## Map A map belongs to a runtime. It owns style documents, sources, layers, images, -camera state, feature state, observer events, and render invalidation. +camera state, observer events, and render invalidation. + +Closing a map consumes its public handle synchronously and returns a completion +for native retirement. That completion runs after earlier map work is terminal +and map-owned callback state has been destroyed. Backend worker and graphics +resource cleanup can continue after that completion. Await it when later host +work depends on cleanup; a runtime close also remains ordered after it. A map is independent of a render target. The host can create, configure, query, and observe a map before the first frame. @@ -36,10 +44,23 @@ aligned with the style specification across every layer type. Typed entry points cover behavior beyond construction, such as source-type validation and per-frame property updates. +Map mutations are commands. A command copies its input before returning +acceptance and later invokes one completion with its terminal disposition. +Ordered queries and lifecycle transitions use typed completions. Bindings expose +one-shot work through their normal future, promise, task, suspension, or +explicit async idiom. + +Published snapshots provide synchronous copies of state needed by UI and display +threads. Snapshot reads never call into mutable MapLibre map state. Each +committed command completion reports the snapshot generation that its commit +published, so a host can fence a snapshot read on it. + ## Render session A render session renders one map to one render target. A map carries at most one -live render session. +live render session. Feature state belongs to the render session, because +MapLibre stores it in the session's render state: the session's feature-state +and query operations read and mutate it in session order. Render targets come in three kinds: @@ -63,29 +84,49 @@ Vulkan, so a macOS host loads an EGL implementation such as ANGLE for the OpenGL backend, or MoltenVK for the Vulkan backend. That implementation brings the headers to build against. -The thread that attaches a render session becomes its owner thread for the -session's lifetime. The attaching thread can differ from the map's owner thread. -A host therefore attaches on the thread that owns its graphics context and draws -frames, while another thread pumps the runtime and map. A session call from any -other thread reports an owner-thread status. +Execution placement is fixed when attachment starts. A core-worker session owns +a native serial graphics worker for transferable Metal or Vulkan state, private +EGL owned texture targets, or transferred `OffscreenCanvas` WebGL state. A +caller-graphics-thread session stores typed work until the host services it +where the graphics context is usable. WGL targets, EGL surfaces, shared EGL +textures, existing WebGL contexts, and browser WebGPU use the caller driver. + +Session control is separate from graphics execution. Any host thread may request +a frame, read a snapshot, start an asynchronous call, abandon a target, or +destroy a detached session. The first successful caller-driver service fixes its +graphics thread identity. Later service calls and thread-current backend +accessors remain affine to that thread. The host services ready work even while +presentation callbacks are paused. + +A frame demand carries a host token, an optional timeout, and a coalescing +boundary. Every accepted demand produces one terminal result. Result records +identify the token and the map-update, extent, and frame generations that the +driver used. A direct frame-result wake callback remains armed until the host +drains all frame results, so coalesced wakeups do not lose results. + +Host-acquirable owned texture targets negotiate a ring of one to three slots. +Acquiring a frame leases one slot and returns producer-completion +synchronization. Releasing the frame supplies consumer-completion +synchronization when the host submitted GPU reads. The driver reuses the slot +only after the host released the handle and those reads completed. A private +OpenGL owned texture target fixes its ring depth at one and exposes CPU readback +instead of frame acquisition. ### OpenGL context ownership OpenGL binds a context to a thread, so an OpenGL render target names how the -session and the host divide the thread's context. +session and the host divide driver-thread context and graphics-object ownership. -A shared session leaves the thread as it found it. Each render makes the -session's context current and restores whatever was current before, and that -context joins the host share group, so the host draws its own graphics on the -same thread and samples session textures from its own context. Texture targets -and WebGL work this way. +A shared session leaves the thread as it found it. Each driver-service call +makes the session's context current and restores whatever was current before, +and that context joins the host share group. Host-acquirable texture targets and +existing WebGL contexts use this mode. -A dedicated session owns the thread's context. It creates a context of its own -from the display or device the host already presents through, joins no share -group, and keeps that context current between renders. Choose it for a surface -target on a thread that exists to draw one map, such as an Android host -rendering into a `SurfaceView`. The host then builds no context of its own, and -each frame saves and restores nothing. +A dedicated session owns its driver thread's context. It creates a context from +the supplied display or device, joins no host share group, and keeps that +context current between renders. A surface target can use a caller thread that +exists to draw one map, such as an Android host rendering into a `SurfaceView`. +A private EGL owned texture target uses a core worker and exposes CPU readback. ## Events @@ -96,32 +137,32 @@ events from the runtime. Events report map lifecycle, rendering progress, resource activity, diagnostics, and asynchronous failures. -Rendering observer events reach the runtime queue through the map's run loop. A -pump after the render call makes those events available to drain. +Rendering observer events reach the runtime queue asynchronously. The runtime's +direct event wake callback reports that the queue is ready to drain. Each map and each runtime carries a subscription: the set of event types it queues. Default options select every event type the library reports, and a host narrows a subscription by naming the types it reads. An unselected event is -never built, never queued, and never raises the wake flag that releases a parked -pump. +never built, never queued, and never invokes the event wake callback. -One drain reports a batch: every queued event in order, plus the message text -that those events carry. Copy any value you keep, because the next drain for -that runtime replaces the batch. +One drain transfers the queued event records and their message storage into an +owned batch. A batch remains readable across later drains and runtime close. +Copy values that must outlive the batch, then release it. -Queued events belong to their source. Destroying a map discards that map's -queued events immediately. Read any state that teardown needs synchronously -while the map is live. +Closing a map or disabling offline-region observation prevents future events +from that source and leaves queued events unchanged. Each queued event keeps a +copied source ID that remains meaningful after the source handle closes. ## Failures -Status-returning calls report synchronous failures. Each binding surfaces them -in its own idiom: an exception, a result type, or an error return. Examples -include a call from the wrong thread and an invalid argument. +The status returned by an immediate call reports validation or inspection +failure. The status returned by a one-shot submission reports whether native +code accepted and copied it. Its completion reports an asynchronous application +failure and a borrowed diagnostic that the binding copies before returning. -Events report asynchronous failures, such as a style load, resource request, or -still-image request that failed. Drain events in addition to checking call -results. +Each binding surfaces these channels in its own idiom: an exception, a result +type, an asynchronous result, or an event stream. Render-driver calls continue +to report their graphics-thread failures directly. ## Language bindings diff --git a/docs/src/content/docs/development/async-architecture.md b/docs/src/content/docs/development/async-architecture.md new file mode 100644 index 000000000..f5ee7723b --- /dev/null +++ b/docs/src/content/docs/development/async-architecture.md @@ -0,0 +1,87 @@ +--- +title: Asynchronous architecture +description: Design and migration plan for one-shot completion and event streams. +sidebar: + order: 4 +--- + +The C API uses separate mechanisms for immediate work, one-shot asynchronous +work, event streams, and graphics-thread service. Each mechanism matches the +shape of the work that it carries. The API has no general readiness multiplexer. + +## Target design + +Immediate functions finish before they return. Creation, release, published +snapshots, detached projection calls, and validation use ordinary return values +and status codes. + +One-shot asynchronous functions accept a completion descriptor. A successful +submission copies its inputs and transfers the descriptor's callback state to +the C API. The C API invokes the completion exactly once with terminal status, a +borrowed diagnostic, and the function's typed result. The completion may run +before the submission function returns. The C API invokes the descriptor's +release callback after the completion returns and after no native path can +invoke it again. + +Command completions additionally carry committed, superseded, failed, or +cancelled disposition and the map snapshot generation that a commit published. +Commands have no numeric correlation ID and do not publish a second completion +through the runtime event stream. + +Bindings copy completion data before the callback returns and resolve the target +language's eager future, promise, task, continuation, or explicit async value. +Cancelling a host wait does not cancel accepted native work unless the C +function explicitly returns a cancellation token. + +An accepted command always resolves to a command-completion value. Failed and +cancelled commands carry their disposition, terminal status, and diagnostic in +that value; they do not reject the asynchronous container. Submission rejection +and binding bridge failures reject the asynchronous container because no +accepted command exists to describe. + +Repeated observations use an owned ABI-format queue. The owning runtime or +render session invokes its direct wake callback when a queue changes from empty +to nonempty. One drain transfers the complete queue into an independently owned +batch. The wake callback schedules a later host drain and returns promptly; it +does not deliver individual records or call host application code. + +Caller-driven render sessions expose a separate driver-work wake callback. The +callback schedules service on the graphics thread. Graphics-thread service is +the only mechanism whose progress depends on a host thread. + +These four mechanisms form the complete execution model: + +| Work shape | C mechanism | +| ------------------------------- | ---------------------------------------- | +| Finishes during the call | Return value and status | +| Finishes once after the call | Owned completion callback | +| Produces repeated records | Owned queue, direct wake, and full drain | +| Requires a host graphics thread | Direct driver-work wake and service | + +## Migration plan + +1. Define the common completion ownership contract and typed completion result + shapes in the public C headers. +2. Convert runtime and map commands from command IDs and command-finished events + to command completions. +3. Convert ordered queries, lifecycle transitions, offline work, and render work + from public operation handles to typed completions. +4. Keep cancellation handles only for work whose public contract supports + cancellation after submission. +5. Give runtime events, frame results, and caller-driver work direct owner + callbacks. Remove notification sources, ready endpoints, and ready batches. +6. Remove operation polling, waiting, status inspection, diagnostic copying, + finish, release, and typed take functions after their callers migrate. +7. Map completions to each binding's ordinary eager asynchronous value. Keep raw + C callback declarations private. +8. Update examples to schedule only the continuation that consumes a result; + submission itself remains immediate and preserves runtime order. +9. Delete command correlation registries, operation waiter maps, notification + dispatchers, and readiness adapters from every binding. +10. Verify synchronous rejection, inline completion, later completion, + cancellation of a host wait, native teardown, callback release, full event + drains, frame-result drains, and graphics-thread wakeups. + +The migration is complete when no public binding exposes command IDs, operation +handles, notification sources, or ready endpoints, and when native one-shot work +requires no host event drain to complete. diff --git a/docs/src/content/docs/development/binding-specification.md b/docs/src/content/docs/development/binding-specification.md index bcf8012f9..2ad997c49 100644 --- a/docs/src/content/docs/development/binding-specification.md +++ b/docs/src/content/docs/development/binding-specification.md @@ -22,8 +22,8 @@ The required binding layer is a low-level FFI API. It MUST NOT add application-framework policy, UI/view lifecycle integration, general async APIs, or scheduler models above the C API concepts. -A subproject that needs host-thread confinement uses the owner-thread helper -design in Threading. No other execution model belongs in the binding layer. +A subproject that needs graphics-thread confinement keeps that policy on the +render-session wrapper. It MUST NOT add an owner thread for runtime or map work. --- @@ -214,7 +214,6 @@ Long-lived C-owned opaque handle concepts include: - `MapHandle` - `MapProjectionHandle` - `RenderSessionHandle` -- `OfflineOperationHandle` - `ResourceRequestHandle` - `GeoJsonSourceDataHandle` @@ -227,10 +226,10 @@ binding; the ownership contract does not. ## Handle Lifetime Public handles are for values that own or control native state across calls, -such as runtimes, maps, render sessions, offline operations, resource requests, -and acquired texture frames. Input values, events, diagnostics, query results, -snapshots, and native-filled structs become copied language values. Native -snapshot, result, and list handles remain internal implementation details. +such as runtimes, maps, render sessions, resource requests, and acquired texture +frames. Input values, events, diagnostics, query results, snapshots, and +native-filled structs become copied language values. Native snapshot, result, +and list handles remain internal implementation details. ### Owned handles @@ -274,13 +273,11 @@ binding's own closed-handle error rather than surfacing the C API's rejection of an id retired underneath the call. The mechanism belongs to the shared handle state, so every handle of that kind gets the same ordering. -Deterministic cleanup hooks follow the same release operation when they can -report release failure through the target language's normal error path. -Non-deterministic cleanup hooks report leaks for thread-affine handles. They -MUST NOT destroy runtime, map, or render-session handles from cleanup hooks. -Infallible language destructors that attempt best-effort release MUST preserve -the explicit release contract and MUST NOT mask native errors from the explicit -release path. +Deterministic cleanup hooks follow the same release or lifecycle operation when +they can report failure through the target language's normal error path. +Non-deterministic cleanup may start detach only when the binding can retain +every dependency and can keep a caller driver serviced until completion. It may +otherwise abandon and report any quarantine, then destroy CPU-only. ### Stale and mismatched handles @@ -301,24 +298,27 @@ argument rather than reaching a later native handle. ### Parent validity -Bindings MUST preserve native parent validity while child wrappers are live: +The C API owns native parent-child validity and rejects a parent release while a +dependent child is live or pending. Bindings retain a parent wrapper only when +the child calls binding-owned behavior through it; they do not duplicate the C +relationship with child counters or close preflights. -Child wrappers retain the parent owner state whenever native validity depends on -the parent. Releasing a parent while children are live MUST fail without -consuming or destroying the parent. +A `MapProjectionHandle` owns a transform snapshot independently of its source +map and runtime, and it MUST remain valid after the source map closes. Every +call after creation, including close, is synchronous and usable from any thread. -`MapProjectionHandle` is the exception: after creation it owns a standalone -projection snapshot. It MUST remain valid after the source map closes, MUST be -usable from any thread, and MUST release with `mln_map_projection_destroy()`. +Accepted one-shot work owns its completion state independently of the initiating +wrapper. A completion copies borrowed native data before returning and resolves +the language's eager async value exactly once. Dropping or cancelling a host +wait does not cancel accepted native work. ### Handle copying Owned handle wrappers are affine: the safe public API MUST NOT create duplicate owners. Reference-copy languages MUST make all references share one owner state. Value-copy languages MUST make owned handles non-copyable or move-only. Public -code MUST NOT be able to fabricate live handles, ID-backed operation tokens, or -request tokens from raw integers, raw addresses, public fields, or ordinary -constructors. +code MUST NOT be able to fabricate live handles or request tokens from raw +integers, raw addresses, public fields, or ordinary constructors. --- @@ -570,14 +570,16 @@ Callback invocation follows this operation: callback boundary. If the public callback returns a recoverable host failure, convert the failure to the C callback's documented behavior. 3. Synchronize callback state that native can invoke concurrently. -4. Return promptly. Callback code hands owner-thread work back to the owner - thread before calling runtime or map APIs. +4. Return promptly. A callback that may run inline schedules later host work and + returns before calling APIs on the object that invoked it. -Callback replacement installs the new native registration before releasing old -callback state. If installation fails, the old callback remains active and the -replacement state is released. Clearing, replacing, or closing prevents new -upcalls, waits for in-flight upcalls, and releases callback roots after native -can no longer invoke them. +Callback replacement retains both old and new roots through native acceptance. +When a descriptor provides a native release callback, make the new root +independently reachable before registration. Reclaim the root when registration +is rejected. After acceptance, replacement, clearing, or owner release retires +the registration. The native release callback then retires the root after every +in-flight upcall returns. For an immediate registration without a release +callback, release the old root only after the call returns successfully. If a leaked native owner can still reach callback user data, non-deterministic cleanup reports the leak and keeps callback memory reachable from native alive. @@ -619,8 +621,8 @@ provider produce no header-transform invocation. Registration remains replaceable for the lifetime of a runtime, including while maps are live. A binding installs a replacement before releasing the previous -callback state. Replacement, clear, and runtime close release the old state only -after the native registration call has retired every in-flight callback. +callback state. Replacement, clear, and runtime teardown release the old state +only after the native registration has retired every in-flight callback. Bindings that cannot answer a synchronous callback expose native rule tables. An exact rule matches the complete transformed URL, a glob rule matches it against @@ -668,9 +670,10 @@ Resource provider invocation follows this operation: 6. Allow deferred or cross-thread completion when the C API allows it, without changing one-shot or release behavior. -Provider registration is replaceable for a runtime's whole life. A binding keeps -the registered callback state reachable until the C call that replaces or clears -the provider returns, and releases it after that call returns. +Provider registration is replaceable for a runtime's whole life. After command +acceptance, a binding transfers the new callback state to the C API. The C API +releases old state after replacement, clear, or runtime teardown retires its +final callback. Handled request completion is terminal. A request can complete once; a completion that reaches C consumes the completion path even when native returns @@ -685,99 +688,101 @@ argument, so one request handle type covers both the owning and the moved use. --- -## Threading - -The C API owner-thread model is visible at the binding layer. Ordinary public -methods call C synchronously on the calling native thread and surface the C -owner-thread status when called from the wrong thread. - -### Owner-thread helpers - -Provide an owner-thread execution helper when the host language can move a -logical task across native threads or cannot otherwise give safe callers a -stable native owner thread for a runtime/map lifecycle. Bindings with stable -native caller identity expose ordinary methods and wrong-thread errors without -this helper. - -The helper follows this design: - -1. It owns or binds one native owner thread before creating thread-affine - handles. -2. It runs submitted operations by calling the ordinary low-level binding - methods on that owner thread. -3. It serializes submitted operations with event draining and close on that - owner thread. -4. It returns the ordinary binding result or error shape, including copied - native diagnostics. -5. Closing rejects new submissions, releases thread-affine handles on the owner - thread, and leaves later submissions in the binding's closed-state error - shape. - -A helper that parks its owner thread between iterations acquires a wake source -and signals it from submission and close, so a submitted operation runs at -submission time. - -### Parking and wake - -The pump is one method taking a timeout and a drain budget. Bindings expose it -alongside a wake source handle. - -The pump wrapper follows this design: - -1. It takes the host language's duration or timeout type, maps zero to a - non-blocking drain, and maps the language's "no timeout" spelling to an - unbounded park. -2. It takes the budget the same way, maps the language's "no budget" spelling to - an unbounded drain, and defaults to unbounded where the language has default - arguments. -3. It releases the host runtime's blocking-call machinery for the duration of - the call, including any interpreter lock, so other host threads run while the - owner thread parks. -4. Its documentation states that a wake signal sets a flag the pump clears, and - that callers drain events after every return. - -The wake source follows this design: - -1. It is a distinct owned handle that the host releases explicitly, and - releasing it is independent of the runtime's lifetime in both orders. -2. It is transferable and callable from any thread, and the binding declares - that where the host language can express it. -3. Signalling after the runtime is closed succeeds and does nothing. +## Execution + +Runtime, map, projection, render-session control, event-batch, and +frame-result-batch handles are callable from any native thread. The C API owns +the runtime scheduler and any core-worker graphics thread. A binding MUST NOT +create an owner thread, expose a pump, or pin a task to a native thread for +native progress. + +Bindings preserve the C execution category: + +1. Immediate functions return synchronously in the language's ordinary result or + error shape. +2. One-shot asynchronous calls return after native code accepts and copies the + input. The binding resolves one eager language-native asynchronous value from + the completion callback. An accepted command resolves to a command-completion + value even when it fails or is cancelled, preserving its disposition, + terminal status, generation, and diagnostic. Submission rejection and bridge + failures reject the asynchronous value. +3. Published snapshots return synchronous language-owned copies. A binding MUST + NOT relabel an ordered query as a snapshot. +4. Result-producing completions use the language's future, promise, task, + suspension, or explicit async idiom. Cooperative and UI executors suspend + instead of blocking. +5. Event and result batches drain into language-owned sequences. A binding that + exposes direct wakes uses them to schedule the drain; a low-level binding may + also expose explicit polling. +6. A caller-driver service call and graphics accessor preserve the target's + graphics-thread requirement. A binding MUST NOT apply that requirement to + render-session control calls. + +Lifecycle wrappers do not publish a map until its creation completion transfers +the live handle. Explicit close performs native release preflight and makes +every public alias observe closed state when native consumes the handle. Native +teardown retains its own callbacks and dependencies. Map and runtime close +expose their teardown completion through the language's asynchronous idiom. A +preflight failure leaves the wrapper open. + +A binding roots completion state before calling C because completion may happen +inline. A rejected submission leaves that state binding-owned. A successful +submission transfers it to C, and the native release callback retires it after +the terminal callback returns. The terminal callback copies borrowed results and +diagnostics before it resolves the language-native asynchronous value. + +Cancelling a host wait retires the host continuation. It does not cancel native +work unless the C function explicitly provides cancellation. The callback root +remains live until native release even when no host continuation remains. + +### Stream wake callbacks + +A binding may expose runtime-event, frame-result, and driver-work wakes, or it +may leave the matching descriptor empty and expose explicit polling. This choice +does not change queue ownership or the full-drain contract. A binding that uses +a wake schedules a later drain or graphics-thread service on an existing host +execution context and returns before calling the owner again. A wake may arrive +inline. + +A binding MUST NOT create a scheduler, executor, worker, or owner thread for +wake delivery. A host integration MAY supply the execution context. Clearing or +releasing an owner retires its callback after in-flight callback entries return. ### Event draining -The public event API is explicit: host code pumps native runtime work, then -drains the queued runtime events. One drain returns the ordered sequence of -events that the queue held, empty when it held none. +The public event API exposes a full drain. The host calls it after a direct wake +or from its own polling cadence. One drain returns the ordered sequence that the +queue held, empty when it held none. The drain follows this operation: -1. Initialize the native batch struct, including its `size` field, before - calling C. -2. Call the C drain function on the runtime owner thread, passing the host's - maximum event count. The binding's default takes the whole queue. -3. Step through the batch by the event stride that the batch reports, rather +1. Initialize a null native batch handle. +2. Call the C drain function from any thread. It transfers the whole queue. +3. Borrow the batch view from the owned batch handle. +4. Step through the batch by the event stride that the batch reports, rather than by the size of the generated event struct. -4. Copy each event's type, source type, status code, message bytes, and typed - payload fields before the next drain invalidates native batch storage. A - lifetime-checked borrowed binding MAY expose views of these fields instead. A - message is the batch's arena bytes at the event's own offset and length. -5. Read a known typed payload as the union member that the payload type names, +5. Copy each event's type, source type, status code, message bytes, and typed + payload fields before releasing the batch. A lifetime-checked borrowed + binding MAY expose views whose lifetime is bounded by the batch. A message is + the batch's arena bytes at the event's own offset and length. +6. Read a known typed payload as the union member that the payload type names, with no size check. Preserve an unknown event or payload domain with its raw value and its bytes as the batch's event stride reports them. -6. Copy the event's source id, resolve any public map wrapper for that id +7. Copy the event's source id, resolve any public map wrapper for that id through binding-owned runtime state, and expose the copied id as the event's source identity. Constructing a public handle from the source id stays outside the safe public API. -7. Apply binding-owned state updates triggered by the event before returning the +8. Apply binding-owned state updates triggered by the event before returning the copied events. -8. Return a language-owned sequence whose elements outlive the next drain, or a - borrowed view whose type prevents that view from surviving the next drain. +9. Release the owned native batch. +10. Return a language-owned sequence, or a borrowed view whose type cannot + outlive the batch. ### Event subscription A map and a runtime each carry a subscription: the event types it queues. An -unselected event is never built, never queued, and raises no wake. +unselected event is never built, never queued, and does not invoke the runtime's +event wake callback. The subscription follows this design: @@ -806,126 +811,149 @@ The subscription follows this design: ### Attaching a render session -A render session's owner thread is the thread that attached it, fixed for the -session's lifetime, and it need not be the map's owner thread. - -1. Attach requires the map to be live, not to be owned by the calling thread. -2. The session a binding returns is affine to the attaching thread. Every - session operation from another thread reports the binding's wrong-thread - error, including close. -3. A binding MUST NOT retain the map in binding-owned state that cannot reach - the attaching thread. Where a binding drops that retention, it documents that - the C API keeps the map alive instead, by rejecting map destroy while a - session is attached, and that releasing a map before its session reports - through the binding's leak channel rather than destroying the map. -4. A binding whose map handle cannot cross threads MUST expose the map to the - attaching thread through a transferable attach reference whose only operation - is attach. Bindings whose map handle is already safe to use from another - thread expose attach on the map handle directly and MUST NOT add a redundant - reference type. A binding whose owner identity is a host construct rather - than the native thread, such as a Dart isolate, states that the two must - coincide for its handles to stay usable. +Attachment selects a driver and returns an attaching render-session handle plus +an asynchronous completion. + +1. The binding MUST retain the map while the session is live. +2. The binding MUST publish the returned session immediately in an attaching + state, because a caller driver needs that handle to service initialization + work before the attach operation can complete. +3. The binding MUST NOT expose ordinary attached-session work until the attach + completion succeeds. Driver service, snapshots, abandonment, and destruction + remain available according to their C lifecycle contract. +4. Map close preflight reports invalid state and leaves the map open while a + session is attached or attaching. +5. The binding MUST expose attach directly on the map handle. It MUST NOT add an + attach-only map reference, owner-thread adapter, driver thread, mailbox, or + queue. ### Transferability -When the language can declare or enforce cross-thread transferability, ordinary -owner-thread handles MUST be non-transferable. A transferable owner-thread -helper handle is allowed only when every operation either is submitted back to -the bound native owner thread or is serviced entirely under native -synchronization. Two handles are the second kind. A map attach reference reaches -no thread-affine map state: attach claims the map's render-session slot under -the C API's map registry lock and posts the new size to the map's own owner -thread. A wake source handle reaches native wake state that carries its own -synchronization and holds no owner-thread state. Both are transferable and MUST -NOT be shareable. Copied immutable values can be transferable when their -contents are independent of native owner-thread state. Unchecked or unsafe -concurrency conformance MUST name the synchronization invariant that makes it -sound. +Runtime, map, projection, render-session control, event-batch, +frame-result-batch, and acquired-frame handles are safe to move between native +threads. A binding may declare them transferable when its wrapper synchronizes +close, release, and in-flight calls. Backend accessors on acquired frames +preserve their documented graphics context requirement. ---- +Unchecked or unsafe concurrency conformance MUST name the wrapper +synchronization invariant that makes concurrent use and close sound. ## Rendering -Rendering bindings expose render sessions, frame lifetimes, and readback without -taking ownership of caller-owned backend resources. - -Render session calls may run on a thread other than the one that pumps the -runtime. Events produced by rendering are still delivered by a runtime event -drain on the runtime owner thread. +Rendering bindings expose the two native driver contracts, frame demand and +results, texture-slot lifetimes, and readback without taking ownership of +caller-owned backend resources. + +### Drivers and wake callbacks + +A binding MUST expose core-worker and caller-graphics-thread placement as the +common attach option. WGL targets, EGL surfaces, shared EGL textures, and +existing browser WebGL contexts use the caller driver. A private EGL owned +texture target uses a core worker and grants readback without frame acquisition. +A transferred `OffscreenCanvas` WebGL target uses the same private-target +contract and creates its WebGL2 context on the worker. Browser WebGPU uses the +caller driver. Transferable Metal and Vulkan targets may support the core +worker. + +The core worker owns a native serial graphics worker. The binding MUST NOT add a +thread, executor, mailbox, or queue around it. The caller driver stores typed +native work and progresses only when the host calls driver service with the +target context usable. The first successful service call fixes the graphics +thread identity. The binding MUST keep later calls affine to that thread and +serialize them against other driver calls. + +The C attach options expose direct callbacks for frame results and driver work. +A binding may expose those callbacks or explicit polling. When it exposes a +callback, it schedules the matching drain or graphics-thread service and returns +promptly. + +Driver-work readiness is independent of frame cadence. A binding MUST let the +host service ready driver work while presentation callbacks are paused. It MUST +NOT teach a runtime pump or use runtime events as the trigger for render +progress. ### Render sessions -Render-session attach APIs cover the C API session families: - -- Surface sessions render and present through a host surface. -- Session-owned texture sessions render into a texture or image created by the - session. -- Caller-owned texture sessions render into a host-owned texture or image. - -Attach follows this operation: - -1. Materialize the backend-specific public descriptor into the matching C - descriptor. -2. Pass backend-native host resources as `NativePointer` values. -3. Call the matching C attach function on the thread that will drive the - session, which for a host graphics API with a thread-current context is the - thread where that context is current. -4. Return a distinct `RenderSessionHandle`, bound to the calling thread, for the - map's one live render session. -5. Surface unsupported backend, unsupported render-target mode, - existing-session, and native errors through the binding's status mapping. - -For host-owned backend resources, the binding does not release or synchronize -those resources. The caller keeps them valid for the C API's documented borrow -window. +Render-session attach APIs cover native surfaces, session-owned textures, and +caller-owned borrowed textures. They materialize the backend descriptor, common +attach options, and `NativePointer` fields without taking ownership of borrowed +resources. The public handle exposes: -- `resize` for session kinds that support resize; -- `set_target` for session kinds whose target the host owns, which is surface - sessions and caller-owned texture sessions; -- `render_update` for the latest available map render update, reporting a public - `RenderUpdate` value that pairs the render result enum with the frame's - repaint flag; -- `detach`, which keeps the public handle live after backend resources detach; -- `close` or `destroy`, using the owned-handle release operation, on the thread - that attached the session. - -`set_target` takes the same public descriptor type its attach function takes, so -a host builds a replacement target the way it built the first one. It is exposed -per backend and target kind, matching the C API's `mln_*_surface_set_target()` -and `mln_*_borrowed_texture_set_target()` functions, and it is bound to the -thread that attached the session like every other session operation. The C API -rejects a descriptor whose graphics context differs from the session's, so -bindings pass the descriptor through. +- immutable negotiated capabilities; +- any-thread snapshots, frame demand, result drains, resize, barriers, + maintenance, feature state, readback, detach, abandon, and destroy; +- driver service and backend accessors with their graphics-thread requirement; +- target replacement for native surfaces and borrowed textures. + +Resize, replacement, queries, maintenance, readback, detach, and barriers are +asynchronous calls routed through the selected driver. A binding MUST use its +common completion bridge and MUST keep servicing a caller driver while such a +call is pending. + +Normal detach runs graphics destruction on the selected owner and detaches the +map. The binding retains the session until its completion runs, then permits +CPU-only destruction from any thread. Abandon performs no graphics calls, +returns busy during an in-flight driver call, invalidates accessors, detaches +the map in CPU state, and reports whether resources entered process-lifetime +quarantine. A binding MUST expose abandon as the failure path for an owner that +cannot service graphics work again. + +Target loss is a distinct snapshot state. A binding MUST preserve target-not- +ready and target-lost outcomes rather than retrying them in a busy loop. + +### Frame demand and results + +A binding MUST expose frame demand as a nonblocking any-thread call. It carries +flags, a token, a coalescing boundary, and an optional timeout. Every accepted +demand produces exactly one terminal result record, including a superseded or +deadline-missed demand. + +Frame results preserve disposition, token, map-update generation, extent +generation, and frame generation. A binding MUST preserve rendered, no-update, +size-pending, target-not-ready, superseded, and deadline-missed as distinct +outcomes. A positive timeout starts when native code accepts the demand and +terminates it as deadline missed when it expires before work begins. The timeout +does not supply cadence. + +Frame readiness is level-triggered until drained. Each drain takes every queued +record into an independently owned batch. A binding MUST preserve the records +until it releases the batch. + +A session barrier completion runs after all preceding accepted render work is +terminal. A binding MUST NOT represent a barrier as a frame request or a runtime +pump. ### Texture frames -Session-owned texture frames are scoped borrows. +Session-owned targets negotiate a ring depth from one to three. Acquisition is +nonblocking and returns an acquired-frame handle that leases one slot. The +binding copies common metadata and exposes backend handles only through +active-handle accessors. -Frame acquisition follows this operation: +The binding MUST expose producer-completion synchronization and accept optional +consumer-completion synchronization on release. CPU-complete synchronization is +valid when the relevant GPU producer or consumer completed before publication. +Release consumes the frame handle synchronously. The session retires the slot +through its selected driver and does not reuse it until consumer GPU completion. +A binding MUST NOT release a slot merely because the CPU wrapper closed while +host GPU reads remain pending. A borrowed non-CPU synchronization object remains +valid until a later session barrier or detach completes. -1. Acquire the native frame and create an explicit frame handle. -2. Copy public metadata from the native frame. -3. Expose backend handles only through active-frame checked accessors. -4. While the frame is active, reject nested frame acquisition and every exposed - session operation whose C contract forbids execution during an active frame. -5. Release follows the owned-handle release operation. Failed native frame - release leaves the frame live for retry. -6. If wrapper construction fails after native frame acquisition, release the - acquired native frame. +After abandonment, releasing an acquired frame still consumes its handle +CPU-only. The binding MUST NOT call a backend accessor after abandonment. -Copyable frame handles include stale-handle protection so old frame copies -cannot expose backend handles after release or after a later frame reuses the -same storage. +Backpressure is the absence of an acquirable or reusable slot. Bindings MUST +surface not ready without blocking, replacing an acquired frame, or silently +expanding the negotiated ring. ### Readback -CPU texture readback accepts caller-owned mutable storage. - -Readback returns copied `TextureImageInfo` metadata. Buffer-capacity failures -preserve the caller's buffer ownership and map to the binding's error mechanism. -Public buffer reads return copied or read-only views unless the binding proves +Texture readback is asynchronous work through the selected driver. Its future +resolves with owned premultiplied RGBA8 bytes and copied image metadata. A +caller driver needs graphics-thread service until the future resolves. Public +buffer reads return copied or read-only views unless the binding proves exclusive mutable access. --- @@ -992,20 +1020,17 @@ that a real native failure would expose. ### Handle lifetime -| ID | Test | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-040 | Runtime creation followed by explicit release destroys the native handle exactly once; every public alias observes release state, and a second release no-ops. | -| BND-041 | A failed native destroy leaves the handle live; a later successful release destroys the native handle. | -| BND-042 | A child handle retains parent owner state, and parent release fails while child handles are live. | -| BND-043 | `MapProjectionHandle` remains usable from another thread after the source map closes and then releases successfully on that thread. | -| BND-045 | A released handle's id, replayed through an internal seam after a new handle of the same kind is created, reports the binding's invalid-argument error naming it stale, and the new handle keeps working. | -| BND-047 | A handle id of one kind passed to another kind's operation through an internal seam reports the binding's invalid-argument error, and the safe public API has no expression of that call. | -| BND-049 | An owner-thread-affine handle id moved to a different native thread and called there reports the binding's wrong-thread error rather than a stale-handle or closed-handle error. | - -BND-049 applies where the host language can reach a second native thread while -the handle stays live. Dart is excluded: an isolate may resume on a different -native thread after an await, so a handle must be closed before the test awaits -the other isolate, which makes its id stale rather than live. +| ID | Test | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BND-040 | Runtime creation followed by explicit close retires the native handle exactly once; every public alias observes closed state, a second binding close no-ops, and the close path exposes the runtime's teardown completion so a host can wait until native teardown finishes. | +| BND-041 | A failed native release preflight leaves the handle live; a later accepted release retires it before returning. | +| BND-042 | A parent close rejects a live or pending child, leaves the parent open, and succeeds after the child closes or creation resolves. | +| BND-043 | A map projection remains usable until explicit close, including after its source map and runtime close. | +| BND-044 | Dropping or cancelling a host wait does not cancel or stall accepted native work, and native teardown preserves every dependency it still needs. | +| BND-045 | A released handle's id, replayed through an internal seam after a new handle of the same kind is created, reports the binding's invalid-argument error naming it stale, and the new handle keeps working. | +| BND-047 | A handle id of one kind passed to another kind's operation through an internal seam reports the binding's invalid-argument error, and the safe public API has no expression of that call. | +| BND-049 | A runtime, map, or projection handle remains usable when an asynchronous continuation resumes on another native thread. | +| BND-050 | Map close exposes a completion that runs after earlier map work and map-owned callback teardown; awaiting it requires no runtime barrier or host polling. | ### Input Structs, Values, and Copied Data @@ -1026,33 +1051,34 @@ the other isolate, which makes its id stale rather than live. ### Runtime and events -| ID | Test | -| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-080 | `pump` drives native event processing through the public runtime API, and a later drain reports an empty batch. | -| BND-081 | Map style loading returns the expected copied map event through a drain and identifies the correct public map identity. | -| BND-082 | Event message and payload data remain valid after the next drain. | -| BND-083 | An unknown event or payload domain preserves its raw value, and an unknown payload keeps the bytes that the batch's event stride reports. | -| BND-084 | Offline operation completion returns copied result data. Native take-result status failures before result ownership transfers leave the operation handle retryable. | -| BND-085 | Offline region observation returns copied status/error events through the public runtime event model. | -| BND-086 | A map-originated event with no provable live public map exposes no public map handle. | -| BND-087 | A binding steps events by the batch's reported event stride. | -| BND-088 | A parked owner thread is released by native work and by a wake source signalled from another thread, and reports a wake rather than a timeout. | -| BND-089 | A pump clears the wake flag it returned on, and a wake source stays signalable and releasable after its runtime closes. | -| BND-090 | One drain reports more than one event and preserves queue order. | -| BND-091 | The default subscription parameter delivers every event type, a narrowed one delivers neither the cleared type nor a wake for it, and an unknown bit is rejected. | -| BND-092 | A batch invalidates at the next drain, and the values a binding copied out of it stay readable. | -| BND-093 | Binding-owned cleanup that a style load drives still runs while the map's subscription leaves out the style-loaded type. | +| ID | Test | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BND-080 | Native runtime work progresses without a host pump, and a runtime barrier completes after every preceding submission reaches a terminal disposition. | +| BND-081 | Map style loading returns the expected copied map event through a drain and identifies the correct public map identity. | +| BND-082 | Event message and payload data remain valid after the native batch is released because the binding copied them. | +| BND-083 | An unknown event or payload domain preserves its raw value, and an unknown payload keeps the bytes that the batch's event stride reports. | +| BND-084 | A completion copies its typed result and diagnostic before native callback storage is released, including inline completion. | +| BND-085 | Offline region observation returns copied status and error events through the public runtime event model. | +| BND-086 | A map-originated event with no provable live public map exposes no public map handle. | +| BND-087 | A binding steps events by the batch's reported event stride. | +| BND-088 | A configured event wake resumes an idle receiver without polling; a polling-only binding drains from an existing host cadence. | +| BND-089 | Releasing a runtime prevents later event-wake callback entries and waits for an in-flight entry before releasing callback state. | +| BND-090 | One drain reports more than one event and preserves queue order. | +| BND-091 | The default subscription delivers every event type; a narrowed one delivers neither the cleared type nor a wake for it; an unknown bit is rejected. | +| BND-092 | Two owned batches remain readable independently until release, and the values a binding copied out of them remain readable afterward. | +| BND-093 | Binding-owned cleanup that a style load drives still runs while the map's subscription leaves out the style-loaded type. | +| BND-094 | Accepted commands resolve once with committed, failed, cancelled, or superseded disposition, status, generation, and copied diagnostic data, without rejecting the asynchronous value or using a correlation ID. | ### Map, camera, projection, style, and query | ID | Test | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-100 | Map creation applies public map options, extent, and mode, then releases through the runtime parent relationship. | -| BND-101 | Style URL and style JSON loading succeed through public map APIs and return copied style-loaded events through a drain. | -| BND-102 | Camera set/get, animated camera commands, transition cancellation, and gesture-in-progress bracketing produce the expected native camera state and statuses. | +| BND-100 | Synchronous runtime creation and asynchronous map creation apply public options, transfer each handle once, enforce close preflight, and preserve parent relationships. | +| BND-101 | Style URL and style JSON commands complete successfully and later return copied style-loaded events through a drain. | +| BND-102 | One atomic camera command applies every selected field; its completion, published snapshots, and ordered queries report the expected committed state and generation. | | BND-103 | Projection helpers round-trip screen, lat/lng, and projected-meter values through copied public values within documented tolerance. | | BND-104 | Representative invalid map and projection inputs propagate native invalid-argument diagnostics through the public error shape. | -| BND-105 | Style source, layer, image, and feature-state workflows add, update, query/list, and remove public input values and copied IDs. | +| BND-105 | Style source, layer, image, and feature-state workflows submit commands, await ordered query/list operations, and preserve copied IDs. | | BND-106 | Query workflows return copied queried-feature values. Each value contains a GeoJSON Feature buffer, optional source and source-layer identifiers, and optional feature-state JSON. | | BND-108 | The loaded style document reads back byte-for-byte through public map APIs, the style URL reads back the last requested URL, and both report empty when absent. | | BND-109 | Source inspection copies a URL-backed source URL and inline tile-source metadata, including multiple tile URLs and absent fields, and the result remains valid after the map no longer owns the source. | @@ -1063,7 +1089,7 @@ the other isolate, which makes its id stale rather than live. | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | BND-120 | Log callback install invokes the registered callback, clear prevents later invocation, and replacement invokes only the replacement callback. | | BND-121 | Host-language failures do not unwind or escape across the C callback boundary, and recoverable callback failures are converted to documented C behavior. | -| BND-122 | Each exposed callback family preserves the previous callback and releases replacement state when replacement fails. | +| BND-122 | Each exposed callback family transfers its state through the C release callback after a successful install; a failed replacement stays binding-owned. | | BND-123 | Callback state remains synchronized for callback families whose C contract allows concurrent invocation. | | BND-124 | Custom geometry or style-scoped callback teardown handles style reload, source removal, source ID reuse, map close, and in-flight upcalls without late use. | @@ -1102,25 +1128,29 @@ When the binding routes provider requests through ### Rendering -| ID | Test | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-160 | Supported render-backend queries gate configured workflows and unsupported backend/mode errors. | -| BND-161 | Render-target descriptors materialize extents and `NativePointer` backend handles without taking ownership. | -| BND-162 | Surface, session-owned texture, and caller-owned texture attach paths call the matching C session family and report the same public session handle shape. | -| BND-163 | Attaching a second render session to the same map reports invalid state. | -| BND-164 | `render_update` reports a result other than a rendered frame without closing the session. | -| BND-165 | Resize updates extent through the public render session API. | -| BND-175 | `set_target` replaces a host-owned render target through the public render session API and updates the session's extent. | -| BND-176 | `set_target` reports unsupported for a target kind the session does not have, covering a session-owned texture and a mismatched surface/texture pairing. | -| BND-166 | CPU readback copies metadata; undersized buffers fail without losing ownership, and sufficiently sized reusable buffers receive image bytes. | -| BND-167 | Owned texture frame acquire returns an explicit frame handle with copied metadata and active-checked backend handles. | -| BND-168 | Owned texture frame access after release fails before exposing backend handles. | -| BND-169 | Failed frame release leaves the frame live and a later successful release closes it. | -| BND-170 | Nested frame acquisition and every exposed session operation forbidden during an active frame fail while a frame is active. | -| BND-171 | Caller-owned texture descriptors do not release or mutate caller-owned backend handles during session close. | -| BND-172 | Bindings with fallible owned-frame wrapper construction release the native frame when construction fails after native frame acquisition. | -| BND-173 | Stale frame handles cannot expose backend handles after release or reuse. | -| BND-174 | Closing a map whose render session was attached on another thread reports the C API's invalid-state error and leaves both handles live. | +| ID | Test | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BND-160 | Backend and target capabilities gate driver and target combinations, including caller-driver WGL/shared-EGL/WebGL and browser WebGPU, and core-worker transferable Metal, Vulkan, private EGL, and `OffscreenCanvas` WebGL. | +| BND-161 | Render-target descriptors copy extents and `NativePointer` backend handles without taking ownership. | +| BND-162 | Every supported target attach returns an attaching session and completion; caller-driver service can complete attachment without a deadlock. | +| BND-163 | Attaching a second session to one map reports invalid state and leaves the first session usable. | +| BND-164 | Every accepted demand yields one result, preserving all dispositions, tokens, timestamps, and generation fields. | +| BND-165 | Resize and target replacement complete through the selected driver and update the extent generation. | +| BND-166 | Readback transfers owned bytes and copied metadata through a completion. | +| BND-167 | Host-acquirable owned texture capability negotiation grants a one-to-three-slot ring, and acquisition leases a slot with copied metadata and active-checked backend handles. | +| BND-168 | Frame access after release fails before exposing backend handles. | +| BND-169 | Frame release consumes the handle synchronously, and the session reuses the slot only after consumer GPU completion. | +| BND-170 | Ring exhaustion reports not ready without blocking or replacing an acquired frame. | +| BND-171 | Borrowed target descriptors do not release or mutate host backend handles during detach. | +| BND-172 | A fallible wrapper releases a natively acquired frame when wrapper construction fails. | +| BND-173 | Stale acquired-frame handles cannot expose backend handles after slot reuse. | +| BND-174 | Render-session control, completion submission, snapshot, demand, abandon, and destroy calls work from a thread other than the graphics service thread. | +| BND-175 | A frame-result drain takes every queued record into an independently owned batch, and multiple batches may remain live. | +| BND-176 | A barrier waits for preceding render work without requesting a frame. | +| BND-177 | An expired positive frame timeout yields deadline missed before graphics work begins. | +| BND-178 | Normal detach performs graphics destruction through the selected owner and permits any-thread CPU-only destroy afterward. | +| BND-179 | Abandon returns busy during a driver call, performs no graphics calls, invalidates accessors, detaches the map, and reports quarantine. | +| BND-180 | Target loss is preserved in snapshots and does not enter a busy retry loop. | ### Conditional tests @@ -1131,40 +1161,38 @@ mechanic, helper, or test fixture. When the host language can run cleanup outside explicit release, include: -| ID | Test | -| ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| BND-044 | Non-deterministic cleanup hooks report leaked thread-affine handles rather than destroying them. | -| BND-048 | Best-effort cleanup failure is reported through the binding's documented leak or failure channel and explicit release can retry. | +| ID | Test | +| ------- | ------------------------------------------------------------------------------------------------------------------------- | +| BND-044 | Non-deterministic cleanup either retains service and dependencies through detach or abandons before CPU-only destruction. | +| BND-048 | Quarantine or cleanup failure reaches the binding's documented leak or failure channel. | #### Cross-thread public handle use -When safe public code can call owner-thread-affine APIs from the wrong native -thread or race release on the same owner-thread handle, include: +When safe public code can use or close the same any-thread handle concurrently, +include: -| ID | Test | -| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-046 | Concurrent releases call native release at most once and public calls fail while release is in progress. | -| BND-190 | Owner-thread-affine calls from a different native thread report the binding's wrong-thread error. | -| BND-191 | Runtime wrong-thread errors include the copied native diagnostic. | -| BND-197 | A release racing a use of the same handle waits for the in-flight use, and a use starting after the release begins reports the binding's closed-handle error. | +| ID | Test | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| BND-046 | Concurrent close or release attempts call the terminal native function at most once, and public calls fail while close or release is in progress. | +| BND-190 | Runtime, map, and completion-based calls from another native thread preserve validity and runtime submission order; projection calls from another thread preserve validity and per-projection serialization. | +| BND-191 | A completion's copied final diagnostic remains stable after unrelated native calls on another thread. | +| BND-197 | A close or release racing a use of the same handle is memory-safe: an entry point that acquired the C handle first completes against its native lease, while a later entry reports a closed handle. | -BND-197 applies to handles the host can use and release from different threads, -which today means the wake source and the resource request. A binding that -orders the two by holding one lock across the native call satisfies it by -construction and has nothing beyond that lock to assert; a binding that counts -in-flight uses and drains them exercises the counter directly. +The C handle table owns the native call lease. Bindings keep close-once state to +reject calls that begin after their wrapper closes; they do not duplicate the +native lease with active-use counters. -#### Render sessions on a second thread +#### Caller-driver graphics threads -When the binding's test suite attaches a render session on a configured render -backend and the host language can start a native thread, include: +When a configured target uses a caller driver and the host language can start a +native thread, include: -| ID | Test | -| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| BND-193 | A native thread that does not own the map attaches its own render session against it and renders while the map is pumped on its own owner thread. | -| BND-194 | Every render-session operation reports the binding's wrong-thread error on a thread other than the one that attached the session, leaving it usable. | -| BND-195 | A session attached and closed on a second native thread destroys the native handle exactly once, after which the map closes successfully. | -| BND-196 | Attaching through a reference to a released map reports the binding's invalid-argument error naming the map stale, including once a later map exists. | +| ID | Test | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| BND-193 | The graphics thread services attachment, frames, ordered asynchronous calls, and detach while map work progresses on the native scheduler. | +| BND-194 | Driver service and thread-current accessors report the binding's wrong-thread error elsewhere, while any-thread control calls remain usable. | +| BND-195 | A session detached by its graphics-thread service is destroyed exactly once from another thread, after which map close succeeds. | +| BND-196 | Attaching through a released map wrapper reports the binding's stale-handle error before crossing into C. | #### Live render session queries @@ -1177,11 +1205,3 @@ backend, include: Bindings without live render-session fixtures cover byte ownership and copying through BND-066 and BND-067. - -#### Owner-thread execution adapters - -When the subproject ships an owner-thread execution adapter, include: - -| ID | Test | -| ------- | --------------------------------------------------------------------------- | -| BND-192 | The adapter confines create, pump, event draining, and close to one thread. | diff --git a/docs/src/content/docs/development/c-conventions.md b/docs/src/content/docs/development/c-conventions.md index e06a5b764..2b3e75808 100644 --- a/docs/src/content/docs/development/c-conventions.md +++ b/docs/src/content/docs/development/c-conventions.md @@ -120,8 +120,9 @@ is what rejects a mismatched handle, so document the handle type each parameter expects. Handle values are safe to copy, compare, hash, and move between threads, and -carry no ownership on their own. Owner-thread rules govern which thread may call -with a handle, not which thread may hold one. +carry no ownership on their own. Runtime, map, projection, render-session, and +event-batch handles are callable from any native thread. Render-driver comments +name the calls that require a graphics thread. The bit layout is internal. Hosts pass handles back as issued, and decoding or synthesizing an id is unsupported. @@ -148,77 +149,134 @@ no-op, so a host cleanup hook that runs twice stays safe. Output handle parameters that create or acquire ownership require `*out_handle` to equal `MLN_HANDLE_NULL` on entry and preserve live host-owned handles on -failure. Document when scoped resource ownership begins, when it ends, and -whether completion may happen inline or later. - -The runtime and map use a host-pumped, owner-thread model. Runtime creation -records the owner thread. Runtime, map, and render session calls that touch -thread-affine state validate the owner thread. A standalone map projection owns -copied transform state and serializes calls from any thread. - -A map shares its runtime's owner thread. A render session records its own: the -thread that attached it, fixed for the session's lifetime. Attach validates that -the map is live rather than that the caller owns it, so a session may be -attached, driven, and destroyed on a thread that never touches the map. Session -calls from any other thread report the owner-thread status. The host may hand -the map handle to the attaching thread by any means, because a handle is a plain -value and attach resolves it under the C API's own lock, rejecting an id that -names a released map. - -Cross-thread dispatch belongs in public functions designed as enqueueing -commands. Document that behavior on the function. Higher-level adapters build -threaded models above the C API. - -Map state a render session reaches for is enqueued to the map owner thread -rather than mutated in place, so resizing a session applies the map's logical -size on the map's next pump. Renderer observer callbacks are forwarded to the -map's run loop for the same reason, so the events a frame produces are drained -by a later `mln_runtime_pump()` rather than inside the render call. - -Graphics contexts that bind to a thread, such as OpenGL, are made current for -the duration of a session call and released before it returns, so a host keeps -its own context current on the thread that owns the session. A context -descriptor may instead give the session the thread's context: the session then -creates that context itself, joins no host share group, and keeps it current -between calls. Offer that mode where a target draws for one thread alone, and -keep the shared mode the default, so a descriptor that names no ownership -behaves as it always has. Attach creates the session's graphics resources on the -calling thread, which is why attach belongs on the thread that draws rather than -on the map's. - -On Apple targets each entry point drains its own Objective-C autorelease pool, -so a host may pump frames from a thread that never returns to a run loop. -Objects that cross the C boundary are retained rather than autoreleased, which -keeps them valid after the entry point that produced them returns. - -MapLibre's `RunLoop` is owner-thread scheduler state. Each owner thread may hold -one live runtime. `mln_runtime_pump()` advances that runtime: it parks the owner -thread when asked, then drains the queued tasks, expired timers, and ready I/O -it finds, including work enqueued while it runs. The budget bounds one pump's -drain at a task boundary: the first queued task always runs, a task runs to -completion once started, and tasks left behind re-arm the wake flag so the next -pump continues them without parking. Document a budgeted pump as bounding the -task queues alone, because timers and ready I/O are serviced regardless. - -One entry point carries both cadence sources: the timeout selects the cadence, -with zero for hosts driven by a callback they do not own and a positive value -for hosts that own their pump thread and take their cadence from the runtime's -own work. Park-and-wake follows these rules: - -- The C API owns the parking primitive. Wake signals reach the owner thread - through runtime state rather than through a host callback, because MapLibre - raises them from arbitrary threads while it holds locks that every thread - queueing owner-thread work needs. -- Wake signals set a flag that the pump clears before it returns. Document a - pump as advancing the runtime, and require the event drain after every return. -- Any-thread wake entry points take a handle that carries its own reference to - the wake state, never the thread-affine runtime handle. -- Document each blocking entry point's deadlock risk, naming the host locks a - caller must not hold across it. -- Queue one event per host-visible outcome. An event whose handling acts on the - latest state, such as a render update, coalesces against an unread one. +failure. Document when scoped resource ownership begins and when it ends. + +One-shot asynchronous work accepts a completion callback, `user_data`, and a +release callback. A successful submission transfers that callback state to the C +API. The completion runs exactly once and may run before the submission returns. +Its result pointers and diagnostic bytes remain borrowed for the callback. The +release callback runs after the completion returns and after no native path can +invoke it again. A rejected submission invokes neither callback and leaves +callback-state ownership with the caller. + +The runtime owns a native scheduler thread and one continuously running MapLibre +`RunLoop`. Runtime and map entry points resolve a handle, acquire its +control-state lease, copy the submission, and commit work to that run loop. They +never make a host thread own or pump MapLibre scheduler state. Map-projection +entry points after creation run on the calling thread over state that creation +captured, serialized by the projection's internal lock. + +A render session selects one of two execution contracts at attachment. A +core-worker driver owns a native serial graphics worker. A +caller-graphics-thread driver stores typed work until the host services it where +the context is usable. WGL targets, EGL surfaces, shared EGL textures, existing +WebGL, and browser WebGPU use the caller driver. Transferable Metal and Vulkan +targets, private EGL owned texture targets, and `OffscreenCanvas` WebGL targets +may use a core worker. + +Keep render-session control separate from graphics execution. Demand, snapshots, +operations, abandonment, and destruction are any-thread calls. The caller-driver +service call and backend accessors are graphics-thread-affine and serialized. +Runtime and map updates wake the core worker or make the caller mailbox's direct +driver-work wake callback run. + +Classify each public function as Immediate, Completion, Published snapshot, +Event batch, or Render-driver call. A binding maps that category to one +target-language shape; it does not add another scheduler or asynchronous +boundary. + +The category follows from the declaration, and +`scripts/check-execution-conventions.py` enforces the mapping as a test: + +| Declaration | Category | +| ---------------------------------------------------- | ------------------ | +| completion callback parameter | Completion | +| `_destroy` or `_release` suffix | Immediate | +| `_drain_` in the name | Event batch | +| `_service_driver_work` suffix | Render-driver call | +| `snapshot` in the name reading a live map or session | Published snapshot | +| anything else | Immediate | + +Name a new function so that its category derives from this table. A call whose +effects surface only through a drained event stream and that accepts no +completion is immediate: `mln_render_session_request_frame` returns its final +status synchronously and reports the frame through frame results. The checker +keeps an exception table for forms that the conventions cannot express; it is +empty today, and growth is a design smell. + +Pick the category from what the function reads or writes: + +- A read of unkeyed, fixed-size map state that changes only through the caller's + own commands or through load progress is a published-snapshot field. Every + committed map command publishes a snapshot and reports the published + generation in its completion, so a snapshot at or past that generation + observes the commit. +- A keyed or parameterized read, a read with an unbounded payload, a read whose + value follows committed work that the caller did not author, and work whose + completion is the product use typed completions. Prefer one info aggregate + with a found flag over per-field scalar or existence completions. +- An ordered mutation uses a command completion. A missing ID reports a + not-found status through that completion. The completion carries disposition + and the generation that a committed map mutation published. +- A call on state that creation captured into a detached object, which no worker + touches afterward, is immediate. This choice is per object: reads, setters, + and close become synchronous together, or their relative order breaks. The map + projection is the model. +- Creation of a root executor is immediate when no earlier work exists to order + against it. The call returns after the worker is ready. Runtime creation is + the model; wrapping startup in a completion only adds an observer and a typed + result transfer before the root object exists. + +Offer a published snapshot and an ordered completion for the same state only +when each form does distinct work, as camera does: the snapshot serves +latest-published consumers, and the ordered query is the fence. Delete an +ordered form that strictly duplicates the snapshot. + +One-shot functions validate and deep-copy every input before returning +acceptance. Each accepted completion reaches one terminal result. Native work +retains its callback state and other dependencies independently from the public +runtime or map handle. Command completions report committed, superseded, failed, +or cancelled disposition. A committed map command reports the snapshot +generation that its commit published. + +Published snapshots copy immutable committed state without entering mutable +MapLibre state. Use an ordered completion instead when a query must observe +every preceding command. + +Graphics contexts that bind to a thread, such as OpenGL, are made current during +caller-driver service and restored afterward under shared ownership. Dedicated +ownership keeps a session-created context current between renders. WGL and EGL +surface targets use a caller driver. Private EGL owned texture targets and +transferred WebGL targets use a core worker. + +On Apple targets each entry point and queued runtime submission drains its own +Objective-C autorelease pool, so a native worker or host render thread does not +need a surrounding run loop. Objects that cross the C boundary are retained +rather than autoreleased, which keeps them valid after the call that produced +them returns. + +The native `RunLoop` is the only runtime scheduler. It owns timers, I/O +readiness, queued invocations, and wakeup behavior. Do not alternate a separate +condition-variable queue with `RunLoop::runOnce()`; that queue cannot derive the +next native timer deadline. + +Runtime submissions follow these rules: + +- One runtime establishes a total commit order across commands, queries, + barriers, and close. +- Committing eligible work invokes the run loop and wakes it when idle. +- Registry locks protect lookup and state transitions only. Release them before + run-loop joins or callback quiescence. +- A close preflight checks live children and pending child reservations before + committing an irreversible close. +- A runtime barrier completes after every preceding submission reaches a + terminal disposition, not merely after its run-loop callback starts. +- Complete every accepted one-shot submission exactly once. State consumers may + coalesce only commands whose public contract permits replacement; every + replaced command completes as `superseded`. - A subscription mask suppresses an event before its payload and message are - built. A suppressed event stays out of the queue and raises no wake flag. + built. A suppressed event stays out of the queue and does not invoke its wake + callback. ## Status And Diagnostics @@ -233,8 +291,8 @@ Use these categories consistently: initialized output handles; - `MLN_STATUS_INVALID_STATE` for otherwise valid objects in the wrong lifecycle state; -- `MLN_STATUS_WRONG_THREAD` for thread-affine handles called from the wrong - owner thread; +- `MLN_STATUS_WRONG_THREAD` for render-driver handles called from a thread other + than the required graphics thread; - `MLN_STATUS_UNSUPPORTED` for backends, platforms, entry points, or requested behavior unavailable in this build; - `MLN_STATUS_NATIVE_ERROR` for native MapLibre errors or C++ exceptions @@ -244,8 +302,9 @@ Every exported `MLN_API` C++ definition must be `noexcept`. Status-returning entry points use the C API boundary helper to clear thread-local diagnostics on entry and convert exceptions to `MLN_STATUS_NATIVE_ERROR`. -Set thread-local diagnostic strings for synchronous non-OK returns. Report -asynchronous native failures through copied runtime events. +Set thread-local diagnostic strings for synchronous non-OK returns. Pass +asynchronous failures and their copied diagnostics through the accepted +completion. ## Events And Callbacks @@ -253,29 +312,28 @@ The C API preserves MapLibre Native's imperative, observer-driven model. C API calls return status for synchronous acceptance or failure; drained events report later native work. -Prefer drained events for native-to-host notifications about map state, -lifecycle, rendering, and errors. A host selects the event types it reads with a -subscription mask, so document the state that each type carries. Options always -read the mask, and a bit outside the documented set of types returns -`MLN_STATUS_INVALID_ARGUMENT`. Use native callbacks for low-level extension -points where MapLibre needs a synchronous decision, an asynchronous request -handle, or process-global integration such as logging. +Prefer drained events for repeated native-to-host observations about map state, +lifecycle, rendering, and errors. One-shot work uses completion callbacks and +does not also report completion through an event. A host selects the event types +it reads with a subscription mask, so document the state that each type carries. +Options always read the mask, and a bit outside the documented set of types +returns `MLN_STATUS_INVALID_ARGUMENT`. Use direct callbacks for completion, +low-level extension points where MapLibre needs a synchronous decision, and +process-global integration such as logging. Event payloads use plain data with documented lifetimes. Each event identifies -its source kind and source handle. Queued events never outlive the source handle -they reference: map teardown discards queued events for that map, and runtime -teardown discards runtime-owned event streams before the runtime handle becomes -invalid. A drained batch holds copies, and it stays readable until the next -drain for its runtime. - -Classify each operation as one of: - -- immediate, where the return status is the final result; -- a command, where return status means accepted and later effects arrive as - events; -- a state snapshot, where the returned data is last-known state; -- a blocking query, used rarely and documented with deadlock risks; -- an event stream, where many events are expected over time. +its source kind and copied source handle value. Closing a map or disabling an +offline-region observer prevents future publication without changing queued +history. Accepted runtime release consumes the runtime handle and discards its +undrained event stream during native teardown. A drain transfers the complete +queue into an owned batch, which stays readable across later drains and runtime +release until the caller releases it. + +Use the five execution categories defined in Ownership And Execution. An +Immediate return is final. A Completion return reports copied acceptance and +later delivers one terminal result. Published snapshots are immutable +synchronous copies. Event batches contain drainable observations. Render-driver +calls execute on the graphics thread named by their target contract. Logging, resource transform, and resource provider callbacks may run on MapLibre worker, network, logging, or render-related threads. @@ -293,13 +351,27 @@ Callbacks must not unwind through the C API. Bindings catch host exceptions, panics, and errors inside the callback and convert them to the callback's documented return behavior. -Render session APIs document owner thread, render target backend handle -ownership, synchronization, borrowed pointer lifetimes, frame generation or -stale-frame behavior, and teardown rules. Frame generations are session-scoped -counters in frame structs and are unrelated to the generation inside a handle -id. Attach entry points also document that the calling thread becomes the -session's owner thread, what the calling thread's graphics context must provide, -and which context ownership modes the target accepts. +Render-session APIs document driver support, backend handle ownership, +synchronization, borrowed pointer lifetimes, generation fields, backpressure, +and teardown rules. Frame generations are session-scoped counters and are +unrelated to the generation inside a handle id. Attach entry points document +which descriptors are transferable, which graphics calls are thread-affine, and +which context ownership modes the target accepts. + +Every accepted frame demand creates one owned terminal result record. Frame +readiness stays level-triggered until the queue drains. Each drain transfers the +complete queue into an independently owned result batch. Host-acquirable owned +textures negotiate a one-to-three-slot ring. An acquired-frame handle leases its +slot until release transfers any consumer GPU-completion synchronization to the +session. A private OpenGL owned texture target fixes its depth at one and grants +readback without acquisition or consumer synchronization. + +Normal detach routes graphics destruction through the selected driver before +CPU-only handle destruction. Abandon closes control and mailboxes without +graphics calls, returns busy during an in-flight driver call, detaches the map +in CPU state, invalidates accessors, and quarantines resources that require the +lost owner. Releasing an acquired frame after abandon remains CPU-only and +reports target lost. ## Callback Adapter diff --git a/docs/src/content/docs/development/map-example-specification.md b/docs/src/content/docs/development/map-example-specification.md index 2ad362ed5..2ab790c76 100644 --- a/docs/src/content/docs/development/map-example-specification.md +++ b/docs/src/content/docs/development/map-example-specification.md @@ -79,8 +79,8 @@ contract is unchanged. - All map, runtime, and render access from application code through the project’s language binding for that language. -- Continuous map mode: runtime pumping, event draining, and repaint driven by - map render events and user input. +- Continuous map mode: full event and frame-result drains, independent + caller-driver service, and repaint driven by map render events and user input. - Initial style URL and camera per [Shared defaults](#shared-defaults). - Camera controls per the active profile ([Desktop profile → Input](#input) or [Mobile profile → Input](#input-1)). @@ -168,116 +168,80 @@ flowchart TB Implementations SHOULD mirror this layout in the source tree (separate files or packages per module). -#### Threads and loops - -Examples run two loops on two native threads: a **render loop** and a **runtime -loop**. - -- The render loop thread owns the host window and its input events, the - viewport, the graphics API context and presentation resources, the compositor, - and the render session for the session's whole lifetime. It attaches the - session, renders through it, resizes it, and closes it. -- The runtime loop thread owns the runtime, the map, `pump`, the event drain, - and every map mutation. It never calls a render-session function. -- Each loop MUST run on a native thread whose identity is stable for the life of - the loop. Host mechanisms that may move a logical task between native threads, - such as thread pools, green threads, and coroutine dispatchers without thread - confinement, are not usable for either loop. -- The render loop thread MUST be the thread on which the host toolkit delivers - window, input, and display-refresh callbacks. -- Cross-thread state MUST be limited to three channels: a camera-command queue - from the render loop to the runtime loop, a render request from the runtime - loop to the render loop, and a one-time publication from the runtime loop to - the render loop carrying the map to attach against and the wake source that - releases the runtime loop's parked pump. A shutdown signal and a first-failure - record MAY accompany them. -- The camera-command queue MUST NOT discard a queued command, and MUST NOT block - the render loop to avoid discarding one; it grows instead. Nothing on it is - safe to drop: a dropped delta is motion the drag never gets back, a dropped - gesture bracket leaves the commands after it attributed to no gesture, and a - dropped absolute command or transition cancellation is a camera state the map - never reaches. Only a stalled runtime loop grows the queue, and a runtime loop - stalled long enough to matter has already stopped the map. - -This split exists because `pump` drains the work it finds rather than a fixed -slice, so a single call can take as long as a style parse. Keeping it off the -display-paced loop is what lets presentation continue during heavy runtime work. +#### Threads and drivers + +Examples have one host render loop and one native scheduler thread owned by the +runtime. The render target chooses one of the driver contracts described in +[Concepts](/maplibre-native-ffi/concepts/). + +- The runtime owns its scheduler thread. Runtime creation starts it, runtime + close joins it, and no host code pumps it. +- A core-worker session owns a native serial graphics worker. +- A caller-driver session stores typed native work until the render loop + services it with the graphics context usable. +- Runtime, map, and render-session control calls may run on any host thread. +- Completion and wake callbacks may run on any native thread. A wake callback + MUST only schedule later receiver work and return promptly. +- When the binding exposes wakes, event wakes schedule a full runtime-event + drain, frame-result wakes schedule a full result drain, and driver-work wakes + schedule service on the graphics thread. A binding that exposes only polling + uses an existing host cadence for the same work. +- Driver service and thread-current accessors MUST run serially on the host + graphics thread. + +Desktop examples use caller drivers because their host WGL, shared EGL, Metal, +or Vulkan presentation context belongs to the render loop. Browser examples use +a caller driver for existing WebGL and WebGPU objects. A transferred +`OffscreenCanvas` WebGL target MAY use a core worker, which creates and uses its +WebGL2 context on that worker. ##### Render loop thread by host toolkit -Where the host toolkit fixes which thread receives display-refresh and window -callbacks, that thread is the render loop thread. Where a graphics API context -is thread-current, such as OpenGL through EGL or WGL, the render loop thread is -the only thread that makes it current. - -| Example | Render loop thread | Runtime loop thread | -| ------------- | -------------------------------------------------- | ------------------- | -| `c-map` | process main thread (SDL window, graphics context) | spawned thread | -| `zig-map` | process main thread (SDL window, graphics context) | spawned thread | -| `go-map` | process main thread (SDL window, graphics context) | pinned goroutine | -| `rust-map` | winit event-loop thread | spawned thread | -| `lwjgl-map` | GLFW main thread | spawned thread | -| `dotnet-map` | GLFW main thread | dedicated `Thread` | -| `swift-map` | main run loop (`CADisplayLink`, AppKit/UIKit) | dedicated `Thread` | -| `android-map` | UI thread (`Choreographer`) | `HandlerThread` | -| `compose-map` | native surface bridge's producer thread | spawned thread | +Where the host toolkit fixes display-refresh and window callbacks, that thread +is the render loop thread. Where a graphics API context is thread-current, the +render loop thread is the only thread that makes it current. + +| Example | Render loop thread | +| ------------- | -------------------------------------------------- | +| `c-map` | process main thread (SDL window, graphics context) | +| `zig-map` | process main thread (SDL window, graphics context) | +| `go-map` | process main thread (SDL window, graphics context) | +| `rust-map` | winit event-loop thread | +| `lwjgl-map` | GLFW main thread | +| `dotnet-map` | GLFW main thread | +| `swift-map` | main run loop (`CADisplayLink`, AppKit/UIKit) | +| `android-map` | UI thread (`Choreographer`) | +| `compose-map` | native surface bridge's producer thread | ##### Attaching the render session -A render session's owner thread is the thread that attached it, and it does not -change for the session's lifetime. The render loop thread MUST therefore be the -thread that attaches the session, and the same thread MUST close it. - -Attach requires only that the map be live, not that the caller own it, so the -render loop attaches against a map owned by the runtime loop. Attach follows -this operation: - -1. The runtime loop creates the runtime and the map, then publishes the map to - the render loop once. The publication provides the happens-before edge. -2. The render loop creates its graphics context and its mode-specific resources, - then attaches, becoming the session's owner thread. -3. The render loop closes the session before the runtime loop destroys the map. - Destroying a map with an attached session fails, so the render loop MUST - signal that its session is closed and the runtime loop MUST wait for that - signal before destroying the map. - -Attach creates the session's graphics resources on the calling thread, so for -graphics APIs whose context is thread-current the host context MUST be current -on the render loop thread when it attaches. That thread is the only one that -makes it current, and it need never give it up. - -The requirement is specific to WGL: the session resolves -`wglCreateContextAttribsARB` through the calling thread's current context, and -without one it falls back to a legacy context that cannot share with a context -current on another thread. Attaching where the host context is already current -avoids both failure modes. Vulkan and Metal have no thread-current context and -are unaffected. - -Reattaching, which a graphics context change requires, is entirely local to the -render loop thread: close the session, rebuild the mode-specific resources, -attach again. - -##### Thread identity on Apple platforms and managed runtimes - -Owner-thread checks are keyed on the native thread, so a host mechanism that -serializes work without pinning it to one native thread is not usable for either -loop. This rules out more than thread pools: - -- A GCD serial `DispatchQueue` guarantees serialization but not thread affinity, - so Swift examples MUST use a dedicated `Thread` for the runtime loop rather - than a queue, an `actor`, or a `Task`. -- .NET examples MUST use a dedicated `System.Threading.Thread` rather than - `Task.Run` or the thread pool. -- Kotlin examples MUST use a `Thread` or `HandlerThread` rather than a coroutine - dispatcher. +Startup awaits map creation, then starts target attachment with the selected +driver. Attach returns an attaching session and a completion future. A +caller-driver example MUST service work before awaiting that future, either when +the driver wake fires or from an independent polling cadence; otherwise +initialization deadlocks. + +The C attach options carry independent frame-result and driver-work wakes, and +the runtime options carry an event wake. A binding may expose those wakes or +explicit polling. Each exposed wake targets the receiver that owns the +corresponding drain or service call. + +For a caller driver, attach descriptors are produced where the graphics context +is usable. Host-shared WGL and EGL contexts use the caller driver. A transferred +`OffscreenCanvas` descriptor instead names its canvas selector and creates its +WebGL2 context on a core worker. + +Reattachment first completes normal detach through the selected driver and +destroys the CPU-only session handle. The example then rebuilds mode resources +and starts a new attachment. If the graphics owner cannot service detach, the +example MUST abandon the session before destroying it. #### Graphics API and mode matrix -The example architecture MUST model the active graphics API separately from the -active render-target mode. Graphics context code owns API-level resources -(Vulkan, Metal, OpenGL/EGL/WGL as applicable). Render target code owns the -attached `RenderSessionHandle`, mode-specific resources, resize behavior, -`render_update`, and close behavior. +The example architecture MUST model the active graphics API, render-target mode, +and driver separately. Graphics context code owns API-level resources. Render +target code owns the session, attachment and detach futures, frame demand and +result drains, driver service, mode resources, resize, and presentation. The loaded native library reports one render backend per library artifact through `mln_supported_render_backend_mask()`. Examples built across native @@ -309,206 +273,110 @@ rather than branching ad hoc through shared draw code. Order MUST be: -1. Parse profile entry configuration and validate the selected render mode. -2. Read and log the loaded library's supported native render backends from - `mln_supported_render_backend_mask()`, then validate that the loaded native - library supports the graphics API selected for this run; fail fast with a - readable message if not. -3. Create the host presentation surface and initialize the graphics backend for - the selected graphics API, on the render loop thread. -4. Start the runtime loop thread. -5. Create runtime (`:memory:` cache) on the runtime loop thread. -6. Create map with extent from the initial viewport and continuous mode. -7. Load style and apply initial camera. -8. Publish the map to the render loop thread. -9. Attach the render target for the selected mode on the render loop thread, - using descriptors produced by the graphics context there. -10. Emit startup information: - - active render-target mode identifier - - active render-target status line - -Steps 5 through 8 run on the runtime loop thread; steps 3 and 9 run on the -render loop thread. A host that cannot create its graphics context before the -window MUST still keep step 3 on the render loop thread. - -On failure after partial setup, release already-created handles in reverse order -(render target → map → runtime → graphics), with the render target closed on the -render loop thread before the map is destroyed. +1. Parse profile entry configuration and validate the selected mode and driver. +2. Validate the loaded library's backend and target capabilities. +3. Create the host presentation surface and graphics resources. +4. Create the runtime and arrange an event drain through its wake or a host + polling cadence. +5. Start and await map creation with the initial extent. +6. Select the event types that the example reads. +7. Submit the style and initial camera commands. +8. Start render-target attachment and arrange frame-result drains and driver + service through wakes or an independent host polling cadence. +9. For a caller driver, service work on the graphics thread until the attach + future resolves. +10. Read negotiated capabilities. +11. Print the active mode and driver. + +Failure cleanup follows the same detach or abandon path as normal shutdown. #### Shutdown -On host termination or fatal error, close resources in order: +On host termination or fatal error: -1. Leave the render loop and finish or wait on in-flight GPU work if the backend - requires it, on the render loop thread. -2. Render target (compositor and borrowed texture/image before or with the - session, according to graphics API lifetime rules), on the render loop - thread. -3. Signal the runtime loop that the session is closed, and stop it. -4. Map -5. Runtime, after which the runtime loop thread exits and the render loop thread - joins it. -6. Graphics context and host presentation surface, on the render loop thread. +1. Stop new frame demand. +2. Release acquired frames with their consumer-completion synchronization. +3. Start normal detach. Continue caller-driver service until it completes. +4. If graphics service is permanently unavailable, abandon instead and report + any quarantined resources. +5. Destroy the detached or abandoned session from any thread. +6. Release compositor and target resources. +7. Release the map, then the runtime. +8. Release graphics resources. -Steps 4 and 5 run on the runtime loop thread, which MUST wait for the step 3 -signal before step 4: destroying a map that still has an attached render session -fails. +A map release preflight rejects an attaching or attached session without +changing map state. #### Handle ownership -- One runtime per process (the runtime loop thread drives `pump`). -- One map per runtime for the demo, sharing the runtime's owner thread. -- One live render target per map at a time. -- One render session owner thread, fixed for the session's lifetime: the thread - that attached it, which is the render loop thread. -- Map configuration (style, camera) uses the map handle; render-target extent - and present use the render target. +- One runtime per process, with one native scheduler thread. +- One map per runtime for the demo. +- One attaching or attached session per map. +- Frame-result batches own their records independently of the session. +- Every acquired-frame handle leases one texture-ring slot until its synchronous + release returns. +- Graphics handles stay valid through normal detach. Abandon quarantines + resources that cannot be destroyed without graphics access. ### Frame loop -The C API treats runtime pumping and presentation as separate concerns. `pump` -advances native scheduler work and fills the event queue; it is not -display-driven. One call drains the work it finds instead of running a fixed -slice, so a single call can take as long as a style parse. `render_update` draws -only when the render request is set. - -`*-map` examples split the two across the loops described in -[Threads and loops](#threads-and-loops): the render loop is display-paced and -draws, and the runtime loop pumps. The runtime loop owns its own thread, so it -passes a positive `timeout_ms` and takes its cadence from the runtime's own work -rather than from the display. `pump` parks the thread for up to that bound, -which is what lets the runtime loop idle while the map is quiet instead of -spinning. +The host display source submits frame demand. Map updates wake the selected +driver directly; runtime events are observations and are not a render-progress +mechanism. #### Render loop iteration -1. Handle window, input, and resize events; translate camera input into commands - and enqueue them for the runtime loop; signal the runtime loop's wake source; - set the render request. -2. Apply pending viewport changes to graphics resources and to the render - target, which follows them without losing its session. -3. Consume the render request; when it was set, call `render_update`. -4. Run `finishFrame()`. - -```mermaid -sequenceDiagram - participant RL as Render loop - participant RQ as Render request - participant RS as Render session - participant BE as Backend - - RL->>RL: Input and resize - RL->>RQ: enqueue camera commands, signal wake source, set request - RL->>RQ: consume request - RL->>RS: render_update() when it was set - RL->>BE: finishFrame() -``` - -`finishFrame()` runs every iteration: swapchain or surface upkeep, resize -handling, and present hooks as required by the host graphics API. - -A render loop iteration MUST NOT call `pump` or drain events. - -#### Runtime loop iteration - -1. Apply every queued camera command. -2. Call `pump` exactly once, with a positive `timeout_ms`. -3. Drain the runtime's events once, updating the render request from the batch. - -```mermaid -sequenceDiagram - participant RTL as Runtime loop - participant CQ as Command queue - participant RT as Runtime - participant RQ as Render request - - RTL->>CQ: apply queued camera commands - RTL->>RT: pump(timeout_ms) - RTL->>RT: drain events - RTL->>RQ: set request when a frame is needed -``` - -#### Cadence - -While the map is visible and the example is active: - -- The render loop MUST run at least one iteration per display refresh period, - and MUST subscribe to the host toolkit's display refresh mechanism (for - example swapchain frame callbacks, `CADisplayLink`, or `Choreographer`) to - pace it. -- The runtime loop MUST wake immediately when the render loop enqueues a camera - command or a viewport change. It acquires a wake source on its own thread, - publishes it to the render loop, and the render loop signals it. The parking - bound it passes to `pump` is a backstop rather than the cadence, so it MAY be - longer than one display refresh period. - -Display refresh paces the render loop; it does not pump. Each runtime loop -iteration MUST call `pump` exactly once, and no other loop calls it. A -single-loop host instead passes zero so the refresh mechanism remains its only -cadence; a two-loop example passes a positive bound because its pump thread has -no display to pace it. - -When the profile stops the loops (for example mobile background), runtime -progress stalls until they resume. - -#### Render requests - -The render request replaces a loop-local `render_pending` flag, because the two -loops set and consume it from different threads. +1. Handle window, input, and resize events. Submit any-thread map and session + work directly. +2. Service caller-driver work on the graphics thread, when its wake fires or + from an independent polling cadence, including while presentation is paused. +3. Drain the complete frame-result queue after its wake or on the host cadence. +4. Drain the complete runtime-event queue after its wake or on the host cadence. +5. For a rendered owned-texture result, acquire one frame, wait for producer + synchronization, and submit the compositor pass. +6. Present and release an acquired frame with consumer-completion + synchronization. ```mermaid sequenceDiagram + participant W as Wake or host cadence participant RL as Render loop participant RS as Render session participant CP as Compositor - participant BE as Backend - - RL->>RS: render_update() - alt texture mode - RS-->>RL: map texture / frame - RL->>CP: draw into swapchain - CP->>BE: present - else native-surface - RS->>BE: present via surface session - end + + RL->>RS: request frame(token, timestamp) + W-->>RL: schedule driver service + RL->>RS: service driver work + W-->>RL: schedule frame-result drain + RL->>RS: drain frame results + RS-->>RL: disposition and generations + RL->>RS: acquire frame + RL->>CP: compose after producer sync + RL->>RS: release frame with consumer sync ``` -Requirements: - -- The render request MUST be published and observed through the host language's - atomic or synchronized mechanism. An unsynchronized field is not sufficient. -- The runtime loop MUST call `pump` once per iteration while it is running. -- The runtime loop MUST drain runtime events once per iteration and set the - render request when: - - `map_render_update_available` targets this map (new map content to draw), or - - `map_render_frame_finished` targets this map and `needs_repaint` is true - (continuous mode needs another frame, for example ongoing camera - transitions). -- The render loop MUST set the render request when input changes the camera and - when a resize or reattach completes. -- The render loop MUST call `render_update` only when it consumed a set request. -- The render loop MUST consume the render request **before** calling - `render_update`, and MUST set it again when `render_update` reports any result - other than a rendered frame. Consuming afterwards would discard a request the - runtime loop published during the render call, and that frame would never be - drawn. -- When `render_update` reports a rendered frame, the render loop MAY set the - render request again from the call's returned repaint flag instead of waiting - for the frame's `map_render_frame_finished` event. -- `map_render_frame_finished` is delivered by the runtime loop's next `pump` - after the render loop rendered. An example MUST NOT treat it as a synchronous - result of the `render_update` it follows, and MUST NOT block a render loop - iteration waiting for it. -- After a session resize, the map applies its logical size on the runtime loop's - next `pump`, so `render_update` reports a pending size until then. The render - loop MUST keep pacing and retry rather than treating it as a failure. -- A compositor that cannot present the frame it was handed MUST report that as - no frame rendered, and the render loop MUST set the render request again. A - minimized or occluded window produces this, and so does a swapchain awaiting - its rebuild. The map retains the update, so the retry draws it. - -Texture modes: after `render_update` reports a rendered frame, MUST run the -compositor pass to copy the map texture into the host swapchain before present. +#### Cadence and results + +- The render loop MUST submit demand from the host display or invalidation + source while visible. +- Each demand MUST carry a unique host token. +- A positive timeout starts when native code accepts the demand. Deadline missed + is terminal and does not enter an immediate retry loop. +- Rendered, no update, size pending, target not ready, superseded, and deadline + missed MUST remain distinct outcomes. +- No update and size pending wait for a newer map update. Target not ready waits + for target readiness or a paced retry. +- Frame-result wake state is level-triggered. Each drain transfers every queued + result into an independently owned batch. +- The frame result's map-update, extent, and frame generations determine what + was rendered. Runtime event order MUST NOT be used as a substitute. +- The example MAY keep presenting the previous completed texture when a newer + frame exceeds its timeout. +- Resize, target replacement, queries, readback, barriers, maintenance, and + detach progress through the same selected driver. + +The example MUST continue servicing a caller-driver mailbox while display +callbacks are paused. If that becomes impossible, it abandons the session. ### Viewport @@ -539,29 +407,34 @@ map-specific setup. #### Creation -- Create runtime with `:memory:` cache. -- Create map with current viewport extent and continuous mode. -- The example MUST select every event type it reads before it loads the style. A - map queues an event only while its subscription selects that type. -- Load [style URL](#style). -- Apply [initial camera](#initial-camera). +- Create the runtime with a `:memory:` cache. +- Create and await the map future with the current viewport extent and + continuous mode. +- Select every event type the example reads before it loads the style. A map + queues an event only while its subscription selects that type. +- Submit the [style URL](#style) command. +- Submit the [initial camera](#initial-camera) command. - Attach a render target by dispatching on active graphics API and selected mode. -#### Event drain +#### Wake and polling handling -- Drain the runtime's events once per runtime loop iteration, and read every - event the batch reports. -- Set the render request when either: - - `map_render_update_available` targets this map, or - - `map_render_frame_finished` targets this map and `needs_repaint` is true. +- Install direct event, frame-result, and driver-work wakes before the + corresponding producer can enqueue work when the binding exposes them. +- Each wake schedules receiver work and returns without draining or calling + application code. A polling-only binding uses existing application and render + cadences instead of adding a worker. +- Drain every queued runtime event or frame result in one call on its receiver. +- Service caller-driver work on the graphics thread after its wake or from a + cadence that remains active while presentation is paused. +- Await one-shot futures directly; their completion does not depend on a drain. #### Resize API -Expose `resize(viewport)` for the active render target, and resize API-level -resources separately when the graphics context requires it. The render target -decides for itself how to follow the new viewport, so a host resizes every mode -through the same call. +Expose `resize(viewport)` for the active render target, resize API-level +resources when the graphics context requires it, and submit one map resize +command with the same logical extent. Map resize is the sole authority for +logical width, height, and scale factor after creation. ### Render-target modes @@ -591,33 +464,30 @@ table: #### `owned-texture` -- Attach with the C API owned-texture descriptor for the active graphics API. -- Pass the host graphics context handles required by that descriptor (see - [Graphics API](#graphics-api)). -- On `render_update`, acquire the frame/image from the session, draw via - compositor, release/close the frame per the C API frame lifetime rules. +- Request a ring depth of two or three for interactive composition. +- After a rendered result, acquire the oldest completed unacquired frame. +- Wait for producer completion before sampling. +- Release the handle with consumer-completion synchronization after submitting + compositor GPU work. +- Keep presenting the previous completed frame when acquisition reports not + ready. #### `borrowed-texture` -- Host creates an exportable texture sized to the viewport (see - [Graphics API](#graphics-api)). +- Create an exportable texture sized to the viewport. - Attach with the borrowed-texture descriptor referencing host-owned handles. -- On `render_update`, sample that texture through the same compositor path as - `owned-texture`. -- On resize, allocate a host texture at the new size and hand it to the live - session with `set_target`, which the render target does inside its own - `resize` (see [Resize mechanics](#resize-mechanics)). +- After a rendered result, sample that texture through the compositor path. +- On resize, allocate a replacement and start the backend target-replacement + future. Retain both allocations until its outcome is known. #### `native-surface` -- Attach with the C API surface descriptor for host presentation (see - [Graphics API](#graphics-api)). -- `render_update` presents through the surface render target directly. -- `drawTexture` MUST NOT be called for this mode. -- On resize, call session `resize` and rebuild host presentation. When the host - toolkit supplies a new surface handle for the same graphics context, call - `set_target` with it; reattach only when the context itself changed or was - lost. +- Attach with the surface descriptor for host presentation. +- Set the present flag on frame demand. +- A rendered result means that the selected driver presented the frame. +- On resize, start the session resize future and rebuild host presentation. + Start target replacement when the toolkit supplies a new surface for the same + graphics context. ### Compositor shaders @@ -635,60 +505,36 @@ that pass. ### Resize mechanics -- Recompute viewport on host size or scale changes; skip rendering if extent is - empty. -- `resize(viewport)` is a render-target method, and each mode follows the new - viewport its own way. `owned-texture` and `native-surface` resize - graphics-context resources, compositor resources for texture modes, and - session extent in place. `borrowed-texture` cannot: the host-owned exportable - texture is fixed to the viewport size, so it allocates one at the new size and - hands it to the live session with `set_target`. A host calls `resize` for - every mode and branches on none of them. -- The session stays live either way, and so does its renderer, so the map keeps - its tiles and atlases across a resize. A scale factor change is the exception - the C API documents, rebuilding the renderer for the new pixel ratio. -- Pick an ownership strategy for the handover and follow the C API's rules for - it. An example that keeps the outgoing target until the call returns can roll - back: a rejected replacement leaves the session on the target it had, so it - releases the replacement and keeps rendering. An example whose graphics layer - frees the outgoing target first cannot roll back, and treats a rejected - handover as a session to close and attach again. Caller-owned textures allow - either, because no backend reads the outgoing texture. -- A Vulkan surface is the exception, and requires the retaining strategy: its - outgoing `VkSurfaceKHR` must still be valid when `set_target` is called, - because the session destroys the swapchain built from it first. -- `MLN_STATUS_NATIVE_ERROR` may mean the replacement was already under way, and - a caller cannot tell it apart from a failure that came earlier. The session - may therefore hold either target. Detach or close it before releasing either - one, and attach again rather than reusing it; stopping before the next frame - is not enough on its own, because the release still happens while the session - holds the target. Every other status is reported before the target changes, - which is what makes rolling back to the outgoing target safe. -- Build the replacement to match what the session attached with, which the C API - states per backend and per function. `set_target` reports - `MLN_STATUS_UNSUPPORTED` for a target that differs, leaving the session on the - one it has. -- Reserve [reattach](#reattach) for a target the live session cannot take: a new - graphics context or device, a target that `set_target` reports as unsupported, - or a context that was lost. -- Set the render request after any resize. -- The render loop owns the session, so an in-place resize is a local call. The - map applies the new logical size on the runtime loop's next `pump`, so - `render_update` reports a pending size until then. +- Recompute the viewport on host size or scale changes. +- Start one absolute session resize future with the new logical extent. Resize + assigns a new extent generation and updates the map viewport through the + selected driver. +- Resize API-level and compositor resources for owned textures and surfaces. +- For a borrowed texture, allocate a matching host texture and start target + replacement instead of resizing the fixed allocation. +- Retain outgoing and replacement borrowed resources until replacement + completes. Release the replacement when the future fails. After an ambiguous + native failure, detach or abandon before releasing either target. +- A Vulkan surface's outgoing `VkSurfaceKHR` MUST remain valid until replacement + completes because graphics teardown is ordered through the driver. +- A frame demand captures the current extent generation. Size pending waits for + a matching map update rather than causing an immediate retry loop. +- Continuous resize MUST keep submitting paced demand and MUST NOT block the + platform resize callback. #### Reattach -Reattaching is for a target the live session cannot take, not for a resize. It -happens entirely on the render loop thread, which owns both the session and the -graphics resources. The sequence MUST be: +Reattach only for a new graphics context or device, an unsupported replacement, +or target loss: -1. Close the session, then destroy and recreate the host texture or surface. -2. Attach the render target again against the published map. -3. Set the render request. +1. Stop frame demand and release acquired frames. +2. Start detach and service the selected driver until it completes. +3. Destroy the detached session. +4. Destroy and recreate host graphics resources. +5. Start attachment and service a caller driver until it completes. -Closing the session first matters: the render loop owns both the session and the -graphics resources it borrows, and the session must stop referencing a texture -before that texture is destroyed. +If the graphics owner is permanently unavailable, replace steps 2 and 3 with +abandon, quarantine reporting, and CPU-only destruction. Profile sections define which host events trigger resize ([Desktop profile → Resize triggers](#resize-triggers) or @@ -831,20 +677,20 @@ Controls: #### Behavioral constants -| Interaction | Behavior | -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Left drag | `move_by` with pointer delta in logical coordinates. | -| Right drag, or Ctrl+left drag | Adjust bearing by `0.5 × Δx` degrees; adjust pitch by `0.5 × Δy` degrees (same sign convention everywhere). | -| Scroll | Zoom about cursor: `scale_by(2^(Δ * 0.25), anchor)`. Δ from the toolkit wheel event; scrolling up zooms in (use OS-adjusted deltas as reported—do not undo platform scroll inversion). | -| Arrow keys / WASD | Pan `120` logical units per key press. | -| `+` / `-` | Zoom `1.25` / `1/1.25` about viewport center. | -| `Q` / `E` | Bearing ±`10`° with keyboard animation. | -| `]` | Pitch +`5`° (clamped to `[0, 60]`) with animation. | -| `[` | Pitch −`5`° (clamped to `[0, 60]`) with animation. | -| `0` | Animate bearing and pitch to `0` with keyboard animation. | +| Interaction | Behavior | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Left drag | Apply a relative move with the pointer delta in logical coordinates. | +| Right drag, or Ctrl+left drag | Adjust bearing by `0.5 × Δx` degrees; adjust pitch by `0.5 × Δy` degrees (same sign convention everywhere). | +| Scroll | Apply a scale of `2^(Δ * 0.25)` about the cursor. Δ comes from the toolkit wheel event; scrolling up zooms in (use OS-adjusted deltas as reported—do not undo platform inversion). | +| Arrow keys / WASD | Pan `120` logical units per key press. | +| `+` / `-` | Zoom `1.25` / `1/1.25` about viewport center. | +| `Q` / `E` | Bearing ±`10`° with keyboard animation. | +| `]` | Pitch +`5`° (clamped to `[0, 60]`) with animation. | +| `[` | Pitch −`5`° (clamped to `[0, 60]`) with animation. | +| `0` | Animate bearing and pitch to `0` with keyboard animation. | Keyboard animated moves SHOULD use ~`160` ms duration. Pointer drags use -immediate `move_by` / `jump_to` / `pitch_by`. +immediate relative move, absolute jump, and relative pitch operations. On pointer down that starts a drag, cancel in-flight camera transitions before applying deltas, and set the map's gesture-in-progress state. Clear that state @@ -852,8 +698,8 @@ when the drag ends, and hold it for the whole drag when a second button goes down and up during one. Keyboard interactions are discrete commands and leave the state clear. -Input handlers return whether the camera changed so the render loop can set the -render request. +Input handlers return whether the camera changed so the next display callback +submits frame demand. ### Resize triggers @@ -880,27 +726,26 @@ termination. Track view visibility and app foreground separately. Run the display-paced render loop only while the view is visible and the app is in the foreground. The -runtime loop keeps running across these transitions, so loading continues while -the view is off screen. - -When the host toolkit supplies a fresh presentation surface on the graphics -context the session attached with, hand it over with `set_target` on the render -loop thread, which is the thread that attached the session. The session keeps -its renderer, so the map returns warm rather than rebuilding its tiles and -atlases. - -Close the render target only when the graphics context itself is gone, on that -same render loop thread. Keep runtime and map handles alive either way, and -attach again on that thread once a context and surface exist, per -[Reattach](#reattach). - -| Transition | Behavior | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| View will appear | Mark the view visible. If the app is in the foreground, start the render loop, refresh viewport, hand a parked session its surface or attach the render target, and set the render request. | -| View did disappear | Mark the view not visible. Stop the render loop. Park the session when the presentation surface goes away and the graphics context survives; close the render target only when that context is gone. | -| App foreground | Mark the app foreground. If the view is visible, start the render loop, refresh viewport, hand a parked session its surface or attach the render target, and set the render request. | -| App background | Mark the app background. Stop the render loop. Park the session when the presentation surface goes away and the graphics context survives; close the render target only when that context is gone. | -| View destroyed / app termination | Run [Shared shutdown](#shutdown). | +runtime's native scheduler keeps running across these transitions, so loading +continues while the view is off screen. + +When the host toolkit supplies a fresh presentation surface for the same +graphics context, start target replacement and keep the outgoing surface alive +until the replacement future completes. The session keeps its renderer, so the +map returns warm. + +When the graphics context is gone, stop demand and detach through the caller +driver before destroying it. Abandon instead when the graphics thread can no +longer service detach. Keep runtime and map handles alive, and attach again once +a context and surface exist. + +| Transition | Behavior | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| View will appear | Mark the view visible. In the foreground, resume display-paced demand, refresh the viewport, and replace or attach the surface. | +| View did disappear | Mark the view hidden. Pause demand but continue servicing caller-driver work. Replace the surface when it disappears. | +| App foreground | Mark the app foreground. If visible, resume display-paced demand and refresh the viewport. | +| App background | Mark the app background. Pause demand but continue servicing caller-driver work. | +| View destroyed / app termination | Run [Shared shutdown](#shutdown). | ### Entry and shell @@ -929,7 +774,7 @@ Implementations MUST provide the following touch interactions: | Interaction | Behavior | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| One-finger drag | `move_by` with pointer delta in logical coordinates. | +| One-finger drag | Apply a relative move with the pointer delta in logical coordinates. | | Pinch | Apply incremental scale deltas while preserving the geographic coordinate under the current two-touch centroid, resetting the scale baseline after each applied delta. | | Two-finger rotate | Apply incremental bearing deltas from the change in the two-touch vector angle while preserving the geographic coordinate under the current two-touch centroid. | | Two-finger vertical drag (shove) | `pitch -= 0.1 × Δy` degrees (clamp to `[0, 60]`), where `Δy` is the change in two-touch centroid Y in logical coordinates since the last applied update. | @@ -942,8 +787,8 @@ concurrently share one state, so a gesture ending while another is still live leaves it set, and the last one to end clears it. Double-tap is a discrete animated command and leaves the state clear. -Input handlers return whether the camera changed so the render loop can set the -render request. +Input handlers return whether the camera changed so the next display callback +submits frame demand. ### Resize triggers diff --git a/docs/src/content/docs/guides/add-geojson-data.mdx b/docs/src/content/docs/guides/add-geojson-data.mdx index 04c9d5713..a56db72db 100644 --- a/docs/src/content/docs/guides/add-geojson-data.mdx +++ b/docs/src/content/docs/guides/add-geojson-data.mdx @@ -63,6 +63,7 @@ report an invalid-argument status when the data is prepared. -A URL source parses and tiles on worker threads after the fetch. [Update +A URL source parses and tiles on worker threads after the fetch. A host that +renders on update draws the new features after tiling finishes. [Update data](/maplibre-native-ffi/guides/update-data/) covers replacing inline data as it changes. diff --git a/docs/src/content/docs/guides/attach-a-render-target.mdx b/docs/src/content/docs/guides/attach-a-render-target.mdx index 484c686a5..bea2c8e94 100644 --- a/docs/src/content/docs/guides/attach-a-render-target.mdx +++ b/docs/src/content/docs/guides/attach-a-render-target.mdx @@ -1,6 +1,6 @@ --- title: Attach a render target -description: Choose a render target, attach it on its owner thread, and replace it when the host allocation changes. +description: Choose a render target and execution placement, then replace or release it safely. sidebar: order: 3 --- @@ -11,72 +11,70 @@ import { region } from "../../../snippets"; import renderTarget from "../../../../snippets/c/render-target.c?raw"; A render session draws one map into one render target. A map has at most one -live render session. Its style, sources, layers, and camera exist independently -of that session and remain after the session is released. +live render session. Its style, sources, layers, and camera remain after the +session detaches. -The thread that attaches a session becomes its owner thread for the session's -lifetime. Attach on the thread that will draw the frames. Another thread can own -the runtime and map. +## Choose a target and driver -## Choose the kind of target +A native surface draws and presents each frame to a window or view. A +session-owned texture draws into a ring that the session allocates. A borrowed +texture draws into a host allocation. [Render a static +image](/maplibre-native-ffi/guides/render-a-static-image/) covers texture +readback. -A native surface draws and presents each frame to a window or view. The host -creates the window, graphics context, and surface. Use this target for an -on-screen map. +Execution placement is a separate choice. A core-worker driver owns a native +serial graphics worker and requires transferable graphics state. A +caller-graphics-thread driver stores work until the host services it where the +graphics context is usable. WGL targets, EGL surfaces, shared EGL textures, +existing WebGL contexts, and browser WebGPU require the caller driver. +Transferable Metal and Vulkan targets, private EGL owned texture targets, and +`OffscreenCanvas` WebGL targets can use a core worker. -A session-owned texture draws offscreen into a texture that the session -allocates. Use it for image generation or when compositing into a scene without -a host-owned destination texture. The host can acquire the last frame for GPU -sampling or copy it into host memory. [Render a static -image](/maplibre-native-ffi/guides/render-a-static-image/) covers readback and -static map mode. +The common attach options select the driver and request an owned-texture ring +depth from one to three. They also carry direct wake callbacks for frame-result +readiness and caller-driver work. A host may omit either wake and poll instead. -A borrowed texture draws offscreen into a host-owned texture, such as a material -in a 3D scene, a compositor frame, or an XR swapchain image. The host controls -the format and lifetime. Use this target when the destination already exists. +Each backend attach function copies its descriptor and options, returns a render +session in the attaching state, and accepts a one-shot completion. A caller +driver must service ready driver work before attachment can complete. Await the +completion before using the attached session for ordinary work. -## Attach to a window +## Attach to a target -Each render backend has its own descriptor type and attach function. This -snippet uses OpenGL through EGL. The descriptor contains the logical extent, -scale factor, graphics context, and presentation surface. +This snippet uses OpenGL through EGL and therefore selects the caller driver. +The descriptor contains the logical extent, graphics context, and presentation +surface. -An OpenGL surface target also chooses context ownership. The default shares the -thread with the host's own context. A dedicated session instead creates its own -context and keeps it current, for a thread that draws one map and nothing else. -[Concepts](/maplibre-native-ffi/concepts/) explains both modes. - -## Attach to a texture - -A session-owned target takes an extent and a context. The session allocates the -texture in the context's share group. The host can then sample it. +A session-owned texture descriptor contains an extent and context. Capability +negotiation reports the granted ring depth and whether acquisition, readback, +consumer synchronization, and presentation are available. -A borrowed target also contains the texture and its physical size in device -pixels. The physical size is separate from the logical extent. OpenGL ES cannot -verify the texture dimensions. The descriptor values must match the actual -texture to prevent clipped or corrupt output. +A borrowed texture descriptor also states its physical device-pixel size. The +values must match the host allocation, including on graphics APIs that cannot +query them. -## Resize, replace, and release +## Replace, detach, or abandon -Resize surface and session-owned texture targets in place on the session's owner -thread. This preserves the target and session. +Resize and target replacement are ordered asynchronous calls. They may start from any +thread. A caller driver applies them when the host next services ready work on +the graphics thread. Resize assigns a new extent generation; later frame demands +capture that generation. -A borrowed texture gets its size from the host allocation. Resizing reports an -unsupported status. Allocate a texture at the new size and replace the target. - -A host can recreate a surface while the map remains live. Android rotation, -Flutter surface lifecycle changes, and reallocating resizes use this path. -Replacing the target in place preserves the session's rendering resources, -including loaded tiles. Map-owned feature state is unchanged. Use the same -operation after allocating a replacement borrowed texture. +A borrowed texture gets its size from the host allocation, so resizing it +reports an unsupported status. Allocate a texture at the new size and replace +the target with a new descriptor. A host can replace a native surface the same +way after recreating a window or view allocation; Android rotation and Flutter +surface lifecycle changes use this path. Both paths preserve the session's +rendering resources, including loaded tiles, and leave map-owned feature state +unchanged. @@ -84,11 +82,18 @@ Changing the scale factor recreates the session's rendering resources because its shaders use a fixed pixel ratio. Map-owned feature state survives, and the next render update pushes it into the new renderer. -Release the session before the map. Destroying a map while a session is attached -reports an invalid-state status. - -When the session and map have different owner threads, send the release request -to the session's owner thread. Use a bounded wait. Teardown can then recover if -that thread has failed. +Normal detach completes asynchronously. The selected driver destroys graphics +resources on its owner and detaches the map. Continue servicing a caller driver +until the completion runs, then destroy the detached session handle from any +thread. + +Abandon is the failure path for an owner that can no longer service graphics +work. It closes control and mailboxes, invalidates graphics accessors, detaches +the map in CPU state, and performs no graphics calls. It returns busy while a +driver call is in flight. Resources that require graphics access enter a +process-lifetime quarantine and the result reports their count. Releasing an +acquired frame after abandon remains CPU-only, consumes the frame handle, and +does not touch quarantined graphics resources. Destroy the abandoned session +afterward. diff --git a/docs/src/content/docs/guides/create-a-map.mdx b/docs/src/content/docs/guides/create-a-map.mdx index b843911bc..4258a00f3 100644 --- a/docs/src/content/docs/guides/create-a-map.mdx +++ b/docs/src/content/docs/guides/create-a-map.mdx @@ -1,6 +1,6 @@ --- title: Create a map -description: Create a runtime and map with their fixed options, then release both in order. +description: Create a runtime and map, then close both in order. sidebar: order: 1 --- @@ -10,30 +10,33 @@ import { region } from "../../../snippets"; import createMap from "../../../../snippets/c/create-map.c?raw"; -A map belongs to a runtime. The thread that creates the runtime becomes the -owner thread for the runtime and every map under it. Create both on the thread -that will pump them. +A map belongs to a runtime. Runtime creation starts the native scheduler thread, +and any host thread can submit later runtime or map work. One runtime serves any +number of maps and holds their shared scheduler, event queue, network stack, and +cache database. -One runtime serves any number of maps and holds the shared work: the scheduler, -the event queue, the network stack, and one cache database. +Pass an event wake in the runtime options when the host wants a prompt signal +that runtime events are ready to drain. The callback only schedules work on the +host receiver; the receiver drains and copies the complete queued batch. -## Choose the options that outlive the call +Runtime creation returns its handle after the scheduler thread is ready. Map +creation is ordered runtime work. Its one-shot completion reports the map +handle, or a terminal error, exactly once. -Three map options are fixed at creation: the scale factor, the map mode, and -FastPFOR decoding. +## Choose the initial map state -The scale factor selects the sprites, glyphs, and raster tiles that the map -downloads. Set it to the display density that the map will use. A render target -has its own scale factor for geometry. Attaching a target with a different value -logs a warning and continues to load imagery at the map's density. +Map options include the initial logical extent, map mode, and FastPFOR decoding. +The extent contains width, height, and scale factor. It initializes camera and +projection state before a render target exists. - +After creation, resize is the only command that changes width, height, or scale +factor. The scale factor selects sprites, glyphs, raster tiles, and +pixel-ratio-dependent renderer state, so send the display density with every +extent change. -The width and height set the viewport until the first render target attaches. -That target's extent then replaces them. Before the first attach, camera and -projection queries use the initial width and height. + A continuous map redraws as data arrives and the camera moves. Use this mode for an on-screen map. A static map produces one image per request, and a tile map @@ -44,9 +47,15 @@ produces one image for a single tile. FastPFOR decoding applies to every MapLibre Tiles source that the map loads. Enable it at creation when your tile sets use that encoding. -## Release in order +## Close in order -Destroy a map before the runtime that owns it. A runtime with live maps reports -an invalid-state status. +Release the map before releasing the runtime. A runtime release preflight +reports invalid state and leaves the handle open while a live child or pending +child creation prevents release. Successful map and runtime release consumes +the public handle immediately; native teardown continues in submission order, +and the runtime release completion reports when all of it, including released +maps' teardown, has finished. Wait for that completion before exiting the +process. A standalone projection snapshot does not keep either source handle +open. diff --git a/docs/src/content/docs/guides/handle-events.mdx b/docs/src/content/docs/guides/handle-events.mdx index 4f79edb72..73975cde0 100644 --- a/docs/src/content/docs/guides/handle-events.mdx +++ b/docs/src/content/docs/guides/handle-events.mdx @@ -1,6 +1,6 @@ --- title: Handle events -description: Select the event types a host reads, drain them as a batch, and read borrowed values before they expire. +description: Select the event types a host reads, drain them into an owned batch, and release the batch after reading it. sidebar: order: 5 --- @@ -12,11 +12,11 @@ import handleEvents from "../../../../snippets/c/handle-events.c?raw"; Events report completed style loads, available frames, download progress, and resource failures. The runtime copies each event into a queue, and the host -drains that queue on the runtime owner thread. +drains that queue from any thread. A host decides which event types it reads. A map and a runtime each carry a subscription, and an event of an unselected type is never built, never queued, -and never wakes a parked pump. +and never invokes the event wake. ## Select the event types you read @@ -32,9 +32,9 @@ keeps that default. ## Drain a batch -One drain reports every queued event, in queue order, together with the message -text that those events carry. The batch belongs to the runtime, and the next -drain for that runtime replaces it. +One drain creates an owned batch that contains every queued event in queue order, +together with the message text that those events carry. The batch remains +readable across later drains and runtime close. @@ -42,12 +42,10 @@ In C, step through the events by the stride that the batch reports rather than b the size of the event struct. A later library version reports a wider event to a host compiled against an earlier header. -Copy any value you keep, because the next drain replaces the batch. A message is -the arena bytes at the event's own offset: a failed style load carries its -failure text there, and a missing-image event carries the image ID. - -Pass a maximum event count to take a bounded slice instead of the whole queue. -The batch then reports how many events stayed queued. +Copy any event or message value that must outlive the batch, then release the +batch. A message is the arena bytes at the event's own offset: a failed style +load carries its failure text there, and a missing-image event carries the image +ID. ## Match an event to its source @@ -74,14 +72,18 @@ Most hosts begin with four event kinds. A style-loaded event means that the style parsed and its sources exist. Add host-owned sources and layers after this event. -A render-update-available event means that the map published a new frame. A -render-frame-finished event carries a repaint flag. Draw another frame when the -flag is set. Additional frames advance camera animations and label placement. +A render-update-available event means that the map published new render state. +The session wakes its core worker or caller-driver mailbox directly, so this +event is observational rather than a requirement for frame progress. Submit +frame demand according to the host's cadence. Drain the session's frame-result +source for terminal demand outcomes and pending-changes state. A loading-failed event carries the failure text for a style or resource request. - -Calls report synchronous failures through the binding's error idiom. Events -report failures from work that completes later. - -Destroying a map discards that map's queued events immediately. Read any mirrored -state that teardown needs while the map is live. +Immediate calls report validation and inspection failures through the binding's +error idiom. Each accepted command or one-shot query completes exactly once +through its completion callback, including its final status and diagnostic. +Command completions also carry the terminal disposition and committed generation. + +Closing a map prevents future events from that source and leaves queued events +unchanged. A queued event keeps the copied source ID even when its map is no +longer live. diff --git a/docs/src/content/docs/guides/intercept-network-requests.mdx b/docs/src/content/docs/guides/intercept-network-requests.mdx index 74a3bc19e..a280773b4 100644 --- a/docs/src/content/docs/guides/intercept-network-requests.mdx +++ b/docs/src/content/docs/guides/intercept-network-requests.mdx @@ -16,13 +16,15 @@ import providerSnippet from "../../../../snippets/c/resource-provider.c?raw"; Three extension points intercept resource requests. A resource transform rewrites a URL before the network request. An HTTP header transform adds headers to the request that follows. A resource provider supplies the response bytes. -Register each one on the runtime owner thread. +Register each one with an any-thread runtime command. MapLibre can invoke any of these callbacks concurrently on network and worker threads. Protect every value that a callback reads and return quickly because -the resource request waits. A transform callback sets a replacement URL. A -header transform callback sets header names and values. A provider callback -operates only on the request handle that it received. +the resource request waits. Transfer callback state to the runtime only after a +registration succeeds; the runtime releases it after its final invocation. A +transform callback sets a replacement URL. A header transform callback sets +header names and values. A provider callback operates only on the request +handle that it received. ## Rewrite a URL with a transform @@ -40,8 +42,9 @@ a non-OK status preserves the original request. -Registering a transform replaces the previous transform. After the registration -call returns, the replaced transform has no in-flight callbacks. +Registering a transform replaces the previous transform in runtime submission +order. The runtime releases the replaced callback state after its final +invocation. @@ -63,10 +66,9 @@ headers from that invocation and sends the request unchanged. -Register the transform on the runtime, and use -`mln_runtime_clear_http_header_transform` to stop adding headers. Both calls -work while maps are live, so a host installs a refreshed credential without -recreating the runtime. +Submit the transform command on the runtime, and submit the corresponding clear +command to stop adding headers. Both commands work while maps are live, so a +host installs a refreshed credential without recreating the runtime. @@ -107,8 +109,7 @@ completion or cancellation. ## Keep callback state reachable -The runtime stores each callback and its user data by reference. Keep callback -state reachable until replacing or clearing the callback, or destroying the -runtime. Each operation waits for in-flight callbacks to return. Release any -host lock that a callback also acquires before destroying the runtime. Holding -that lock can deadlock destruction. +The runtime owns callback state after accepting a registration. It releases the +state after replacement, clear, or runtime teardown retires every in-flight +callback. Release any host lock that a callback also acquires before starting +close; holding that lock can deadlock teardown. diff --git a/docs/src/content/docs/guides/load-a-style.mdx b/docs/src/content/docs/guides/load-a-style.mdx index 3fe818316..f40601371 100644 --- a/docs/src/content/docs/guides/load-a-style.mdx +++ b/docs/src/content/docs/guides/load-a-style.mdx @@ -22,16 +22,17 @@ offline regions, and resource transform. Set style text when the host already has the document, such as a bundled base -style or one composed at runtime. MapLibre parses and copies the text before the -call returns. +style or one composed at runtime. The command copies the text before returning +acceptance. -Either call returns when the map accepts the request. Later pumps load the -style, sources, glyphs, and first tiles. A style-loaded event reports that the -style has parsed and its sources exist. A loading-failed event carries the -failure text. -[Handle events](/maplibre-native-ffi/guides/handle-events/) covers reading both. +Either command returns after copying and accepting its input. The native +scheduler loads the style, sources, glyphs, and first tiles. A style-loaded +event reports that the style parsed and its sources exist. A rejected or failed +command completes with diagnostic text; a later loading failure arrives as an +event. [Handle events](/maplibre-native-ffi/guides/handle-events/) covers reading +the event stream. ## Load a bundled style on Android diff --git a/docs/src/content/docs/guides/move-the-camera.mdx b/docs/src/content/docs/guides/move-the-camera.mdx index 554d2b5d8..2b01bbe15 100644 --- a/docs/src/content/docs/guides/move-the-camera.mdx +++ b/docs/src/content/docs/guides/move-the-camera.mdx @@ -13,31 +13,31 @@ import cameraSnippet from "../../../../snippets/c/camera.c?raw"; import fitBoundsSnippet from "../../../../snippets/c/camera-fit-bounds.c?raw"; import gestureSnippet from "../../../../snippets/c/camera-gesture.c?raw"; -The camera belongs to the map. A camera call needs a live map and the map's -owner thread, and it works before any render session exists. - -Every command takes the same camera options. Set the fields that you want to -change, and MapLibre leaves every other field at its current value. +The camera belongs to the map. Any host thread can submit a camera command while +the map is live, including before a render session exists. Every command copies +one atomic camera update: set the fields to change, and MapLibre leaves every +other field at its committed value. ## Choose how the camera moves -Three commands apply that camera, and they differ only in timing. +One command, `mln_map_update_camera`, applies that camera. Its update mode +selects the timing, and the three modes differ only in timing. -`mln_map_jump_to` applies the camera immediately. Use it for the first position +Jump mode applies the camera immediately. Use it for the first position of a session, a position restored from saved state, or a camera that the host recomputes every frame during a gesture. -`mln_map_ease_to` interpolates every field that you set along a cubic easing +Ease mode interpolates every field that you set along a cubic easing curve. Use it for a short move that remains easy to follow. A duration of zero, the default, applies the camera immediately. -`mln_map_fly_to` takes the same animation options and follows a curved path. Use +Fly mode takes the same animation options and follows a curved path. Use it for a long move. The zoom decreases through the middle of the transition to cross the distance without loading every high-zoom tile between the positions. When the duration is absent, MapLibre derives one from the distance and a @@ -45,16 +45,14 @@ velocity of 1.2 screenfuls per second. -An animated transition advances while the host pumps the runtime and draws -frames. Its duration uses wall-clock time, but pausing the pump also pauses the -transition. A host that pumps on demand continues until the transition ends. -The repaint flag in +An animated transition advances on the native scheduler while the host draws +frames. Its duration uses wall-clock time. The repaint flag in [Run the render loop](/maplibre-native-ffi/guides/run-the-render-loop/) signals that more frames are required. ## Move by a gesture -The three commands above take a destination. Gestures instead provide relative +A camera update takes a destination. Gestures instead provide relative input: a drag provides a distance, and a pinch provides a scale factor and a screen position. Relative moves apply those values to the current camera. @@ -81,9 +79,9 @@ continue the gesture's momentum and take the same animation options as an ease. -Call each camera operation on the map's owner thread. When another thread reads -gestures, send relative deltas to the owner thread. An absolute camera computed -from an earlier frame can be stale when the owner thread applies it. +Submit relative gesture deltas instead of a camera computed from an earlier +snapshot. The scheduler applies each command atomically to the committed camera +state in runtime submission order. ## Fit the camera to bounds @@ -94,7 +92,7 @@ viewport, inset by the padding that you pass in logical pixels. The query reports a camera and leaves the map unchanged. Apply the result with -the command that provides the required timing. +the update mode that provides the required timing. diff --git a/docs/src/content/docs/guides/query-a-map.mdx b/docs/src/content/docs/guides/query-a-map.mdx index 70080db26..aa98b60a7 100644 --- a/docs/src/content/docs/guides/query-a-map.mdx +++ b/docs/src/content/docs/guides/query-a-map.mdx @@ -14,9 +14,11 @@ A rendered feature query finds features in the last frame at a screen position. Use it for hit testing. To read source features that no layer draws, use [Query a source](/maplibre-native-ffi/guides/query-a-source/). -A rendered query belongs to the render session and runs on the session's owner -thread. It reads the last frame that the session drew. A query before the first -render update reports an invalid-state status. +A rendered query belongs to the render session and is an asynchronous call that +may start from any thread. The selected driver executes it after preceding +render work. A caller driver therefore needs graphics-thread service while the +completion is pending. A query before the first rendered frame reports invalid +state. The query uses the last frame that the session drew. Render after a camera move or style change when the result must match the new state. A query finds features diff --git a/docs/src/content/docs/guides/query-a-source.mdx b/docs/src/content/docs/guides/query-a-source.mdx index fa5bbd110..147fb2d5f 100644 --- a/docs/src/content/docs/guides/query-a-source.mdx +++ b/docs/src/content/docs/guides/query-a-source.mdx @@ -15,9 +15,11 @@ A source query reports the loaded features in a source, including features that no layer draws. To find features at a screen position, use [Query a map](/maplibre-native-ffi/guides/query-a-map/). -The query belongs to the render session and runs on the session's owner thread. -Source queries become available after the first render update. An earlier query -reports an invalid-state status. +The query belongs to the render session and is an asynchronous call that may +start from any thread. The selected driver executes it after preceding render +work. A caller driver therefore needs graphics-thread service while the +completion is pending. Source queries become available after the first rendered +frame. ## Run the query diff --git a/docs/src/content/docs/guides/render-a-static-image.mdx b/docs/src/content/docs/guides/render-a-static-image.mdx index 2c4b89886..5bddf2281 100644 --- a/docs/src/content/docs/guides/render-a-static-image.mdx +++ b/docs/src/content/docs/guides/render-a-static-image.mdx @@ -1,6 +1,6 @@ --- title: Render a static image -description: Create a map in static mode, request one image, and read the finished pixels into host memory. +description: Attach an owned texture target, request one frame, and read the finished pixels. sidebar: order: 7 --- @@ -10,39 +10,36 @@ import { region } from "../../../snippets"; import stillImage from "../../../../snippets/c/still-image.c?raw"; -A static map renders one image per request. Use it for a tile service, thumbnail -job, or image export that waits for completion and reads pixels into host -memory. - -Static mode is fixed when the map is created. Create a dedicated static map, as -[Create a map](/maplibre-native-ffi/guides/create-a-map/) describes. Reading -pixels also requires a session-owned texture. The render session allocates this -target and can copy its contents into host memory. +A still-image job uses a session-owned texture target with a ring depth of one. +The host requests a frame, waits for its terminal result, and starts readback of +the completed texture. ## Request one image -Create the map at the render target's size. Attaching a session with a different -size queues a map-size update on the map's owner thread. Pump before requesting -an image. The map then uses the new size. +Create the map and target with the required logical extent. Start attachment and +retain the returned attaching session until its completion callback runs. This +snippet uses private OpenGL through EGL, so the native core worker creates and +owns its context. The host keeps the EGL display initialized until detach +completes. -The render session allocates the texture at the extent that the descriptor -names. Each render backend has its own descriptor type and attach function, and -this snippet uses OpenGL through EGL. - -Render on every turn of the loop. A static map advances loading through the -render calls. The still-image-finished event can arrive before or after the -render call that produced the pixels. The image is ready after both the event -and a successfully rendered frame. +Submit one frame demand with a token that identifies the job. A core-worker +target needs no host graphics service. + +The frame wake schedules a host turn. Drain every queued result in that turn and +continue until the matching token has a rendered disposition and the still-image +completion has run. No update and size pending require a newer map update or a +matching extent generation. ## Read the pixels -Probe for the size first. The scale factor determines the physical extent. The -copy uses premultiplied RGBA8 with the row stride that the probe reports. +Start readback after the rendered result. Its completion borrows premultiplied +RGBA8 bytes and their width, height, row stride, and byte length. Copy the bytes +before the callback returns. diff --git a/docs/src/content/docs/guides/run-the-render-loop.mdx b/docs/src/content/docs/guides/run-the-render-loop.mdx index 6b0696713..4e38bd734 100644 --- a/docs/src/content/docs/guides/run-the-render-loop.mdx +++ b/docs/src/content/docs/guides/run-the-render-loop.mdx @@ -1,6 +1,6 @@ --- title: Run the render loop -description: Choose the runtime owner thread, then pump it and draw frames. +description: Submit frame demand, service graphics work when required, and drain owned results. sidebar: order: 4 --- @@ -8,100 +8,84 @@ sidebar: import { Code } from "@astrojs/starlight/components"; import { region } from "../../../snippets"; -import frameLoop from "../../../../snippets/c/frame-loop.c?raw"; -import pumpThread from "../../../../snippets/c/pump-thread.c?raw"; +import frameLoopSnippet from "../../../../snippets/c/frame-loop.c?raw"; -Pumping the runtime advances MapLibre Native. A pump drains queued map work and -services network and database work. Drawing is separate: the map publishes a -render update, and the render session draws it. +MapLibre Native advances runtime and map work on its native scheduler. A render +session advances through either a native graphics worker or a narrow service +call on the host graphics thread. Neither contract requires a host runtime pump. +The snippets on this page come from one turn of a display-paced host loop over a +caller-driver session. -Pump the runtime and drain its events on the runtime owner thread. Both -operations report a wrong-thread status from another thread. Choose the owner -thread before creating the runtime. +## Submit frame demand -## Choose the runtime owner thread +Submit a nonblocking frame demand from any thread. It carries a host token, an +optional timeout, flags, and a coalescing boundary. Matching demands may +coalesce before work starts, but every accepted demand produces one terminal +result record. A positive timeout starts when native code accepts the demand; +when it expires before work begins, the demand terminates as deadline missed. +The timeout does not supply cadence. -Pump from the display thread when the host already has a frame callback and the -map is the main work on that thread. The runtime, map, and render session then -share one thread, and the loop needs no shared state. Pass a drain budget so -one busy pump spreads its work across frames instead of spanning an entire -style parse inside one callback. A single task still runs to completion, so -keep headroom in the frame for the longest one. +A core-worker session wakes its serial graphics worker. A caller-driver session +invokes its driver-work wake callback. Map updates use the same paths, so +progress does not depend on runtime-event drains or display callbacks. -Use a dedicated thread when a long pump would stall the display, the map must -load while the display is idle, or the host draws no frames. That thread parks -inside the pump, and runtime work sets the loop cadence. The display thread -attaches the render session and draws. This design needs shared state and a wake -source. +The host may submit demand from a display source, after an earlier result, or +from another scheduler. A demand captures the current extent generation. Its +result identifies the token and the map-update, extent, and frame generations +that the driver used. -## Pump from the frame callback +A host that renders on demand derives that demand from runtime events: an +update-available event, or a frame-finished event whose repaint flag is set. -Pass a timeout of zero. The call drains what is queued and returns, because the -display already sets the pace. Pass a budget of a few milliseconds so the pump -leaves the rest of the frame to drawing; work that the budget defers re-arms -the pump, so the next callback continues it. One drain then takes the events -that the pump produced. + -This loop reads two event types, render-update-available and -render-frame-finished. It selects them before the map loads a style, as -[Handle events](/maplibre-native-ffi/guides/handle-events/) describes. +While a frame is pending, each loop turn submits one demand with the if-needed +and present flags and a fresh host token. - + -The helper compares each event's source against the map before interpreting the -event. Every map in one runtime shares the queue. It requests a frame after a -render update and after a finished frame with its repaint flag set. Repaint -frames advance camera animations and label placement. +## Service a caller driver -A display-paced host stops pumping when it stops requesting frames. Request a -frame after every map change. This lets an idle loop process the change. +When the driver-work callback wakes the host, schedule the driver-service call on the +thread or realm where the target's graphics context is usable. The first +successful call fixes that graphics-thread identity. Service work in serialized +calls until the mailbox drains. Continue service while presentation callbacks +are paused, because attach, resize, queries, readback, barriers, and detach also +use the mailbox. -Draw a pending frame after draining events. The render call reports which -outcome it reached. A no-update result and a pending-size result both mean that -the map has not published an update for the session's extent, which is expected -at startup and after an attach or resize. A target-not-ready result means that -the render target had no frame to draw into, so the next turn tries again. + -A rendered frame clears the pending flag, unless the map asked for another -frame while rendering this one, as during a camera transition. The render call -returns that repaint flag directly, so the loop re-arms in the same turn -instead of waiting for the frame's events to arrive. +A core-worker session rejects the service call because its native worker already +owns execution. WGL targets, EGL surfaces, shared EGL textures, existing WebGL +contexts, and browser WebGPU use caller drivers. Transferable Metal and Vulkan +targets, private EGL owned texture targets, and `OffscreenCanvas` WebGL targets +can use core workers. - +If the graphics owner cannot run again, abandon the target. Leaving ready work +unserviced can leave operations pending indefinitely. -## Pump on a thread of your own +## Drain results and present -The pump thread owns the runtime and map. The display thread owns the render -session. Each operation reports a wrong-thread status when called from the -other thread. +Frame readiness is level-triggered. Drain an owned frame-result batch from any +thread after the frame wake runs. Each batch remains stable until release. Keep +draining until the queue is empty. -The two threads share a flag that the pump thread sets when a frame is due, a -flag that the display thread sets to stop the loop, and a wake source. +A rendered result means that a frame was presented or became available for +acquisition or readback. No update and size pending wait for a newer map update. +Target not ready waits for host target readiness or paced retry. Superseded +means that a newer demand replaced this demand within the same coalescing +boundary. Deadline missed means that work did not begin within the positive +timeout. These outcomes have different retry conditions, so preserve them in +host pacing. - + -Acquire the wake source and select the event types the loop reads, both on the -runtime owner thread before the loop starts. +Owned texture acquisition is nonblocking and leases the oldest completed slot +that is not already acquired. Wait for or submit work after its producer sync. +Release the acquired handle with consumer-completion sync when host GPU work +reads the texture. CPU-complete sync is valid only when that work completed +before release. The session retires the slot through its selected driver before +reusing it. - - -Any thread may signal the source, and the signal releases a parked pump. The -display thread signals it after it queues work for the map or asks the loop to -stop. - - - -The loop parks until owner-thread work, a selected event, or the wake source -signals it. Use a positive timeout to reach the stop check even when no wake -arrives. The runtime registers no timers of its own. This snippet parks for 100 -ms and checks for a stop at least ten times a second. - - - -Call a parking pump outside any lock that a signalling thread also acquires. The -display thread reads the pending flag on its next frame and draws. - -Use a dedicated operating-system thread. The runtime records that specific -thread at creation. Serial dispatch queues, actors, and pooled executors provide -serialization but can move work between threads. Calls that move report a -wrong-thread status. +Use a session barrier when later work must wait for every preceding render item +to become terminal. A barrier orders work; it does not request a frame. diff --git a/docs/src/content/docs/guides/set-feature-state.mdx b/docs/src/content/docs/guides/set-feature-state.mdx index 87d688dcd..7992e3ae8 100644 --- a/docs/src/content/docs/guides/set-feature-state.mdx +++ b/docs/src/content/docs/guides/set-feature-state.mdx @@ -16,8 +16,9 @@ style expressions read while the map draws. One styling rule then covers selection, hover, a live value, or a computed status, because each feature supplies its own input to that rule. -Feature state belongs to the [map](/maplibre-native-ffi/concepts/). Set or -remove it on the map's owner thread. A render session pushes it into the +Feature state belongs to the [map](/maplibre-native-ffi/concepts/). Its set, +get, and remove calls are callable from any thread, execute in order, and +report through a completion. A render session pushes the map's store into the renderer on the next render update, including the first presented frame that contains the source. @@ -52,20 +53,19 @@ with neither one removes the state of every feature in the source layer. ## Read state back -Reading state copies the map's store into a snapshot handle that the host owns. -The snapshot contains an empty object when the source or feature has no state. -The copy does not require a render session or a loaded source. +Reading state copies the map's store and reports the copy through the +completion. The read does not require a render session or a loaded source. -The snapshot's root is a JSON object with one member per state key. Reading one -value iterates over those members and compares each key against the one that you -want. +The result is UTF-8 bytes holding one JSON object with one member per state +key, or an empty object when the source or feature has no state. Parse it with +the host's JSON library to read the value that you want. -The root value and everything under it point into the snapshot. Copy any value -that you keep, because destroying the snapshot invalidates those pointers. +Copy any value that you keep, because the result bytes stay valid only for the +completion callback. diff --git a/docs/src/content/docs/guides/style-layers-by-data.mdx b/docs/src/content/docs/guides/style-layers-by-data.mdx index 55a41e42e..cdc740baa 100644 --- a/docs/src/content/docs/guides/style-layers-by-data.mdx +++ b/docs/src/content/docs/guides/style-layers-by-data.mdx @@ -48,6 +48,6 @@ the layer's filter. The snippet passes the complete filter expression. The host owns the byte storage. MapLibre parses or copies a property value or filter before the call returns. -Both calls run on the map's owner thread and apply to the live style -immediately. A host that pumps the runtime and renders on update draws the new -appearance in its next frame. +Both commands apply to the live style in runtime submission order. A host that +drains render events and renders on update draws the new appearance in its next +frame. diff --git a/docs/src/content/docs/guides/update-data.mdx b/docs/src/content/docs/guides/update-data.mdx index be55f1d0f..856152978 100644 --- a/docs/src/content/docs/guides/update-data.mdx +++ b/docs/src/content/docs/guides/update-data.mdx @@ -10,9 +10,10 @@ import { region } from "../../../snippets"; import liveUpdates from "../../../../snippets/c/geojson-live-updates.c?raw"; -A GeoJSON source accepts prepared inline data or a new URL. Both updates -replace the whole dataset. A host that tracks individual features keeps its own -collection and publishes the collection after each change. +A GeoJSON source accepts prepared inline data or a new URL. Both commands +replace the whole dataset in runtime submission order. A host that tracks +individual features keeps its own collection and publishes the collection after +each change. Initialize the source with an empty feature collection. Every later update uses the same source ID, preserving the layers that draw it. @@ -33,18 +34,18 @@ reports an invalid-argument status. -The install runs on the map's owner thread and swaps the prepared index into -the source, which is cheap at any data size. Destroy the prepared handle after -the install, or keep it to install the same data on other matching sources. +The install is a command that swaps the prepared index into the source, which +is cheap at any data size. Destroy the prepared handle after the install, or +keep it to install the same data on other matching sources. MapLibre copies what it keeps, so the serialization buffer is free for reuse once preparation returns. -A host with a worker thread prepares there and installs from the owner thread, -so a large dataset never blocks the pump. When a feed delivers faster than the -display refreshes, collect the messages that arrive between frames and publish -once per frame. +A host with a worker thread prepares there and installs from any thread, so a +large dataset never blocks the thread that publishes it. When a feed delivers +faster than the display refreshes, collect the messages that arrive between +frames and publish once per frame. ## Synchronous tiling @@ -54,9 +55,8 @@ does not extend the current frame. Use this default for most feeds. Set the synchronous-tiling option when the next frame must include the update. Examples include a simulation that advances with rendering and a still image -captured immediately after an update. Tile slicing then runs during the render -call on the render session's owner thread, which extends that frame by the -slicing work. +captured immediately after an update. Tiling then runs on native worker threads. +A large dataset can delay the render update that publishes the new tiles. The option chosen when adding the source is the baseline, and a runtime override raises it temporarily. Enable the override around a burst of diff --git a/docs/src/content/docs/guides/work-offline.mdx b/docs/src/content/docs/guides/work-offline.mdx index 9b00def03..6cb0d4bc4 100644 --- a/docs/src/content/docs/guides/work-offline.mdx +++ b/docs/src/content/docs/guides/work-offline.mdx @@ -22,14 +22,10 @@ contains every resource that a defined area needs and remains outside the ambient budget. Download a region when the host must draw a known area without a network. -Database operations are asynchronous. A start call validates its arguments, -returns an operation ID, and queues the work. The runtime later queues one -operation-completed event with the ID and result status while its subscription -selects that type, as -[Handle events](/maplibre-native-ffi/guides/handle-events/) describes. This -result is separate from the start call's status. Take the result value when an -operation produces one. Take or discard every operation to release its runtime -state. +Database operations are asynchronous. A submission validates its arguments and +queues the work. Each accepted submission invokes its one-shot completion with +the terminal status, diagnostic, and any borrowed result. Copy values that must +outlive the callback. ## Download a region @@ -51,9 +47,8 @@ call returns. -Taking the result produces a snapshot handle that contains the region record. A -failed take leaves the operation live. Discard the operation after the host -stops retrying. +The completion receives the region record directly. Its definition and metadata +are borrowed for the callback, while the numeric region ID remains valid. @@ -61,9 +56,9 @@ The region ID is the part of the record that the remaining calls need. -Mark the region as observed to receive progress and error events. Then set its -download state to active. MapLibre fetches the resources while the host pumps -the runtime. +Mark the region as observed to receive progress and error events. Then submit +the active download-state command. MapLibre fetches resources on the native +scheduler without a host pump. @@ -85,8 +80,8 @@ operation covers the whole database. -Taking the result produces a list handle that the host owns. Destroy that handle -after reading the entries. +The list completion borrows a contiguous array of region records. Copy the +records and nested definition or metadata values that the host keeps. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 0950d4f31..2244cf324 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -14,9 +14,11 @@ and custom integrations that need direct control of the map and render session. The MapLibre [Android](https://maplibre.org/maplibre-native/android/api/) and [iOS](https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre/) SDKs provide a map view that owns a window, a frame loop, and gesture handling. -This API provides the map and render session. The host creates the window and -graphics context, pumps the runtime, and decides when to draw each frame. A -texture render target supports image export and compositing into a host scene. +This API provides the map and render session. The host supplies a render target, +selects native-worker or caller-graphics-thread execution where the target +permits it, drains events and frame results, and submits frame demand from its +own cadence. A texture render target supports image export and compositing into +a host scene. See the [examples](https://github.com/maplibre/maplibre-native-ffi/tree/main/examples) in the repository for complete integration examples for each language binding. diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/AndroidMapView.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/AndroidMapView.kt index 2c725d47b..ba4ea16a1 100644 --- a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/AndroidMapView.kt +++ b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/AndroidMapView.kt @@ -8,22 +8,17 @@ import android.view.SurfaceHolder import android.view.SurfaceView /** - * The render loop. + * The Choreographer-paced render loop. * - * The UI thread owns the surface, touch input, the viewport, the graphics context, and the render - * session it attaches. It touches the map only to attach, which native serves from any thread; - * every other map call belongs to [MapRuntimeLoop]. - * - * The platform destroys and recreates this view's surface across a rotation and a trip to the - * background. A session survives that only while its graphics context does, so a context that goes - * with its surface leaves the next frame attaching a cold session. + * The UI thread owns the surface, touch input, viewport, graphics context, and render session. + * Runtime and map updates are submitted directly to the core-owned runtime worker. */ internal class AndroidMapView(context: Context) : SurfaceView(context), SurfaceHolder.Callback2, Choreographer.FrameCallback, AutoCloseable { - private val input = InputController(context, ::enqueueCameraCommand) + private val input = InputController(context) { mapState } private var graphics: GraphicsContext? = null private var renderTarget: SurfaceRenderTarget? = null - private var runtimeLoop: MapRuntimeLoop? = null + private var mapState: MapState? = null private var viewport: Viewport? = null private var viewVisible = false private var appForeground = false @@ -95,18 +90,17 @@ internal class AndroidMapView(context: Context) : override fun doFrame(frameTimeNanos: Long) { frameCallbackPosted = false - val loop = runtimeLoop - if (loop != null) { + val state = mapState + if (state != null) { try { - val target = ensureRenderTarget(loop) - if (target != null && loop.renderRequest.consume()) { + state.pollEvents() + val target = ensureRenderTarget(state) + if (target != null && state.renderRequest.consume()) { if (target.renderUpdate()) { contextRebuildSpent = false finishPendingDrawing() } else { - // The map applies its logical size on the runtime loop's next pump, so an attach is - // followed by frames with nothing to render. - loop.renderRequest.set() + state.renderRequest.set() } } } catch (error: RuntimeException) { @@ -118,7 +112,7 @@ internal class AndroidMapView(context: Context) : } fun requestRender() { - runtimeLoop?.renderRequest?.set() + mapState?.renderRequest?.set() startLoopIfReady() } @@ -126,16 +120,10 @@ internal class AndroidMapView(context: Context) : if (closed) return closed = true stopLoop() - // Close the session before the runtime loop closes the map: a map with an attached session - // cannot be destroyed. + // Close the render session before closing the map and runtime. detachSurface() - runtimeLoop?.close() - runtimeLoop = null - } - - private fun enqueueCameraCommand(command: CameraCommand) { - runtimeLoop?.enqueue(command) - requestRender() + mapState?.close() + mapState = null } private fun surfaceAvailable(holder: SurfaceHolder) { @@ -155,10 +143,10 @@ internal class AndroidMapView(context: Context) : graphics = nextGraphics Log.i(TAG, "render-target=native-surface status=${nextGraphics.backendName}") } - // The runtime loop outlives surface changes, so loading continues while there is nothing to - // present to. - if (runtimeLoop == null) { - runtimeLoop = MapRuntimeLoop(nextViewport) + if (mapState == null) { + mapState = MapState(nextViewport, ::requestRender) + } else { + mapState?.resize(nextViewport) } followSurface("surface available") requestRender() @@ -224,18 +212,17 @@ internal class AndroidMapView(context: Context) : finishPendingDrawing() } - /** Attaches a session against the published map, on this thread, which then owns it. */ - private fun ensureRenderTarget(loop: MapRuntimeLoop): SurfaceRenderTarget? { + /** Attaches a render session on the UI thread, which owns it until close. */ + private fun ensureRenderTarget(state: MapState): SurfaceRenderTarget? { renderTarget?.let { return it } val currentGraphics = graphics ?: return null val currentViewport = viewport?.takeUnless { it.isEmpty } ?: return null - val map = loop.map ?: return null - val attached = SurfaceRenderTarget.attach(map, currentGraphics, currentViewport) + val attached = SurfaceRenderTarget.attach(state.map, currentGraphics, currentViewport) renderTarget = attached - loop.requestRepaint() - loop.renderRequest.set() + state.requestRepaint() + state.renderRequest.set() return attached } @@ -253,9 +240,7 @@ internal class AndroidMapView(context: Context) : appForeground && graphics?.hasSurface == true && !frameFailed && - runtimeLoop != null && - // A loop whose setup failed never publishes a map. - runtimeLoop?.setupFailure == null + mapState != null private fun finishPendingDrawing() { while (pendingDrawingFinished.isNotEmpty()) { diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/Channels.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/Channels.kt deleted file mode 100644 index 97c9f6bfa..000000000 --- a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/Channels.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.maplibre.nativeffi.examples.androidmap - -import java.util.concurrent.atomic.AtomicBoolean -import org.maplibre.nativeffi.geo.ScreenPoint - -/** - * A camera change decoded on the render loop and applied on the map's owner thread. - * - * Commands carry deltas rather than absolute targets, because reading the camera and writing the - * new one has to happen together on the thread that owns the map. Coordinates are logical map - * units, converted by the render loop that owns the viewport. - */ -internal sealed interface CameraCommand { - data object CancelTransitions : CameraCommand - - data class SetGestureInProgress(val inProgress: Boolean) : CameraCommand - - data class MoveBy(val deltaX: Double, val deltaY: Double) : CameraCommand - - data class ScaleBy(val scale: Double, val anchor: ScreenPoint) : CameraCommand - - data class AdjustBearing(val degrees: Double, val anchor: ScreenPoint) : CameraCommand - - data class AdjustPitch(val degrees: Double) : CameraCommand - - /** Double tap: animate to one zoom level past the nearest whole level, about [anchor]. */ - data class ZoomToNextWholeLevel(val anchor: ScreenPoint) : CameraCommand -} - -/** - * One-bit signal that a frame is worth drawing. The render loop consumes before it renders and sets - * again when nothing was rendered, so a request published during a render is not lost. - */ -internal class RenderRequest { - private val value = AtomicBoolean(true) - - fun set() { - value.set(true) - } - - fun consume(): Boolean = value.getAndSet(false) -} diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/InputController.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/InputController.kt index 3350798f9..32b96f675 100644 --- a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/InputController.kt +++ b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/InputController.kt @@ -8,11 +8,8 @@ import kotlin.math.atan2 import kotlin.math.hypot import org.maplibre.nativeffi.geo.ScreenPoint -/** - * Decodes touch input into camera commands. This runs on the UI thread, which does not own the map, - * so it only produces commands; the runtime loop applies them on the map's owner thread. - */ -internal class InputController(context: Context, private val enqueue: (CameraCommand) -> Unit) { +/** Decodes touch input into camera updates on the UI thread. */ +internal class InputController(context: Context, private val mapState: () -> MapState?) { private val density = context.resources.displayMetrics.density.takeIf { it > 0f } ?: 1f private val tapGestureDetector = GestureDetector(context, TapListener()) private val pointerTracker = PointerTracker() @@ -31,8 +28,8 @@ internal class InputController(context: Context, private val enqueue: (CameraCom override fun onDoubleTap(event: MotionEvent): Boolean { doubleTapActive = true - enqueue(CameraCommand.CancelTransitions) - enqueue(CameraCommand.ZoomToNextWholeLevel(screenPoint(event.x, event.y))) + mapState()?.cancelTransitions() + mapState()?.zoomToNextWholeLevel(screenPoint(event.x, event.y)) return true } @@ -57,13 +54,13 @@ internal class InputController(context: Context, private val enqueue: (CameraCom fun onTouchEvent(event: MotionEvent) { when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { - enqueue(CameraCommand.CancelTransitions) - enqueue(CameraCommand.SetGestureInProgress(true)) + mapState()?.cancelTransitions() + mapState()?.setGestureInProgress(true) beginPan(event) } MotionEvent.ACTION_POINTER_DOWN -> if (event.pointerCount == 2) { - enqueue(CameraCommand.CancelTransitions) + mapState()?.cancelTransitions() beginTwoFinger(event) } MotionEvent.ACTION_MOVE -> { @@ -107,7 +104,7 @@ internal class InputController(context: Context, private val enqueue: (CameraCom lastPrimaryX = x lastPrimaryY = y if (dx == 0.0 && dy == 0.0) return - enqueue(CameraCommand.MoveBy(dx, dy)) + mapState()?.moveBy(dx, dy) } private fun beginTwoFinger(event: MotionEvent) { @@ -137,15 +134,15 @@ internal class InputController(context: Context, private val enqueue: (CameraCom if (mode == Mode.SCALE_ROTATE) { val anchor = screenPoint(current.centroidX, current.centroidY) if (scale.isFinite() && abs(scale - 1.0) >= SCALE_EPSILON) { - enqueue(CameraCommand.ScaleBy(scale, anchor)) + mapState()?.scaleBy(scale, anchor) changed = true } if (abs(deltaDegrees) >= ROTATION_EPSILON_DEGREES) { - enqueue(CameraCommand.AdjustBearing(-deltaDegrees, anchor)) + mapState()?.adjustBearing(-deltaDegrees, anchor) changed = true } } else if (mode == Mode.SHOVE && deltaY != 0.0) { - enqueue(CameraCommand.AdjustPitch(-deltaY * SHOVE_PITCH_FACTOR)) + mapState()?.adjustPitch(-deltaY * SHOVE_PITCH_FACTOR) changed = true } @@ -175,7 +172,7 @@ internal class InputController(context: Context, private val enqueue: (CameraCom /** Every path that ends a gesture, including `ACTION_CANCEL`, runs through here. */ private fun reset() { if (mode != Mode.NONE) { - enqueue(CameraCommand.SetGestureInProgress(false)) + mapState()?.setGestureInProgress(false) } mode = Mode.NONE primaryPointerId = MotionEvent.INVALID_POINTER_ID diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapRuntimeLoop.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapRuntimeLoop.kt deleted file mode 100644 index fbb5dfaa4..000000000 --- a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapRuntimeLoop.kt +++ /dev/null @@ -1,231 +0,0 @@ -package org.maplibre.nativeffi.examples.androidmap - -import android.os.Handler -import android.os.HandlerThread -import android.util.Log -import java.util.concurrent.ConcurrentLinkedQueue -import kotlin.math.pow -import kotlin.math.round -import org.maplibre.nativeffi.camera.AnimationOptions -import org.maplibre.nativeffi.camera.CameraOptions -import org.maplibre.nativeffi.geo.LatLng -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapMode -import org.maplibre.nativeffi.map.MapOptions -import org.maplibre.nativeffi.runtime.RuntimeEventMask -import org.maplibre.nativeffi.runtime.RuntimeEventPayload -import org.maplibre.nativeffi.runtime.RuntimeEventType -import org.maplibre.nativeffi.runtime.RuntimeHandle -import org.maplibre.nativeffi.runtime.RuntimeOptions -import org.maplibre.nativeffi.runtime.WakeSource - -/** - * Owns the runtime and the map for their whole lifetime, off the UI thread. - * - * Native owner-thread checks are keyed on the OS thread, so this is a [HandlerThread] rather than a - * coroutine dispatcher or a pooled executor. The render loop attaches its own session against the - * map published here, and closes it before [close] destroys the map. - * - * The loop keeps pumping while the view is off screen or the app is backgrounded, so style and tile - * loading continue. - */ -internal class MapRuntimeLoop(private val initialViewport: Viewport) : AutoCloseable { - private val commands = ConcurrentLinkedQueue() - private val thread = HandlerThread("maplibre-runtime").apply { start() } - private val handler = Handler(thread.looper) - private var runtime: RuntimeHandle? = null - private var owned: MapHandle? = null - - /** - * Releases the parked pump on the handler thread. Every path that posts work to that thread - * signals this too, because a parked pump would otherwise hold the message until its bound. - */ - @Volatile private var wake: WakeSource? = null - - /** The map to attach against, once the loop has created one. */ - @Volatile - var map: MapHandle? = null - private set - - /** Set when setup failed, so the view stops scheduling frames against a loop with no map. */ - @Volatile - var setupFailure: Throwable? = null - private set - - /** Set by the runtime loop when the map wants another frame, consumed by the render loop. */ - val renderRequest: RenderRequest = RenderRequest() - - private val pump = - object : Runnable { - override fun run() { - handler.removeCallbacks(this) - val currentRuntime = runtime - val currentMap = owned - if (currentRuntime != null && currentMap != null) { - try { - while (true) { - apply(currentMap, commands.poll() ?: break) - } - // This thread has no display to pace it, so the pump's park is the cadence. - currentRuntime.pump(PARK_TIMEOUT_MS) - if (drainEvents(currentRuntime, currentMap)) { - renderRequest.set() - } - } catch (error: RuntimeException) { - // The handles stay live: the UI thread may still hold an attached session, and a map - // cannot be destroyed until that session closes. close() tears down in that order. - Log.e(TAG, "runtime loop iteration failed", error) - setupFailure = error - return - } - } - handler.post(this) - } - } - - init { - handler.post { create() } - } - - fun enqueue(command: CameraCommand) { - commands.add(command) - handler.post(pump) - wake?.signal() - } - - /** Asks the map to redraw, for instance after the render loop attached a fresh session. */ - fun requestRepaint() { - handler.post { owned?.requestRepaint() } - wake?.signal() - } - - override fun close() { - handler.removeCallbacks(pump) - handler.post { closeHandles() } - wake?.signal() - thread.quitSafely() - thread.join() - } - - private fun create() { - try { - val createdRuntime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) - runtime = createdRuntime - wake = createdRuntime.acquireWakeSource() - val createdMap = - MapHandle.create( - createdRuntime, - MapOptions().apply { - width = initialViewport.logicalWidth - height = initialViewport.logicalHeight - scaleFactor = initialViewport.scaleFactor - mapMode = MapMode.CONTINUOUS - // The two event types this loop reads. A map queues no event of an - // unselected type, so nothing accumulates before the style load. - eventMask = - RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + - RuntimeEventMask.MAP_RENDER_FRAME_FINISHED - }, - ) - owned = createdMap - createdMap.setStyleUrl(STYLE_URL) - createdMap.jumpTo( - CameraOptions().apply { - center = LatLng(37.7749, -122.4194) - zoom = 13.0 - bearing = 12.0 - pitch = 30.0 - } - ) - map = createdMap - handler.post(pump) - } catch (error: RuntimeException) { - Log.e(TAG, "runtime loop setup failed", error) - setupFailure = error - closeHandles() - } - } - - private fun closeHandles() { - map = null - val closingMap = owned - val closingRuntime = runtime - val closingWake = wake - owned = null - runtime = null - wake = null - try { - closingMap?.close() - } finally { - try { - closingRuntime?.close() - } finally { - // A wake source outlives its runtime, so this is safe in either order. - closingWake?.close() - } - } - } - - /** - * Applies one decoded camera command on the map's owner thread, so read-modify-write commands - * read the current camera here rather than on the render loop that produced them. - */ - private fun apply(map: MapHandle, command: CameraCommand) { - when (command) { - CameraCommand.CancelTransitions -> map.cancelTransitions() - is CameraCommand.SetGestureInProgress -> map.isGestureInProgress = command.inProgress - is CameraCommand.MoveBy -> map.moveBy(command.deltaX, command.deltaY) - is CameraCommand.ScaleBy -> map.scaleBy(command.scale, command.anchor) - is CameraCommand.AdjustBearing -> { - val camera = map.camera - map.jumpTo( - CameraOptions().apply { - bearing = (camera.bearing ?: 0.0) + command.degrees - anchor = command.anchor - } - ) - } - is CameraCommand.AdjustPitch -> { - val camera = map.camera - map.jumpTo( - CameraOptions().apply { - pitch = ((camera.pitch ?: 0.0) + command.degrees).coerceIn(MIN_PITCH, MAX_PITCH) - } - ) - } - is CameraCommand.ZoomToNextWholeLevel -> { - val zoom = map.camera.zoom ?: 0.0 - val targetZoom = round(zoom) + 1.0 - map.scaleByAnimated(2.0.pow(targetZoom - zoom), command.anchor, DOUBLE_TAP_ANIMATION) - } - } - } - - /** Drains one batch of runtime events, reporting whether the map wants another frame. */ - private fun drainEvents(runtime: RuntimeHandle, map: MapHandle): Boolean { - var renderUpdateAvailable = false - // One drain takes every event the pump produced. - for (event in runtime.drainEvents().events) { - if (event.mapSource != map) continue - if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) { - renderUpdateAvailable = true - } else if ( - event.type == RuntimeEventType.MAP_RENDER_FRAME_FINISHED && - (event.payload as? RuntimeEventPayload.RenderFrame)?.needsRepaint == true - ) { - renderUpdateAvailable = true - } - } - return renderUpdateAvailable - } - - private companion object { - private const val TAG = "MapLibreRuntimeLoop" - private const val STYLE_URL = "https://tiles.openfreemap.org/styles/bright" - /** Backstop bound for a parked pump that nothing signals. */ - private const val PARK_TIMEOUT_MS = 100L - private const val MIN_PITCH = 0.0 - private const val MAX_PITCH = 60.0 - private val DOUBLE_TAP_ANIMATION = AnimationOptions().apply { durationMs = 160.0 } - } -} diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapState.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapState.kt new file mode 100644 index 000000000..697f36a07 --- /dev/null +++ b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/MapState.kt @@ -0,0 +1,170 @@ +package org.maplibre.nativeffi.examples.androidmap + +import java.util.concurrent.atomic.AtomicBoolean +import org.maplibre.nativeffi.camera.AnimationOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraDeltaKind +import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate +import org.maplibre.nativeffi.camera.GesturePhase +import org.maplibre.nativeffi.geo.LatLng +import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.map.MapMode +import org.maplibre.nativeffi.map.MapOptions +import org.maplibre.nativeffi.map.MapSize +import org.maplibre.nativeffi.runtime.RuntimeEventMask +import org.maplibre.nativeffi.runtime.RuntimeEventPayload +import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.runtime.RuntimeHandle +import org.maplibre.nativeffi.runtime.RuntimeOptions + +/** Runtime and map state driven by the core-owned runtime worker. */ +internal class MapState(initialViewport: Viewport, private val requestRender: () -> Unit) : + AutoCloseable { + private var closed = false + private val initialCamera = + CameraOptions().apply { + center = LatLng(37.7749, -122.4194) + zoom = 13.0 + bearing = 12.0 + pitch = 30.0 + } + + private val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) + private lateinit var ownedMap: MapHandle + val map: MapHandle + get() = ownedMap + + val renderRequest = RenderRequest() + + init { + try { + ownedMap = runSuspend { + MapHandle.create( + runtime, + MapOptions().apply { + width = initialViewport.logicalWidth + height = initialViewport.logicalHeight + scaleFactor = initialViewport.scaleFactor + mapMode = MapMode.CONTINUOUS + eventMask = + RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + + RuntimeEventMask.MAP_RENDER_FRAME_FINISHED + }, + ) + .await() + } + map.setStyleUrl(STYLE_URL) + map.updateCamera(CameraUpdate(camera = initialCamera)) + } catch (error: Throwable) { + if (::ownedMap.isInitialized) ownedMap.close() + runtime.close() + throw error + } + } + + fun cancelTransitions() { + map.updateCamera(CameraUpdate()) + requestRender() + } + + fun setGestureInProgress(inProgress: Boolean) { + map.updateCamera( + CameraUpdate(gesturePhase = if (inProgress) GesturePhase.BEGIN else GesturePhase.END) + ) + requestRender() + } + + fun moveBy(deltaX: Double, deltaY: Double) { + map.applyCameraDelta( + CameraDelta(offset = org.maplibre.nativeffi.geo.ScreenPoint(deltaX, deltaY)) + ) + requestRender() + } + + fun scaleBy(scale: Double, anchor: org.maplibre.nativeffi.geo.ScreenPoint) { + map.applyCameraDelta(CameraDelta(kind = CameraDeltaKind.SCALE, amount = scale, anchor = anchor)) + requestRender() + } + + fun adjustBearing(degrees: Double, anchor: org.maplibre.nativeffi.geo.ScreenPoint) { + map.applyCameraDelta( + CameraDelta(kind = CameraDeltaKind.BEARING, amount = degrees, anchor = anchor) + ) + requestRender() + } + + fun adjustPitch(degrees: Double) { + map.applyCameraDelta(CameraDelta(kind = CameraDeltaKind.PITCH, amount = degrees)) + requestRender() + } + + fun zoomToNextWholeLevel(anchor: org.maplibre.nativeffi.geo.ScreenPoint) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.SCALE, + amount = 2.0, + anchor = anchor, + animation = animation(DOUBLE_TAP_DURATION_MS), + ) + ) + requestRender() + } + + fun resize(viewport: Viewport) { + map.resize(MapSize(viewport.logicalWidth, viewport.logicalHeight, viewport.scaleFactor)) + } + + fun requestRepaint() { + map.requestRepaint() + } + + fun pollEvents() { + if (drainEvents()) renderRequest.set() + } + + private fun animation(durationMs: Double) = + AnimationOptions().apply { this.durationMs = durationMs } + + private fun drainEvents(): Boolean { + var renderUpdateAvailable = false + for (event in runtime.drainEvents().events) { + if (event.mapSource != map) continue + if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) { + renderUpdateAvailable = true + } else if ( + event.type == RuntimeEventType.MAP_RENDER_FRAME_FINISHED && + (event.payload as? RuntimeEventPayload.RenderFrame)?.needsRepaint == true + ) { + renderUpdateAvailable = true + } + } + return renderUpdateAvailable + } + + override fun close() { + if (closed) return + closed = true + try { + map.close() + } finally { + runtime.close() + } + } + + private companion object { + private const val STYLE_URL = "https://tiles.openfreemap.org/styles/bright" + private const val DOUBLE_TAP_DURATION_MS = 160.0 + } +} + +/** One-bit signal that a frame is worth drawing. */ +internal class RenderRequest { + private val value = AtomicBoolean(true) + + fun set() { + value.set(true) + } + + fun consume(): Boolean = value.getAndSet(false) +} diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SurfaceRenderTarget.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SurfaceRenderTarget.kt index 5617815f6..c0dcdfb6a 100644 --- a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SurfaceRenderTarget.kt +++ b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SurfaceRenderTarget.kt @@ -1,58 +1,81 @@ package org.maplibre.nativeffi.examples.androidmap +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.render.FrameDemand import org.maplibre.nativeffi.render.OpenGLSurfaceDescriptor +import org.maplibre.nativeffi.render.RenderDriver import org.maplibre.nativeffi.render.RenderResult +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.VulkanSurfaceDescriptor -/** - * The native-surface render target. The thread that attaches a session owns it for the session's - * lifetime, so every method here runs on the UI thread, where the host surface lives. - */ +/** A caller-driver native surface serviced on the UI graphics thread. */ internal class SurfaceRenderTarget private constructor(private val session: RenderSessionHandle) : AutoCloseable { - fun renderUpdate(): Boolean = session.renderUpdate().result == RenderResult.RENDERED - - /** - * Follows the host viewport and the surface [graphics] presents through now, keeping the session - * and its renderer. - * - * The OpenGL path carries a resize and a surface replacement through one call, because the - * surface is made current on the next render and may name a replacement for one the platform - * already destroyed. A Vulkan session destroys its swapchain before taking a new surface, which - * needs the outgoing surface to still be valid, so only a resize reaches it here. - */ + fun renderUpdate(): Boolean { + session.requestFrame(FrameDemand(present = true)) + session.serviceDriverWork() + return session.drainFrameResults().lastOrNull()?.disposition == RenderResult.RENDERED + } + + /** Applies target changes through the native typed driver mailbox. */ fun resize(graphics: GraphicsContext, viewport: Viewport) { - when (graphics) { - is EglGraphicsContext -> - session.setOpenGLSurfaceTarget( - OpenGLSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) - ) - is VulkanGraphicsContext -> - session.resize(viewport.logicalWidth, viewport.logicalHeight, viewport.scaleFactor) - else -> error("Unsupported graphics context: ${graphics::class.java.name}") - } + val completed = + when (graphics) { + is EglGraphicsContext -> + session.setOpenGLSurfaceTarget( + OpenGLSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) + ) + is VulkanGraphicsContext -> session.resize(viewport.extent) + else -> error("Unsupported graphics context: ${graphics::class.java.name}") + } + complete(completed) } override fun close() { + complete(session.detach()) session.close() } + private fun complete(completed: Deferred) { + while (!completed.isCompleted) session.serviceDriverWork() + runSuspend { completed.await() } + } + companion object { - fun attach(map: MapHandle, graphics: GraphicsContext, viewport: Viewport): SurfaceRenderTarget = - when (graphics) { - is EglGraphicsContext -> { - val descriptor = - OpenGLSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) - SurfaceRenderTarget(map.attachOpenGLSurface(descriptor)) - } - is VulkanGraphicsContext -> { - val descriptor = - VulkanSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) - SurfaceRenderTarget(map.attachVulkanSurface(descriptor)) + private val callerDriver = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD) + + fun attach(map: MapHandle, graphics: GraphicsContext, viewport: Viewport): SurfaceRenderTarget { + val attachment = + when (graphics) { + is EglGraphicsContext -> { + val descriptor = + OpenGLSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) + map.attachOpenGLSurface(descriptor, callerDriver) + } + is VulkanGraphicsContext -> { + val descriptor = + VulkanSurfaceDescriptor(viewport.extent, graphics.descriptor, graphics.surfacePointer) + map.attachVulkanSurface(descriptor, callerDriver) + } + else -> error("Unsupported graphics context: ${graphics::class.java.name}") } - else -> error("Unsupported graphics context: ${graphics::class.java.name}") + return fromAttachment(attachment) + } + + private fun fromAttachment(attachment: RenderSessionAttachment): SurfaceRenderTarget { + val target = SurfaceRenderTarget(attachment.session) + try { + target.complete(attachment.completed) + return target + } catch (error: Throwable) { + runCatching { attachment.session.abandon() } + runCatching { attachment.session.close() } + throw error } + } } } diff --git a/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SuspendBridge.kt b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SuspendBridge.kt new file mode 100644 index 000000000..be45a0dba --- /dev/null +++ b/examples/android-map/src/main/kotlin/org/maplibre/nativeffi/examples/androidmap/SuspendBridge.kt @@ -0,0 +1,23 @@ +package org.maplibre.nativeffi.examples.androidmap + +import java.util.concurrent.CountDownLatch +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine + +internal fun runSuspend(block: suspend () -> T): T { + val completed = CountDownLatch(1) + var outcome: Result? = null + block.startCoroutine( + object : Continuation { + override val context = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + outcome = result + completed.countDown() + } + } + ) + completed.await() + return outcome?.getOrThrow() ?: error("suspend call completed without a result") +} diff --git a/examples/c-map/CMakeLists.txt b/examples/c-map/CMakeLists.txt index b49db8a71..ddf8cbc48 100644 --- a/examples/c-map/CMakeLists.txt +++ b/examples/c-map/CMakeLists.txt @@ -25,7 +25,7 @@ pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3) add_executable( c-map - channel.c + render_request.c diagnostics.c input.c main.c diff --git a/examples/c-map/channel.c b/examples/c-map/channel.c deleted file mode 100644 index 88eccf949..000000000 --- a/examples/c-map/channel.c +++ /dev/null @@ -1,145 +0,0 @@ -#include -#include - -#include "channel.h" - -#include "util.h" - -void command_list_deinit(command_list* list) { - free(list->items); - *list = (command_list){}; -} - -void command_queue_init(command_queue* queue) { - *queue = (command_queue){}; - queue->lock = SDL_CreateMutex(); - if (queue->lock == nullptr) { - abort(); - } -} - -void command_queue_deinit(command_queue* queue) { - command_list_deinit(&queue->pending); - SDL_DestroyMutex(queue->lock); -} - -void command_queue_push(command_queue* queue, camera_command command) { - SDL_LockMutex(queue->lock); - command_list* pending = &queue->pending; - if (pending->len == pending->cap) { - const size_t cap = pending->cap == 0 ? 16 : pending->cap * 2; - camera_command* items = - realloc(pending->items, cap * sizeof(camera_command)); - if (items == nullptr) { - fputs("camera command queue out of memory\n", stderr); - abort(); - } - pending->items = items; - pending->cap = cap; - } - pending->items[pending->len++] = command; - SDL_UnlockMutex(queue->lock); -} - -void command_queue_drain_into(command_queue* queue, command_list* out) { - out->len = 0; - SDL_LockMutex(queue->lock); - const command_list drained = queue->pending; - queue->pending = *out; - *out = drained; - SDL_UnlockMutex(queue->lock); -} - -void render_request_init(render_request* request) { - // Starts set, so the render loop draws a first frame unprompted. - render_request_set(request); -} - -void render_request_set(render_request* request) { - atomic_store_explicit(&request->value, true, memory_order_release); -} - -bool render_request_consume(render_request* request) { - return atomic_exchange_explicit(&request->value, false, memory_order_acq_rel); -} - -void map_channel_init(map_channel* channel) { - *channel = (map_channel){ - .map = MLN_HANDLE_NULL, - .wake = MLN_HANDLE_NULL, - }; - channel->lock = SDL_CreateMutex(); - if (channel->lock == nullptr) { - abort(); - } -} - -void map_channel_deinit(map_channel* channel) { - SDL_DestroyMutex(channel->lock); -} - -void map_channel_publish( - map_channel* channel, mln_map map, mln_wake_source wake -) { - SDL_LockMutex(channel->lock); - channel->map = map; - channel->wake = wake; - atomic_store_explicit(&channel->published, true, memory_order_release); - SDL_UnlockMutex(channel->lock); -} - -void map_channel_wake_runtime_loop(map_channel* channel) { - if (!atomic_load_explicit(&channel->published, memory_order_acquire)) { - return; - } - SDL_LockMutex(channel->lock); - if (channel->wake != MLN_HANDLE_NULL) { - mln_wake_source_signal(channel->wake); - } - SDL_UnlockMutex(channel->lock); -} - -bool map_channel_try_map(map_channel* channel, mln_map* out_map) { - if (!atomic_load_explicit(&channel->published, memory_order_acquire)) { - return false; - } - SDL_LockMutex(channel->lock); - *out_map = channel->map; - SDL_UnlockMutex(channel->lock); - return *out_map != MLN_HANDLE_NULL; -} - -void map_channel_request_shutdown(map_channel* channel) { - atomic_store_explicit(&channel->shutdown, true, memory_order_release); - // Release the pump so shutdown is observed now. - map_channel_wake_runtime_loop(channel); -} - -bool map_channel_shutdown_requested(map_channel* channel) { - return atomic_load_explicit(&channel->shutdown, memory_order_acquire); -} - -void map_channel_await_shutdown(map_channel* channel) { - while (!map_channel_shutdown_requested(channel)) { - sleep_milliseconds(1); - } -} - -void map_channel_fail(map_channel* channel, app_error error) { - SDL_LockMutex(channel->lock); - if (channel->failure == APP_OK) { - channel->failure = error; - } - atomic_store_explicit(&channel->failed, true, memory_order_release); - SDL_UnlockMutex(channel->lock); -} - -bool map_channel_failure(map_channel* channel, app_error* out_error) { - if (!atomic_load_explicit(&channel->failed, memory_order_acquire)) { - return false; - } - SDL_LockMutex(channel->lock); - *out_error = channel->failure; - SDL_UnlockMutex(channel->lock); - return true; -} diff --git a/examples/c-map/channel.h b/examples/c-map/channel.h deleted file mode 100644 index e1349c0d4..000000000 --- a/examples/c-map/channel.h +++ /dev/null @@ -1,152 +0,0 @@ -// The cross-thread surface between the render loop, which owns the window and -// the render session, and the runtime loop, which owns the runtime and the map. - -#ifndef C_MAP_CHANNEL_H -#define C_MAP_CHANNEL_H - -#include -#include -#include -#include - -#include "types.h" - -typedef enum camera_command_kind : uint8_t { - CAMERA_COMMAND_CANCEL_TRANSITIONS, - CAMERA_COMMAND_SET_GESTURE_IN_PROGRESS, - CAMERA_COMMAND_MOVE_BY, - CAMERA_COMMAND_MOVE_BY_ANIMATED, - CAMERA_COMMAND_SCALE_BY, - CAMERA_COMMAND_SCALE_BY_ANIMATED, - CAMERA_COMMAND_PITCH_BY, - CAMERA_COMMAND_ADJUST_BEARING, - CAMERA_COMMAND_ADJUST_BEARING_ANIMATED, - CAMERA_COMMAND_ADJUST_PITCH_ANIMATED, - CAMERA_COMMAND_RESET_ORIENTATION, -} camera_command_kind; - -/// A camera change decoded on the render loop and applied on the map's owner -/// thread. Commands carry deltas rather than absolute targets, because reading -/// the camera and writing the new one has to happen together on that thread. -typedef struct camera_command { - camera_command_kind kind; - union { - struct { - bool in_progress; - } set_gesture_in_progress; - struct { - double dx; - double dy; - } move_by; - struct { - double dx; - double dy; - double duration_ms; - } move_by_animated; - struct { - double scale; - mln_screen_point anchor; - } scale_by; - struct { - double scale; - mln_screen_point anchor; - double duration_ms; - } scale_by_animated; - struct { - double delta; - } delta; - struct { - double delta; - double duration_ms; - } animated_delta; - struct { - double duration_ms; - } reset_orientation; - } as; -} camera_command; - -typedef struct command_list { - camera_command* items; - size_t len; - size_t cap; -} command_list; - -void command_list_deinit(command_list* list); - -/// Pending camera commands, filled by the render loop and drained by the -/// runtime loop. -/// -/// The queue grows rather than dropping: a dropped delta is motion the drag -/// never gets back, and a dropped gesture bracket leaves every delta after it -/// attributed to no gesture. -typedef struct command_queue { - SDL_Mutex* lock; - command_list pending; -} command_queue; - -void command_queue_init(command_queue* queue); -void command_queue_deinit(command_queue* queue); - -/// Render loop: queues one decoded camera change. Aborts on allocation -/// failure. -void command_queue_push(command_queue* queue, camera_command command); - -/// Runtime loop: hands over everything queued so far and takes `out` in -/// exchange. -void command_queue_drain_into(command_queue* queue, command_list* out); - -/// One-bit signal that a frame is worth drawing. -typedef struct render_request { - atomic_bool value; -} render_request; - -void render_request_init(render_request* request); -void render_request_set(render_request* request); -bool render_request_consume(render_request* request); - -/// Publishes the map and the runtime's wake source from the runtime loop to -/// the render loop, and carries shutdown and failure the other way. -/// -/// The render loop uses its copy of the map handle only to attach a session, -/// which native serves from any thread; every other map call stays on the -/// runtime loop. -typedef struct map_channel { - SDL_Mutex* lock; - mln_map map; - mln_wake_source wake; - atomic_bool published; - atomic_bool shutdown; - atomic_bool failed; - app_error failure; -} map_channel; - -void map_channel_init(map_channel* channel); -void map_channel_deinit(map_channel* channel); - -/// Runtime loop: announces the map it just created and its wake source. -void map_channel_publish( - map_channel* channel, mln_map map, mln_wake_source wake -); - -/// Render loop: releases the runtime loop's parked pump. A no-op before the -/// runtime loop has published, when there is nothing parked yet. -void map_channel_wake_runtime_loop(map_channel* channel); - -/// Render loop: the map to attach against, once the runtime loop has one. -bool map_channel_try_map(map_channel* channel, mln_map* out_map); - -/// Render loop: asks the runtime loop to stop. Called only after the render -/// session is closed, because the map cannot be destroyed before then. -void map_channel_request_shutdown(map_channel* channel); -bool map_channel_shutdown_requested(map_channel* channel); - -/// Runtime loop: blocks until the render loop has closed its session. The map -/// cannot be destroyed before then. -void map_channel_await_shutdown(map_channel* channel); - -void map_channel_fail(map_channel* channel, app_error error); - -/// Returns whether a failure was recorded, writing the first one when so. -bool map_channel_failure(map_channel* channel, app_error* out_error); - -#endif // C_MAP_CHANNEL_H diff --git a/examples/c-map/input.c b/examples/c-map/input.c index d2229fa7f..66cac94f5 100644 --- a/examples/c-map/input.c +++ b/examples/c-map/input.c @@ -3,296 +3,292 @@ #include "input.h" +#include "diagnostics.h" + static constexpr double keyboard_animation_ms = 160.0; static constexpr double reset_animation_ms = 220.0; static double logical_coordinate( double value, uint32_t window_size, uint32_t logical_size ) { - if (window_size == 0) { - return value; - } - return value * (double)logical_size / (double)window_size; + return window_size == 0 ? value + : value * (double)logical_size / (double)window_size; } -static mln_screen_point logical_point( - double x, double y, viewport current_viewport -) { +static mln_screen_point logical_point(double x, double y, viewport value) { return (mln_screen_point){ - .x = logical_coordinate( - x, current_viewport.window_width, current_viewport.logical_width - ), - .y = logical_coordinate( - y, current_viewport.window_height, current_viewport.logical_height - ), + .x = logical_coordinate(x, value.window_width, value.logical_width), + .y = logical_coordinate(y, value.window_height, value.logical_height), }; } -static camera_command pan_animated(double dx, double dy) { - return (camera_command){ - .kind = CAMERA_COMMAND_MOVE_BY_ANIMATED, - .as.move_by_animated = { - .dx = dx, .dy = dy, .duration_ms = keyboard_animation_ms - }, - }; +static mln_animation_options animation(double duration_ms) { + mln_animation_options options = mln_animation_options_default(); + options.fields = MLN_ANIMATION_OPTION_DURATION; + options.duration_ms = duration_ms; + return options; } -static camera_command zoom_animated(double scale, mln_screen_point anchor) { - return (camera_command){ - .kind = CAMERA_COMMAND_SCALE_BY_ANIMATED, - .as.scale_by_animated = { - .scale = scale, .anchor = anchor, .duration_ms = keyboard_animation_ms - }, +static input_result submitted(app_error error) { + return (input_result){ + .handled = true, .camera_changed = error == APP_OK, .error = error }; } -static camera_command rotate_animated(double delta) { - return (camera_command){ - .kind = CAMERA_COMMAND_ADJUST_BEARING_ANIMATED, - .as.animated_delta = {.delta = delta, .duration_ms = keyboard_animation_ms}, - }; +static app_error submit_camera( + map_state* state, const mln_camera_options* camera, uint32_t mode, + double duration_ms, uint32_t gesture_phase +) { + mln_animation_options options = animation(duration_ms); + return map_state_update_camera( + state, camera, mode, mode == MLN_CAMERA_UPDATE_MODE_JUMP ? NULL : &options, + gesture_phase + ); } -static camera_command pitch_animated(double delta) { - return (camera_command){ - .kind = CAMERA_COMMAND_ADJUST_PITCH_ANIMATED, - .as.animated_delta = {.delta = delta, .duration_ms = keyboard_animation_ms}, - }; +static app_error camera_status(mln_status status) { + if (status == MLN_STATUS_OK) return APP_OK; + diagnostics_log_status("camera command failed", status); + return APP_ERROR_CAMERA_COMMAND_FAILED; } -static drag_mode drag_mode_for_button(uint8_t button) { - if (button == SDL_BUTTON_RIGHT) { - return DRAG_MODE_ROTATE; - } - if (button != SDL_BUTTON_LEFT) { - return DRAG_MODE_NONE; +static app_error pan( + map_state* state, double dx, double dy, uint32_t mode, double duration_ms +) { + mln_camera_delta delta = mln_camera_delta_default(); + delta.offset = (mln_screen_point){.x = dx, .y = dy}; + if (mode != MLN_CAMERA_UPDATE_MODE_JUMP) + delta.animation = animation(duration_ms); + return camera_status(mln_map_apply_camera_delta( + state->map, &delta, map_state_discarded_completion() + )); +} + +static app_error zoom( + map_state* state, double scale, mln_screen_point anchor, uint32_t mode, + double duration_ms +) { + mln_camera_delta delta = mln_camera_delta_default(); + delta.kind = MLN_CAMERA_DELTA_SCALE; + delta.amount = scale; + delta.has_anchor = true; + delta.anchor = anchor; + if (mode != MLN_CAMERA_UPDATE_MODE_JUMP) + delta.animation = animation(duration_ms); + return camera_status(mln_map_apply_camera_delta( + state->map, &delta, map_state_discarded_completion() + )); +} + +static app_error adjust_orientation( + map_state* state, double bearing_delta, double pitch_delta, uint32_t mode, + double duration_ms +) { + mln_camera_delta delta = mln_camera_delta_default(); + if (mode != MLN_CAMERA_UPDATE_MODE_JUMP) + delta.animation = animation(duration_ms); + mln_status status = MLN_STATUS_OK; + if (bearing_delta != 0.0) { + delta.kind = MLN_CAMERA_DELTA_BEARING; + delta.amount = bearing_delta; + status = mln_map_apply_camera_delta( + state->map, &delta, map_state_discarded_completion() + ); } - if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0) { - return DRAG_MODE_ROTATE; + if (status == MLN_STATUS_OK && pitch_delta != 0.0) { + delta.kind = MLN_CAMERA_DELTA_PITCH; + delta.amount = pitch_delta; + status = mln_map_apply_camera_delta( + state->map, &delta, map_state_discarded_completion() + ); } - return DRAG_MODE_PAN; + return camera_status(status); +} + +static drag_mode drag_mode_for_button(uint8_t button) { + if (button == SDL_BUTTON_RIGHT) return DRAG_MODE_ROTATE; + if (button != SDL_BUTTON_LEFT) return DRAG_MODE_NONE; + return (SDL_GetModState() & SDL_KMOD_CTRL) != 0 ? DRAG_MODE_ROTATE + : DRAG_MODE_PAN; } static input_result handle_mouse_button_down( input_controller* controller, const SDL_MouseButtonEvent* button, - command_queue* commands, viewport current_viewport + map_state* state, viewport value ) { - // A second button joins the live drag rather than starting one, leaving the - // drag's baseline position alone. - if (controller->drag_mode != DRAG_MODE_NONE) { + if (controller->drag_mode != DRAG_MODE_NONE) return (input_result){.handled = true}; - } - const drag_mode mode = drag_mode_for_button(button->button); - if (mode == DRAG_MODE_NONE) { - return (input_result){}; - } - - const mln_screen_point cursor = - logical_point(button->x, button->y, current_viewport); + if (mode == DRAG_MODE_NONE) return (input_result){}; + const mln_screen_point cursor = logical_point(button->x, button->y, value); controller->last_x = cursor.x; controller->last_y = cursor.y; - - // Queued first, so any transition stops before the first delta lands. - command_queue_push( - commands, (camera_command){.kind = CAMERA_COMMAND_CANCEL_TRANSITIONS} - ); - command_queue_push( - commands, (camera_command){ - .kind = CAMERA_COMMAND_SET_GESTURE_IN_PROGRESS, - .as.set_gesture_in_progress = {.in_progress = true}, - } - ); controller->drag_mode = mode; controller->drag_button = button->button; - return (input_result){.handled = true}; -} - -/// Every path that ends a drag runs through here, so the gesture bracket the -/// drag opened is always closed. -static void end_drag(input_controller* controller, command_queue* commands) { - if (controller->drag_mode == DRAG_MODE_NONE) { - return; - } - controller->drag_mode = DRAG_MODE_NONE; - controller->drag_button = 0; - command_queue_push( - commands, (camera_command){ - .kind = CAMERA_COMMAND_SET_GESTURE_IN_PROGRESS, - .as.set_gesture_in_progress = {.in_progress = false}, - } + mln_camera_options camera = mln_camera_options_default(); + const app_error error = map_state_update_camera( + state, &camera, MLN_CAMERA_UPDATE_MODE_JUMP, NULL, MLN_GESTURE_PHASE_BEGIN ); + return submitted(error); } static input_result handle_mouse_button_up( input_controller* controller, const SDL_MouseButtonEvent* button, - command_queue* commands + map_state* state ) { - if (button->button != SDL_BUTTON_LEFT && button->button != SDL_BUTTON_RIGHT) { + if (button->button != SDL_BUTTON_LEFT && button->button != SDL_BUTTON_RIGHT) return (input_result){}; - } - // The drag ends once, when the button that started it comes up. - if (button->button != controller->drag_button) { + if (button->button != controller->drag_button) return (input_result){.handled = true}; - } - end_drag(controller, commands); - controller->last_x = button->x; - controller->last_y = button->y; - return (input_result){.handled = true}; + controller->drag_mode = DRAG_MODE_NONE; + controller->drag_button = 0; + mln_camera_options camera = mln_camera_options_default(); + const app_error error = map_state_update_camera( + state, &camera, MLN_CAMERA_UPDATE_MODE_JUMP, NULL, MLN_GESTURE_PHASE_END + ); + return (input_result){.handled = true, .error = error}; } static input_result handle_mouse_motion( input_controller* controller, const SDL_MouseMotionEvent* motion, - command_queue* commands, viewport current_viewport + map_state* state, viewport value ) { - if (controller->drag_mode == DRAG_MODE_NONE) { - return (input_result){}; - } - - const mln_screen_point cursor = - logical_point(motion->x, motion->y, current_viewport); + if (controller->drag_mode == DRAG_MODE_NONE) return (input_result){}; + const mln_screen_point cursor = logical_point(motion->x, motion->y, value); const double dx = cursor.x - controller->last_x; const double dy = cursor.y - controller->last_y; controller->last_x = cursor.x; controller->last_y = cursor.y; - if (dx == 0 && dy == 0) { - return (input_result){.handled = true}; - } - - switch (controller->drag_mode) { - case DRAG_MODE_NONE: - break; - case DRAG_MODE_PAN: - command_queue_push( - commands, (camera_command){ - .kind = CAMERA_COMMAND_MOVE_BY, - .as.move_by = {.dx = dx, .dy = dy}, - } - ); - break; - case DRAG_MODE_ROTATE: - command_queue_push( - commands, (camera_command){ - .kind = CAMERA_COMMAND_ADJUST_BEARING, - .as.delta = {.delta = dx * 0.5}, - } - ); - command_queue_push( - commands, (camera_command){ - .kind = CAMERA_COMMAND_PITCH_BY, - .as.delta = {.delta = dy / 2.0}, - } - ); - break; - } - return (input_result){.handled = true, .camera_changed = true}; + if (dx == 0.0 && dy == 0.0) return (input_result){.handled = true}; + return submitted( + controller->drag_mode == DRAG_MODE_PAN + ? pan(state, dx, dy, MLN_CAMERA_UPDATE_MODE_JUMP, 0.0) + : adjust_orientation( + state, dx * 0.5, dy / 2.0, MLN_CAMERA_UPDATE_MODE_JUMP, 0.0 + ) + ); } static input_result handle_mouse_wheel( - const SDL_MouseWheelEvent* wheel, command_queue* commands, - viewport current_viewport + const SDL_MouseWheelEvent* wheel, map_state* state, viewport value ) { - const double delta = wheel->y; - if (delta == 0) { - return (input_result){.handled = true}; - } - - const mln_screen_point anchor = - logical_point(wheel->mouse_x, wheel->mouse_y, current_viewport); - command_queue_push( - commands, - (camera_command){ - .kind = CAMERA_COMMAND_SCALE_BY, - .as.scale_by = {.scale = pow(2.0, delta * 0.25), .anchor = anchor}, - } - ); - return (input_result){.handled = true, .camera_changed = true}; + if (wheel->y == 0.0) return (input_result){.handled = true}; + return submitted(zoom( + state, pow(2.0, wheel->y * 0.25), + logical_point(wheel->mouse_x, wheel->mouse_y, value), + MLN_CAMERA_UPDATE_MODE_JUMP, 0.0 + )); } static input_result handle_key_down( - const SDL_KeyboardEvent* key, command_queue* commands, - viewport current_viewport + const SDL_KeyboardEvent* key, map_state* state, viewport value ) { constexpr double pan_step = 120.0; constexpr double zoom_step = 1.25; constexpr double bearing_step = 10.0; constexpr double pitch_step = 5.0; const mln_screen_point center = { - .x = (double)current_viewport.logical_width / 2.0, - .y = (double)current_viewport.logical_height / 2.0, + .x = (double)value.logical_width / 2.0, + .y = (double)value.logical_height / 2.0, }; - - camera_command command; + app_error error; switch (key->scancode) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_A: - command = pan_animated(pan_step, 0); + error = pan( + state, pan_step, 0.0, MLN_CAMERA_UPDATE_MODE_EASE, keyboard_animation_ms + ); break; case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_D: - command = pan_animated(-pan_step, 0); + error = pan( + state, -pan_step, 0.0, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_UP: case SDL_SCANCODE_W: - command = pan_animated(0, pan_step); + error = pan( + state, 0.0, pan_step, MLN_CAMERA_UPDATE_MODE_EASE, keyboard_animation_ms + ); break; case SDL_SCANCODE_DOWN: case SDL_SCANCODE_S: - command = pan_animated(0, -pan_step); + error = pan( + state, 0.0, -pan_step, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_EQUALS: case SDL_SCANCODE_KP_PLUS: - command = zoom_animated(zoom_step, center); + error = zoom( + state, zoom_step, center, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_MINUS: case SDL_SCANCODE_KP_MINUS: - command = zoom_animated(1.0 / zoom_step, center); + error = zoom( + state, 1.0 / zoom_step, center, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_Q: - command = rotate_animated(-bearing_step); + error = adjust_orientation( + state, -bearing_step, 0.0, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_E: - command = rotate_animated(bearing_step); + error = adjust_orientation( + state, bearing_step, 0.0, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_RIGHTBRACKET: - command = pitch_animated(pitch_step); + error = adjust_orientation( + state, 0.0, pitch_step, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; case SDL_SCANCODE_LEFTBRACKET: - command = pitch_animated(-pitch_step); + error = adjust_orientation( + state, 0.0, -pitch_step, MLN_CAMERA_UPDATE_MODE_EASE, + keyboard_animation_ms + ); break; - case SDL_SCANCODE_0: - command = (camera_command){ - .kind = CAMERA_COMMAND_RESET_ORIENTATION, - .as.reset_orientation = {.duration_ms = reset_animation_ms}, - }; + case SDL_SCANCODE_0: { + mln_camera_options target = mln_camera_options_default(); + target.fields = MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH; + target.bearing = 0.0; + target.pitch = 0.0; + error = submit_camera( + state, &target, MLN_CAMERA_UPDATE_MODE_EASE, reset_animation_ms, + MLN_GESTURE_PHASE_NONE + ); break; + } default: return (input_result){}; } - - command_queue_push(commands, command); - return (input_result){.handled = true, .camera_changed = true}; + return submitted(error); } input_result input_controller_handle_event( - input_controller* controller, const SDL_Event* event, command_queue* commands, - viewport current_viewport + input_controller* controller, const SDL_Event* event, map_state* state, + viewport value ) { switch (event->type) { case SDL_EVENT_MOUSE_BUTTON_DOWN: - return handle_mouse_button_down( - controller, &event->button, commands, current_viewport - ); + return handle_mouse_button_down(controller, &event->button, state, value); case SDL_EVENT_MOUSE_BUTTON_UP: - return handle_mouse_button_up(controller, &event->button, commands); + return handle_mouse_button_up(controller, &event->button, state); case SDL_EVENT_MOUSE_MOTION: - return handle_mouse_motion( - controller, &event->motion, commands, current_viewport - ); + return handle_mouse_motion(controller, &event->motion, state, value); case SDL_EVENT_MOUSE_WHEEL: - return handle_mouse_wheel(&event->wheel, commands, current_viewport); + return handle_mouse_wheel(&event->wheel, state, value); case SDL_EVENT_KEY_DOWN: - return handle_key_down(&event->key, commands, current_viewport); + return handle_key_down(&event->key, state, value); default: return (input_result){}; } diff --git a/examples/c-map/input.h b/examples/c-map/input.h index 60ae1a409..326953133 100644 --- a/examples/c-map/input.h +++ b/examples/c-map/input.h @@ -1,12 +1,11 @@ -// Decodes host input into camera commands. Runs on the render loop, which does -// not own the map, so it only queues commands for the runtime loop to apply. +// Decodes host input and submits camera commands directly. #ifndef C_MAP_INPUT_H #define C_MAP_INPUT_H #include -#include "channel.h" +#include "map_state.h" #include "types.h" typedef enum drag_mode : uint8_t { @@ -18,18 +17,18 @@ typedef enum drag_mode : uint8_t { typedef struct input_result { bool handled; bool camera_changed; + app_error error; } input_result; typedef struct input_controller { drag_mode drag_mode; - /// The button that started the live drag; only its release ends the drag. uint8_t drag_button; double last_x; double last_y; } input_controller; input_result input_controller_handle_event( - input_controller* controller, const SDL_Event* event, command_queue* commands, + input_controller* controller, const SDL_Event* event, map_state* state, viewport current_viewport ); diff --git a/examples/c-map/main.c b/examples/c-map/main.c index 82d1a9bb9..d11d34fb6 100644 --- a/examples/c-map/main.c +++ b/examples/c-map/main.c @@ -1,113 +1,58 @@ -// App shell: command-line parsing, the SDL window, and the two loops. +// App shell: command-line parsing, the SDL window, and the render loop. #include #include +#include #include #include #include -#include "channel.h" #include "diagnostics.h" #include "input.h" #include "map_state.h" #include "render/render.h" +#include "render_request.h" #include "types.h" #include "util.h" #include "viewport.h" -/// Backstop for a parked pump that nothing signals; the wake source is what -/// normally releases it. -static constexpr int64_t park_timeout_milliseconds = 100; +typedef struct wake_receiver { + atomic_bool scheduled; + Uint32 event_type; +} wake_receiver; -typedef struct runtime_loop_args { - viewport initial_viewport; - command_queue* commands; - render_request* request; - map_channel* channel; -} runtime_loop_args; - -static app_error runtime_loop_body(runtime_loop_args* args, map_state* state) { - // The render loop signals this to release the parked pump. - mln_wake_source wake = MLN_HANDLE_NULL; - const mln_status wake_status = - mln_runtime_wake_source_acquire(state->runtime, &wake); - if (wake_status != MLN_STATUS_OK) { - diagnostics_log_status("wake source acquire failed", wake_status); - return APP_ERROR_WAKE_SOURCE_FAILED; - } - - command_list batch = {}; - - map_channel_publish(args->channel, state->map, wake); - - app_error error = APP_OK; - app_error failure = APP_OK; - while (!map_channel_shutdown_requested(args->channel) && - !map_channel_failure(args->channel, &failure)) { - error = map_state_apply_commands(state, args->commands, &batch); - if (error != APP_OK) { - break; - } - const mln_status pump_status = - mln_runtime_pump(state->runtime, park_timeout_milliseconds, -1); - if (pump_status != MLN_STATUS_OK) { - diagnostics_log_status("runtime pump failed", pump_status); - error = APP_ERROR_RUNTIME_PUMP_FAILED; - break; - } - bool render_update = false; - error = map_state_drain_events(state, &render_update); - if (error != APP_OK) { - break; - } - if (render_update) { - render_request_set(args->request); - } - } - - command_list_deinit(&batch); - mln_wake_source_destroy(wake); - return error; -} - -/// Owns the runtime and the map for their whole lifetime, on a thread that is -/// not the one presenting. -static int runtime_loop(void* userdata) { - runtime_loop_args* args = userdata; - - map_state state; - app_error error = map_state_init(&state, args->initial_viewport); - if (error != APP_OK) { - map_channel_fail(args->channel, error); - return 0; +/// Native callbacks only schedule receiver work. The SDL event loop performs +/// every C API drain later on the render-loop thread. +static void schedule_event_drain(void* user_data) { + wake_receiver* receiver = user_data; + if ( + atomic_exchange_explicit(&receiver->scheduled, true, memory_order_acq_rel) + ) { + return; } - - error = runtime_loop_body(args, &state); - if (error != APP_OK) { - map_channel_fail(args->channel, error); + SDL_Event event = {.type = receiver->event_type}; + if (!SDL_PushEvent(&event)) { + atomic_store_explicit(&receiver->scheduled, false, memory_order_release); } - - // A map with an attached session cannot be destroyed, so wait for the render - // loop to close its session first. - map_channel_await_shutdown(args->channel); - map_state_deinit(&state); - return 0; } -/// One render-loop iteration: input, resize handling, and at most one -/// consumed render request. Runs inside the frame scope the caller opened. static app_error render_loop_iteration( SDL_Window* window, render_target* target, viewport* current_viewport, - command_queue* commands, render_request* request, map_channel* channel, + map_state* state, render_request* request, wake_receiver* receiver, input_controller* controller, bool* running ) { - app_error failure; - if (map_channel_failure(channel, &failure)) { - return failure; - } - SDL_Event event; while (SDL_PollEvent(&event)) { + if (event.type == receiver->event_type) { + atomic_store_explicit(&receiver->scheduled, false, memory_order_release); + bool render_update = false; + MAP_TRY(map_state_drain_events(state, &render_update)); + if (render_update) { + render_request_set(request); + } + continue; + } + switch (event.type) { case SDL_EVENT_QUIT: case SDL_EVENT_WINDOW_CLOSE_REQUESTED: @@ -118,17 +63,16 @@ static app_error render_loop_iteration( case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: *current_viewport = viewport_get(window); viewport_log("resized viewport", *current_viewport); + MAP_TRY(map_state_resize(state, *current_viewport)); MAP_TRY(render_target_resize(target, *current_viewport)); - // The resize is queued to the map's owner thread; release its pump. - map_channel_wake_runtime_loop(channel); render_request_set(request); break; default: { const input_result result = input_controller_handle_event( - controller, &event, commands, *current_viewport + controller, &event, state, *current_viewport ); - if (result.handled) { - map_channel_wake_runtime_loop(channel); + if (result.error != APP_OK) { + return result.error; } if (result.camera_changed) { render_request_set(request); @@ -138,8 +82,6 @@ static app_error render_loop_iteration( } } - MAP_TRY(render_target_finish_frame(target)); - // Consume before rendering, so a request published during the render call is // not discarded. if (render_request_consume(request)) { @@ -149,28 +91,19 @@ static app_error render_loop_iteration( render_request_set(request); } } + MAP_TRY(render_target_finish_frame(target)); // Stand-in for a display-refresh subscription. sleep_milliseconds(8); return APP_OK; } -/// The display-paced render loop. Owns the window, input, and the render -/// session once it adopts it. static app_error render_loop( SDL_Window* window, render_target_mode mode, render_target* target, - viewport* current_viewport, command_queue* commands, render_request* request, - map_channel* channel + viewport* current_viewport, map_state* state, render_request* request, + wake_receiver* receiver ) { - mln_map map = MLN_HANDLE_NULL; - while (!map_channel_try_map(channel, &map)) { - app_error failure; - if (map_channel_failure(channel, &failure)) { - return failure; - } - sleep_milliseconds(1); - } - app_error error = render_target_attach(target, map, *current_viewport); + app_error error = render_target_attach(target, state->map, *current_viewport); if (error != APP_OK) { return error; } @@ -178,13 +111,14 @@ static app_error render_loop( printf("render target: %s\n", render_target_mode_label(mode)); printf("render target status: %s\n", render_target_mode_status_line(mode)); input_log_controls(); + render_request_set(request); bool running = true; input_controller controller = {}; while (running) { void* frame_scope = render_target_frame_scope_open(); error = render_loop_iteration( - window, target, current_viewport, commands, request, channel, &controller, + window, target, current_viewport, state, request, receiver, &controller, &running ); render_target_frame_scope_close(frame_scope); @@ -254,17 +188,13 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } - mln_log_set_callback(diagnostics_log_record, nullptr); - + mln_log_set_callback(diagnostics_log_record, nullptr, nullptr); int exit_code = EXIT_FAILURE; - render_target_apply_sdl_hints(); - if (!SDL_Init(SDL_INIT_VIDEO)) { fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); goto out_log_callback; } - error = render_target_configure_video(); if (error != APP_OK) { fprintf(stderr, "c-map failed: %s\n", app_error_name(error)); @@ -286,9 +216,6 @@ int main(int argc, char** argv) { viewport current_viewport = viewport_get(window); viewport_log("initial viewport", current_viewport); - - // The graphics context, the render session, and every presentation resource - // belong to this thread, which owns the window. render_target* target = nullptr; error = render_target_init(&target, window, current_viewport, mode); if (error != APP_OK) { @@ -296,53 +223,42 @@ int main(int argc, char** argv) { goto out_window; } - command_queue commands; - command_queue_init(&commands); - render_request request; - render_request_init(&request); - map_channel channel; - map_channel_init(&channel); - - runtime_loop_args args = { - .initial_viewport = current_viewport, - .commands = &commands, - .request = &request, - .channel = &channel, + wake_receiver receiver = { + .event_type = SDL_RegisterEvents(1), }; - SDL_Thread* runtime_thread = - SDL_CreateThread(runtime_loop, "runtime-loop", &args); - if (runtime_thread == nullptr) { - fprintf( - stderr, "c-map failed: %s\n", - app_error_name(APP_ERROR_THREAD_SPAWN_FAILED) - ); - render_target_deinit(target); - goto out_channels; + atomic_init(&receiver.scheduled, false); + if (receiver.event_type == 0) { + error = APP_ERROR_EVENT_DRAIN_FAILED; + goto out_target; } + map_state state; + error = + map_state_init(&state, current_viewport, schedule_event_drain, &receiver); + if (error != APP_OK) { + goto out_target; + } + + render_request request; + render_request_init(&request); error = render_loop( - window, mode, target, ¤t_viewport, &commands, &request, &channel + window, mode, target, ¤t_viewport, &state, &request, &receiver ); - // Destroy the session before the runtime loop destroys the map: a map with - // an attached session cannot be destroyed. + // The graphics-thread-affine session closes before its map and runtime. render_target_deinit(target); - map_channel_request_shutdown(&channel); - SDL_WaitThread(runtime_thread, nullptr); + target = nullptr; + map_state_deinit(&state); - app_error failure = APP_OK; - if (error == APP_OK && map_channel_failure(&channel, &failure)) { - error = failure; - } if (error == APP_OK) { exit_code = EXIT_SUCCESS; } else { fprintf(stderr, "c-map failed: %s\n", app_error_name(error)); } + goto out_window; -out_channels: - map_channel_deinit(&channel); - command_queue_deinit(&commands); +out_target: + render_target_deinit(target); out_window: SDL_DestroyWindow(window); out_sdl: diff --git a/examples/c-map/map_state.c b/examples/c-map/map_state.c index b5e85d588..96beb142c 100644 --- a/examples/c-map/map_state.c +++ b/examples/c-map/map_state.c @@ -1,177 +1,113 @@ +#include +#include + #include "map_state.h" #include "diagnostics.h" -#include "util.h" -static double clamp(double value, double min, double max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; +typedef struct map_create_completion { + atomic_bool completed; + mln_status status; + mln_map map; +} map_create_completion; + +static void discard_completion( + void* user_data, const mln_completion_result* result +) { + (void)user_data; + (void)result; } -static app_error expect_camera_status(mln_status status, const char* message) { - if (status != MLN_STATUS_OK) { - diagnostics_log_status(message, status); - return APP_ERROR_CAMERA_COMMAND_FAILED; +static const mln_completion discarded_completion = { + .size = sizeof(mln_completion), + .callback = discard_completion, +}; + +const mln_completion* map_state_discarded_completion(void) { + return &discarded_completion; +} + +static void complete_map_create( + void* user_data, const mln_completion_result* result +) { + map_create_completion* state = user_data; + state->status = result->status; + if (result->status == MLN_STATUS_OK && result->value_count == 1) { + state->map = *(const mln_map*)result->value; } - return APP_OK; + atomic_store_explicit(&state->completed, true, memory_order_release); } -static app_error current_camera(mln_map map, mln_camera_options* out_camera) { - *out_camera = mln_camera_options_default(); - const mln_status status = mln_map_get_camera(map, out_camera); +static app_error create_runtime( + map_state* state, mln_wake_callback callback, void* user_data +) { + mln_runtime_options options = mln_runtime_options_default(); + options.cache_path = ":memory:"; + options.event_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = callback, + .user_data = user_data, + }; + const mln_status status = mln_runtime_create(&options, &state->runtime); if (status != MLN_STATUS_OK) { - diagnostics_log_status("camera snapshot failed", status); - return APP_ERROR_CAMERA_COMMAND_FAILED; + diagnostics_log_status("runtime create failed", status); + return APP_ERROR_RUNTIME_CREATE_FAILED; } return APP_OK; } -static mln_animation_options animation(double duration_ms) { - mln_animation_options options = mln_animation_options_default(); - options.fields = MLN_ANIMATION_OPTION_DURATION; - options.duration_ms = duration_ms; - return options; -} +static app_error create_map(map_state* state, viewport initial_viewport) { + mln_map_options options = mln_map_options_default(); + options.initial_extent = (mln_logical_extent){ + .width = initial_viewport.logical_width, + .height = initial_viewport.logical_height, + .scale_factor = initial_viewport.scale_factor, + }; + options.map_mode = MLN_MAP_MODE_CONTINUOUS; -/// Applies one decoded camera command. Runs on the map's owner thread, so the -/// read-modify-write commands read the current camera here. -static app_error apply_camera_command(mln_map map, camera_command command) { - switch (command.kind) { - case CAMERA_COMMAND_CANCEL_TRANSITIONS: - return expect_camera_status( - mln_map_cancel_transitions(map), "cancel camera transitions failed" - ); - case CAMERA_COMMAND_SET_GESTURE_IN_PROGRESS: - return expect_camera_status( - mln_map_set_gesture_in_progress( - map, command.as.set_gesture_in_progress.in_progress - ), - "set gesture in progress failed" - ); - case CAMERA_COMMAND_MOVE_BY: - return expect_camera_status( - mln_map_move_by(map, command.as.move_by.dx, command.as.move_by.dy), - "camera pan failed" - ); - case CAMERA_COMMAND_MOVE_BY_ANIMATED: { - const mln_animation_options options = - animation(command.as.move_by_animated.duration_ms); - return expect_camera_status( - mln_map_move_by_animated( - map, command.as.move_by_animated.dx, command.as.move_by_animated.dy, - &options - ), - "keyboard pan failed" - ); - } - case CAMERA_COMMAND_SCALE_BY: - return expect_camera_status( - mln_map_scale_by( - map, command.as.scale_by.scale, &command.as.scale_by.anchor - ), - "camera zoom failed" - ); - case CAMERA_COMMAND_SCALE_BY_ANIMATED: { - const mln_animation_options options = - animation(command.as.scale_by_animated.duration_ms); - return expect_camera_status( - mln_map_scale_by_animated( - map, command.as.scale_by_animated.scale, - &command.as.scale_by_animated.anchor, &options - ), - "keyboard zoom failed" - ); - } - case CAMERA_COMMAND_PITCH_BY: - return expect_camera_status( - mln_map_pitch_by(map, command.as.delta.delta), "camera pitch failed" - ); - case CAMERA_COMMAND_ADJUST_BEARING: { - mln_camera_options camera; - MAP_TRY(current_camera(map, &camera)); - const double bearing = - (camera.fields & MLN_CAMERA_OPTION_BEARING) != 0 ? camera.bearing : 0; - mln_camera_options target = mln_camera_options_default(); - target.fields = MLN_CAMERA_OPTION_BEARING; - target.bearing = bearing + command.as.delta.delta; - return expect_camera_status( - mln_map_jump_to(map, &target), "camera rotate failed" - ); - } - case CAMERA_COMMAND_ADJUST_BEARING_ANIMATED: { - mln_camera_options camera; - MAP_TRY(current_camera(map, &camera)); - const double bearing = - (camera.fields & MLN_CAMERA_OPTION_BEARING) != 0 ? camera.bearing : 0; - mln_camera_options target = mln_camera_options_default(); - target.fields = MLN_CAMERA_OPTION_BEARING; - target.bearing = bearing + command.as.animated_delta.delta; - const mln_animation_options options = - animation(command.as.animated_delta.duration_ms); - return expect_camera_status( - mln_map_ease_to(map, &target, &options), "keyboard rotate failed" - ); - } - case CAMERA_COMMAND_ADJUST_PITCH_ANIMATED: { - mln_camera_options camera; - MAP_TRY(current_camera(map, &camera)); - const double pitch = - (camera.fields & MLN_CAMERA_OPTION_PITCH) != 0 ? camera.pitch : 0; - mln_camera_options target = mln_camera_options_default(); - target.fields = MLN_CAMERA_OPTION_PITCH; - target.pitch = clamp(pitch + command.as.animated_delta.delta, 0.0, 60.0); - const mln_animation_options options = - animation(command.as.animated_delta.duration_ms); - return expect_camera_status( - mln_map_ease_to(map, &target, &options), "keyboard pitch failed" - ); - } - case CAMERA_COMMAND_RESET_ORIENTATION: { - mln_camera_options target = mln_camera_options_default(); - target.fields = MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH; - target.bearing = 0; - target.pitch = 0; - const mln_animation_options options = - animation(command.as.reset_orientation.duration_ms); - return expect_camera_status( - mln_map_ease_to(map, &target, &options), "camera reset failed" - ); - } + map_create_completion result = {.completed = false}; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = complete_map_create, + .user_data = &result, + }; + mln_status status = mln_map_create(state->runtime, &options, &completion); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("map create start failed", status); + return APP_ERROR_MAP_CREATE_FAILED; } - return APP_ERROR_CAMERA_COMMAND_FAILED; + while (!atomic_load_explicit(&result.completed, memory_order_acquire)) { + thrd_yield(); + } + if (result.status != MLN_STATUS_OK || result.map == MLN_HANDLE_NULL) { + diagnostics_log_status("map create failed", result.status); + return APP_ERROR_MAP_CREATE_FAILED; + } + state->map = result.map; + return APP_OK; } -/// Selects the two event types the runtime loop reads. The map queues no other -/// type once this returns, and it runs before the style load, because a map -/// keeps the events it has already queued. -static app_error select_events(mln_map map) { - const mln_status status = mln_map_set_event_mask( - map, MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED +static app_error configure_map(map_state* state) { + mln_status status = mln_map_set_event_mask( + state->map, + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE | + MLN_RUNTIME_EVENT_MASK_MAP_RENDER_FRAME_FINISHED, + &discarded_completion ); if (status != MLN_STATUS_OK) { diagnostics_log_status("event mask select failed", status); return APP_ERROR_EVENT_MASK_FAILED; } - return APP_OK; -} -static app_error load_style(mln_map map) { - const mln_status status = - mln_map_set_style_url(map, "https://tiles.openfreemap.org/styles/bright"); + status = mln_map_set_style_url( + state->map, "https://tiles.openfreemap.org/styles/bright", + &discarded_completion + ); if (status != MLN_STATUS_OK) { diagnostics_log_status("style load failed", status); return APP_ERROR_STYLE_LOAD_FAILED; } - return APP_OK; -} -static app_error set_camera(mln_map map) { mln_camera_options camera = mln_camera_options_default(); camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM | MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH; @@ -180,92 +116,128 @@ static app_error set_camera(mln_map map) { camera.zoom = 13.0; camera.bearing = 12.0; camera.pitch = 30.0; - const mln_status status = mln_map_jump_to(map, &camera); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("camera jump failed", status); - return APP_ERROR_CAMERA_JUMP_FAILED; - } - return APP_OK; + return map_state_update_camera( + state, &camera, MLN_CAMERA_UPDATE_MODE_JUMP, NULL, MLN_GESTURE_PHASE_NONE + ); } -app_error map_state_init(map_state* out_state, viewport initial_viewport) { - *out_state = (map_state){ - .runtime = MLN_HANDLE_NULL, - .map = MLN_HANDLE_NULL, - }; - - mln_runtime_options runtime_options = mln_runtime_options_default(); - runtime_options.cache_path = ":memory:"; - mln_status status = mln_runtime_create(&runtime_options, &out_state->runtime); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("runtime create failed", status); - return APP_ERROR_RUNTIME_CREATE_FAILED; - } - - mln_map_options map_options = mln_map_options_default(); - map_options.width = initial_viewport.logical_width; - map_options.height = initial_viewport.logical_height; - map_options.scale_factor = initial_viewport.scale_factor; - map_options.map_mode = MLN_MAP_MODE_CONTINUOUS; - status = mln_map_create(out_state->runtime, &map_options, &out_state->map); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("map create failed", status); - map_state_deinit(out_state); - return APP_ERROR_MAP_CREATE_FAILED; - } - - app_error error = select_events(out_state->map); +app_error map_state_init( + map_state* out_state, viewport initial_viewport, mln_wake_callback event_wake, + void* event_wake_user_data +) { + *out_state = (map_state){}; + app_error error = create_runtime(out_state, event_wake, event_wake_user_data); if (error == APP_OK) { - error = load_style(out_state->map); + error = create_map(out_state, initial_viewport); } if (error == APP_OK) { - error = set_camera(out_state->map); + error = configure_map(out_state); } if (error != APP_OK) { map_state_deinit(out_state); - return error; } - return APP_OK; + return error; +} + +typedef struct runtime_teardown_completion { + atomic_bool completed; +} runtime_teardown_completion; + +static void complete_runtime_teardown( + void* user_data, const mln_completion_result* result +) { + (void)result; + runtime_teardown_completion* teardown = user_data; + atomic_store_explicit(&teardown->completed, true, memory_order_release); } void map_state_deinit(map_state* state) { - // Children first: a runtime cannot be destroyed while its maps are live. + runtime_teardown_completion teardown = {.completed = false}; + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = complete_runtime_teardown, + .user_data = &teardown, + }; if (state->map != MLN_HANDLE_NULL) { - mln_map_destroy(state->map); + const mln_status status = mln_map_release(state->map, &completion); + if (status != MLN_STATUS_OK) + diagnostics_log_status("map release failed", status); + else + while (!atomic_load_explicit(&teardown.completed, memory_order_acquire)) + thrd_yield(); state->map = MLN_HANDLE_NULL; } if (state->runtime != MLN_HANDLE_NULL) { - mln_runtime_destroy(state->runtime); + atomic_store_explicit(&teardown.completed, false, memory_order_release); + const mln_status status = mln_runtime_release(state->runtime, &completion); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("runtime release failed", status); + } else { + // Waiting for the completion keeps process exit ordered after native + // teardown. + while (!atomic_load_explicit(&teardown.completed, memory_order_acquire)) { + thrd_yield(); + } + } state->runtime = MLN_HANDLE_NULL; } } -app_error map_state_apply_commands( - map_state* state, command_queue* commands, command_list* batch +app_error map_state_update_camera( + map_state* state, const mln_camera_options* camera, uint32_t mode, + const mln_animation_options* animation, uint32_t gesture_phase ) { - command_queue_drain_into(commands, batch); - for (size_t i = 0; i < batch->len; i += 1) { - const app_error error = apply_camera_command(state->map, batch->items[i]); - if (error != APP_OK) { - return error; - } + mln_camera_update update = mln_camera_update_default(); + update.mode = mode; + update.camera = *camera; + if (animation != NULL) { + update.animation = *animation; + } + update.gesture_phase = gesture_phase; + const mln_status status = + mln_map_update_camera(state->map, &update, &discarded_completion); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("camera command failed", status); + return APP_ERROR_CAMERA_COMMAND_FAILED; } return APP_OK; } -app_error map_state_drain_events(map_state* state, bool* out_render_update) { - *out_render_update = false; - // One drain takes every event the pump produced. The batch borrows runtime - // storage, and this loop keeps nothing from it. - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - const mln_status status = mln_runtime_drain_events(state->runtime, 0, &batch); +app_error map_state_resize(map_state* state, viewport value) { + const mln_logical_extent extent = { + .width = value.logical_width, + .height = value.logical_height, + .scale_factor = value.scale_factor, + }; + const mln_status status = + mln_map_resize(state->map, extent, &discarded_completion); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("map resize failed", status); + return APP_ERROR_MAP_RESIZE_FAILED; + } + return APP_OK; +} + +static app_error drain_runtime_events( + map_state* state, bool* out_render_update +) { + mln_event_batch batch = MLN_HANDLE_NULL; + mln_status status = mln_runtime_drain_events(state->runtime, &batch); if (status != MLN_STATUS_OK) { diagnostics_log_status("event drain failed", status); return APP_ERROR_EVENT_DRAIN_FAILED; } - for (size_t index = 0; index < batch.event_count; index += 1) { - // Step by the batch's stride: a newer runtime reports a wider event. - const char* bytes = (const char*)batch.events + index * batch.event_size; + mln_runtime_event_batch_view view = { + .size = sizeof(mln_runtime_event_batch_view), + }; + status = mln_event_batch_get(batch, &view); + if (status != MLN_STATUS_OK) { + mln_event_batch_release(batch); + diagnostics_log_status("event batch read failed", status); + return APP_ERROR_EVENT_DRAIN_FAILED; + } + for (size_t index = 0; index < view.event_count; index += 1) { + const char* bytes = (const char*)view.events + index * view.event_size; const mln_runtime_event* event = (const mln_runtime_event*)bytes; if ( event->source_type != MLN_RUNTIME_EVENT_SOURCE_MAP || @@ -273,18 +245,19 @@ app_error map_state_drain_events(map_state* state, bool* out_render_update) { ) { continue; } - switch (event->type) { - case MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE: - *out_render_update = true; - break; - case MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED: - if (event->payload.render_frame.needs_repaint) { - *out_render_update = true; - } - break; - default: - break; + if ( + event->type == MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE || + (event->type == MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED && + event->payload.render_frame.needs_repaint) + ) { + *out_render_update = true; } } + mln_event_batch_release(batch); return APP_OK; } + +app_error map_state_drain_events(map_state* state, bool* out_render_update) { + *out_render_update = false; + return drain_runtime_events(state, out_render_update); +} diff --git a/examples/c-map/map_state.h b/examples/c-map/map_state.h index a12268a6c..cb7f141f1 100644 --- a/examples/c-map/map_state.h +++ b/examples/c-map/map_state.h @@ -1,12 +1,10 @@ -// The runtime and the map, owned for their whole lifetime by the runtime loop -// thread. +// The runtime and map driven by the core-owned scheduler. #ifndef C_MAP_MAP_STATE_H #define C_MAP_MAP_STATE_H #include -#include "channel.h" #include "types.h" typedef struct map_state { @@ -15,18 +13,21 @@ typedef struct map_state { } map_state; [[nodiscard]] app_error map_state_init( - map_state* out_state, viewport initial_viewport + map_state* out_state, viewport initial_viewport, mln_wake_callback event_wake, + void* event_wake_user_data ); void map_state_deinit(map_state* state); -/// Applies every queued camera command on the map's owner thread. `batch` is -/// owned by the runtime loop and reused across drains. -[[nodiscard]] app_error map_state_apply_commands( - map_state* state, command_queue* commands, command_list* batch +/// Completion for accepted commands whose terminal metadata is not consumed. +const mln_completion* map_state_discarded_completion(void); + +[[nodiscard]] app_error map_state_update_camera( + map_state* state, const mln_camera_options* camera, uint32_t mode, + const mln_animation_options* animation, uint32_t gesture_phase ); +[[nodiscard]] app_error map_state_resize(map_state* state, viewport value); -/// Drains one batch of runtime events, reporting whether the map wants another -/// frame. +/// Drains the owned event queue on the render-loop receiver. [[nodiscard]] app_error map_state_drain_events( map_state* state, bool* out_render_update ); diff --git a/examples/c-map/render/metal/metal.c b/examples/c-map/render/metal/metal.c index 195c30129..b83deeaa7 100644 --- a/examples/c-map/render/metal/metal.c +++ b/examples/c-map/render/metal/metal.c @@ -453,6 +453,13 @@ app_error render_target_attach( render_target* target, mln_map map, viewport current_viewport ) { mln_render_session session = MLN_HANDLE_NULL; + render_completion completion; + render_completion_init(&completion); + const mln_render_session_attach_options options = + render_session_attach_options(); + render_session_kind kind = RENDER_SESSION_TEXTURE; + app_error error = APP_ERROR_TEXTURE_ATTACH_FAILED; + mln_status status = MLN_STATUS_INVALID_STATE; switch (target->mode) { case RENDER_TARGET_MODE_OWNED_TEXTURE: { mln_metal_owned_texture_descriptor descriptor = @@ -460,28 +467,18 @@ app_error render_target_attach( descriptor.extent = render_target_extent(current_viewport); descriptor.context = metal_context_descriptor(target->as.owned.compositor.view.device); - const mln_status status = - mln_metal_owned_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Metal texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_metal_owned_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_BORROWED_TEXTURE: { const mln_metal_borrowed_texture_descriptor descriptor = borrowed_texture_descriptor(target, current_viewport); - const mln_status status = - mln_metal_borrowed_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Metal borrowed texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_metal_borrowed_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_NATIVE_SURFACE: { mln_metal_surface_descriptor descriptor = @@ -490,18 +487,22 @@ app_error render_target_attach( descriptor.context = metal_context_descriptor(target->as.surface.view.device); descriptor.layer = target->as.surface.view.layer; - const mln_status status = - mln_metal_surface_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Metal surface attach failed", status); - return APP_ERROR_SURFACE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_SURFACE, .handle = session}; - return APP_OK; + kind = RENDER_SESSION_SURFACE; + error = APP_ERROR_SURFACE_ATTACH_FAILED; + status = mln_metal_surface_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } } - return APP_ERROR_BACKEND_SETUP_FAILED; + if (status != MLN_STATUS_OK) { + diagnostics_log_status("Metal render target attach failed", status); + return error; + } + target->session = (render_session){.kind = kind, .handle = session}; + return render_session_await_completion( + &target->session, &completion, error, "Metal render target attach failed" + ); } void render_target_deinit(render_target* target) { @@ -542,18 +543,24 @@ static app_error resize_borrowed( target->as.borrowed.texture = replacement; const mln_metal_borrowed_texture_descriptor descriptor = borrowed_texture_descriptor(target, current_viewport); - const mln_status status = - mln_metal_borrowed_texture_set_target(target->session.handle, &descriptor); + render_completion completion; + render_completion_init(&completion); + const mln_status status = mln_metal_borrowed_texture_set_target( + target->session.handle, &descriptor, &completion.descriptor + ); if (status != MLN_STATUS_OK) { - // The session may have taken the replacement before failing, so detach - // before either texture is released. - mln_render_session_detach(target->session.handle); diagnostics_log_status("Metal borrowed texture set target failed", status); target->as.borrowed.texture = previous; release_object(&replacement); return APP_ERROR_TEXTURE_RESIZE_FAILED; } - // Released only once the session has taken the replacement. + const app_error completed = render_session_await_completion( + &target->session, &completion, APP_ERROR_TEXTURE_RESIZE_FAILED, + "Metal borrowed texture set target failed" + ); + if (completed != APP_OK) { + return completed; + } release_object(&previous); return APP_OK; } @@ -588,25 +595,29 @@ static app_error render_update_owned( return APP_OK; } - mln_metal_owned_texture_frame frame = {.size = sizeof(frame)}; - const mln_status status = - mln_metal_owned_texture_acquire_frame(target->session.handle, &frame); - if (status == MLN_STATUS_INVALID_STATE) { + mln_acquired_frame acquired = MLN_HANDLE_NULL; + mln_status status = + mln_render_session_acquire_frame(target->session.handle, &acquired); + if (status == MLN_STATUS_NOT_READY) { return APP_OK; } if (status != MLN_STATUS_OK) { diagnostics_log_status("Metal texture acquire failed", status); return APP_ERROR_BACKEND_DRAW_FAILED; } - + mln_metal_owned_texture_frame frame = {.size = sizeof(frame)}; + status = mln_acquired_frame_get_metal_texture(acquired, &frame); bool presented = false; - const app_error error = metal_compositor_draw_texture( - &target->as.owned.compositor, (id)frame.texture, &presented - ); - const mln_status release_status = - mln_metal_owned_texture_release_frame(target->session.handle, &frame); - if (release_status != MLN_STATUS_OK) { - diagnostics_log_status("Metal texture release failed", release_status); + const app_error error = + status == MLN_STATUS_OK + ? metal_compositor_draw_texture( + &target->as.owned.compositor, (id)frame.texture, &presented + ) + : APP_ERROR_BACKEND_DRAW_FAILED; + mln_gpu_sync sync = mln_gpu_sync_default(); + status = mln_acquired_frame_release(&acquired, &sync); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("Metal texture release failed", status); } MAP_TRY(error); *out_rendered = presented; diff --git a/examples/c-map/render/opengl/opengl.c b/examples/c-map/render/opengl/opengl.c index 1ff8596a8..f45d39a72 100644 --- a/examples/c-map/render/opengl/opengl.c +++ b/examples/c-map/render/opengl/opengl.c @@ -579,6 +579,13 @@ app_error render_target_attach( render_target* target, mln_map map, viewport current_viewport ) { mln_render_session session = MLN_HANDLE_NULL; + render_completion completion; + render_completion_init(&completion); + const mln_render_session_attach_options options = + render_session_attach_options(); + render_session_kind kind = RENDER_SESSION_TEXTURE; + app_error error = APP_ERROR_TEXTURE_ATTACH_FAILED; + mln_status status = MLN_STATUS_INVALID_STATE; switch (target->mode) { case RENDER_TARGET_MODE_OWNED_TEXTURE: { mln_opengl_owned_texture_descriptor descriptor = @@ -586,44 +593,38 @@ app_error render_target_attach( descriptor.extent = render_target_extent(current_viewport); descriptor.context = opengl_context_descriptor(&target->as.owned.compositor.context); - const mln_status status = - mln_opengl_owned_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("OpenGL texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_opengl_owned_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_BORROWED_TEXTURE: { const mln_opengl_borrowed_texture_descriptor descriptor = borrowed_texture_descriptor(target, current_viewport); - const mln_status status = - mln_opengl_borrowed_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("OpenGL borrowed texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_opengl_borrowed_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_NATIVE_SURFACE: { const mln_opengl_surface_descriptor descriptor = surface_descriptor(target, current_viewport); - const mln_status status = - mln_opengl_surface_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("OpenGL surface attach failed", status); - return APP_ERROR_SURFACE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_SURFACE, .handle = session}; - return APP_OK; + kind = RENDER_SESSION_SURFACE; + error = APP_ERROR_SURFACE_ATTACH_FAILED; + status = mln_opengl_surface_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } } - return APP_ERROR_BACKEND_SETUP_FAILED; + if (status != MLN_STATUS_OK) { + diagnostics_log_status("OpenGL render target attach failed", status); + return error; + } + target->session = (render_session){.kind = kind, .handle = session}; + return render_session_await_completion( + &target->session, &completion, error, "OpenGL render target attach failed" + ); } void render_target_deinit(render_target* target) { @@ -666,12 +667,12 @@ static app_error resize_borrowed( target->as.borrowed.texture = replacement; const mln_opengl_borrowed_texture_descriptor descriptor = borrowed_texture_descriptor(target, current_viewport); - const mln_status status = - mln_opengl_borrowed_texture_set_target(target->session.handle, &descriptor); + render_completion completion; + render_completion_init(&completion); + const mln_status status = mln_opengl_borrowed_texture_set_target( + target->session.handle, &descriptor, &completion.descriptor + ); if (status != MLN_STATUS_OK) { - // The session may have taken the replacement before failing, so detach - // before either texture is released. - mln_render_session_detach(target->session.handle); diagnostics_log_status("OpenGL borrowed texture set target failed", status); target->as.borrowed.texture = previous; borrowed_texture_destroy( @@ -680,7 +681,13 @@ static app_error resize_borrowed( ); return APP_ERROR_TEXTURE_RESIZE_FAILED; } - // Released only once the session has taken the replacement. + const app_error completed = render_session_await_completion( + &target->session, &completion, APP_ERROR_TEXTURE_RESIZE_FAILED, + "OpenGL borrowed texture set target failed" + ); + if (completed != APP_OK) { + return completed; + } borrowed_texture_destroy( &target->as.borrowed.compositor.context, &target->as.borrowed.compositor.procs, &previous @@ -699,11 +706,6 @@ static app_error resize_surface( &target->as.surface.context, &replaced ); if (error != APP_OK) { - // SDL may already have dropped the surface the session presents through, - // so detach rather than leave it naming a surface that is gone. - if (target->session.kind == RENDER_SESSION_SURFACE) { - mln_render_session_detach(target->session.handle); - } return APP_ERROR_SURFACE_ATTACH_FAILED; } if (!replaced) { @@ -714,16 +716,19 @@ static app_error resize_surface( } const mln_opengl_surface_descriptor descriptor = surface_descriptor(target, current_viewport); - const mln_status status = - mln_opengl_surface_set_target(target->session.handle, &descriptor); + render_completion completion; + render_completion_init(&completion); + const mln_status status = mln_opengl_surface_set_target( + target->session.handle, &descriptor, &completion.descriptor + ); if (status != MLN_STATUS_OK) { - // SDL already dropped the outgoing surface and the session may have taken - // the replacement before failing, so detach. - mln_render_session_detach(target->session.handle); diagnostics_log_status("OpenGL surface set target failed", status); return APP_ERROR_SURFACE_ATTACH_FAILED; } - return APP_OK; + return render_session_await_completion( + &target->session, &completion, APP_ERROR_SURFACE_ATTACH_FAILED, + "OpenGL surface set target failed" + ); } app_error render_target_resize( @@ -766,24 +771,29 @@ static app_error render_update_owned( return APP_OK; } - mln_opengl_owned_texture_frame frame = {.size = sizeof(frame)}; - const mln_status status = - mln_opengl_owned_texture_acquire_frame(target->session.handle, &frame); - if (status == MLN_STATUS_INVALID_STATE) { + mln_acquired_frame acquired = MLN_HANDLE_NULL; + mln_status status = + mln_render_session_acquire_frame(target->session.handle, &acquired); + if (status == MLN_STATUS_NOT_READY) { return APP_OK; } if (status != MLN_STATUS_OK) { diagnostics_log_status("OpenGL texture acquire failed", status); return APP_ERROR_BACKEND_DRAW_FAILED; } - - const app_error error = opengl_compositor_draw_texture( - &target->as.owned.compositor, frame.texture, current_viewport - ); - const mln_status release_status = - mln_opengl_owned_texture_release_frame(target->session.handle, &frame); - if (release_status != MLN_STATUS_OK) { - diagnostics_log_status("OpenGL texture release failed", release_status); + mln_opengl_owned_texture_frame frame = {.size = sizeof(frame)}; + status = mln_acquired_frame_get_opengl_texture(acquired, &frame); + const app_error error = + status == MLN_STATUS_OK + ? opengl_compositor_draw_texture( + &target->as.owned.compositor, frame.texture, current_viewport + ) + : APP_ERROR_BACKEND_DRAW_FAILED; + target->as.owned.compositor.procs.Finish(); + mln_gpu_sync sync = mln_gpu_sync_default(); + status = mln_acquired_frame_release(&acquired, &sync); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("OpenGL texture release failed", status); } MAP_TRY(error); *out_rendered = true; diff --git a/examples/c-map/render/render.h b/examples/c-map/render/render.h index e654848bf..2b474d85e 100644 --- a/examples/c-map/render/render.h +++ b/examples/c-map/render/render.h @@ -39,8 +39,8 @@ void render_target_frame_scope_close(void* scope); render_target_mode mode ); -/// Attaches a render session against the published map. The calling thread -/// becomes the session's owner thread for its whole life. +/// Attaches a render session to the live map. The calling render-loop thread +/// remains the session's graphics-affine thread for its whole life. [[nodiscard]] app_error render_target_attach( render_target* target, mln_map map, viewport current_viewport ); diff --git a/examples/c-map/render/vulkan/vulkan.c b/examples/c-map/render/vulkan/vulkan.c index 3d835b528..db62f3528 100644 --- a/examples/c-map/render/vulkan/vulkan.c +++ b/examples/c-map/render/vulkan/vulkan.c @@ -333,7 +333,7 @@ struct render_target { union { struct { vulkan_compositor compositor; - mln_vulkan_owned_texture_frame pending_frame; + mln_acquired_frame pending_frame; bool has_pending_frame; } owned; struct { @@ -407,9 +407,9 @@ static void release_pending_frame(render_target* target) { if (!target->as.owned.has_pending_frame) { return; } - const mln_status status = mln_vulkan_owned_texture_release_frame( - target->session.handle, &target->as.owned.pending_frame - ); + mln_gpu_sync sync = mln_gpu_sync_default(); + const mln_status status = + mln_acquired_frame_release(&target->as.owned.pending_frame, &sync); if (status != MLN_STATUS_OK) { diagnostics_log_status("Vulkan texture release failed", status); } @@ -438,6 +438,13 @@ app_error render_target_attach( render_target* target, mln_map map, viewport current_viewport ) { mln_render_session session = MLN_HANDLE_NULL; + render_completion completion; + render_completion_init(&completion); + const mln_render_session_attach_options options = + render_session_attach_options(); + render_session_kind kind = RENDER_SESSION_TEXTURE; + app_error error = APP_ERROR_TEXTURE_ATTACH_FAILED; + mln_status status = MLN_STATUS_INVALID_STATE; switch (target->mode) { case RENDER_TARGET_MODE_OWNED_TEXTURE: { mln_vulkan_owned_texture_descriptor descriptor = @@ -445,28 +452,18 @@ app_error render_target_attach( descriptor.extent = render_target_extent(current_viewport); descriptor.context = vulkan_context_descriptor(&target->as.owned.compositor.context); - const mln_status status = - mln_vulkan_owned_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Vulkan texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_vulkan_owned_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_BORROWED_TEXTURE: { const mln_vulkan_borrowed_texture_descriptor descriptor = borrowed_image_descriptor(target, current_viewport); - const mln_status status = - mln_vulkan_borrowed_texture_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Vulkan borrowed texture attach failed", status); - return APP_ERROR_TEXTURE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_TEXTURE, .handle = session}; - return APP_OK; + status = mln_vulkan_borrowed_texture_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } case RENDER_TARGET_MODE_NATIVE_SURFACE: { mln_vulkan_surface_descriptor descriptor = @@ -475,18 +472,22 @@ app_error render_target_attach( descriptor.context = vulkan_context_descriptor(&target->as.surface.context); descriptor.surface = target->as.surface.context.surface; - const mln_status status = - mln_vulkan_surface_attach(map, &descriptor, &session); - if (status != MLN_STATUS_OK) { - diagnostics_log_status("Vulkan surface attach failed", status); - return APP_ERROR_SURFACE_ATTACH_FAILED; - } - target->session = - (render_session){.kind = RENDER_SESSION_SURFACE, .handle = session}; - return APP_OK; + kind = RENDER_SESSION_SURFACE; + error = APP_ERROR_SURFACE_ATTACH_FAILED; + status = mln_vulkan_surface_attach( + map, &descriptor, &options, &session, &completion.descriptor + ); + break; } } - return APP_ERROR_BACKEND_SETUP_FAILED; + if (status != MLN_STATUS_OK) { + diagnostics_log_status("Vulkan render target attach failed", status); + return error; + } + target->session = (render_session){.kind = kind, .handle = session}; + return render_session_await_completion( + &target->session, &completion, error, "Vulkan render target attach failed" + ); } void render_target_deinit(render_target* target) { @@ -537,12 +538,12 @@ static app_error resize_borrowed( target->as.borrowed.image = replacement; const mln_vulkan_borrowed_texture_descriptor descriptor = borrowed_image_descriptor(target, current_viewport); - const mln_status status = - mln_vulkan_borrowed_texture_set_target(target->session.handle, &descriptor); + render_completion completion; + render_completion_init(&completion); + const mln_status status = mln_vulkan_borrowed_texture_set_target( + target->session.handle, &descriptor, &completion.descriptor + ); if (status != MLN_STATUS_OK) { - // The session may have taken the replacement before failing, so detach - // before either image is released. - mln_render_session_detach(target->session.handle); diagnostics_log_status("Vulkan borrowed texture set target failed", status); target->as.borrowed.image = previous; borrowed_image_deinit( @@ -550,7 +551,13 @@ static app_error resize_borrowed( ); return APP_ERROR_TEXTURE_RESIZE_FAILED; } - // Released only once the session has taken the replacement. + const app_error completed = render_session_await_completion( + &target->session, &completion, APP_ERROR_TEXTURE_RESIZE_FAILED, + "Vulkan borrowed texture set target failed" + ); + if (completed != APP_OK) { + return completed; + } borrowed_image_deinit( &previous, target->as.borrowed.compositor.context.device ); @@ -600,33 +607,36 @@ static app_error render_update_owned( return APP_OK; } - mln_vulkan_owned_texture_frame frame = {.size = sizeof(frame)}; - const mln_status status = - mln_vulkan_owned_texture_acquire_frame(target->session.handle, &frame); - if (status == MLN_STATUS_INVALID_STATE) { + mln_acquired_frame acquired = MLN_HANDLE_NULL; + mln_status status = + mln_render_session_acquire_frame(target->session.handle, &acquired); + if (status == MLN_STATUS_NOT_READY) { return APP_OK; } if (status != MLN_STATUS_OK) { diagnostics_log_status("Vulkan texture acquire failed", status); return APP_ERROR_BACKEND_DRAW_FAILED; } - + mln_vulkan_owned_texture_frame frame = {.size = sizeof(frame)}; + status = mln_acquired_frame_get_vulkan_texture(acquired, &frame); + if (status != MLN_STATUS_OK) { + diagnostics_log_status("Vulkan texture access failed", status); + mln_gpu_sync sync = mln_gpu_sync_default(); + (void)mln_acquired_frame_release(&acquired, &sync); + return APP_ERROR_BACKEND_DRAW_FAILED; + } bool presented = false; const app_error error = vulkan_compositor_present_image_view( &target->as.owned.compositor, frame.image_view, &presented ); if (error != APP_OK || !presented) { - const mln_status release_status = - mln_vulkan_owned_texture_release_frame(target->session.handle, &frame); - if (release_status != MLN_STATUS_OK) { - diagnostics_log_status("Vulkan texture release failed", release_status); - } + mln_gpu_sync sync = mln_gpu_sync_default(); + status = mln_acquired_frame_release(&acquired, &sync); + if (status != MLN_STATUS_OK) + diagnostics_log_status("Vulkan texture release failed", status); return error; } - - // The frame stays acquired until the compositor's fence proves the sampling - // pass finished; finish_frame releases it. - target->as.owned.pending_frame = frame; + target->as.owned.pending_frame = acquired; target->as.owned.has_pending_frame = true; *out_rendered = true; return APP_OK; diff --git a/examples/c-map/render_request.c b/examples/c-map/render_request.c new file mode 100644 index 000000000..78ed36f30 --- /dev/null +++ b/examples/c-map/render_request.c @@ -0,0 +1,13 @@ +#include "render_request.h" + +void render_request_init(render_request* request) { + atomic_init(&request->value, false); +} + +void render_request_set(render_request* request) { + atomic_store_explicit(&request->value, true, memory_order_release); +} + +bool render_request_consume(render_request* request) { + return atomic_exchange_explicit(&request->value, false, memory_order_acq_rel); +} diff --git a/examples/c-map/render_request.h b/examples/c-map/render_request.h new file mode 100644 index 000000000..7cb884923 --- /dev/null +++ b/examples/c-map/render_request.h @@ -0,0 +1,17 @@ +// The one-bit render request shared with native wake callbacks. + +#ifndef C_MAP_RENDER_REQUEST_H +#define C_MAP_RENDER_REQUEST_H + +#include + +/// One-bit signal that a frame is worth drawing. +typedef struct render_request { + atomic_bool value; +} render_request; + +void render_request_init(render_request* request); +void render_request_set(render_request* request); +bool render_request_consume(render_request* request); + +#endif // C_MAP_RENDER_REQUEST_H diff --git a/examples/c-map/render_target.c b/examples/c-map/render_target.c index 9f507a784..4412a37ec 100644 --- a/examples/c-map/render_target.c +++ b/examples/c-map/render_target.c @@ -2,9 +2,80 @@ #include "diagnostics.h" +static app_error completion_failure( + app_error error, const char* message, mln_status status +) { + diagnostics_log_status(message, status); + return error; +} + +static void complete_render_work( + void* user_data, const mln_completion_result* result +) { + render_completion* completion = user_data; + completion->status = result->status; + atomic_store_explicit(&completion->completed, true, memory_order_release); +} + +void render_completion_init(render_completion* completion) { + *completion = (render_completion){ + .completed = false, + .descriptor = { + .size = sizeof(mln_completion), + .callback = complete_render_work, + .user_data = completion, + }, + }; +} + +app_error render_session_await_completion( + render_session* session, render_completion* completion, app_error error, + const char* message +) { + mln_status status = MLN_STATUS_OK; + while (!atomic_load_explicit(&completion->completed, memory_order_acquire)) { + size_t serviced = 0; + status = mln_render_session_service_driver_work( + session->handle, SIZE_MAX, &serviced + ); + if (status != MLN_STATUS_OK) { + return completion_failure(error, message, status); + } + } + if (completion->status != MLN_STATUS_OK) { + return completion_failure(error, message, completion->status); + } + return APP_OK; +} + +mln_render_session_attach_options render_session_attach_options(void) { + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; + options.requested_texture_ring_depth = 2; + return options; +} + void render_session_close(render_session* session) { if (session->kind != RENDER_SESSION_NONE) { - mln_render_session_destroy(session->handle); + render_completion completion; + render_completion_init(&completion); + const mln_status status = + mln_render_session_detach(session->handle, &completion.descriptor); + if (status == MLN_STATUS_OK) { + const app_error detached = render_session_await_completion( + session, &completion, APP_ERROR_BACKEND_SETUP_FAILED, + "render session detach failed" + ); + if (detached != APP_OK) { + mln_render_abandon_result result = {.size = sizeof(result)}; + (void)mln_render_session_abandon(session->handle, &result); + } + } else { + mln_render_abandon_result result = {.size = sizeof(result)}; + (void)mln_render_session_abandon(session->handle, &result); + } + (void)mln_render_session_destroy(session->handle); } *session = (render_session){.handle = MLN_HANDLE_NULL}; } @@ -16,18 +87,20 @@ app_error render_session_resize( return APP_ERROR_TEXTURE_RESIZE_FAILED; } const bool is_surface = session->kind == RENDER_SESSION_SURFACE; - const mln_status status = mln_render_session_resize( - session->handle, current_viewport.logical_width, - current_viewport.logical_height, current_viewport.scale_factor - ); + const mln_render_target_extent extent = + render_target_extent(current_viewport); + render_completion completion; + render_completion_init(&completion); + const mln_status status = + mln_render_session_resize(session->handle, &extent, &completion.descriptor); + const app_error error = is_surface ? APP_ERROR_SURFACE_RESIZE_FAILED + : APP_ERROR_TEXTURE_RESIZE_FAILED; + const char* message = + is_surface ? "surface resize failed" : "texture resize failed"; if (status != MLN_STATUS_OK) { - diagnostics_log_status( - is_surface ? "surface resize failed" : "texture resize failed", status - ); - return is_surface ? APP_ERROR_SURFACE_RESIZE_FAILED - : APP_ERROR_TEXTURE_RESIZE_FAILED; + return completion_failure(error, message, status); } - return APP_OK; + return render_session_await_completion(session, &completion, error, message); } app_error render_session_render_update( @@ -38,18 +111,58 @@ app_error render_session_render_update( return APP_OK; } const bool is_surface = session->kind == RENDER_SESSION_SURFACE; - mln_render_result result = MLN_RENDER_RESULT_RENDERED; - bool needs_repaint = false; - const mln_status status = - mln_render_session_render_update(session->handle, &result, &needs_repaint); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags |= is_surface ? MLN_FRAME_DEMAND_PRESENT : 0; + demand.token = ++session->next_frame_token; + mln_status status = + mln_render_session_request_frame(session->handle, &demand); + if (status != MLN_STATUS_OK) { + return completion_failure( + is_surface ? APP_ERROR_SURFACE_RENDER_FAILED + : APP_ERROR_TEXTURE_RENDER_FAILED, + "frame demand failed", status + ); + } + size_t serviced = 0; + status = mln_render_session_service_driver_work( + session->handle, SIZE_MAX, &serviced + ); + if (status != MLN_STATUS_OK) { + return completion_failure( + is_surface ? APP_ERROR_SURFACE_RENDER_FAILED + : APP_ERROR_TEXTURE_RENDER_FAILED, + "driver service failed", status + ); + } + mln_render_frame_batch batch = MLN_HANDLE_NULL; + status = mln_render_session_drain_frame_results(session->handle, &batch); + if (status == MLN_STATUS_NOT_READY) { + return APP_OK; + } + if (status != MLN_STATUS_OK) { + return completion_failure( + is_surface ? APP_ERROR_SURFACE_RENDER_FAILED + : APP_ERROR_TEXTURE_RENDER_FAILED, + "frame result drain failed", status + ); + } + size_t count = 0; + status = mln_render_frame_batch_count(batch, &count); + for (size_t i = 0; status == MLN_STATUS_OK && i < count; ++i) { + mln_render_frame_result result = {.size = sizeof(result)}; + status = mln_render_frame_batch_get(batch, i, &result); + if (status == MLN_STATUS_OK && result.token == demand.token) { + *out_rendered = result.disposition == MLN_RENDER_RESULT_RENDERED; + } + } + mln_render_frame_batch_release(batch); if (status != MLN_STATUS_OK) { - diagnostics_log_status( - is_surface ? "surface render failed" : "texture render failed", status + return completion_failure( + is_surface ? APP_ERROR_SURFACE_RENDER_FAILED + : APP_ERROR_TEXTURE_RENDER_FAILED, + "frame result read failed", status ); - return is_surface ? APP_ERROR_SURFACE_RENDER_FAILED - : APP_ERROR_TEXTURE_RENDER_FAILED; } - *out_rendered = result == MLN_RENDER_RESULT_RENDERED; return APP_OK; } diff --git a/examples/c-map/render_target.h b/examples/c-map/render_target.h index 8a902c7df..68b66e210 100644 --- a/examples/c-map/render_target.h +++ b/examples/c-map/render_target.h @@ -5,6 +5,7 @@ #define C_MAP_RENDER_TARGET_H #include +#include #include "types.h" @@ -17,22 +18,37 @@ typedef enum render_session_kind : uint8_t { typedef struct render_session { render_session_kind kind; mln_render_session handle; + uint64_t next_frame_token; } render_session; +typedef struct render_completion { + atomic_bool completed; + mln_status status; + mln_completion descriptor; +} render_completion; + +void render_completion_init(render_completion* completion); + void render_session_close(render_session* session); +/// Services caller-driver work until one accepted completion arrives. +[[nodiscard]] app_error render_session_await_completion( + render_session* session, render_completion* completion, app_error error, + const char* message +); + [[nodiscard]] app_error render_session_resize( render_session* session, viewport current_viewport ); -/// Renders the latest map update. Reports true only for -/// MLN_RENDER_RESULT_RENDERED, so the render loop sets the render request again -/// for a map that has no update yet, a size the map has not applied yet, and a -/// target that had no frame to draw into. +/// Submits one frame demand, services caller-driver work, and drains its +/// result. [[nodiscard]] app_error render_session_render_update( render_session* session, bool* out_rendered ); +mln_render_session_attach_options render_session_attach_options(void); + mln_render_target_extent render_target_extent(viewport current_viewport); #endif // C_MAP_RENDER_TARGET_H diff --git a/examples/c-map/types.c b/examples/c-map/types.c index 1cb3f5ab8..ef70f89cd 100644 --- a/examples/c-map/types.c +++ b/examples/c-map/types.c @@ -14,10 +14,10 @@ const char* app_error_name(app_error error) { return "texture attach failed"; case APP_ERROR_STYLE_LOAD_FAILED: return "style load failed"; - case APP_ERROR_CAMERA_JUMP_FAILED: - return "camera jump failed"; case APP_ERROR_CAMERA_COMMAND_FAILED: return "camera command failed"; + case APP_ERROR_MAP_RESIZE_FAILED: + return "map resize failed"; case APP_ERROR_TEXTURE_RESIZE_FAILED: return "texture resize failed"; case APP_ERROR_TEXTURE_RENDER_FAILED: @@ -34,16 +34,10 @@ const char* app_error_name(app_error error) { return "backend draw failed"; case APP_ERROR_RENDER_BACKEND_MISMATCH: return "render backend mismatch"; - case APP_ERROR_WAKE_SOURCE_FAILED: - return "wake source acquire failed"; - case APP_ERROR_RUNTIME_PUMP_FAILED: - return "runtime pump failed"; case APP_ERROR_EVENT_MASK_FAILED: return "event mask select failed"; case APP_ERROR_EVENT_DRAIN_FAILED: return "event drain failed"; - case APP_ERROR_THREAD_SPAWN_FAILED: - return "thread spawn failed"; } return "unknown error"; } diff --git a/examples/c-map/types.h b/examples/c-map/types.h index f98efb128..7c41510e7 100644 --- a/examples/c-map/types.h +++ b/examples/c-map/types.h @@ -12,8 +12,8 @@ typedef enum app_error : uint8_t { APP_ERROR_MAP_CREATE_FAILED, APP_ERROR_TEXTURE_ATTACH_FAILED, APP_ERROR_STYLE_LOAD_FAILED, - APP_ERROR_CAMERA_JUMP_FAILED, APP_ERROR_CAMERA_COMMAND_FAILED, + APP_ERROR_MAP_RESIZE_FAILED, APP_ERROR_TEXTURE_RESIZE_FAILED, APP_ERROR_TEXTURE_RENDER_FAILED, APP_ERROR_SURFACE_ATTACH_FAILED, @@ -22,11 +22,8 @@ typedef enum app_error : uint8_t { APP_ERROR_BACKEND_SETUP_FAILED, APP_ERROR_BACKEND_DRAW_FAILED, APP_ERROR_RENDER_BACKEND_MISMATCH, - APP_ERROR_WAKE_SOURCE_FAILED, - APP_ERROR_RUNTIME_PUMP_FAILED, APP_ERROR_EVENT_MASK_FAILED, APP_ERROR_EVENT_DRAIN_FAILED, - APP_ERROR_THREAD_SPAWN_FAILED, } app_error; const char* app_error_name(app_error error); diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/app/ComposeMapApp.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/app/ComposeMapApp.kt index 73eb18c7d..2af506e4e 100644 --- a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/app/ComposeMapApp.kt +++ b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/app/ComposeMapApp.kt @@ -92,7 +92,7 @@ private fun Modifier.mapGestures(renderer: MapLibreSurfaceRenderer, scaleFactor: val current = change?.takeIf { it.pressed } if (current == null) { if (previous != null) { - renderer.setGestureInProgress(false) + renderer.setGestureActive(false) } previous = null continue @@ -100,23 +100,23 @@ private fun Modifier.mapGestures(renderer: MapLibreSurfaceRenderer, scaleFactor: val last = previous previous = current if (last == null) { - renderer.cancelTransitions() - renderer.setGestureInProgress(true) + renderer.stopCameraAnimation() + renderer.setGestureActive(true) continue } val delta = current.position - last.position val rotate = event.buttons.isSecondaryPressed || event.keyboardModifiers.isCtrlPressed if (rotate) { - renderer.rotateAndPitchBy(delta.x.toDouble() / scale, delta.y.toDouble() / scale) + renderer.rotateAndPitch(delta.x.toDouble() / scale, delta.y.toDouble() / scale) } else { - renderer.moveBy(delta.x.toDouble() / scale, delta.y.toDouble() / scale) + renderer.pan(delta.x.toDouble() / scale, delta.y.toDouble() / scale) } current.consume() } } finally { if (previous != null) { - renderer.setGestureInProgress(false) + renderer.setGestureActive(false) } } } @@ -131,7 +131,7 @@ private fun Modifier.mapGestures(renderer: MapLibreSurfaceRenderer, scaleFactor: change.consume() val coordinateScale = scaleFactor.takeIf { it > 0.0 && it.isFinite() } ?: 1.0 val scale = 2.0.pow(-scrollY.toDouble() * SCROLL_ZOOM_FACTOR) - renderer.scaleBy( + renderer.zoom( scale, change.position.x.toDouble() / coordinateScale, change.position.y.toDouble() / coordinateScale, @@ -145,23 +145,23 @@ private fun Modifier.mapKeyboard(renderer: MapLibreSurfaceRenderer): Modifier = } when (event.key) { Key.DirectionLeft, - Key.A -> renderer.moveByAnimated(KEYBOARD_PAN, 0.0) + Key.A -> renderer.animatePan(KEYBOARD_PAN, 0.0) Key.DirectionRight, - Key.D -> renderer.moveByAnimated(-KEYBOARD_PAN, 0.0) + Key.D -> renderer.animatePan(-KEYBOARD_PAN, 0.0) Key.DirectionUp, - Key.W -> renderer.moveByAnimated(0.0, KEYBOARD_PAN) + Key.W -> renderer.animatePan(0.0, KEYBOARD_PAN) Key.DirectionDown, - Key.S -> renderer.moveByAnimated(0.0, -KEYBOARD_PAN) + Key.S -> renderer.animatePan(0.0, -KEYBOARD_PAN) Key.Equals, - Key.NumPadAdd -> renderer.scaleByAnimated(KEYBOARD_ZOOM) + Key.NumPadAdd -> renderer.animateZoom(KEYBOARD_ZOOM) Key.Minus, - Key.NumPadSubtract -> renderer.scaleByAnimated(1.0 / KEYBOARD_ZOOM) - Key.Q -> renderer.rotateBy(-KEYBOARD_BEARING) - Key.E -> renderer.rotateBy(KEYBOARD_BEARING) - Key.RightBracket -> renderer.pitchBy(KEYBOARD_PITCH) - Key.LeftBracket -> renderer.pitchBy(-KEYBOARD_PITCH) + Key.NumPadSubtract -> renderer.animateZoom(1.0 / KEYBOARD_ZOOM) + Key.Q -> renderer.animateBearing(-KEYBOARD_BEARING) + Key.E -> renderer.animateBearing(KEYBOARD_BEARING) + Key.RightBracket -> renderer.animatePitch(KEYBOARD_PITCH) + Key.LeftBracket -> renderer.animatePitch(-KEYBOARD_PITCH) Key.Zero, - Key.NumPad0 -> renderer.resetPitchAndBearing() + Key.NumPad0 -> renderer.resetOrientation() else -> return@onPreviewKeyEvent false } true diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapChannels.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapChannels.kt deleted file mode 100644 index 394d34b88..000000000 --- a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapChannels.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.maplibre.nativeffi.examples.composemap.map - -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicBoolean -import org.maplibre.nativeffi.geo.ScreenPoint - -/** - * A camera change decoded on the render loop and applied on the map's owner thread. - * - * Commands carry deltas rather than absolute targets, because reading the camera and writing the - * new one has to happen together on the thread that owns the map. - */ -internal sealed interface CameraCommand { - data object CancelTransitions : CameraCommand - - data class SetGestureInProgress(val inProgress: Boolean) : CameraCommand - - data class MoveBy(val deltaX: Double, val deltaY: Double) : CameraCommand - - data class MoveByAnimated(val deltaX: Double, val deltaY: Double) : CameraCommand - - data class ScaleBy(val scale: Double, val anchor: ScreenPoint) : CameraCommand - - data class ScaleByAnimated(val scale: Double, val anchor: ScreenPoint) : CameraCommand - - data class AdjustBearingAndPitch(val bearingDegrees: Double, val pitchDegrees: Double) : - CameraCommand - - data class AdjustBearingAnimated(val bearingDegrees: Double) : CameraCommand - - data class AdjustPitchAnimated(val pitchDegrees: Double) : CameraCommand - - data object ResetOrientation : CameraCommand -} - -/** - * Queue of pending camera commands, written by input handlers and read by the runtime loop. - * - * [onEnqueue] releases the runtime loop's parked pump, so input reaches it without waiting out the - * parking bound. - */ -internal class CameraCommandQueue { - private val queue = ConcurrentLinkedQueue() - - /** Set once the runtime loop has acquired its wake source. */ - @Volatile var onEnqueue: (() -> Unit)? = null - - fun enqueue(command: CameraCommand) { - queue.add(command) - wake() - } - - /** Moves every queued command into [out] so the runtime loop can apply them in order. */ - fun drainInto(out: MutableList) { - while (true) { - out.add(queue.poll() ?: return) - } - } - - fun wake() { - onEnqueue?.invoke() - } -} - -/** - * One-bit signal that a frame is worth drawing. The render loop consumes before it renders and sets - * again when nothing was rendered, so a request published during a render is not lost. - */ -internal class RenderRequest { - private val value = AtomicBoolean(true) - - fun set() { - value.set(true) - } - - fun consume(): Boolean = value.getAndSet(false) -} diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreNativeSurfaceAdapter.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreNativeSurfaceAdapter.kt index ad259f3ac..9dea5b754 100644 --- a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreNativeSurfaceAdapter.kt +++ b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreNativeSurfaceAdapter.kt @@ -1,5 +1,6 @@ package org.maplibre.nativeffi.examples.composemap.map +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.Maplibre import org.maplibre.nativeffi.examples.composemap.surface.EglContextHandles import org.maplibre.nativeffi.examples.composemap.surface.MetalTextureTarget @@ -19,6 +20,9 @@ import org.maplibre.nativeffi.render.NativePointer import org.maplibre.nativeffi.render.OpenGLBorrowedTextureDescriptor import org.maplibre.nativeffi.render.OpenGLContextDescriptor import org.maplibre.nativeffi.render.RenderBackend +import org.maplibre.nativeffi.render.RenderDriver +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.RenderTargetExtent import org.maplibre.nativeffi.render.VulkanBorrowedTextureDescriptor @@ -50,7 +54,7 @@ internal object MapLibreNativeSurfaceAdapter { return BorrowedTarget( sessionKey = SessionKey.Metal(target.device, target.pixelFormat), targetKey = TargetKey(target.generation, extent), - attach = { map -> map.attachMetalBorrowedTexture(descriptor) }, + attach = { map -> map.attachMetalBorrowedTexture(descriptor, callerDriverOptions) }, setTarget = { session -> session.setMetalBorrowedTextureTarget(descriptor) }, ) } @@ -77,7 +81,7 @@ internal object MapLibreNativeSurfaceAdapter { finalLayout = target.finalLayout, ), targetKey = TargetKey(target.generation, extent), - attach = { map -> map.attachVulkanBorrowedTexture(descriptor) }, + attach = { map -> map.attachVulkanBorrowedTexture(descriptor, callerDriverOptions) }, setTarget = { session -> session.setVulkanBorrowedTextureTarget(descriptor) }, ) } @@ -95,7 +99,7 @@ internal object MapLibreNativeSurfaceAdapter { return BorrowedTarget( sessionKey = SessionKey.OpenGl(target.context), targetKey = TargetKey(target.generation, extent), - attach = { map -> map.attachOpenGLBorrowedTexture(descriptor) }, + attach = { map -> map.attachOpenGLBorrowedTexture(descriptor, callerDriverOptions) }, setTarget = { session -> session.setOpenGLBorrowedTextureTarget(descriptor) }, ) } @@ -133,9 +137,12 @@ internal object MapLibreNativeSurfaceAdapter { class BorrowedTarget( val sessionKey: SessionKey, val targetKey: TargetKey, - val attach: (MapHandle) -> RenderSessionHandle, - val setTarget: (RenderSessionHandle) -> Unit, + val attach: (MapHandle) -> RenderSessionAttachment, + val setTarget: (RenderSessionHandle) -> Deferred, ) + + private val callerDriverOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD) } private fun SurfaceExtent.toRenderTargetExtent(): RenderTargetExtent = diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreSurfaceRenderer.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreSurfaceRenderer.kt index 5aead2936..df14333d1 100644 --- a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreSurfaceRenderer.kt +++ b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapLibreSurfaceRenderer.kt @@ -1,6 +1,7 @@ package org.maplibre.nativeffi.examples.composemap.map import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.examples.composemap.surface.NativeSurfaceFrame import org.maplibre.nativeffi.examples.composemap.surface.NativeSurfaceRenderResult import org.maplibre.nativeffi.examples.composemap.surface.NativeSurfaceRenderer @@ -9,30 +10,25 @@ import org.maplibre.nativeffi.examples.composemap.surface.ProducerBackend import org.maplibre.nativeffi.examples.composemap.surface.SurfaceExtent import org.maplibre.nativeffi.geo.ScreenPoint import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.render.FrameDemand import org.maplibre.nativeffi.render.RenderResult import org.maplibre.nativeffi.render.RenderSessionHandle /** - * The render loop. + * The native-surface render loop. * - * [render] runs on the bridge's producer thread, which owns the host graphics context and the - * borrowed texture, so that thread attaches the render session, renders through it, and closes it. - * It touches the map only to attach, which native serves from any thread. - * - * Input decoding runs on the Compose thread and only enqueues camera commands; [MapRuntimeLoop] - * applies them on the thread that owns the map. + * [render] runs on the bridge's producer thread, which owns the graphics context, borrowed texture, + * and render session. Runtime and map updates are submitted directly to the core-owned worker. */ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { override val backend: ProducerBackend = MapLibreNativeSurfaceAdapter.backend - private val commands = CameraCommandQueue() private val renderRequest = RenderRequest() private val closed = AtomicBoolean(false) - private val failureReported = AtomicBoolean(false) @Volatile private var surfaceSession: NativeSurfaceSession? = null @Volatile private var ownerSession: NativeSurfaceSession? = null - @Volatile private var runtimeLoop: MapRuntimeLoop? = null + @Volatile private var mapState: MapState? = null @Volatile private var renderSession: AttachedRenderSession? = null @Volatile private var currentExtent = SurfaceExtent.Empty @@ -55,22 +51,13 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { return NativeSurfaceRenderResult.Skipped } - val loop = ensureRuntimeLoop(frame.extent) - loop.failure?.let { error -> - if (failureReported.compareAndSet(false, true)) { - // The caller stops driving frames after this, so nothing else would close the session the - // runtime loop is waiting on before it can destroy the map. - close() - throw IllegalStateException("map runtime loop failed", error) - } - return NativeSurfaceRenderResult.Skipped - } - val map = loop.map ?: return NativeSurfaceRenderResult.Skipped + val state = ensureMapState(frame.extent) + state.resize(frame.extent) + state.pollEvents() return try { - renderAttached(map, frame) + renderAttached(state.map, frame) } catch (error: Throwable) { - // The caller stops driving frames after this, so nothing else would close the session the - // runtime loop is waiting on before it can destroy the map. + // The caller stops driving frames after this, so close the render session before the map. close() throw error } @@ -82,11 +69,12 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { if (!renderRequest.consume()) { return NativeSurfaceRenderResult.Skipped } - if (attached.session.renderUpdate().result == RenderResult.RENDERED) { + attached.session.requestFrame(FrameDemand()) + attached.session.serviceDriverWork() + if (attached.session.drainFrameResults().lastOrNull()?.disposition == RenderResult.RENDERED) { return NativeSurfaceRenderResult.Rendered } - // The map applies a new logical size on the runtime loop's next pump, so an attach or resize - // is followed by frames with nothing to render. + // A newly accepted map or target update may not have reached the render session yet. requestRender() return NativeSurfaceRenderResult.Skipped } @@ -107,10 +95,13 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { if (renderSession != null && owner != null) { owner.withRendererAccess { closeRenderSession() - stopRuntimeLoop() + stopMapState() } + } else if (renderSession != null) { + abandonRenderSession() + stopMapState() } else { - stopRuntimeLoop() + stopMapState() } } @@ -119,70 +110,69 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { surfaceSession?.requestFrame() } - fun moveBy(deltaX: Double, deltaY: Double) { - enqueue(CameraCommand.MoveBy(deltaX, deltaY)) - } - - fun scaleBy(scale: Double, anchorX: Double, anchorY: Double) { - enqueue(CameraCommand.ScaleBy(scale, ScreenPoint(anchorX, anchorY))) + fun pan(deltaX: Double, deltaY: Double) { + mapState?.moveBy(deltaX, deltaY) + requestRender() } - fun moveByAnimated(deltaX: Double, deltaY: Double) { - enqueue(CameraCommand.MoveByAnimated(deltaX, deltaY)) + fun zoom(scale: Double, anchorX: Double, anchorY: Double) { + mapState?.scaleBy(scale, ScreenPoint(anchorX, anchorY)) + requestRender() } - fun scaleByAnimated(scale: Double) { - enqueue(CameraCommand.ScaleByAnimated(scale, viewportCenter())) + fun animatePan(deltaX: Double, deltaY: Double) { + mapState?.moveByAnimated(deltaX, deltaY) + requestRender() } - fun rotateAndPitchBy(deltaX: Double, deltaY: Double) { - enqueue( - CameraCommand.AdjustBearingAndPitch(deltaX * DRAG_ROTATE_FACTOR, -deltaY * DRAG_PITCH_FACTOR) - ) + fun animateZoom(scale: Double) { + mapState?.scaleByAnimated(scale, viewportCenter()) + requestRender() } - fun rotateBy(deltaDegrees: Double) { - enqueue(CameraCommand.AdjustBearingAnimated(deltaDegrees)) + fun rotateAndPitch(deltaX: Double, deltaY: Double) { + mapState?.adjustBearingAndPitch(deltaX * DRAG_ROTATE_FACTOR, deltaY * DRAG_PITCH_FACTOR) + requestRender() } - fun pitchBy(deltaDegrees: Double) { - enqueue(CameraCommand.AdjustPitchAnimated(deltaDegrees)) + fun animateBearing(deltaDegrees: Double) { + mapState?.adjustBearingAnimated(deltaDegrees) + requestRender() } - fun resetPitchAndBearing() { - enqueue(CameraCommand.ResetOrientation) + fun animatePitch(deltaDegrees: Double) { + mapState?.adjustPitchAnimated(deltaDegrees) + requestRender() } - fun cancelTransitions() { - enqueue(CameraCommand.CancelTransitions) + fun resetOrientation() { + mapState?.resetOrientation() + requestRender() } - fun setGestureInProgress(inProgress: Boolean) { - enqueue(CameraCommand.SetGestureInProgress(inProgress)) + fun stopCameraAnimation() { + mapState?.cancelTransitions() + requestRender() } - private fun enqueue(command: CameraCommand) { - commands.enqueue(command) + fun setGestureActive(inProgress: Boolean) { + mapState?.setGestureInProgress(inProgress) requestRender() } private fun withRendererAccess(action: () -> T): T = ownerSession?.withRendererAccess(action) ?: action() - private fun ensureRuntimeLoop(extent: SurfaceExtent): MapRuntimeLoop { - runtimeLoop?.let { existing -> - if (existing.scaleFactor == extent.scaleFactor) { - return existing - } - closeRenderSession() - stopRuntimeLoop() + private fun ensureMapState(extent: SurfaceExtent): MapState { + mapState?.let { + return it } - return MapRuntimeLoop(extent, commands, ::requestRender).also { runtimeLoop = it } + return MapState(extent, ::requestRender).also { mapState = it } } - private fun stopRuntimeLoop() { - val stopping = runtimeLoop - runtimeLoop = null + private fun stopMapState() { + val stopping = mapState + mapState = null stopping?.close() } @@ -202,7 +192,7 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { return existing } try { - borrowed.setTarget(existing.session) + completeDriverOperation(existing.session, borrowed.setTarget(existing.session)) } catch (error: RuntimeException) { // A failed handover leaves it unknown which texture the session holds, and Skiko frees // the outgoing one as soon as it moves on, so close the session. @@ -221,8 +211,16 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { } closeRenderSession() + val attachment = borrowed.attach(map) + try { + completeDriverOperation(attachment.session, attachment.completed) + } catch (error: Throwable) { + runCatching { attachment.session.abandon() } + runCatching { attachment.session.close() } + throw error + } val attached = - AttachedRenderSession(borrowed.sessionKey, borrowed.targetKey, borrowed.attach(map)) + AttachedRenderSession(borrowed.sessionKey, borrowed.targetKey, attachment.session) renderSession = attached renderRequest.set() return attached @@ -231,7 +229,24 @@ internal class MapLibreSurfaceRenderer : NativeSurfaceRenderer { private fun closeRenderSession() { val closing = renderSession renderSession = null - closing?.session?.close() + closing?.session?.let { session -> + completeDriverOperation(session, session.detach()) + session.close() + } + } + + private fun abandonRenderSession() { + val closing = renderSession + renderSession = null + closing?.session?.let { session -> + session.abandon() + session.close() + } + } + + private fun completeDriverOperation(session: RenderSessionHandle, completed: Deferred) { + while (!completed.isCompleted) session.serviceDriverWork() + runSuspend { completed.await() } } private fun viewportCenter(): ScreenPoint { diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapRuntimeLoop.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapRuntimeLoop.kt deleted file mode 100644 index b5e8c73e0..000000000 --- a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapRuntimeLoop.kt +++ /dev/null @@ -1,225 +0,0 @@ -package org.maplibre.nativeffi.examples.composemap.map - -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.max -import kotlin.math.min -import kotlin.time.Duration.Companion.seconds -import kotlin.time.TimeSource -import org.maplibre.nativeffi.camera.AnimationOptions -import org.maplibre.nativeffi.camera.CameraOptions -import org.maplibre.nativeffi.examples.composemap.surface.SurfaceExtent -import org.maplibre.nativeffi.geo.LatLng -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.map.MapMode -import org.maplibre.nativeffi.map.MapOptions -import org.maplibre.nativeffi.runtime.RuntimeEventMask -import org.maplibre.nativeffi.runtime.RuntimeEventPayload -import org.maplibre.nativeffi.runtime.RuntimeEventType -import org.maplibre.nativeffi.runtime.RuntimeHandle -import org.maplibre.nativeffi.runtime.RuntimeOptions -import org.maplibre.nativeffi.runtime.WakeSource - -/** - * Owns the runtime and the map for their whole lifetime, on a dedicated thread that is not the one - * presenting. - * - * Native owner-thread checks are keyed on the OS thread, so this is a plain [Thread] rather than a - * coroutine dispatcher or a pooled executor. The render loop attaches its own session against the - * map published here, and closes it before [close] destroys the map. - */ -internal class MapRuntimeLoop( - private val initialExtent: SurfaceExtent, - private val commands: CameraCommandQueue, - private val requestRender: () -> Unit, -) : AutoCloseable { - /** The map's scale factor is fixed at creation, so a density change restarts the whole loop. */ - val scaleFactor: Double - get() = initialExtent.scaleFactor - - private val shutdownRequested = AtomicBoolean(false) - - @Volatile private var publishedMap: MapHandle? = null - - /** The first failure seen on this loop, republished by the render loop. */ - @Volatile - var failure: Throwable? = null - private set - - /** The map to attach against, once the loop has created one. */ - val map: MapHandle? - get() = publishedMap - - @Volatile private var wake: WakeSource? = null - - private val thread = Thread({ run() }, "compose-map-runtime").apply { isDaemon = true } - - init { - thread.start() - } - - override fun close() { - shutdownRequested.set(true) - commands.wake() - thread.join() - } - - private fun run() { - var runtime: RuntimeHandle? = null - var map: MapHandle? = null - try { - val createdRuntime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) - runtime = createdRuntime - val extent = initialExtent - val createdMap = - MapHandle.create( - createdRuntime, - MapOptions().apply { - width = extent.width - height = extent.height - scaleFactor = extent.scaleFactor - mapMode = MapMode.CONTINUOUS - // The two event types this loop reads. A map queues no event of an - // unselected type, so nothing accumulates before the style load. - eventMask = - RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + - RuntimeEventMask.MAP_RENDER_FRAME_FINISHED - }, - ) - map = createdMap - createdMap.setStyleUrl(STYLE_URL) - createdMap.jumpTo( - CameraOptions().apply { - center = LatLng(37.7749, -122.4194) - zoom = 13.0 - bearing = 12.0 - pitch = 30.0 - } - ) - publishedMap = createdMap - pump(createdRuntime, createdMap) - } catch (error: Throwable) { - failure = error - } finally { - publishedMap = null - // A map with an attached session cannot be destroyed, so wait for the render loop to close - // its session. Bounded, so a render loop that already stopped cannot wedge teardown. - awaitShutdown() - // A wake source is its own native handle, so closing the runtime does not release it. - commands.onEnqueue = null - wake?.close() - wake = null - try { - map?.close() - } finally { - runtime?.close() - } - } - } - - /** Blocks until [close] is called, or the bound expires. */ - private fun awaitShutdown() { - val deadline = TimeSource.Monotonic.markNow() + SHUTDOWN_WAIT - while (!shutdownRequested.get() && deadline.hasNotPassedNow()) { - Thread.sleep(SHUTDOWN_POLL_MS) - } - } - - private fun pump(runtime: RuntimeHandle, map: MapHandle) { - val source = runtime.acquireWakeSource() - wake = source - commands.onEnqueue = { source.signal() } - val batch = ArrayList() - while (!shutdownRequested.get()) { - batch.clear() - commands.drainInto(batch) - batch.forEach { command -> apply(map, command) } - // This thread has no display to pace it, so the pump's park is the cadence. - runtime.pump(PARK_TIMEOUT_MS) - if (drainEvents(runtime, map)) { - // Compose draws on demand, so tiles, style loads, and animation ticks reach the screen - // only if something asks for a frame. - requestRender() - } - } - } - - /** - * Applies one decoded camera command on the map's owner thread, so read-modify-write commands - * read the current camera here rather than on the render loop that produced them. - */ - private fun apply(map: MapHandle, command: CameraCommand) { - when (command) { - CameraCommand.CancelTransitions -> map.cancelTransitions() - is CameraCommand.SetGestureInProgress -> map.isGestureInProgress = command.inProgress - is CameraCommand.MoveBy -> map.moveBy(command.deltaX, command.deltaY) - is CameraCommand.MoveByAnimated -> - map.moveByAnimated(command.deltaX, command.deltaY, KEYBOARD_ANIMATION) - is CameraCommand.ScaleBy -> map.scaleBy(command.scale, command.anchor) - is CameraCommand.ScaleByAnimated -> - map.scaleByAnimated(command.scale, command.anchor, KEYBOARD_ANIMATION) - is CameraCommand.AdjustBearingAndPitch -> { - val camera = map.camera - map.jumpTo( - CameraOptions().apply { - bearing = (camera.bearing ?: 0.0) + command.bearingDegrees - pitch = ((camera.pitch ?: 0.0) + command.pitchDegrees).clampPitch() - } - ) - } - is CameraCommand.AdjustBearingAnimated -> - map.easeTo( - CameraOptions().apply { bearing = (map.camera.bearing ?: 0.0) + command.bearingDegrees }, - KEYBOARD_ANIMATION, - ) - is CameraCommand.AdjustPitchAnimated -> - map.easeTo( - CameraOptions().apply { - pitch = ((map.camera.pitch ?: 0.0) + command.pitchDegrees).clampPitch() - }, - KEYBOARD_ANIMATION, - ) - CameraCommand.ResetOrientation -> - map.easeTo( - CameraOptions().apply { - bearing = 0.0 - pitch = 0.0 - }, - RESET_ANIMATION, - ) - } - } - - /** Drains one batch of runtime events, reporting whether the map wants another frame. */ - private fun drainEvents(runtime: RuntimeHandle, map: MapHandle): Boolean { - var renderUpdateAvailable = false - // One drain takes every event the pump produced. - for (event in runtime.drainEvents().events) { - if (event.mapSource != map) { - continue - } - if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) { - renderUpdateAvailable = true - } else if ( - event.type == RuntimeEventType.MAP_RENDER_FRAME_FINISHED && - (event.payload as? RuntimeEventPayload.RenderFrame)?.needsRepaint == true - ) { - renderUpdateAvailable = true - } - } - return renderUpdateAvailable - } - - private companion object { - private const val STYLE_URL = "https://tiles.openfreemap.org/styles/bright" - /** Backstop bound for a parked pump that nothing signals. */ - private const val PARK_TIMEOUT_MS = 100L - - /** Bound on waiting for the render loop to close its session at teardown. */ - private val SHUTDOWN_WAIT = 5.seconds - private const val SHUTDOWN_POLL_MS = 2L - private val KEYBOARD_ANIMATION = AnimationOptions().apply { durationMs = 160.0 } - private val RESET_ANIMATION = AnimationOptions().apply { durationMs = 160.0 } - } -} - -private fun Double.clampPitch(): Double = max(0.0, min(60.0, this)) diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapState.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapState.kt new file mode 100644 index 000000000..cc6853f5b --- /dev/null +++ b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/MapState.kt @@ -0,0 +1,211 @@ +package org.maplibre.nativeffi.examples.composemap.map + +import java.util.concurrent.atomic.AtomicBoolean +import org.maplibre.nativeffi.camera.AnimationOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraDeltaKind +import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate +import org.maplibre.nativeffi.camera.CameraUpdateMode +import org.maplibre.nativeffi.camera.GesturePhase +import org.maplibre.nativeffi.examples.composemap.surface.SurfaceExtent +import org.maplibre.nativeffi.geo.LatLng +import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.map.MapMode +import org.maplibre.nativeffi.map.MapOptions +import org.maplibre.nativeffi.map.MapSize +import org.maplibre.nativeffi.runtime.RuntimeEventMask +import org.maplibre.nativeffi.runtime.RuntimeEventPayload +import org.maplibre.nativeffi.runtime.RuntimeEventType +import org.maplibre.nativeffi.runtime.RuntimeHandle +import org.maplibre.nativeffi.runtime.RuntimeOptions + +/** Runtime and map state driven by the core-owned runtime worker. */ +internal class MapState(initialExtent: SurfaceExtent, private val requestRender: () -> Unit) : + AutoCloseable { + private var closed = false + private var currentSize = + MapSize(initialExtent.width, initialExtent.height, initialExtent.scaleFactor) + private val initialCamera = + CameraOptions().apply { + center = LatLng(37.7749, -122.4194) + zoom = 13.0 + bearing = 12.0 + pitch = 30.0 + } + + private val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) + private lateinit var ownedMap: MapHandle + val map: MapHandle + get() = ownedMap + + init { + try { + ownedMap = runSuspend { + MapHandle.create( + runtime, + MapOptions().apply { + width = initialExtent.width + height = initialExtent.height + scaleFactor = initialExtent.scaleFactor + mapMode = MapMode.CONTINUOUS + eventMask = + RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + + RuntimeEventMask.MAP_RENDER_FRAME_FINISHED + }, + ) + .await() + } + map.setStyleUrl(STYLE_URL) + map.updateCamera(CameraUpdate(camera = initialCamera)) + } catch (error: Throwable) { + if (::ownedMap.isInitialized) ownedMap.close() + runtime.close() + throw error + } + } + + fun cancelTransitions() { + map.updateCamera(CameraUpdate()) + } + + fun setGestureInProgress(inProgress: Boolean) { + map.updateCamera( + CameraUpdate(gesturePhase = if (inProgress) GesturePhase.BEGIN else GesturePhase.END) + ) + } + + fun moveBy(deltaX: Double, deltaY: Double) { + map.applyCameraDelta( + CameraDelta(offset = org.maplibre.nativeffi.geo.ScreenPoint(deltaX, deltaY)) + ) + } + + fun moveByAnimated(deltaX: Double, deltaY: Double) { + map.applyCameraDelta( + CameraDelta( + offset = org.maplibre.nativeffi.geo.ScreenPoint(deltaX, deltaY), + animation = animation(KEYBOARD_ANIMATION_MS), + ) + ) + } + + fun scaleBy(scale: Double, anchor: org.maplibre.nativeffi.geo.ScreenPoint) { + map.applyCameraDelta(CameraDelta(kind = CameraDeltaKind.SCALE, amount = scale, anchor = anchor)) + } + + fun scaleByAnimated(scale: Double, anchor: org.maplibre.nativeffi.geo.ScreenPoint) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.SCALE, + amount = scale, + anchor = anchor, + animation = animation(KEYBOARD_ANIMATION_MS), + ) + ) + } + + fun adjustBearingAndPitch(bearingDegrees: Double, pitchDegrees: Double) { + map.applyCameraDelta(CameraDelta(kind = CameraDeltaKind.BEARING, amount = bearingDegrees)) + map.applyCameraDelta(CameraDelta(kind = CameraDeltaKind.PITCH, amount = pitchDegrees)) + } + + fun adjustBearingAnimated(bearingDegrees: Double) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.BEARING, + amount = bearingDegrees, + animation = animation(KEYBOARD_ANIMATION_MS), + ) + ) + } + + fun adjustPitchAnimated(pitchDegrees: Double) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.PITCH, + amount = pitchDegrees, + animation = animation(KEYBOARD_ANIMATION_MS), + ) + ) + } + + fun resetOrientation() { + update( + CameraOptions().apply { + bearing = 0.0 + pitch = 0.0 + }, + RESET_ANIMATION_MS, + ) + } + + fun resize(extent: SurfaceExtent) { + val size = MapSize(extent.width, extent.height, extent.scaleFactor) + if (size != currentSize) { + currentSize = size + map.resize(size) + } + } + + /** Drains runtime events during the native-surface producer's render turn. */ + fun pollEvents() { + if (drainEvents()) requestRender() + } + + private fun update(camera: CameraOptions, durationMs: Double? = null) { + map.updateCamera( + CameraUpdate( + mode = if (durationMs == null) CameraUpdateMode.JUMP else CameraUpdateMode.EASE, + camera = camera, + animation = AnimationOptions().apply { durationMs?.let { this.durationMs = it } }, + ) + ) + } + + private fun animation(durationMs: Double) = + AnimationOptions().apply { this.durationMs = durationMs } + + private fun drainEvents(): Boolean { + var renderUpdateAvailable = false + for (event in runtime.drainEvents().events) { + if (event.mapSource != map) continue + if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) { + renderUpdateAvailable = true + } else if ( + event.type == RuntimeEventType.MAP_RENDER_FRAME_FINISHED && + (event.payload as? RuntimeEventPayload.RenderFrame)?.needsRepaint == true + ) { + renderUpdateAvailable = true + } + } + return renderUpdateAvailable + } + + override fun close() { + if (closed) return + closed = true + try { + map.close() + } finally { + runtime.close() + } + } + + private companion object { + private const val STYLE_URL = "https://tiles.openfreemap.org/styles/bright" + private const val KEYBOARD_ANIMATION_MS = 160.0 + private const val RESET_ANIMATION_MS = 160.0 + } +} + +/** A one-bit frame request shared with native wake callbacks. */ +internal class RenderRequest { + private val value = AtomicBoolean(true) + + fun set() { + value.set(true) + } + + fun consume(): Boolean = value.getAndSet(false) +} diff --git a/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/SuspendBridge.kt b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/SuspendBridge.kt new file mode 100644 index 000000000..e55fce0af --- /dev/null +++ b/examples/compose-map/src/main/kotlin/org/maplibre/nativeffi/examples/composemap/map/SuspendBridge.kt @@ -0,0 +1,23 @@ +package org.maplibre.nativeffi.examples.composemap.map + +import java.util.concurrent.CountDownLatch +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine + +internal fun runSuspend(block: suspend () -> T): T { + val completed = CountDownLatch(1) + var outcome: Result? = null + block.startCoroutine( + object : Continuation { + override val context = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + outcome = result + completed.countDown() + } + } + ) + completed.await() + return outcome?.getOrThrow() ?: error("suspend call completed without a result") +} diff --git a/examples/dotnet-map/Channels.cs b/examples/dotnet-map/Channels.cs deleted file mode 100644 index 6d5d11737..000000000 --- a/examples/dotnet-map/Channels.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.ExceptionServices; -using Maplibre.NativeFfi.Camera; -using Maplibre.NativeFfi.Geo; -using Maplibre.NativeFfi.Map; -using Maplibre.NativeFfi.Runtime; - -namespace Maplibre.NativeFfi.Examples.DotnetMap; - -/// A camera change decoded on the render loop and applied on the map's owner thread. -/// -/// Commands carry deltas wherever the current camera is an input, because the read and the write -/// have to happen together on the owner thread. A null animation applies the command immediately. -/// -internal abstract record CameraCommand; - -internal sealed record CancelTransitionsCommand : CameraCommand; - -internal sealed record SetGestureInProgressCommand(bool InProgress) : CameraCommand; - -internal sealed record MoveByCommand(double DeltaX, double DeltaY, AnimationOptions? Animation) - : CameraCommand; - -internal sealed record ScaleByCommand( - double Scale, - ScreenPoint? Anchor, - AnimationOptions? Animation -) : CameraCommand; - -internal sealed record AdjustBearingCommand(double Delta, AnimationOptions? Animation) - : CameraCommand; - -internal sealed record AdjustPitchCommand(double Delta, AnimationOptions? Animation) - : CameraCommand; - -internal sealed record ResetOrientationCommand(AnimationOptions Animation) : CameraCommand; - -/// Camera commands queued by the render loop for the runtime loop to apply. -internal sealed class CommandQueue -{ - private readonly ConcurrentQueue queue = new(); - - /// - /// Released by so a queued command reaches the runtime loop without waiting - /// out its parking bound. The runtime loop parks inside the native pump, so only the native - /// wake source releases it. - /// - public Action? OnEnqueue { get; set; } - - /// Render loop: queues a command and wakes the runtime loop. - public void Push(CameraCommand command) - { - queue.Enqueue(command); - OnEnqueue?.Invoke(); - } - - /// Runtime loop: takes the next queued command, if there is one. - public bool TryDequeue([NotNullWhen(true)] out CameraCommand? command) - { - return queue.TryDequeue(out command); - } -} - -/// One-bit signal that a frame is worth drawing. -internal sealed class RenderRequest -{ - private int value = 1; - - public void Set() - { - Volatile.Write(ref value, 1); - } - - public bool Consume() - { - return Interlocked.Exchange(ref value, 0) == 1; - } -} - -/// -/// Publishes the map from the runtime loop to the render loop, and carries shutdown and failure -/// the other way. The render loop uses the published handle only to attach its own session; every -/// other map call stays on the runtime loop. -/// -internal sealed class MapChannel : IDisposable -{ - private static readonly TimeSpan MapPollInterval = TimeSpan.FromMilliseconds(1); - - private readonly ManualResetEventSlim mapPublished = new(); - private readonly ManualResetEventSlim shutdownRequested = new(); - private MapHandle? map; - private WakeSource? wake; - private ExceptionDispatchInfo? failure; - - public bool ShutdownRequested => shutdownRequested.IsSet; - - /// Runtime loop: announces the map it just created. - public void PublishMap(MapHandle value, WakeSource source) - { - wake = source; - map = value; - mapPublished.Set(); - } - - /// Render loop: releases the runtime loop's parked pump. - public void WakeRuntimeLoop() - { - // The runtime loop clears this before disposing on another thread; a lost race costs one - // parking bound. - try - { - wake?.Signal(); - } - catch (ObjectDisposedException) { } - } - - /// Runtime loop: stops handing out its wake source before disposing it. - public void ClearWake() - { - wake = null; - } - - /// Render loop: blocks until the map to attach against exists. - public MapHandle WaitForMap() - { - while (!mapPublished.Wait(MapPollInterval)) - { - ThrowIfFailed(); - } - - return map!; - } - - /// - /// Render loop: asks the runtime loop to stop. Call only after closing the render session, - /// because the map cannot be destroyed before then. - /// - public void RequestShutdown() - { - shutdownRequested.Set(); - WakeRuntimeLoop(); - } - - /// Runtime loop: blocks until the render loop is done with its session. - public void WaitForShutdown() - { - shutdownRequested.Wait(); - } - - /// Records the first failure either loop reported. - public void Fail(Exception error) - { - Interlocked.CompareExchange(ref failure, ExceptionDispatchInfo.Capture(error), null); - } - - public void ThrowIfFailed() - { - Volatile.Read(ref failure)?.Throw(); - } - - public void Dispose() - { - mapPublished.Dispose(); - shutdownRequested.Dispose(); - } -} diff --git a/examples/dotnet-map/IRenderTarget.cs b/examples/dotnet-map/IRenderTarget.cs index 2d87697e7..5124a96f4 100644 --- a/examples/dotnet-map/IRenderTarget.cs +++ b/examples/dotnet-map/IRenderTarget.cs @@ -5,54 +5,119 @@ namespace Maplibre.NativeFfi.Examples.DotnetMap; internal interface IRenderTarget : IDisposable { - /// - /// Renders the latest map update, and reports whether a frame reached the screen. It reports - /// true only for a rendered frame, so the render loop asks for another one when the map has no - /// update yet, the map has not applied the session's size yet, or the target had no frame to - /// draw into. - /// bool Render(); - - /// Follows a resized host without detaching the session. void Resize(Viewport viewport); } -internal static class RenderTargetFactory +internal static class RenderTargetDriver { - /// - /// Attaches a render session on the calling thread, which owns it for its whole lifetime. - /// - public static IRenderTarget Attach( - IGraphicsContext graphics, - MapHandle map, - RenderTargetMode mode - ) + internal static void Wait(RenderSessionHandle session, Task operation) { - ArgumentNullException.ThrowIfNull(graphics); - ArgumentNullException.ThrowIfNull(map); + while (!operation.IsCompleted) + { + Service(session); + Thread.Yield(); + } + operation.GetAwaiter().GetResult(); + } - return mode.Kind switch + internal static bool Render(RenderSessionHandle session, bool present) + { + session.RequestFrame( + new FrameDemand( + FrameDemandFlags.IfNeeded + | (present ? FrameDemandFlags.Present : FrameDemandFlags.None), + 0, + 0, + 0 + ) + ); + Service(session); + using var batch = session.DrainFrameResults(); + return batch.Any(result => result.Disposition == RenderResult.Rendered); + } + + internal static void Close(RenderSessionHandle session) + { + try { - RenderTargetModeKind.OwnedTexture => AttachOwnedTexture(graphics, map), - RenderTargetModeKind.BorrowedTexture => AttachBorrowedTexture(graphics, map), - RenderTargetModeKind.NativeSurface => AttachNativeSurface(graphics, map), - _ => throw new ArgumentOutOfRangeException(nameof(mode)), - }; + Wait(session, session.DetachAsync()); + } + catch + { + try + { + session.Abandon(); + } + finally + { + session.Close(); + } + throw; + } + session.Close(); } - private static IRenderTarget AttachOwnedTexture(IGraphicsContext graphics, MapHandle map) + internal static void CompleteAttachment(RenderSessionHandle session) { - return OwnedTextureRenderTarget.Attach(graphics, map, graphics.ReadViewport()); + try + { + Wait(session, session.Attachment); + } + catch + { + try + { + session.Abandon(); + } + finally + { + session.Close(); + } + throw; + } } - private static IRenderTarget AttachBorrowedTexture(IGraphicsContext graphics, MapHandle map) + private static void Service(RenderSessionHandle session) { - return BorrowedTextureRenderTarget.Attach(graphics, map, graphics.ReadViewport()); + if (session.Capabilities.Driver == RenderDriverKind.CallerGraphicsThread) + { + session.ServiceDriverWork(0); + } } +} - private static IRenderTarget AttachNativeSurface(IGraphicsContext graphics, MapHandle map) +internal static class RenderTargetFactory +{ + public static IRenderTarget Attach( + IGraphicsContext graphics, + MapHandle map, + RenderTargetMode mode + ) { - return NativeSurfaceRenderTarget.Attach(graphics, map, graphics.ReadViewport()); + if (graphics is OpenGLContext openGl) + { + openGl.MakeCurrentForRendering(); + } + return mode.Kind switch + { + RenderTargetModeKind.OwnedTexture => OwnedTextureRenderTarget.Attach( + graphics, + map, + graphics.ReadViewport() + ), + RenderTargetModeKind.BorrowedTexture => BorrowedTextureRenderTarget.Attach( + graphics, + map, + graphics.ReadViewport() + ), + RenderTargetModeKind.NativeSurface => NativeSurfaceRenderTarget.Attach( + graphics, + map, + graphics.ReadViewport() + ), + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; } } @@ -71,6 +136,7 @@ RenderSessionHandle session this.graphics = graphics; this.compositor = compositor; this.session = session; + RenderTargetDriver.CompleteAttachment(session); } public static OwnedTextureRenderTarget Attach( @@ -79,119 +145,91 @@ public static OwnedTextureRenderTarget Attach( Viewport viewport ) { - return graphics switch - { - MetalContext metal => AttachWithCleanup( - metal, - () => - RenderSessionHandle.AttachMetalOwnedTexture( - map, - new MetalOwnedTextureDescriptor - { - Extent = viewport.RenderTargetExtent, - Context = metal.Descriptor(), - } - ), - () => new MetalTextureCompositor(metal) - ), - VulkanContext vulkan => AttachWithCleanup( - vulkan, - () => - RenderSessionHandle.AttachVulkanOwnedTexture( - map, - new VulkanOwnedTextureDescriptor - { - Extent = viewport.RenderTargetExtent, - Context = vulkan.Descriptor(), - } - ), - () => new VulkanTextureCompositor(vulkan, viewport) - ), - OpenGLContext openGl => AttachWithCleanup( - openGl, - () => - RenderSessionHandle.AttachOpenGLOwnedTexture( - map, - new OpenGLOwnedTextureDescriptor - { - Extent = viewport.RenderTargetExtent, - Context = openGl.Descriptor(requirePbufferConfig: true), - } - ), - () => new OpenGLTextureCompositor(openGl, viewport) - ), + ITextureCompositor compositor = graphics switch + { + MetalContext metal => new MetalTextureCompositor(metal), + VulkanContext vulkan => new VulkanTextureCompositor(vulkan, viewport), + OpenGLContext openGl => new OpenGLTextureCompositor(openGl, viewport), _ => throw new InvalidOperationException( $"Owned textures are not implemented for {graphics.Backend}." ), }; - } - - private static OwnedTextureRenderTarget AttachWithCleanup( - IGraphicsContext graphics, - Func attachSession, - Func createCompositor - ) - { - RenderSessionHandle? session = null; - ITextureCompositor? compositor = null; try { - session = attachSession(); - compositor = createCompositor(); - return new OwnedTextureRenderTarget(graphics, compositor, session); + var session = graphics switch + { + MetalContext metal => RenderSessionHandle.AttachMetalOwnedTexture( + map, + new MetalOwnedTextureDescriptor + { + Extent = viewport.RenderTargetExtent, + Context = metal.Descriptor(), + }, + null + ), + VulkanContext vulkan => RenderSessionHandle.AttachVulkanOwnedTexture( + map, + new VulkanOwnedTextureDescriptor + { + Extent = viewport.RenderTargetExtent, + Context = vulkan.Descriptor(), + }, + null + ), + OpenGLContext openGl => RenderSessionHandle.AttachOpenGLOwnedTexture( + map, + new OpenGLOwnedTextureDescriptor + { + Extent = viewport.RenderTargetExtent, + Context = openGl.Descriptor(requirePbufferConfig: true), + }, + null + ), + _ => throw new InvalidOperationException( + $"Owned textures are not implemented for {graphics.Backend}." + ), + }; + return new(graphics, compositor, session); } catch { - DisposeAfterFailure(compositor); - DisposeAfterFailure(session); + compositor.Dispose(); throw; } } public bool Render() { - if (session.RenderUpdate().Result != RenderResult.Rendered) + if ( + !RenderTargetDriver.Render(session, present: false) + || !session.TryAcquireFrame(out var frame) + ) { return false; } - var presented = false; - switch (graphics) - { - case MetalContext: - using (var frame = session.AcquireMetalOwnedTextureFrame()) - { - presented = compositor.Draw(frame.Frame); - } - break; - case VulkanContext: - using (var frame = session.AcquireVulkanOwnedTextureFrame()) - { - presented = compositor.Draw(frame.Frame); - } - break; - case OpenGLContext: - using (var frame = session.AcquireOpenGLOwnedTextureFrame()) - { - presented = compositor.Draw(frame.Frame); - } - break; - default: - throw new InvalidOperationException( - $"Owned textures are not implemented for {graphics.Backend}." - ); - } - - if (presented) + using (frame) { - graphics.FinishFrame(); + var presented = graphics switch + { + MetalContext => compositor.Draw(frame.GetMetalTexture()), + VulkanContext => compositor.Draw(frame.GetVulkanTexture()), + OpenGLContext => compositor.Draw(frame.GetOpenGLTexture()), + _ => false, + }; + if (presented) + graphics.FinishFrame(); + if (graphics is OpenGLContext openGl) + { + openGl.FinishGpuWork(); + } + frame.Release(GpuSync.CpuComplete); + return presented; } - - return presented; } public void Resize(Viewport viewport) { - session.Resize(viewport.LogicalWidth, viewport.LogicalHeight, viewport.ScaleFactor); + RenderTargetDriver.Wait(session, session.ResizeAsync(viewport.RenderTargetExtent)); compositor.Resize(viewport); } @@ -199,28 +237,11 @@ public void Dispose() { try { - compositor.Dispose(); + RenderTargetDriver.Close(session); } finally { - session.Dispose(); - } - } - - private static void DisposeAfterFailure(IDisposable? disposable) - { - if (disposable is null) - { - return; - } - - try - { - disposable.Dispose(); - } - catch - { - // Preserve the setup failure that triggered cleanup. + compositor.Dispose(); } } } @@ -243,15 +264,15 @@ RenderSessionHandle session this.compositor = compositor; this.texture = texture; this.session = session; + RenderTargetDriver.CompleteAttachment(session); } public static BorrowedTextureRenderTarget Attach( IGraphicsContext graphics, MapHandle map, Viewport viewport - ) - { - return graphics switch + ) => + graphics switch { MetalContext metal => AttachMetal(metal, map, viewport), VulkanContext vulkan => AttachVulkan(vulkan, map, viewport), @@ -260,78 +281,54 @@ Viewport viewport $"Borrowed textures are not implemented for {graphics.Backend}." ), }; - } public bool Render() { - if (session.RenderUpdate().Result != RenderResult.Rendered) - { + if (!RenderTargetDriver.Render(session, present: false)) return false; - } - var presented = true; - switch (texture) - { - case MetalBorrowedTexture metalTexture - when compositor is MetalTextureCompositor metalCompositor: - presented = metalCompositor.DrawTexture(metalTexture.Texture); - break; - case VulkanBorrowedImage vulkanImage - when compositor is VulkanTextureCompositor vulkanCompositor: - presented = vulkanCompositor.DrawImageView(vulkanImage.View); - break; - case OpenGLBorrowedTexture openGlTexture - when compositor is OpenGLTextureCompositor openGlCompositor: - openGlCompositor.DrawTexture(openGlTexture.Texture); - break; - default: - throw new InvalidOperationException("Unsupported borrowed texture compositor."); - } - + var presented = texture switch + { + MetalBorrowedTexture metalTexture + when compositor is MetalTextureCompositor metalCompositor => + metalCompositor.DrawTexture(metalTexture.Texture), + VulkanBorrowedImage vulkanImage + when compositor is VulkanTextureCompositor vulkanCompositor => + vulkanCompositor.DrawImageView(vulkanImage.View), + OpenGLBorrowedTexture openGlTexture + when compositor is OpenGLTextureCompositor openGlCompositor => DrawOpenGL( + openGlCompositor, + openGlTexture + ), + _ => throw new InvalidOperationException("Unsupported borrowed texture compositor."), + }; if (presented) - { graphics.FinishFrame(); - } - return presented; } - /// - /// Allocates a texture at the new size and hands it to the live session, which keeps its - /// renderer across the handover. - /// public void Resize(Viewport viewport) { + IDisposable replacement; + Task handover; switch (graphics) { case MetalContext metal: - var metalReplacement = new MetalBorrowedTexture(metal, viewport); - HandOver( - metalReplacement, - () => - session.SetMetalBorrowedTextureTarget(Describe(metalReplacement, viewport)), - viewport - ); + var metalTexture = new MetalBorrowedTexture(metal, viewport); + replacement = metalTexture; + handover = session.SetMetalBorrowedTextureAsync(Describe(metalTexture, viewport)); break; case VulkanContext vulkan: - var vulkanReplacement = new VulkanBorrowedImage(vulkan, viewport); - HandOver( - vulkanReplacement, - () => - session.SetVulkanBorrowedTextureTarget( - Describe(vulkan, vulkanReplacement, viewport) - ), - viewport + var vulkanImage = new VulkanBorrowedImage(vulkan, viewport); + replacement = vulkanImage; + handover = session.SetVulkanBorrowedTextureAsync( + Describe(vulkan, vulkanImage, viewport) ); break; case OpenGLContext openGl: - var openGlReplacement = new OpenGLBorrowedTexture(openGl, viewport); - HandOver( - openGlReplacement, - () => - session.SetOpenGLBorrowedTextureTarget( - Describe(openGl, openGlReplacement, viewport) - ), - viewport + var openGlTexture = new OpenGLBorrowedTexture(openGl, viewport); + replacement = openGlTexture; + handover = session.SetOpenGLBorrowedTextureAsync( + Describe(openGl, openGlTexture, viewport) ); break; default: @@ -339,33 +336,16 @@ public void Resize(Viewport viewport) $"Borrowed textures are not implemented for {graphics.Backend}." ); } - } - /// - /// Hands the live session a replacement texture, then retires the outgoing one. A failed - /// handover may or may not have left the session holding the replacement, so detach before - /// releasing either texture. - /// - private void HandOver(IDisposable replacement, Action setTarget, Viewport viewport) - { try { - setTarget(); + RenderTargetDriver.Wait(session, handover); } catch { - try - { - session.Detach(); - } - catch - { - // Preserve the handover failure that triggered cleanup. - } - DisposeAfterFailure(replacement); + replacement.Dispose(); throw; } - var outgoing = texture; texture = replacement; try @@ -382,13 +362,13 @@ public void Dispose() { try { - compositor.Dispose(); + RenderTargetDriver.Close(session); } finally { try { - session.Dispose(); + compositor.Dispose(); } finally { @@ -397,58 +377,66 @@ public void Dispose() } } - private static BorrowedTextureRenderTarget AttachVulkan( - VulkanContext vulkan, + private static BorrowedTextureRenderTarget AttachMetal( + MetalContext metal, MapHandle map, Viewport viewport ) { - VulkanBorrowedImage? texture = null; - VulkanTextureCompositor? compositor = null; - RenderSessionHandle? session = null; + var texture = new MetalBorrowedTexture(metal, viewport); try { - texture = new VulkanBorrowedImage(vulkan, viewport); - compositor = new VulkanTextureCompositor(vulkan, viewport); - session = RenderSessionHandle.AttachVulkanBorrowedTexture( - map, - Describe(vulkan, texture, viewport) - ); - return new BorrowedTextureRenderTarget(vulkan, compositor, texture, session); + var compositor = new MetalTextureCompositor(metal); + try + { + var session = RenderSessionHandle.AttachMetalBorrowedTexture( + map, + Describe(texture, viewport), + null + ); + return new(metal, compositor, texture, session); + } + catch + { + compositor.Dispose(); + throw; + } } catch { - DisposeAfterFailure(compositor); - DisposeAfterFailure(session); - DisposeAfterFailure(texture); + texture.Dispose(); throw; } } - private static BorrowedTextureRenderTarget AttachMetal( - MetalContext metal, + private static BorrowedTextureRenderTarget AttachVulkan( + VulkanContext vulkan, MapHandle map, Viewport viewport ) { - MetalBorrowedTexture? texture = null; - MetalTextureCompositor? compositor = null; - RenderSessionHandle? session = null; + var texture = new VulkanBorrowedImage(vulkan, viewport); try { - texture = new MetalBorrowedTexture(metal, viewport); - compositor = new MetalTextureCompositor(metal); - session = RenderSessionHandle.AttachMetalBorrowedTexture( - map, - Describe(texture, viewport) - ); - return new BorrowedTextureRenderTarget(metal, compositor, texture, session); + var compositor = new VulkanTextureCompositor(vulkan, viewport); + try + { + var session = RenderSessionHandle.AttachVulkanBorrowedTexture( + map, + Describe(vulkan, texture, viewport), + null + ); + return new(vulkan, compositor, texture, session); + } + catch + { + compositor.Dispose(); + throw; + } } catch { - DisposeAfterFailure(compositor); - DisposeAfterFailure(session); - DisposeAfterFailure(texture); + texture.Dispose(); throw; } } @@ -459,97 +447,85 @@ private static BorrowedTextureRenderTarget AttachOpenGL( Viewport viewport ) { - OpenGLBorrowedTexture? texture = null; - OpenGLTextureCompositor? compositor = null; - RenderSessionHandle? session = null; + var texture = new OpenGLBorrowedTexture(openGl, viewport); try { - texture = new OpenGLBorrowedTexture(openGl, viewport); - compositor = new OpenGLTextureCompositor(openGl, viewport); - session = RenderSessionHandle.AttachOpenGLBorrowedTexture( - map, - Describe(openGl, texture, viewport) - ); - return new BorrowedTextureRenderTarget(openGl, compositor, texture, session); + var compositor = new OpenGLTextureCompositor(openGl, viewport); + try + { + var session = RenderSessionHandle.AttachOpenGLBorrowedTexture( + map, + Describe(openGl, texture, viewport), + null + ); + return new(openGl, compositor, texture, session); + } + catch + { + compositor.Dispose(); + throw; + } } catch { - DisposeAfterFailure(compositor); - DisposeAfterFailure(session); - DisposeAfterFailure(texture); + texture.Dispose(); throw; } } - // Attach and set-target share these descriptors: a resize replacement has to be described the - // same way the session attached with. + private static bool DrawOpenGL( + OpenGLTextureCompositor compositor, + OpenGLBorrowedTexture texture + ) + { + compositor.DrawTexture(texture.Texture); + return true; + } + private static MetalBorrowedTextureDescriptor Describe( MetalBorrowedTexture texture, Viewport viewport - ) - { - return new MetalBorrowedTextureDescriptor + ) => + new() { Extent = viewport.RenderTargetExtent, PhysicalWidth = viewport.PhysicalWidth, PhysicalHeight = viewport.PhysicalHeight, Texture = texture.Pointer, }; - } private static VulkanBorrowedTextureDescriptor Describe( - VulkanContext vulkan, + VulkanContext context, VulkanBorrowedImage image, Viewport viewport - ) - { - return new VulkanBorrowedTextureDescriptor + ) => + new() { Extent = viewport.RenderTargetExtent, PhysicalWidth = viewport.PhysicalWidth, PhysicalHeight = viewport.PhysicalHeight, - Context = vulkan.Descriptor(), + Context = context.Descriptor(), Image = image.ImagePointer, ImageView = image.ViewPointer, Format = (uint)VulkanBorrowedImage.ImageFormat, InitialLayout = (uint)VulkanBorrowedImage.InitialLayout, FinalLayout = (uint)VulkanBorrowedImage.FinalLayout, }; - } private static OpenGLBorrowedTextureDescriptor Describe( - OpenGLContext openGl, + OpenGLContext context, OpenGLBorrowedTexture texture, Viewport viewport - ) - { - return new OpenGLBorrowedTextureDescriptor + ) => + new() { Extent = viewport.RenderTargetExtent, PhysicalWidth = viewport.PhysicalWidth, PhysicalHeight = viewport.PhysicalHeight, - Context = openGl.Descriptor(requirePbufferConfig: true), + Context = context.Descriptor(requirePbufferConfig: true), Texture = texture.Texture, Target = texture.Target, }; - } - - private static void DisposeAfterFailure(IDisposable? disposable) - { - if (disposable is null) - { - return; - } - - try - { - disposable.Dispose(); - } - catch - { - // Preserve the setup failure that triggered cleanup. - } - } } internal sealed class NativeSurfaceRenderTarget : IRenderTarget @@ -559,67 +535,57 @@ internal sealed class NativeSurfaceRenderTarget : IRenderTarget private NativeSurfaceRenderTarget(RenderSessionHandle session) { this.session = session; + RenderTargetDriver.CompleteAttachment(session); } public static NativeSurfaceRenderTarget Attach( IGraphicsContext graphics, MapHandle map, Viewport viewport - ) - { - return graphics switch - { - MetalContext metal => new NativeSurfaceRenderTarget( - RenderSessionHandle.AttachMetalSurface( + ) => + new( + graphics switch + { + MetalContext metal => RenderSessionHandle.AttachMetalSurface( map, new MetalSurfaceDescriptor { Extent = viewport.RenderTargetExtent, Layer = metal.LayerPointer(), Context = metal.Descriptor(), - } - ) - ), - VulkanContext vulkan => new NativeSurfaceRenderTarget( - RenderSessionHandle.AttachVulkanSurface( + }, + null + ), + VulkanContext vulkan => RenderSessionHandle.AttachVulkanSurface( map, new VulkanSurfaceDescriptor { Extent = viewport.RenderTargetExtent, Surface = vulkan.SurfacePointer(), Context = vulkan.Descriptor(), - } - ) - ), - OpenGLContext openGl => new NativeSurfaceRenderTarget( - RenderSessionHandle.AttachOpenGLSurface( + }, + null + ), + OpenGLContext openGl => RenderSessionHandle.AttachOpenGLSurface( map, new OpenGLSurfaceDescriptor { Extent = viewport.RenderTargetExtent, Surface = openGl.SurfacePointer(), Context = openGl.Descriptor(requirePbufferConfig: false), - } - ) - ), - _ => throw new InvalidOperationException( - $"Native surfaces are not implemented for {graphics.Backend}." - ), - }; - } + }, + null + ), + _ => throw new InvalidOperationException( + $"Native surfaces are not implemented for {graphics.Backend}." + ), + } + ); - public bool Render() - { - return session.RenderUpdate().Result == RenderResult.Rendered; - } + public bool Render() => RenderTargetDriver.Render(session, present: true); - public void Resize(Viewport viewport) - { - session.Resize(viewport.LogicalWidth, viewport.LogicalHeight, viewport.ScaleFactor); - } + public void Resize(Viewport viewport) => + RenderTargetDriver.Wait(session, session.ResizeAsync(viewport.RenderTargetExtent)); - public void Dispose() - { - session.Dispose(); - } + public void Dispose() => RenderTargetDriver.Close(session); } diff --git a/examples/dotnet-map/InputController.cs b/examples/dotnet-map/InputController.cs index efbc73ea2..2701211f8 100644 --- a/examples/dotnet-map/InputController.cs +++ b/examples/dotnet-map/InputController.cs @@ -4,11 +4,8 @@ namespace Maplibre.NativeFfi.Examples.DotnetMap; -/// Decodes host input into camera commands. -/// -/// This runs on the render loop, which does not own the map, so it only queues commands for the -/// runtime loop. GLFW reports pointer positions in the map's logical coordinates already. -/// +/// Decodes host input into any-thread camera submissions. +/// GLFW reports pointer positions in the map's logical coordinates already. internal sealed unsafe class InputController : IDisposable { private const double DragRotateFactor = 0.5; @@ -21,7 +18,7 @@ internal sealed unsafe class InputController : IDisposable private static readonly AnimationOptions ResetAnimation = new() { Duration = 220 }; private readonly GlfwWindow window; - private readonly CommandQueue commands; + private readonly MapState state; private readonly RenderRequest renderRequest; private readonly GlfwCallbacks.CursorPosCallback cursorCallback; private readonly GlfwCallbacks.MouseButtonCallback mouseButtonCallback; @@ -36,11 +33,11 @@ internal sealed unsafe class InputController : IDisposable private double cursorY; private bool closed; - public InputController(GlfwWindow window, CommandQueue commands, RenderRequest renderRequest) + public InputController(GlfwWindow window, MapState state, RenderRequest renderRequest) { ArgumentNullException.ThrowIfNull(window); this.window = window; - this.commands = commands; + this.state = state; this.renderRequest = renderRequest; cursorCallback = OnCursor; mouseButtonCallback = OnMouseButton; @@ -94,15 +91,18 @@ private void OnCursor(WindowHandle* handle, double x, double y) lastY = y; if (rightDown || (leftDown && ctrlDown)) { - commands.Push(new AdjustBearingCommand(dx * DragRotateFactor, null)); - commands.Push(new AdjustPitchCommand(-dy * DragPitchFactor, null)); - renderRequest.Set(); + state.AdjustBearing(dx * DragRotateFactor); + state.AdjustPitch(dy * DragPitchFactor); } else if (leftDown) { - commands.Push(new MoveByCommand(dx, dy, null)); - renderRequest.Set(); + state.MoveBy(dx, dy); } + else + { + return; + } + renderRequest.Set(); } private void OnMouseButton( @@ -136,14 +136,13 @@ KeyModifiers mods lastX = cursorX; lastY = cursorY; - // Queued ahead of the drag's own commands, so the transition stops before the first - // delta lands. - commands.Push(new CancelTransitionsCommand()); + // Stop the transition before applying the drag's first delta. + state.CancelTransitions(); } if (Dragging != wasDragging) { - commands.Push(new SetGestureInProgressCommand(Dragging)); + state.SetGestureInProgress(Dragging); } } @@ -154,7 +153,7 @@ private void OnScroll(WindowHandle* handle, double xOffset, double yOffset) _ = handle; _ = xOffset; var scale = Math.Pow(2.0, yOffset * 0.25); - commands.Push(new ScaleByCommand(scale, new ScreenPoint(cursorX, cursorY), null)); + state.ScaleBy(scale, new ScreenPoint(cursorX, cursorY)); renderRequest.Set(); } @@ -174,55 +173,49 @@ KeyModifiers mods return; } - var changed = true; switch (key) { case Keys.Left: case Keys.A: - commands.Push(new MoveByCommand(KeyboardPan, 0.0, KeyboardAnimation)); + state.MoveBy(KeyboardPan, 0.0, KeyboardAnimation); break; case Keys.Right: case Keys.D: - commands.Push(new MoveByCommand(-KeyboardPan, 0.0, KeyboardAnimation)); + state.MoveBy(-KeyboardPan, 0.0, KeyboardAnimation); break; case Keys.Up: case Keys.W: - commands.Push(new MoveByCommand(0.0, KeyboardPan, KeyboardAnimation)); + state.MoveBy(0.0, KeyboardPan, KeyboardAnimation); break; case Keys.Down: case Keys.S: - commands.Push(new MoveByCommand(0.0, -KeyboardPan, KeyboardAnimation)); + state.MoveBy(0.0, -KeyboardPan, KeyboardAnimation); break; case Keys.Equal: case Keys.KeypadEqual: - commands.Push(new ScaleByCommand(KeyboardZoom, null, KeyboardAnimation)); + state.ScaleBy(KeyboardZoom, null, KeyboardAnimation); break; case Keys.Minus: - commands.Push(new ScaleByCommand(1.0 / KeyboardZoom, null, KeyboardAnimation)); + state.ScaleBy(1.0 / KeyboardZoom, null, KeyboardAnimation); break; case Keys.Q: - commands.Push(new AdjustBearingCommand(-KeyboardBearing, KeyboardAnimation)); + state.AdjustBearing(-KeyboardBearing, KeyboardAnimation); break; case Keys.E: - commands.Push(new AdjustBearingCommand(KeyboardBearing, KeyboardAnimation)); + state.AdjustBearing(KeyboardBearing, KeyboardAnimation); break; case Keys.RightBracket: - commands.Push(new AdjustPitchCommand(KeyboardPitch, KeyboardAnimation)); + state.AdjustPitch(KeyboardPitch, KeyboardAnimation); break; case Keys.LeftBracket: - commands.Push(new AdjustPitchCommand(-KeyboardPitch, KeyboardAnimation)); + state.AdjustPitch(-KeyboardPitch, KeyboardAnimation); break; case Keys.Number0: - commands.Push(new ResetOrientationCommand(ResetAnimation)); + state.ResetOrientation(ResetAnimation); break; default: - changed = false; - break; - } - - if (changed) - { - renderRequest.Set(); + return; } + renderRequest.Set(); } } diff --git a/examples/dotnet-map/MapState.cs b/examples/dotnet-map/MapState.cs index c243e0716..a94a9e72b 100644 --- a/examples/dotnet-map/MapState.cs +++ b/examples/dotnet-map/MapState.cs @@ -5,12 +5,10 @@ namespace Maplibre.NativeFfi.Examples.DotnetMap; -/// Runtime and map, owned for their whole lifetime by the runtime loop thread. +/// Autonomous runtime and any-thread map state. internal sealed class MapState : IDisposable { private const string StyleUrl = "https://tiles.openfreemap.org/styles/bright"; - private const double MinimumPitch = 0.0; - private const double MaximumPitch = 60.0; private readonly RuntimeHandle runtime; private bool closed; @@ -29,31 +27,38 @@ public static MapState Create(Viewport viewport) MapHandle? map = null; try { - map = MapHandle.Create( - runtime, - new MapOptions - { - Width = viewport.LogicalWidth, - Height = viewport.LogicalHeight, - ScaleFactor = viewport.ScaleFactor, - MapMode = MapMode.Continuous, - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, from its first style load on. - EventMask = - RuntimeEventMask.MapRenderUpdateAvailable - | RuntimeEventMask.MapRenderFrameFinished, - } - ); - map.SetStyleUrl(StyleUrl); - map.JumpTo( - new CameraOptions - { - Center = new LatLng(37.7749, -122.4194), - Zoom = 13.0, - Bearing = 12.0, - Pitch = 30.0, - } - ); + map = MapHandle + .CreateAsync( + runtime, + new MapOptions + { + Width = viewport.LogicalWidth, + Height = viewport.LogicalHeight, + ScaleFactor = viewport.ScaleFactor, + MapMode = MapMode.Continuous, + EventMask = + RuntimeEventMask.MapRenderUpdateAvailable + | RuntimeEventMask.MapRenderFrameFinished, + } + ) + .GetAwaiter() + .GetResult(); + map.SetStyleUrlAsync(StyleUrl).GetAwaiter().GetResult(); + map.UpdateCameraAsync( + new CameraUpdate + { + Mode = CameraUpdateMode.Jump, + Camera = new CameraOptions + { + Center = new LatLng(37.7749, -122.4194), + Zoom = 13.0, + Bearing = 12.0, + Pitch = 30.0, + }, + } + ) + .GetAwaiter() + .GetResult(); return new MapState(runtime, map); } catch @@ -64,143 +69,137 @@ public static MapState Create(Viewport viewport) } } - /// Applies every queued camera command on the map's owner thread. - public void ApplyCommands(CommandQueue commands) + public void CancelTransitions() { - ArgumentNullException.ThrowIfNull(commands); - while (commands.TryDequeue(out var command)) - { - Apply(command); - } + _ = Map.UpdateCameraAsync(new CameraUpdate()); } - /// Acquires the wake source the render loop uses to release this loop's parked pump. - public WakeSource AcquireWakeSource() => runtime.AcquireWakeSource(); + public void SetGestureInProgress(bool inProgress) + { + _ = Map.UpdateCameraAsync( + new CameraUpdate { GesturePhase = inProgress ? GesturePhase.Begin : GesturePhase.End } + ); + } - /// Pumps the runtime once, reporting whether the map wants another frame. - public bool Step(TimeSpan parkTimeout) + public void MoveBy(double deltaX, double deltaY, AnimationOptions? animation = null) { - runtime.Pump(parkTimeout); - return DrainEvents(); + _ = Map.ApplyCameraDeltaAsync( + new CameraDelta + { + Offset = new ScreenPoint(deltaX, deltaY), + Animation = animation ?? new AnimationOptions(), + } + ); } - public void Dispose() + public void ScaleBy(double scale, ScreenPoint? anchor, AnimationOptions? animation = null) { - if (closed) - { - return; - } + _ = Map.ApplyCameraDeltaAsync( + new CameraDelta + { + Kind = CameraDeltaKind.Scale, + Amount = scale, + Anchor = anchor, + Animation = animation ?? new AnimationOptions(), + } + ); + } - closed = true; - try - { - Map.Dispose(); - } - finally - { - runtime.Dispose(); - } + public void AdjustBearing(double delta, AnimationOptions? animation = null) + { + _ = Map.ApplyCameraDeltaAsync( + new CameraDelta + { + Kind = CameraDeltaKind.Bearing, + Amount = delta, + Animation = animation ?? new AnimationOptions(), + } + ); } - private bool DrainEvents() + public void AdjustPitch(double delta, AnimationOptions? animation = null) { - var renderUpdateAvailable = false; - // One drain takes every event the pump produced. + _ = Map.ApplyCameraDeltaAsync( + new CameraDelta + { + Kind = CameraDeltaKind.Pitch, + Amount = delta, + Animation = animation ?? new AnimationOptions(), + } + ); + } + + public void ResetOrientation(AnimationOptions animation) + { + Update(new CameraOptions { Bearing = 0, Pitch = 0 }, animation); + } + + public bool DrainRenderRequests() + { + var requested = false; foreach (var runtimeEvent in runtime.DrainEvents().Events) { if (!ReferenceEquals(runtimeEvent.MapSource, Map)) { continue; } - if ( runtimeEvent.Type == RuntimeEventType.MapRenderUpdateAvailable - || ( - runtimeEvent.Type == RuntimeEventType.MapRenderFrameFinished + || runtimeEvent.Type == RuntimeEventType.MapRenderFrameFinished && runtimeEvent.Payload is RuntimeEventPayload.RenderFrame { NeedsRepaint: true } - ) ) { - renderUpdateAvailable = true; + requested = true; } } - - return renderUpdateAvailable; + return requested; } - /// - /// Applies one decoded camera command on the map's owner thread, where the read-modify-write - /// commands also read the current camera. - /// - private void Apply(CameraCommand command) + public void Dispose() { - switch (command) + if (closed) { - case CancelTransitionsCommand: - Map.CancelTransitions(); - break; - case SetGestureInProgressCommand gesture: - Map.SetGestureInProgress(gesture.InProgress); - break; - case MoveByCommand { Animation: null } move: - Map.MoveBy(move.DeltaX, move.DeltaY); - break; - case MoveByCommand move: - Map.MoveByAnimated(move.DeltaX, move.DeltaY, move.Animation); - break; - case ScaleByCommand { Animation: null } zoom: - Map.ScaleBy(zoom.Scale, zoom.Anchor); - break; - case ScaleByCommand zoom: - Map.ScaleByAnimated(zoom.Scale, zoom.Anchor, zoom.Animation); - break; - case AdjustBearingCommand bearing: - ApplyCamera( - new CameraOptions { Bearing = CurrentBearing() + bearing.Delta }, - bearing.Animation - ); - break; - case AdjustPitchCommand pitch: - ApplyCamera( - new CameraOptions - { - Pitch = Math.Clamp( - CurrentPitch() + pitch.Delta, - MinimumPitch, - MaximumPitch - ), - }, - pitch.Animation - ); - break; - case ResetOrientationCommand reset: - Map.EaseTo(new CameraOptions { Bearing = 0.0, Pitch = 0.0 }, reset.Animation); - break; - default: - throw new ArgumentOutOfRangeException(nameof(command)); + return; } - } - - private void ApplyCamera(CameraOptions camera, AnimationOptions? animation) - { - if (animation is null) + closed = true; + try { - Map.JumpTo(camera); + Map.Close(); } - else + finally { - Map.EaseTo(camera, animation); + runtime.Close(); } } - private double CurrentBearing() + private void Update(CameraOptions camera, AnimationOptions? animation) + { + _ = Map.UpdateCameraAsync( + new CameraUpdate + { + Mode = animation is null ? CameraUpdateMode.Jump : CameraUpdateMode.Ease, + Camera = camera, + Animation = animation ?? new AnimationOptions(), + } + ); + } +} + +/// One-bit signal that a frame is worth drawing. +internal sealed class RenderRequest +{ + private bool requested = true; + + public void Set() { - return Map.GetCamera().Bearing ?? 0.0; + requested = true; } - private double CurrentPitch() + public bool Consume() { - return Map.GetCamera().Pitch ?? 0.0; + var current = requested; + requested = false; + return current; } } diff --git a/examples/dotnet-map/NativeLibraryResolver.cs b/examples/dotnet-map/NativeLibraryResolver.cs index d59d6d6b1..f747c1a9b 100644 --- a/examples/dotnet-map/NativeLibraryResolver.cs +++ b/examples/dotnet-map/NativeLibraryResolver.cs @@ -101,6 +101,15 @@ private static IEnumerable LibraryCandidates(string libraryName) private static IEnumerable CandidateLibraryDirectories() { + foreach ( + var directory in SplitLibraryDirectories( + Environment.GetEnvironmentVariable("MAPLIBRE_NATIVE_FFI_LIBRARY_DIRS") + ) + ) + { + yield return directory; + } + // The MapLibre install's runtime, including the ANGLE libEGL the OpenGL presets ship. yield return AppContext.BaseDirectory; @@ -112,4 +121,22 @@ private static IEnumerable CandidateLibraryDirectories() "native" ); } + + private static IEnumerable SplitLibraryDirectories(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + yield break; + } + + foreach ( + var directory in value.Split( + Path.PathSeparator, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) + ) + { + yield return directory; + } + } } diff --git a/examples/dotnet-map/OpenGLContext.cs b/examples/dotnet-map/OpenGLContext.cs index 0261385de..878d59b1d 100644 --- a/examples/dotnet-map/OpenGLContext.cs +++ b/examples/dotnet-map/OpenGLContext.cs @@ -436,6 +436,18 @@ public void DrawArrays(uint mode, int first, uint count) public uint GetError() => gles ? (uint)glesGl!.GetError() : (uint)desktopGl!.GetError(); + public void FinishGpuWork() + { + if (gles) + { + glesGl!.Finish(); + } + else + { + desktopGl!.Finish(); + } + } + public void Dispose() { if (closed) diff --git a/examples/dotnet-map/Shell.cs b/examples/dotnet-map/Shell.cs index 890de3233..53156e6cd 100644 --- a/examples/dotnet-map/Shell.cs +++ b/examples/dotnet-map/Shell.cs @@ -1,9 +1,8 @@ using Maplibre.NativeFfi.Render; -using Maplibre.NativeFfi.Runtime; namespace Maplibre.NativeFfi.Examples.DotnetMap; -/// App shell: toolkit lifetime, the two loops, and shutdown ordering. +/// App shell: GLFW/render-session affinity and autonomous map execution. internal static class Shell { public const int InitialWidth = 960; @@ -12,125 +11,45 @@ internal static class Shell // TODO(map-example-spec): Replace the fixed interval with a display-paced host loop. See Frame loop. private static readonly TimeSpan RenderLoopInterval = TimeSpan.FromMilliseconds(8); - /// - /// Backstop for the runtime loop's park; the render loop's wake source normally releases it. - /// - private static readonly TimeSpan ParkTimeout = TimeSpan.FromMilliseconds(100); - public static void Run(RenderTargetMode mode, RenderBackend backends) { - // GLFW creates windows and polls events on the main thread only, so the main thread is the - // render loop and the runtime loop gets a thread of its own. + // GLFW, the graphics context, and the render session remain on the main thread. using var graphics = GraphicsContext.Create( "dotnet-map", InitialWidth, InitialHeight, backends ); - var commands = new CommandQueue(); - using var channel = new MapChannel(); + using var state = MapState.Create(graphics.ReadViewport()); var renderRequest = new RenderRequest(); - var initialViewport = graphics.ReadViewport(); - - // A dedicated thread, because the native owner-thread checks are keyed on the OS thread; - // thread pools and async continuations do not guarantee that affinity. - var runtimeThread = new Thread(() => - RuntimeLoop(initialViewport, commands, renderRequest, channel) - ) - { - IsBackground = true, - Name = "maplibre-runtime-loop", - }; - runtimeThread.Start(); - - try - { - RenderLoop(graphics, mode, commands, renderRequest, channel); - } - finally - { - // Only here: the render loop has closed its session by the time it returns, and a map - // with an attached session cannot be destroyed. - channel.RequestShutdown(); - runtimeThread.Join(); - } - channel.ThrowIfFailed(); + RenderLoop(graphics, mode, state, renderRequest); } - /// - /// Owns the runtime and the map for their whole lifetime. It never touches the render session: - /// the render loop attaches its own against the map published here. - /// - private static void RuntimeLoop( - Viewport initialViewport, - CommandQueue commands, - RenderRequest renderRequest, - MapChannel channel - ) - { - MapState? state = null; - WakeSource? wake = null; - try - { - state = MapState.Create(initialViewport); - wake = state.AcquireWakeSource(); - channel.PublishMap(state.Map, wake); - commands.OnEnqueue = channel.WakeRuntimeLoop; - - while (!channel.ShutdownRequested) - { - state.ApplyCommands(commands); - if (state.Step(ParkTimeout)) - { - renderRequest.Set(); - } - } - } - catch (Exception error) - { - channel.Fail(error); - } - finally - { - // Wait even after a failure: the render loop closes its session before signalling - // shutdown, and the map cannot be destroyed until then. - channel.WaitForShutdown(); - // Stop handing out the wake source before disposing it, or the channel signals a closed - // handle and throws over the original failure. - commands.OnEnqueue = null; - channel.ClearWake(); - wake?.Dispose(); - state?.Dispose(); - } - } - - /// - /// Owns the window, input decoding, the graphics context, and the render session it attaches. - /// private static void RenderLoop( IGraphicsContext graphics, RenderTargetMode mode, - CommandQueue commands, - RenderRequest renderRequest, - MapChannel channel + MapState state, + RenderRequest renderRequest ) { - var map = channel.WaitForMap(); var viewport = graphics.ReadViewport(); IRenderTarget? target = null; try { - target = RenderTargetFactory.Attach(graphics, map, mode); + target = RenderTargetFactory.Attach(graphics, state.Map, mode); Console.WriteLine($"render target: {mode.CliName}"); Console.WriteLine($"render target status: {mode.Status}"); InputController.PrintControls(); - using var input = new InputController(graphics.Window, commands, renderRequest); + using var input = new InputController(graphics.Window, state, renderRequest); while (!graphics.ShouldClose) { - channel.ThrowIfFailed(); graphics.PollEvents(); + if (state.DrainRenderRequests()) + { + renderRequest.Set(); + } var currentViewport = graphics.ReadViewport(); if (currentViewport != viewport) @@ -139,17 +58,20 @@ MapChannel channel if (!viewport.IsEmpty) { graphics.Resize(viewport); - // Every mode resizes against the live session; none needs a re-attach. target.Resize(viewport); + _ = state.Map.ResizeAsync( + new global::Maplibre.NativeFfi.Map.LogicalExtent( + viewport.LogicalWidth, + viewport.LogicalHeight, + viewport.ScaleFactor + ) + ); renderRequest.Set(); } } - // Consume before rendering, so a request the runtime loop publishes during the - // render call is not discarded. if (graphics.CanRenderFrame && renderRequest.Consume() && !Render(graphics, target)) { - // Nothing reached the screen; ask again rather than dropping the frame. renderRequest.Set(); } @@ -158,7 +80,7 @@ MapChannel channel } finally { - // Close the session before the runtime loop destroys the map. + // The thread-affine session closes before the map and runtime are released. target?.Dispose(); } } diff --git a/examples/dotnet-map/mise.toml b/examples/dotnet-map/mise.toml index 8a25a061d..f366f5b6f 100644 --- a/examples/dotnet-map/mise.toml +++ b/examples/dotnet-map/mise.toml @@ -8,16 +8,23 @@ run = ''' MaplibreNativeCInstallDir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" \ dotnet build \ examples/dotnet-map/Maplibre.NativeFfi.Examples.DotnetMap.csproj \ - -p:Platform="Any CPU" + -p:Platform="Any CPU" \ + -p:BaseOutputPath="$MISE_MONOREPO_ROOT/build/dotnet/$usage_preset/" ''' [tasks.run] extends = "ffi:example-run" dir = "../.." run = ''' +if [[ -n "${MLN_FFI_VULKAN_LOADER_DIR:-}" ]]; then + export MAPLIBRE_NATIVE_FFI_LIBRARY_DIRS="$MLN_FFI_VULKAN_LOADER_DIR${MAPLIBRE_NATIVE_FFI_LIBRARY_DIRS:+:$MAPLIBRE_NATIVE_FFI_LIBRARY_DIRS}" + export DYLD_LIBRARY_PATH="$MLN_FFI_VULKAN_LOADER_DIR${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + export LD_LIBRARY_PATH="$MLN_FFI_VULKAN_LOADER_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +fi MaplibreNativeCInstallDir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" \ dotnet run \ --project examples/dotnet-map/Maplibre.NativeFfi.Examples.DotnetMap.csproj \ -p:Platform="Any CPU" \ + -p:BaseOutputPath="$MISE_MONOREPO_ROOT/build/dotnet/$usage_preset/" \ -- "$usage_render_target" ''' diff --git a/examples/go-map/channels.go b/examples/go-map/channels.go deleted file mode 100644 index e3e208bfc..000000000 --- a/examples/go-map/channels.go +++ /dev/null @@ -1,110 +0,0 @@ -package main - -import ( - "sync" - "sync/atomic" - - maplibre "github.com/maplibre/maplibre-native-ffi/bindings/go" -) - -// cameraCommand is decoded on the render loop and applied on the runtime loop. -// Commands that depend on the current camera carry deltas so the read and write -// stay together on the map's owner thread. -type cameraCommand struct { - kind cameraCommandKind - deltaX float64 - deltaY float64 - scale float64 - anchor maplibre.ScreenPoint - durationMS float64 - inProgress bool -} - -type cameraCommandKind int - -const ( - commandCancelTransitions cameraCommandKind = iota - commandSetGestureInProgress - commandMoveBy - commandMoveByAnimated - commandScaleBy - commandScaleByAnimated - commandPitchBy - commandAdjustBearing - commandAdjustBearingAnimated - commandAdjustPitchAnimated - commandResetOrientation -) - -// commandQueue holds the camera commands the render loop has decoded and the -// runtime loop has not applied yet. It grows rather than dropping, because -// deltas and gesture brackets are not recoverable once discarded. -type commandQueue struct { - mu sync.Mutex - pending []cameraCommand -} - -// push is called on the render loop. -func (queue *commandQueue) push(command cameraCommand) { - queue.mu.Lock() - queue.pending = append(queue.pending, command) - queue.mu.Unlock() -} - -// drain is called on the runtime loop. It swaps the caller's buffer in for the -// pending slice, keeping the locked section O(1). -func (queue *commandQueue) drain(out []cameraCommand) []cameraCommand { - queue.mu.Lock() - defer queue.mu.Unlock() - pending := queue.pending - queue.pending = out[:0] - return pending -} - -// sharedState carries the render request, shutdown, and first failure between -// the render and runtime loops. -type sharedState struct { - renderRequested atomic.Bool - shutdown atomic.Bool - failureMu sync.Mutex - failure error -} - -func newSharedState() *sharedState { - shared := &sharedState{} - shared.renderRequested.Store(true) - return shared -} - -func (shared *sharedState) requestRender() { - shared.renderRequested.Store(true) -} - -func (shared *sharedState) consumeRenderRequest() bool { - return shared.renderRequested.Swap(false) -} - -func (shared *sharedState) requestShutdown() { - shared.shutdown.Store(true) -} - -func (shared *sharedState) shutdownRequested() bool { - return shared.shutdown.Load() -} - -func (shared *sharedState) fail(err error) { - if err == nil { - return - } - shared.failureMu.Lock() - defer shared.failureMu.Unlock() - if shared.failure == nil { - shared.failure = err - } -} - -func (shared *sharedState) firstFailure() error { - shared.failureMu.Lock() - defer shared.failureMu.Unlock() - return shared.failure -} diff --git a/examples/go-map/input.go b/examples/go-map/input.go index 720bb3bb9..74b903756 100644 --- a/examples/go-map/input.go +++ b/examples/go-map/input.go @@ -35,67 +35,70 @@ func logControls() { 0: reset pitch and bearing`) } -func (input *inputController) handleEvent(event *sdl.Event, commands *commandQueue, v viewport) bool { +func (input *inputController) handleEvent(event *sdl.Event, state *runtimeMapState, v viewport) (bool, error) { switch event.Type() { case sdl.EventMouseButtonDown: - return input.handleMouseButtonDown(event.MouseButton(), commands, v) + return input.handleMouseButtonDown(event.MouseButton(), state, v) case sdl.EventMouseButtonUp: - return input.handleMouseButtonUp(event.MouseButton(), commands, v) + return input.handleMouseButtonUp(event.MouseButton(), state, v) case sdl.EventMouseMotion: - return input.handleMouseMotion(event.MouseMotion(), commands, v) + return input.handleMouseMotion(event.MouseMotion(), state, v) case sdl.EventMouseWheel: - return handleMouseWheel(event.MouseWheel(), commands, v) + return handleMouseWheel(event.MouseWheel(), state, v) case sdl.EventKeyDown: - return handleKeyDown(event.Keyboard(), commands, v) + return handleKeyDown(event.Keyboard(), state, v) default: - return false + return false, nil } } -func (input *inputController) handleMouseButtonDown(event *sdl.MouseButtonEvent, commands *commandQueue, v viewport) bool { +func (input *inputController) handleMouseButtonDown(event *sdl.MouseButtonEvent, state *runtimeMapState, v viewport) (bool, error) { if event == nil { - return false + return false, nil } // A second button pressed during a live drag joins it, leaving the drag // baseline alone. if input.dragMode != dragNone { - return false + return false, nil } mode := dragModeForButton(event.Button) if mode == dragNone { - return false + return false, nil } cursor := logicalPoint(float64(event.X), float64(event.Y), v) input.lastX = cursor.X input.lastY = cursor.Y input.dragMode = mode input.dragButton = event.Button - // Cancel first, so the running transition stops before the first delta. - cancelQueued := enqueueCameraCommand(commands, cameraCommand{kind: commandCancelTransitions}) - gestureQueued := enqueueCameraCommand(commands, cameraCommand{kind: commandSetGestureInProgress, inProgress: true}) - return cancelQueued || gestureQueued + if err := state.setGestureInProgress(true); err != nil { + return false, err + } + return true, nil } // handleMouseButtonUp ends the drag only for the button that started it, so the // gesture bracket stays paired. -func (input *inputController) handleMouseButtonUp(event *sdl.MouseButtonEvent, commands *commandQueue, v viewport) bool { +func (input *inputController) handleMouseButtonUp(event *sdl.MouseButtonEvent, state *runtimeMapState, v viewport) (bool, error) { if event == nil || (event.Button != sdl.ButtonLeft && event.Button != sdl.ButtonRight) { - return false + return false, nil } if input.dragMode == dragNone || event.Button != input.dragButton { - return false + return false, nil } cursor := logicalPoint(float64(event.X), float64(event.Y), v) input.dragMode = dragNone input.dragButton = 0 input.lastX = cursor.X input.lastY = cursor.Y - return enqueueCameraCommand(commands, cameraCommand{kind: commandSetGestureInProgress, inProgress: false}) + if err := state.setGestureInProgress(false); err != nil { + return false, err + } + return true, nil } -func (input *inputController) handleMouseMotion(event *sdl.MouseMotionEvent, commands *commandQueue, v viewport) bool { +func (input *inputController) handleMouseMotion(event *sdl.MouseMotionEvent, state *runtimeMapState, v viewport) (bool, error) { if event == nil || input.dragMode == dragNone { - return false + return false, nil } cursor := logicalPoint(float64(event.X), float64(event.Y), v) dx := cursor.X - input.lastX @@ -103,31 +106,32 @@ func (input *inputController) handleMouseMotion(event *sdl.MouseMotionEvent, com input.lastX = cursor.X input.lastY = cursor.Y if dx == 0 && dy == 0 { - return false + return false, nil } switch input.dragMode { case dragPan: - return enqueueCameraCommand(commands, cameraCommand{kind: commandMoveBy, deltaX: dx, deltaY: dy}) + return true, state.moveBy(dx, dy, nil) case dragRotate: - bearingQueued := enqueueCameraCommand(commands, cameraCommand{kind: commandAdjustBearing, deltaX: dx * 0.5}) - pitchQueued := enqueueCameraCommand(commands, cameraCommand{kind: commandPitchBy, deltaY: dy * 0.5}) - return bearingQueued || pitchQueued + if err := state.adjustBearing(dx*0.5, nil); err != nil { + return false, err + } + return true, state.adjustPitch(dy*0.5, nil) } - return false + return false, nil } -func handleMouseWheel(event *sdl.MouseWheelEvent, commands *commandQueue, v viewport) bool { +func handleMouseWheel(event *sdl.MouseWheelEvent, state *runtimeMapState, v viewport) (bool, error) { if event == nil || event.Y == 0 { - return false + return false, nil } anchor := logicalPoint(float64(event.MouseX), float64(event.MouseY), v) - return enqueueCameraCommand(commands, cameraCommand{kind: commandScaleBy, scale: math.Pow(2, float64(event.Y)*0.25), anchor: anchor}) + return true, state.scaleBy(math.Pow(2, float64(event.Y)*0.25), anchor, nil) } -func handleKeyDown(event *sdl.KeyboardEvent, commands *commandQueue, v viewport) bool { +func handleKeyDown(event *sdl.KeyboardEvent, state *runtimeMapState, v viewport) (bool, error) { if event == nil { - return false + return false, nil } const ( panStep = 120.0 @@ -136,40 +140,36 @@ func handleKeyDown(event *sdl.KeyboardEvent, commands *commandQueue, v viewport) pitchStep = 5.0 ) center := maplibre.ScreenPoint{X: float64(v.logicalWidth) / 2, Y: float64(v.logicalHeight) / 2} - command := cameraCommand{durationMS: 160} + durationMS := 160.0 + var err error switch event.Scancode { case sdl.ScancodeLeft, sdl.ScancodeA: - command.kind, command.deltaX = commandMoveByAnimated, panStep + err = state.moveBy(panStep, 0, &durationMS) case sdl.ScancodeRight, sdl.ScancodeD: - command.kind, command.deltaX = commandMoveByAnimated, -panStep + err = state.moveBy(-panStep, 0, &durationMS) case sdl.ScancodeUp, sdl.ScancodeW: - command.kind, command.deltaY = commandMoveByAnimated, panStep + err = state.moveBy(0, panStep, &durationMS) case sdl.ScancodeDown, sdl.ScancodeS: - command.kind, command.deltaY = commandMoveByAnimated, -panStep + err = state.moveBy(0, -panStep, &durationMS) case sdl.ScancodeEquals, sdl.ScancodeKPPlus: - command.kind, command.scale, command.anchor = commandScaleByAnimated, zoomStep, center + err = state.scaleBy(zoomStep, center, &durationMS) case sdl.ScancodeMinus, sdl.ScancodeKPMinus: - command.kind, command.scale, command.anchor = commandScaleByAnimated, 1/zoomStep, center + err = state.scaleBy(1/zoomStep, center, &durationMS) case sdl.ScancodeQ: - command.kind, command.deltaX = commandAdjustBearingAnimated, -bearingStep + err = state.adjustBearing(-bearingStep, &durationMS) case sdl.ScancodeE: - command.kind, command.deltaX = commandAdjustBearingAnimated, bearingStep + err = state.adjustBearing(bearingStep, &durationMS) case sdl.ScancodeRightbracket: - command.kind, command.deltaY = commandAdjustPitchAnimated, pitchStep + err = state.adjustPitch(pitchStep, &durationMS) case sdl.ScancodeLeftbracket: - command.kind, command.deltaY = commandAdjustPitchAnimated, -pitchStep + err = state.adjustPitch(-pitchStep, &durationMS) case sdl.Scancode0: - command.kind = commandResetOrientation + err = state.resetOrientation(220) default: - return false + return false, nil } - return enqueueCameraCommand(commands, command) -} - -func enqueueCameraCommand(commands *commandQueue, command cameraCommand) bool { - commands.push(command) - return true + return err == nil, err } func dragModeForButton(button byte) dragMode { diff --git a/examples/go-map/main.go b/examples/go-map/main.go index 9f96e43cb..3e36ddc8e 100644 --- a/examples/go-map/main.go +++ b/examples/go-map/main.go @@ -5,7 +5,7 @@ import ( "fmt" "math" "os" - "runtime" + stdruntime "runtime" "strings" "github.com/jfreymuth/go-sdl3/sdl" @@ -13,8 +13,9 @@ import ( ) func main() { - runtime.LockOSThread() - defer runtime.UnlockOSThread() + // SDL and OpenGL keep the render-session graphics calls on this thread. + stdruntime.LockOSThread() + defer stdruntime.UnlockOSThread() mode, ok := parseArgs(os.Args[1:]) if !ok { @@ -104,41 +105,27 @@ func run(mode renderTargetMode) (result error) { } _ = sdl.GL_SetSwapInterval(1) - shared := newSharedState() - commands := &commandQueue{} - published := make(chan runtimeLoopHandles, 1) - runtimeDone := make(chan struct{}) - go func() { - defer close(runtimeDone) - runRuntimeLoop(view, commands, published, shared) - }() - handles, ok := <-published - if !ok { - <-runtimeDone + mapState, err := newRuntimeMapState(view) + if err != nil { _ = graphics.Close() - if failure := shared.firstFailure(); failure != nil { - return fmt.Errorf("runtime loop startup failed: %w", failure) - } - return errors.New("runtime loop stopped before publishing the map") + return err } - - state, err := newRenderMapState(graphics, handles.mapRef, view, mode) + state, err := newRenderMapState(graphics, mapState.mapRef, view, mode) if err != nil { - shared.requestShutdown() - _ = handles.wake.Signal() - <-runtimeDone return errors.Join( fmt.Errorf("render target attach failed: %w", err), - shared.firstFailure(), + mapState.Close(), graphics.Close(), ) } defer func() { - result = errors.Join(result, state.finishFrame(), state.closeTarget()) - shared.requestShutdown() - _ = handles.wake.Signal() - <-runtimeDone - result = errors.Join(result, shared.firstFailure(), graphics.Close()) + result = errors.Join( + result, + state.finishFrame(), + state.closeTarget(), + mapState.Close(), + graphics.Close(), + ) }() fmt.Printf("render target: %s\n", mode) @@ -146,6 +133,7 @@ func run(mode renderTargetMode) (result error) { logControls() running := true + renderRequested := true input := inputController{} handleEvent := func(event *sdl.Event) error { switch event.Type() { @@ -160,24 +148,30 @@ func run(mode renderTargetMode) (result error) { if err := state.resize(view); err != nil { return err } - shared.requestRender() + _, err := mapState.mapRef.Resize(maplibre.LogicalExtent{ + Width: view.logicalWidth, + Height: view.logicalHeight, + ScaleFactor: view.scaleFactor, + }) + if err != nil { + return err + } + renderRequested = true default: if view.empty() { return nil } - if input.handleEvent(event, commands, view) { - if err := handles.wake.Signal(); err != nil { - return fmt.Errorf("wake runtime loop failed: %w", err) - } - shared.requestRender() + changed, err := input.handleEvent(event, mapState, view) + if err != nil { + return err + } + if changed { + renderRequested = true } } return nil } for running { - if failure := shared.firstFailure(); failure != nil { - return fmt.Errorf("runtime loop failed: %w", failure) - } didWork := false var event sdl.Event for sdl.PollEvent(&event) { @@ -186,16 +180,25 @@ func run(mode renderTargetMode) (result error) { return err } } + requested, err := drainEvents(mapState.runtime, mapState.mapID) + if err != nil { + return err + } + if requested { + renderRequested = true + didWork = true + } - if shared.consumeRenderRequest() && !view.empty() && running { - rendered, err := state.renderUpdate() + if renderRequested && !view.empty() && running { + renderRequested = false + rendered, err := state.driveFrame() if err != nil { return err } if rendered { didWork = true } else { - shared.requestRender() + renderRequested = true } } if err := state.finishFrame(); err != nil { @@ -244,7 +247,7 @@ func validateNativeRenderBackend() error { } providers := maplibre.SupportedOpenGLContextProviders() required := maplibre.OpenGLContextProviderEGL - if runtime.GOOS == "windows" { + if stdruntime.GOOS == "windows" { required = maplibre.OpenGLContextProviderWGL } if !providers.Has(required) { diff --git a/examples/go-map/map_state.go b/examples/go-map/map_state.go index ac0e706b5..7e149eb8d 100644 --- a/examples/go-map/map_state.go +++ b/examples/go-map/map_state.go @@ -1,76 +1,17 @@ package main import ( + "context" "errors" "fmt" - "runtime" - "time" maplibre "github.com/maplibre/maplibre-native-ffi/bindings/go" ) -const runtimeParkTimeout = 100 * time.Millisecond - -// runtimeLoopHandles is published once after the runtime loop creates the map. -// The render loop uses the map only to attach its own render session and uses -// the wake source to release a parked pump after queuing work. -type runtimeLoopHandles struct { - mapRef *maplibre.MapHandle - wake *maplibre.WakeSource -} - -// runRuntimeLoop owns the runtime, map, pump, event queue, and every camera -// mutation on one stable native thread for their whole lifetime. -func runRuntimeLoop(v viewport, commands *commandQueue, published chan<- runtimeLoopHandles, shared *sharedState) { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - defer close(published) - - state, err := newRuntimeMapState(v) - if err != nil { - shared.fail(err) - return - } - defer func() { shared.fail(state.Close()) }() - - wake, err := state.runtime.WakeSource() - if err != nil { - shared.fail(fmt.Errorf("wake source acquire failed: %w", err)) - return - } - defer wake.Close() - published <- runtimeLoopHandles{mapRef: state.mapRef, wake: wake} - - for !shared.shutdownRequested() { - if err := state.applyCommands(commands); err != nil { - shared.fail(err) - break - } - if err := state.runtime.Pump(runtimeParkTimeout, -1); err != nil { - shared.fail(fmt.Errorf("runtime pump failed: %w", err)) - break - } - renderRequested, err := drainEvents(state.runtime, state.mapID) - if err != nil { - shared.fail(err) - break - } - if renderRequested { - shared.requestRender() - } - } - // Destroying a map with an attached session is invalid, so keep the map - // alive until the render loop signals shutdown. - for !shared.shutdownRequested() { - time.Sleep(time.Millisecond) - } -} - type runtimeMapState struct { runtime *maplibre.RuntimeHandle mapRef *maplibre.MapHandle mapID maplibre.MapID - batch []cameraCommand } func newRuntimeMapState(v viewport) (*runtimeMapState, error) { @@ -79,26 +20,27 @@ func newRuntimeMapState(v viewport) (*runtimeMapState, error) { return nil, fmt.Errorf("runtime create failed: %w", err) } state := &runtimeMapState{runtime: runtimeHandle} - mapOptions := maplibre.NewMapOptions(v.logicalWidth, v.logicalHeight, v.scaleFactor) - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, so the first style load already queues nothing else. mapOptions.EventMask = maplibre.RuntimeEventMaskMapRenderUpdateAvailable | maplibre.RuntimeEventMaskMapRenderFrameFinished - mapHandle, err := runtimeHandle.NewMapWithOptions(mapOptions) + mapFuture, err := runtimeHandle.NewMapWithOptions(mapOptions) + if err != nil { + _ = state.Close() + return nil, fmt.Errorf("map create failed: %w", err) + } + mapHandle, err := mapFuture.Await(context.Background()) if err != nil { _ = state.Close() return nil, fmt.Errorf("map create failed: %w", err) } state.mapRef = mapHandle - mapID, err := mapHandle.ID() + state.mapID, err = mapHandle.ID() if err != nil { _ = state.Close() return nil, fmt.Errorf("map identity read failed: %w", err) } - state.mapID = mapID - - if err := mapHandle.SetStyleURL("https://tiles.openfreemap.org/styles/bright"); err != nil { + _, err = mapHandle.SetStyleURL("https://tiles.openfreemap.org/styles/bright") + if err != nil { _ = state.Close() return nil, fmt.Errorf("style load failed: %w", err) } @@ -107,11 +49,13 @@ func newRuntimeMapState(v viewport) (*runtimeMapState, error) { WithZoom(13). WithBearing(12). WithPitch(30) - if err := mapHandle.JumpTo(initialCamera); err != nil { + _, err = mapHandle.JumpTo(initialCamera) + if err != nil { _ = state.Close() return nil, fmt.Errorf("camera jump failed: %w", err) } - if err := mapHandle.RequestRepaint(); err != nil { + _, err = mapHandle.RequestRepaint() + if err != nil { _ = state.Close() return nil, fmt.Errorf("initial repaint request failed: %w", err) } @@ -125,92 +69,89 @@ func (state *runtimeMapState) Close() error { state.mapRef = nil } if state.runtime != nil { - result = errors.Join(result, state.runtime.Close()) + teardown, err := state.runtime.Close() + result = errors.Join(result, err) + if err == nil { + // Awaiting the release completion keeps process exit ordered + // after native teardown. + _, waitErr := teardown.Await(context.Background()) + result = errors.Join(result, waitErr) + } state.runtime = nil } return result } -func (state *runtimeMapState) applyCommands(commands *commandQueue) error { - state.batch = commands.drain(state.batch) - for _, command := range state.batch { - if err := state.applyCommand(command); err != nil { - return err - } +func (state *runtimeMapState) setGestureInProgress(inProgress bool) error { + phase := maplibre.GesturePhaseEnd + if inProgress { + phase = maplibre.GesturePhaseBegin } - return nil + return state.updateCamera(maplibre.CameraUpdate{GesturePhase: phase}) } -func (state *runtimeMapState) applyCommand(command cameraCommand) error { - m := state.mapRef - animation := maplibre.AnimationOptions{}.WithDurationMS(command.durationMS) - var err error - switch command.kind { - case commandCancelTransitions: - err = m.CancelTransitions() - case commandSetGestureInProgress: - err = m.SetGestureInProgress(command.inProgress) - case commandMoveBy: - err = m.MoveBy(maplibre.ScreenPoint{X: command.deltaX, Y: command.deltaY}) - case commandMoveByAnimated: - err = m.MoveByAnimated(maplibre.ScreenPoint{X: command.deltaX, Y: command.deltaY}, &animation) - case commandScaleBy: - err = m.ScaleBy(command.scale, &command.anchor) - case commandScaleByAnimated: - err = m.ScaleByAnimated(command.scale, &command.anchor, &animation) - case commandPitchBy: - err = m.PitchBy(command.deltaY) - case commandAdjustBearing: - err = state.adjustBearing(command.deltaX, nil) - case commandAdjustBearingAnimated: - err = state.adjustBearing(command.deltaX, &animation) - case commandAdjustPitchAnimated: - err = state.adjustPitch(command.deltaY, &animation) - case commandResetOrientation: - err = m.EaseTo(maplibre.CameraOptions{}.WithBearing(0).WithPitch(0), &animation) - default: - err = fmt.Errorf("unknown camera command: %d", command.kind) - } - if err != nil { - return fmt.Errorf("camera command failed: %w", err) +func (state *runtimeMapState) moveBy(dx, dy float64, durationMS *float64) error { + _, err := state.mapRef.ApplyCameraDelta(maplibre.CameraDelta{ + Offset: maplibre.ScreenPoint{X: dx, Y: dy}, Animation: animationOptions(durationMS), + }) + return err +} + +func (state *runtimeMapState) scaleBy(scale float64, anchor maplibre.ScreenPoint, durationMS *float64) error { + _, err := state.mapRef.ApplyCameraDelta(maplibre.CameraDelta{ + Kind: maplibre.CameraDeltaKindScale, Amount: scale, Anchor: &anchor, Animation: animationOptions(durationMS), + }) + return err +} + +func (state *runtimeMapState) adjustPitch(delta float64, durationMS *float64) error { + _, err := state.mapRef.ApplyCameraDelta(maplibre.CameraDelta{ + Kind: maplibre.CameraDeltaKindPitch, Amount: delta, Animation: animationOptions(durationMS), + }) + return err +} + +func (state *runtimeMapState) adjustBearing(delta float64, durationMS *float64) error { + _, err := state.mapRef.ApplyCameraDelta(maplibre.CameraDelta{ + Kind: maplibre.CameraDeltaKindBearing, Amount: delta, Animation: animationOptions(durationMS), + }) + return err +} + +func (state *runtimeMapState) resetOrientation(durationMS float64) error { + return state.updateCamera(cameraUpdate(maplibre.CameraOptions{}.WithBearing(0).WithPitch(0), &durationMS)) +} + +func (state *runtimeMapState) updateCamera(update maplibre.CameraUpdate) error { + if _, err := state.mapRef.UpdateCamera(update); err != nil { + return fmt.Errorf("camera update failed: %w", err) } return nil } -func (state *runtimeMapState) adjustBearing(delta float64, animation *maplibre.AnimationOptions) error { - camera, err := state.mapRef.Camera() - if err != nil { - return err - } - bearing := delta - if camera.Bearing != nil { - bearing += *camera.Bearing - } - options := maplibre.CameraOptions{}.WithBearing(bearing) - if animation == nil { - return state.mapRef.JumpTo(options) +func cameraUpdate(options maplibre.CameraOptions, durationMS *float64) maplibre.CameraUpdate { + update := maplibre.CameraUpdate{Camera: options} + if durationMS != nil { + animation := maplibre.AnimationOptions{}.WithDurationMS(*durationMS) + update.Mode = maplibre.CameraUpdateModeEase + update.Animation = &animation } - return state.mapRef.EaseTo(options, animation) + return update } -func (state *runtimeMapState) adjustPitch(delta float64, animation *maplibre.AnimationOptions) error { - camera, err := state.mapRef.Camera() - if err != nil { - return err - } - pitch := delta - if camera.Pitch != nil { - pitch += *camera.Pitch +func animationOptions(durationMS *float64) *maplibre.AnimationOptions { + if durationMS == nil { + return nil } - return state.mapRef.EaseTo(maplibre.CameraOptions{}.WithPitch(clamp(pitch, 0, 60)), animation) + animation := maplibre.AnimationOptions{}.WithDurationMS(*durationMS) + return &animation } func drainEvents(runtimeHandle *maplibre.RuntimeHandle, mapID maplibre.MapID) (bool, error) { renderRequested := false - // One drain takes every event the pump produced. - batch, err := runtimeHandle.DrainEvents(0) + batch, err := runtimeHandle.DrainEvents() if err != nil { - return renderRequested, fmt.Errorf("runtime event drain failed: %w", err) + return false, fmt.Errorf("runtime event drain failed: %w", err) } for _, event := range batch.Events { if event.Source.Type != maplibre.RuntimeEventSourceMap || event.Source.MapID != mapID { @@ -265,9 +206,9 @@ func (state *renderMapState) finishFrame() error { return state.target.FinishFrame() } -func (state *renderMapState) renderUpdate() (bool, error) { +func (state *renderMapState) driveFrame() (bool, error) { if state.target == nil { return false, nil } - return state.target.RenderUpdate() + return state.target.DriveFrame() } diff --git a/examples/go-map/mise.toml b/examples/go-map/mise.toml index 1db4b6d80..163e768cb 100644 --- a/examples/go-map/mise.toml +++ b/examples/go-map/mise.toml @@ -14,6 +14,7 @@ fi native_install_dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" sdl_prefix="$(mise where "conda:sdl3@{{vars.sdl3_version}}")" export PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig:$sdl_prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export CGO_CPPFLAGS="-I$native_install_dir/include -I$MISE_MONOREPO_ROOT/third_party/khronos/include ${CGO_CPPFLAGS:-}" export CGO_LDFLAGS="-Wl,-rpath,$native_install_dir/lib -Wl,-rpath,$sdl_prefix/lib ${CGO_LDFLAGS:-}" go build -o "$MISE_MONOREPO_ROOT/build/$usage_preset/go-map" . ''' @@ -30,6 +31,7 @@ fi native_install_dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" sdl_prefix="$(mise where "conda:sdl3@{{vars.sdl3_version}}")" export PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig:$sdl_prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export CGO_CPPFLAGS="-I$native_install_dir/include -I$MISE_MONOREPO_ROOT/third_party/khronos/include ${CGO_CPPFLAGS:-}" go vet ./... ''' @@ -39,6 +41,7 @@ run = ''' native_install_dir="$MISE_MONOREPO_ROOT/build/$usage_preset/install" sdl_prefix="$(mise where "conda:sdl3@{{vars.sdl3_version}}")" export PKG_CONFIG_PATH="$native_install_dir/share/pkgconfig:$sdl_prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export CGO_CPPFLAGS="-I$native_install_dir/include -I$MISE_MONOREPO_ROOT/third_party/khronos/include ${CGO_CPPFLAGS:-}" export LD_LIBRARY_PATH="$native_install_dir/lib:$sdl_prefix/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" go run . "$usage_render_target" ''' diff --git a/examples/go-map/opengl.go b/examples/go-map/opengl.go index 95e6ffb5c..efc3d608e 100644 --- a/examples/go-map/opengl.go +++ b/examples/go-map/opengl.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "runtime" @@ -18,12 +19,66 @@ type renderTarget interface { // handing the session a replacement. Resize(viewport) error FinishFrame() error - // RenderUpdate renders the latest map update, and reports whether a frame - // reached the screen. It reports true only for a rendered frame, so the - // render loop asks for another one when the map has no update yet, the map - // has not applied the session's size yet, or the target had no frame to - // draw into. - RenderUpdate() (bool, error) + // DriveFrame submits demand and services the caller graphics-thread driver. + // It reports true only when a frame reaches the screen. + DriveFrame() (bool, error) +} + +func serviceFuture(session *maplibre.RenderSessionHandle, future *maplibre.Future[struct{}]) error { + for { + select { + case <-future.Done(): + _, err := future.Await(context.Background()) + return err + default: + } + if _, err := session.ServiceDriverWork(16); err != nil { + return err + } + } +} + +func detachCallerSession(session *maplibre.RenderSessionHandle) error { + completed, err := session.Detach() + if err != nil { + return err + } + return serviceFuture(session, completed) +} + +func closeCallerSession(session *maplibre.RenderSessionHandle) error { + if err := detachCallerSession(session); err == nil { + return session.Close() + } + if _, err := session.Abandon(); err != nil { + return err + } + return session.Close() +} + +func renderCallerFrame(session *maplibre.RenderSessionHandle) (maplibre.RenderResult, error) { + if err := session.RequestFrame(maplibre.NewFrameDemand()); err != nil { + return 0, err + } + if _, err := session.ServiceDriverWork(16); err != nil { + return 0, err + } + batch, err := session.DrainFrameResults() + if err != nil { + return 0, err + } + defer batch.Close() + results, err := batch.Results() + if err != nil || len(results) == 0 { + return 0, err + } + result := results[len(results)-1].Disposition + for _, frame := range results { + if frame.Disposition == maplibre.RenderResultRendered { + return frame.Disposition, nil + } + } + return result, nil } type openGLContext struct { @@ -305,19 +360,33 @@ func newOpenGLOwnedTextureTarget(context *openGLContext, v viewport, m *maplibre _ = target.Close() return nil, err } - session, err := m.AttachOpenGLOwnedTexture(maplibre.OpenGLOwnedTextureDescriptor{Extent: v.extent(), Context: descriptor}) + options := maplibre.NewRenderSessionAttachOptions() + options.Driver = maplibre.RenderDriverCallerGraphicsThread + options.RequestedTextureRingDepth = 2 + session, operation, err := m.AttachOpenGLOwnedTexture( + maplibre.OpenGLOwnedTextureDescriptor{ + Extent: v.extent(), + Context: descriptor, + }, + options, + ) + if err == nil { + target.session = session + err = serviceFuture(session, operation) + } if err != nil { _ = target.Close() return nil, fmt.Errorf("OpenGL texture attach failed: %w", err) } - target.session = session return target, nil } func (target *openGLOwnedTextureTarget) Close() error { var result error if target.session != nil { - result = errors.Join(result, target.session.Close()) + if err := closeCallerSession(target.session); err != nil { + return err + } target.session = nil } if target.compositor != nil { @@ -331,36 +400,38 @@ func (target *openGLOwnedTextureTarget) Resize(v viewport) error { if err := target.compositor.Resize(v); err != nil { return err } - return target.session.Resize(v.extent()) + operation, err := target.session.Resize(v.extent()) + if err != nil { + return err + } + return serviceFuture(target.session, operation) } -func (target *openGLOwnedTextureTarget) FinishFrame() error { return target.compositor.FinishFrame() } +func (target *openGLOwnedTextureTarget) FinishFrame() error { + return target.compositor.FinishFrame() +} -func (target *openGLOwnedTextureTarget) RenderUpdate() (bool, error) { - update, err := target.session.RenderUpdate() +func (target *openGLOwnedTextureTarget) DriveFrame() (bool, error) { + result, err := renderCallerFrame(target.session) if err != nil { return false, fmt.Errorf("OpenGL texture render failed: %w", err) } - if update.Result != maplibre.RenderResultRendered { + if result != maplibre.RenderResultRendered { return false, nil } - frame, err := target.session.AcquireOpenGLTextureFrame() - if err != nil { - if errors.Is(err, maplibre.ErrInvalidState) { - return false, nil - } - return false, fmt.Errorf("OpenGL texture acquire failed: %w", err) - } - defer func() { _ = frame.Close() }() - texture, err := frame.Texture() + frame, err := target.session.AcquireFrame() if err != nil { return false, err } - textureTarget, err := frame.Target() - if err != nil { - return false, err + info, accessErr := frame.OpenGLTexture() + var drawErr error + if accessErr == nil { + drawErr = target.compositor.DrawTexture(info.Target, info.Texture) } - return true, target.compositor.DrawTexture(textureTarget, texture) + releaseErr := frame.Release(maplibre.GPUSync{ + Kind: maplibre.GPUSyncCPUComplete, + }) + return accessErr == nil, errors.Join(accessErr, drawErr, releaseErr) } type openGLBorrowedTextureTarget struct { @@ -386,30 +457,39 @@ func newOpenGLBorrowedTextureTarget(context *openGLContext, v viewport, m *mapli _ = target.Close() return nil, err } - session, err := m.AttachOpenGLBorrowedTexture(maplibre.OpenGLBorrowedTextureDescriptor{ + options := maplibre.NewRenderSessionAttachOptions() + options.Driver = maplibre.RenderDriverCallerGraphicsThread + session, operation, err := m.AttachOpenGLBorrowedTexture(maplibre.OpenGLBorrowedTextureDescriptor{ Extent: v.extent(), PhysicalWidth: v.physicalWidth, PhysicalHeight: v.physicalHeight, Context: descriptor, Texture: texture, Target: glTexture2D, - }) + }, options) + if err == nil { + target.session = session + err = serviceFuture(session, operation) + } if err != nil { _ = target.Close() return nil, fmt.Errorf("OpenGL borrowed texture attach failed: %w", err) } - target.session = session return target, nil } func (target *openGLBorrowedTextureTarget) Close() error { var result error if target.session != nil { - result = errors.Join(result, target.session.Close()) + if err := closeCallerSession(target.session); err != nil { + return err + } target.session = nil } if target.texture != 0 { - result = errors.Join(result, target.compositor.context.MakeCurrent()) + if err := target.compositor.context.MakeCurrent(); err != nil { + return errors.Join(result, err) + } glDeleteTexture(target.texture) target.texture = 0 } @@ -431,17 +511,18 @@ func (target *openGLBorrowedTextureTarget) Resize(v viewport) error { if err != nil { return err } - if err := target.session.SetOpenGLBorrowedTextureTarget(maplibre.OpenGLBorrowedTextureDescriptor{ + operation, err := target.session.SetOpenGLBorrowedTextureTarget(maplibre.OpenGLBorrowedTextureDescriptor{ Extent: v.extent(), PhysicalWidth: v.physicalWidth, PhysicalHeight: v.physicalHeight, Context: descriptor, Texture: replacement, Target: glTexture2D, - }); err != nil { - // The session may have taken the replacement before failing, so detach - // before either texture is deleted. - _ = target.session.Detach() + }) + if err == nil { + err = serviceFuture(target.session, operation) + } + if err != nil { glDeleteTexture(replacement) return fmt.Errorf("OpenGL borrowed texture set target failed: %w", err) } @@ -457,12 +538,12 @@ func (target *openGLBorrowedTextureTarget) FinishFrame() error { return target.compositor.FinishFrame() } -func (target *openGLBorrowedTextureTarget) RenderUpdate() (bool, error) { - update, err := target.session.RenderUpdate() +func (target *openGLBorrowedTextureTarget) DriveFrame() (bool, error) { + result, err := renderCallerFrame(target.session) if err != nil { return false, fmt.Errorf("OpenGL borrowed texture render failed: %w", err) } - if update.Result != maplibre.RenderResultRendered { + if result != maplibre.RenderResultRendered { return false, nil } return true, target.compositor.DrawTexture(glTexture2D, target.texture) @@ -501,22 +582,36 @@ func newOpenGLSurfaceTarget(context *openGLContext, v viewport, m *maplibre.MapH if err != nil { return nil, err } - session, err := m.AttachOpenGLSurface(maplibre.OpenGLSurfaceDescriptor{Extent: v.extent(), Context: descriptor, Surface: context.surface()}) + options := maplibre.NewRenderSessionAttachOptions() + options.Driver = maplibre.RenderDriverCallerGraphicsThread + session, operation, err := m.AttachOpenGLSurface( + maplibre.OpenGLSurfaceDescriptor{ + Extent: v.extent(), + Context: descriptor, + Surface: context.surface(), + }, + options, + ) + if err == nil { + target.session = session + err = serviceFuture(session, operation) + } if err != nil { _ = target.Close() return nil, fmt.Errorf("OpenGL surface attach failed: %w", err) } - target.session = session return target, nil } func (target *openGLSurfaceTarget) Close() error { - var result error - if target.session != nil { - result = errors.Join(result, target.session.Close()) - target.session = nil + if target.session == nil { + return nil } - return result + if err := closeCallerSession(target.session); err != nil { + return err + } + target.session = nil + return nil } // Resize handles SDL returning a different EGL window surface for the resized @@ -526,24 +621,30 @@ func (target *openGLSurfaceTarget) Resize(v viewport) error { if err := target.context.refreshPlatformSurface(); err != nil { // SDL may already have dropped the surface the session presents // through, so detach rather than leave it naming a dead surface. - _ = target.session.Detach() + _ = detachCallerSession(target.session) return err } if target.context.surface() == outgoing { - return target.session.Resize(v.extent()) + operation, err := target.session.Resize(v.extent()) + if err != nil { + return err + } + return serviceFuture(target.session, operation) } descriptor, err := target.context.descriptor(false) if err != nil { return err } - if err := target.session.SetOpenGLSurfaceTarget(maplibre.OpenGLSurfaceDescriptor{ + operation, err := target.session.SetOpenGLSurfaceTarget(maplibre.OpenGLSurfaceDescriptor{ Extent: v.extent(), Context: descriptor, Surface: target.context.surface(), - }); err != nil { - // SDL already dropped the outgoing surface, so on failure the session - // may hold a dead surface; detach to stop it naming either. - _ = target.session.Detach() + }) + if err == nil { + err = serviceFuture(target.session, operation) + } + if err != nil { + _ = detachCallerSession(target.session) return fmt.Errorf("OpenGL surface set target failed: %w", err) } return nil @@ -557,12 +658,12 @@ func (target *openGLSurfaceTarget) FinishFrame() error { return nil } -func (target *openGLSurfaceTarget) RenderUpdate() (bool, error) { - update, err := target.session.RenderUpdate() +func (target *openGLSurfaceTarget) DriveFrame() (bool, error) { + result, err := renderCallerFrame(target.session) if err != nil { return false, fmt.Errorf("OpenGL surface render failed: %w", err) } - return update.Result == maplibre.RenderResultRendered, nil + return result == maplibre.RenderResultRendered, nil } func createTextureProgram() (uint32, error) { diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Channels.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Channels.kt deleted file mode 100644 index 28a0e72f9..000000000 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Channels.kt +++ /dev/null @@ -1,147 +0,0 @@ -package org.maplibre.nativeffi.examples.lwjglmap - -import java.util.ArrayDeque -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import kotlin.time.Duration.Companion.seconds -import org.maplibre.nativeffi.geo.ScreenPoint -import org.maplibre.nativeffi.map.MapHandle -import org.maplibre.nativeffi.runtime.WakeSource - -/** - * A camera change decoded on the render loop and applied on the map's thread. - * - * Commands carry deltas rather than absolute targets, because reading the camera and writing the - * new one has to happen together on the thread that owns the map. - */ -internal sealed interface CameraCommand { - data object CancelTransitions : CameraCommand - - data class SetGestureInProgress(val inProgress: Boolean) : CameraCommand - - data class MoveBy(val dx: Double, val dy: Double) : CameraCommand - - data class MoveByAnimated(val dx: Double, val dy: Double, val durationMs: Double) : CameraCommand - - data class ScaleBy(val scale: Double, val anchor: ScreenPoint) : CameraCommand - - data class ScaleByAnimated(val scale: Double, val anchor: ScreenPoint, val durationMs: Double) : - CameraCommand - - data class PitchBy(val delta: Double) : CameraCommand - - data class AdjustBearing(val delta: Double) : CameraCommand - - data class AdjustBearingAnimated(val delta: Double, val durationMs: Double) : CameraCommand - - data class AdjustPitchAnimated(val delta: Double, val durationMs: Double) : CameraCommand - - data class ResetOrientation(val durationMs: Double) : CameraCommand -} - -/** - * Pending camera commands, filled by the render loop and drained by the runtime loop. - * - * The queue grows rather than dropping: its commands are deltas and a gesture bracket, and - * discarding either corrupts the gesture. - */ -internal class CommandQueue { - private val lock = Any() - private var pending = ArrayDeque() - - /** - * Released by [push] so a queued command reaches the runtime loop without waiting out its parking - * bound. Set once the runtime loop has published its wake source. - */ - @Volatile var onEnqueue: (() -> Unit)? = null - - fun push(command: CameraCommand) { - synchronized(lock) { pending.addLast(command) } - onEnqueue?.invoke() - } - - /** Runtime loop: hands the pending deque over and takes [out] in exchange. */ - fun drain(out: ArrayDeque): ArrayDeque { - out.clear() - return synchronized(lock) { - val drained = pending - pending = out - drained - } - } -} - -/** - * One-bit signal that a frame is worth drawing. The render loop consumes before it renders and sets - * again when nothing was rendered, so a request published during a render is not lost. - */ -internal class RenderRequest { - private val requested = AtomicBoolean(true) - - fun set() { - requested.set(true) - } - - fun consume(): Boolean = requested.getAndSet(false) -} - -/** - * Publishes the map and the runtime's wake source from the runtime loop to the render loop, and - * carries shutdown and failure the other way. - * - * The render loop uses the published handle only to attach its own render session, which native - * serves from any thread; every other map call stays on the runtime loop. - */ -internal class MapChannel { - private val map = AtomicReference(null) - private val wake = AtomicReference(null) - private val shutdown = AtomicBoolean(false) - private val failure = AtomicReference(null) - - /** Runtime loop: announces the map it just created and its wake source. */ - fun publish(handle: MapHandle, source: WakeSource) { - wake.set(source) - map.set(handle) - } - - /** Render loop: releases the runtime loop's parked pump. */ - fun wakeRuntimeLoop() { - wake.get()?.signal() - } - - /** Render loop: the map to attach against, once the runtime loop has one. */ - fun mapHandle(): MapHandle? = map.get() - - /** - * Render loop: asks the runtime loop to stop. Called only after the render session is closed, - * because the map cannot be destroyed before then. - */ - fun requestShutdown() { - shutdown.set(true) - wakeRuntimeLoop() - } - - fun shutdownRequested(): Boolean = shutdown.get() - - /** - * Runtime loop: blocks until [requestShutdown], or until the bound expires, so a render loop that - * stopped without signalling cannot wedge teardown. - */ - fun awaitShutdown() { - val deadline = System.nanoTime() + SHUTDOWN_WAIT.inWholeNanoseconds - while (!shutdown.get() && System.nanoTime() < deadline) { - Thread.sleep(SHUTDOWN_POLL_MS) - } - } - - fun fail(error: Throwable) { - failure.compareAndSet(null, error) - } - - fun failure(): Throwable? = failure.get() - - private companion object { - val SHUTDOWN_WAIT = 5.seconds - const val SHUTDOWN_POLL_MS = 2L - } -} diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/InputController.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/InputController.kt index ab98becbe..39a9c9e1b 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/InputController.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/InputController.kt @@ -28,14 +28,10 @@ import org.lwjgl.glfw.GLFW.glfwSetMouseButtonCallback import org.lwjgl.glfw.GLFW.glfwSetScrollCallback import org.maplibre.nativeffi.geo.ScreenPoint -/** - * Decodes GLFW input into camera commands. GLFW delivers these callbacks on the render loop thread, - * which does not own the map, so this only produces commands; the runtime loop applies them on the - * map's thread. - */ +/** Decodes GLFW input into camera updates on the GLFW thread. */ internal class InputController( private val window: Long, - private val commands: CommandQueue, + private val mapState: MapState, private val renderRequest: RenderRequest, private val viewport: () -> Viewport, ) : AutoCloseable { @@ -65,10 +61,10 @@ internal class InputController( return } if (rightDown || (leftDown && ctrlDown)) { - commands.push(CameraCommand.AdjustBearing(dx * DRAG_ROTATE_FACTOR)) - commands.push(CameraCommand.PitchBy(dy * DRAG_PITCH_FACTOR)) + mapState.adjustBearing(dx * DRAG_ROTATE_FACTOR) + mapState.adjustPitch(dy * DRAG_PITCH_FACTOR) } else if (leftDown) { - commands.push(CameraCommand.MoveBy(dx, dy)) + mapState.moveBy(dx, dy) } else { return } @@ -88,11 +84,10 @@ internal class InputController( if (action == GLFW_PRESS) true else if (action == GLFW_RELEASE) false else rightDown } if (action == GLFW_PRESS) { - // Queued ahead of the drag's own commands, so the transition stops before the first delta. - commands.push(CameraCommand.CancelTransitions) + mapState.cancelTransitions() } if (dragging != wasDragging) { - commands.push(CameraCommand.SetGestureInProgress(dragging)) + mapState.setGestureInProgress(dragging) } } @@ -102,7 +97,7 @@ internal class InputController( private fun onScroll(yOffset: Double) { // GLFW reports OS-adjusted scroll deltas, so natural scrolling needs no correction here. val scale = 2.0.pow(yOffset * 0.25) - commands.push(CameraCommand.ScaleBy(scale, ScreenPoint(cursorX, cursorY))) + mapState.scaleBy(scale, ScreenPoint(cursorX, cursorY)) renderRequest.set() } @@ -111,44 +106,68 @@ internal class InputController( if (action != GLFW_PRESS && action != GLFW_REPEAT) { return } - val command = + val cameraChanged = when (key) { GLFW_KEY_LEFT, - GLFW_KEY_A -> CameraCommand.MoveByAnimated(KEYBOARD_PAN, 0.0, KEYBOARD_ANIMATION_MS) + GLFW_KEY_A -> { + mapState.moveBy(KEYBOARD_PAN, 0.0, KEYBOARD_ANIMATION_MS) + true + } GLFW_KEY_RIGHT, - GLFW_KEY_D -> CameraCommand.MoveByAnimated(-KEYBOARD_PAN, 0.0, KEYBOARD_ANIMATION_MS) + GLFW_KEY_D -> { + mapState.moveBy(-KEYBOARD_PAN, 0.0, KEYBOARD_ANIMATION_MS) + true + } GLFW_KEY_UP, - GLFW_KEY_W -> CameraCommand.MoveByAnimated(0.0, KEYBOARD_PAN, KEYBOARD_ANIMATION_MS) + GLFW_KEY_W -> { + mapState.moveBy(0.0, KEYBOARD_PAN, KEYBOARD_ANIMATION_MS) + true + } GLFW_KEY_DOWN, - GLFW_KEY_S -> CameraCommand.MoveByAnimated(0.0, -KEYBOARD_PAN, KEYBOARD_ANIMATION_MS) - - GLFW_KEY_EQUAL -> - CameraCommand.ScaleByAnimated(KEYBOARD_ZOOM, viewportCenter(), KEYBOARD_ANIMATION_MS) - - GLFW_KEY_MINUS -> - CameraCommand.ScaleByAnimated( - 1.0 / KEYBOARD_ZOOM, - viewportCenter(), - KEYBOARD_ANIMATION_MS, - ) - - GLFW_KEY_Q -> CameraCommand.AdjustBearingAnimated(-KEYBOARD_BEARING, KEYBOARD_ANIMATION_MS) - - GLFW_KEY_E -> CameraCommand.AdjustBearingAnimated(KEYBOARD_BEARING, KEYBOARD_ANIMATION_MS) - GLFW_KEY_RIGHT_BRACKET -> - CameraCommand.AdjustPitchAnimated(KEYBOARD_PITCH, KEYBOARD_ANIMATION_MS) - - GLFW_KEY_LEFT_BRACKET -> - CameraCommand.AdjustPitchAnimated(-KEYBOARD_PITCH, KEYBOARD_ANIMATION_MS) - - GLFW_KEY_0 -> CameraCommand.ResetOrientation(RESET_ANIMATION_MS) - else -> null + GLFW_KEY_S -> { + mapState.moveBy(0.0, -KEYBOARD_PAN, KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_EQUAL -> { + mapState.scaleBy(KEYBOARD_ZOOM, viewportCenter(), KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_MINUS -> { + mapState.scaleBy(1.0 / KEYBOARD_ZOOM, viewportCenter(), KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_Q -> { + mapState.adjustBearing(-KEYBOARD_BEARING, KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_E -> { + mapState.adjustBearing(KEYBOARD_BEARING, KEYBOARD_ANIMATION_MS) + true + } + GLFW_KEY_RIGHT_BRACKET -> { + mapState.adjustPitch(KEYBOARD_PITCH, KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_LEFT_BRACKET -> { + mapState.adjustPitch(-KEYBOARD_PITCH, KEYBOARD_ANIMATION_MS) + true + } + + GLFW_KEY_0 -> { + mapState.resetOrientation(RESET_ANIMATION_MS) + true + } + else -> false } - if (command != null) { - commands.push(command) + if (cameraChanged) { renderRequest.set() } } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MapState.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MapState.kt index 8db8c6cfc..82248824a 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MapState.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MapState.kt @@ -1,96 +1,123 @@ package org.maplibre.nativeffi.examples.lwjglmap -import java.util.ArrayDeque -import kotlin.math.max -import kotlin.math.min +import java.util.concurrent.atomic.AtomicBoolean import org.maplibre.nativeffi.camera.AnimationOptions +import org.maplibre.nativeffi.camera.CameraDelta +import org.maplibre.nativeffi.camera.CameraDeltaKind import org.maplibre.nativeffi.camera.CameraOptions +import org.maplibre.nativeffi.camera.CameraUpdate +import org.maplibre.nativeffi.camera.CameraUpdateMode +import org.maplibre.nativeffi.camera.GesturePhase import org.maplibre.nativeffi.geo.LatLng import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.map.MapMode import org.maplibre.nativeffi.map.MapOptions +import org.maplibre.nativeffi.map.MapSize import org.maplibre.nativeffi.runtime.RuntimeEventMask import org.maplibre.nativeffi.runtime.RuntimeEventPayload import org.maplibre.nativeffi.runtime.RuntimeEventType import org.maplibre.nativeffi.runtime.RuntimeHandle import org.maplibre.nativeffi.runtime.RuntimeOptions -import org.maplibre.nativeffi.runtime.WakeSource -/** - * Runtime and map, owned for their whole lifetime by the runtime loop thread. The render target - * belongs to the render loop thread, which owns the window and the graphics context. - */ +/** Runtime and map state driven by the core-owned runtime worker. */ internal class MapState private constructor(private val runtime: RuntimeHandle, val map: MapHandle) : AutoCloseable { - /** Acquires the wake source the render loop uses to release this loop's parked pump. */ - fun acquireWakeSource(): WakeSource = runtime.acquireWakeSource() + fun cancelTransitions() { + map.updateCamera(CameraUpdate()) + } - /** Reused across drains, so applying a batch allocates nothing. */ - private var batch = ArrayDeque() + fun setGestureInProgress(inProgress: Boolean) { + map.updateCamera( + CameraUpdate(gesturePhase = if (inProgress) GesturePhase.BEGIN else GesturePhase.END) + ) + } - /** One runtime loop iteration: apply queued commands, pump once, drain events. */ - fun step(commands: CommandQueue, renderRequest: RenderRequest) { - batch = commands.drain(batch) - for (command in batch) { - apply(command) - } - // This thread has no display to pace it, so the pump's park is the cadence. - runtime.pump(PARK_TIMEOUT_MS) - if (drainEvents()) { - renderRequest.set() - } + fun moveBy(dx: Double, dy: Double, durationMs: Double? = null) { + map.applyCameraDelta( + CameraDelta( + offset = org.maplibre.nativeffi.geo.ScreenPoint(dx, dy), + animation = animation(durationMs) ?: AnimationOptions(), + ) + ) } - /** - * Applies one decoded camera command on the map's thread, so read-modify-write commands read the - * current camera here rather than on the render loop that produced them. - */ - private fun apply(command: CameraCommand) { - when (command) { - is CameraCommand.CancelTransitions -> map.cancelTransitions() - is CameraCommand.SetGestureInProgress -> map.isGestureInProgress = command.inProgress - is CameraCommand.MoveBy -> map.moveBy(command.dx, command.dy) - is CameraCommand.MoveByAnimated -> - map.moveByAnimated(command.dx, command.dy, animation(command.durationMs)) - - is CameraCommand.ScaleBy -> map.scaleBy(command.scale, command.anchor) - is CameraCommand.ScaleByAnimated -> - map.scaleByAnimated(command.scale, command.anchor, animation(command.durationMs)) - - is CameraCommand.PitchBy -> map.pitchBy(command.delta) - is CameraCommand.AdjustBearing -> map.jumpTo(bearingCamera(command.delta)) - is CameraCommand.AdjustBearingAnimated -> - map.easeTo(bearingCamera(command.delta), animation(command.durationMs)) - - is CameraCommand.AdjustPitchAnimated -> - map.easeTo(pitchCamera(command.delta), animation(command.durationMs)) - - is CameraCommand.ResetOrientation -> - map.easeTo( - CameraOptions().apply { - bearing = 0.0 - pitch = 0.0 - }, - animation(command.durationMs), - ) - } + fun scaleBy( + scale: Double, + anchor: org.maplibre.nativeffi.geo.ScreenPoint, + durationMs: Double? = null, + ) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.SCALE, + amount = scale, + anchor = anchor, + animation = animation(durationMs) ?: AnimationOptions(), + ) + ) + } + + fun adjustPitch(delta: Double, durationMs: Double? = null) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.PITCH, + amount = delta, + animation = animation(durationMs) ?: AnimationOptions(), + ) + ) } - private fun bearingCamera(delta: Double): CameraOptions = - CameraOptions().apply { bearing = (map.camera.bearing ?: 0.0) + delta } + fun adjustBearing(delta: Double, durationMs: Double? = null) { + map.applyCameraDelta( + CameraDelta( + kind = CameraDeltaKind.BEARING, + amount = delta, + animation = animation(durationMs) ?: AnimationOptions(), + ) + ) + } + + fun resetOrientation(durationMs: Double) { + update( + CameraOptions().apply { + bearing = 0.0 + pitch = 0.0 + }, + durationMs, + ) + } - private fun pitchCamera(delta: Double): CameraOptions = - CameraOptions().apply { pitch = max(0.0, min(60.0, (map.camera.pitch ?: 0.0) + delta)) } + fun resize(viewport: Viewport) { + map.resize(MapSize(viewport.width(), viewport.height(), viewport.scaleFactor())) + } + + fun requestRepaint() { + map.requestRepaint() + } + + /** Drains the runtime event stream during the host's paced loop turn. */ + fun pollEvents(renderRequest: RenderRequest) { + if (drainEvents()) renderRequest.set() + } + + private fun update(camera: CameraOptions, durationMs: Double? = null) { + map.updateCamera( + CameraUpdate( + mode = if (durationMs == null) CameraUpdateMode.JUMP else CameraUpdateMode.EASE, + camera = camera, + animation = AnimationOptions().apply { durationMs?.let { this.durationMs = it } }, + ) + ) + } + + private fun animation(durationMs: Double?): AnimationOptions? = durationMs?.let { duration -> + AnimationOptions().apply { this.durationMs = duration } + } - /** Drains one batch of runtime events, reporting whether the map wants another frame. */ private fun drainEvents(): Boolean { var renderUpdateAvailable = false - // One drain takes every event the pump produced. for (event in runtime.drainEvents().events) { - if (event.mapSource != map) { - continue - } + if (event.mapSource != map) continue if (event.type == RuntimeEventType.MAP_RENDER_UPDATE_AVAILABLE) { renderUpdateAvailable = true } else if ( @@ -114,49 +141,57 @@ private constructor(private val runtime: RuntimeHandle, val map: MapHandle) : Au companion object { private const val STYLE_URL = "https://tiles.openfreemap.org/styles/bright" - /** Backstop bound for a parked pump that nothing signals. */ - private const val PARK_TIMEOUT_MS = 100L - fun create(viewport: Viewport): MapState { val runtime = RuntimeHandle.create(RuntimeOptions().apply { cachePath = ":memory:" }) - val mapOptions = - MapOptions().apply { - width = viewport.width() - height = viewport.height() - scaleFactor = viewport.scaleFactor() - mapMode = MapMode.CONTINUOUS - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, so nothing accumulates before the style load. - eventMask = - RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + - RuntimeEventMask.MAP_RENDER_FRAME_FINISHED + val initialCamera = + CameraOptions().apply { + center = LatLng(37.7749, -122.4194) + zoom = 13.0 + bearing = 12.0 + pitch = 30.0 } val map = try { - MapHandle.create(runtime, mapOptions) - } catch (error: RuntimeException) { + runSuspend { + MapHandle.create( + runtime, + MapOptions().apply { + width = viewport.width() + height = viewport.height() + scaleFactor = viewport.scaleFactor() + mapMode = MapMode.CONTINUOUS + eventMask = + RuntimeEventMask.MAP_RENDER_UPDATE_AVAILABLE + + RuntimeEventMask.MAP_RENDER_FRAME_FINISHED + }, + ) + .await() + } + } catch (error: Throwable) { runtime.close() throw error } try { + val state = MapState(runtime, map) map.setStyleUrl(STYLE_URL) - map.jumpTo( - CameraOptions().apply { - center = LatLng(37.7749, -122.4194) - zoom = 13.0 - bearing = 12.0 - pitch = 30.0 - } - ) - return MapState(runtime, map) - } catch (error: RuntimeException) { + map.updateCamera(CameraUpdate(camera = initialCamera)) + return state + } catch (error: Throwable) { map.close() runtime.close() throw error } } + } +} + +/** A one-bit frame request shared with native wake callbacks. */ +internal class RenderRequest { + private val requested = AtomicBoolean(true) - private fun animation(durationMs: Double): AnimationOptions = - AnimationOptions().apply { this.durationMs = durationMs } + fun set() { + requested.set(true) } + + fun consume(): Boolean = requested.getAndSet(false) } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MetalRenderTarget.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MetalRenderTarget.kt index d8361270f..8a38b8714 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MetalRenderTarget.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/MetalRenderTarget.kt @@ -33,7 +33,11 @@ internal object MetalRenderTarget { descriptor(context), NativePointer.ofAddress(context.layerAddress()), ) - return Surface(map.attachMetalSurface(descriptor)) + return Surface( + RenderTarget.completeAttachment( + map.attachMetalSurface(descriptor, RenderTarget.callerDriverOptions) + ) + ) } private fun attachOwnedTexture( @@ -45,7 +49,10 @@ internal object MetalRenderTarget { var session: RenderSessionHandle? = null var compositor: MetalTextureCompositor? = null try { - session = map.attachMetalOwnedTexture(descriptor) + session = + RenderTarget.completeAttachment( + map.attachMetalOwnedTexture(descriptor, RenderTarget.callerDriverOptions) + ) compositor = MetalTextureCompositor(context) return OwnedTexture(session, compositor) } catch (error: RuntimeException) { @@ -65,7 +72,13 @@ internal object MetalRenderTarget { var compositor: MetalTextureCompositor? = null try { texture = MetalBorrowedTexture(context, viewport) - session = map.attachMetalBorrowedTexture(borrowedDescriptor(viewport, texture)) + session = + RenderTarget.completeAttachment( + map.attachMetalBorrowedTexture( + borrowedDescriptor(viewport, texture), + RenderTarget.callerDriverOptions, + ) + ) compositor = MetalTextureCompositor(context) return BorrowedTexture(context, session, compositor, texture) } catch (error: RuntimeException) { @@ -94,13 +107,14 @@ internal object MetalRenderTarget { override fun needsMetalAutoreleasePool(): Boolean = true override fun resize(viewport: Viewport) { - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } - override fun renderUpdate(): Boolean = session.renderUpdate().result == RenderResult.RENDERED + override fun renderUpdate(): Boolean = + RenderTarget.renderFrame(session)?.disposition == RenderResult.RENDERED override fun close() { - session.close() + RenderTarget.closeSession(session) } } @@ -111,19 +125,21 @@ internal object MetalRenderTarget { override fun needsMetalAutoreleasePool(): Boolean = true override fun resize(viewport: Viewport) { - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } - return session.acquireMetalOwnedTextureFrame().use { frameHandle -> - val frame = frameHandle.frame() + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false + val frameHandle = + checkNotNull(session.acquireFrame()) { "rendered Metal frame was unavailable" } + return try { + val frame = frameHandle.metalTexture() check(frame.width() != 0 && frame.height() != 0 && !frame.texture().isNull) { "owned Metal frame has an empty extent or null texture" } compositor.drawTexture(frame.texture().address) + } finally { + frameHandle.release() } } @@ -131,7 +147,7 @@ internal object MetalRenderTarget { try { compositor.close() } finally { - session.close() + RenderTarget.closeSession(session) } } } @@ -148,7 +164,10 @@ internal object MetalRenderTarget { override fun resize(viewport: Viewport) { val replacement = MetalBorrowedTexture(context, viewport) try { - session.setMetalBorrowedTextureTarget(borrowedDescriptor(viewport, replacement)) + RenderTarget.completeDriverOperation( + session, + session.setMetalBorrowedTextureTarget(borrowedDescriptor(viewport, replacement)), + ) } catch (error: RuntimeException) { // A failed handover leaves it unknown which texture the session holds, so detach before // either is released. @@ -162,9 +181,7 @@ internal object MetalRenderTarget { } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false return compositor.drawTexture(texture.texture()) } @@ -173,7 +190,7 @@ internal object MetalRenderTarget { compositor.close() } finally { try { - session.close() + RenderTarget.closeSession(session) } finally { texture.close() } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLRenderTarget.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLRenderTarget.kt index 31f471b17..00ccaa994 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLRenderTarget.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLRenderTarget.kt @@ -35,7 +35,7 @@ internal object OpenGLRenderTarget { descriptor(context), NativePointer.ofAddress(context.surfaceAddress()), ) - return Surface(map.attachOpenGLSurface(descriptor)) + return Surface(RenderTarget.completeAttachment(map.attachOpenGLSurface(descriptor))) } private fun attachOwnedTexture( @@ -48,7 +48,7 @@ internal object OpenGLRenderTarget { var session: RenderSessionHandle? = null var compositor: OpenGLTextureCompositor? = null try { - session = map.attachOpenGLOwnedTexture(descriptor) + session = RenderTarget.completeAttachment(map.attachOpenGLOwnedTexture(descriptor)) compositor = OpenGLTextureCompositor(context, viewport) return OwnedTexture(session, compositor) } catch (error: RuntimeException) { @@ -68,7 +68,10 @@ internal object OpenGLRenderTarget { var compositor: OpenGLTextureCompositor? = null try { texture = OpenGLBorrowedTexture(context, viewport) - session = map.attachOpenGLBorrowedTexture(borrowedDescriptor(context, viewport, texture)) + session = + RenderTarget.completeAttachment( + map.attachOpenGLBorrowedTexture(borrowedDescriptor(context, viewport, texture)) + ) compositor = OpenGLTextureCompositor(context, viewport) return BorrowedTexture(context, session, compositor, texture) } catch (error: RuntimeException) { @@ -111,13 +114,14 @@ internal object OpenGLRenderTarget { private class Surface(private val session: RenderSessionHandle) : RenderTarget { override fun resize(viewport: Viewport) { - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } - override fun renderUpdate(): Boolean = session.renderUpdate().result == RenderResult.RENDERED + override fun renderUpdate(): Boolean = + RenderTarget.renderFrame(session)?.disposition == RenderResult.RENDERED override fun close() { - session.close() + RenderTarget.closeSession(session) } } @@ -127,15 +131,15 @@ internal object OpenGLRenderTarget { ) : RenderTarget { override fun resize(viewport: Viewport) { compositor.resize(viewport) - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } - session.acquireOpenGLOwnedTextureFrame().use { frameHandle -> - val frame = frameHandle.frame() + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false + val frameHandle = + checkNotNull(session.acquireFrame()) { "rendered OpenGL frame was unavailable" } + try { + val frame = frameHandle.openGLTexture() check(frame.width() > 0 && frame.height() > 0) { "MapLibre returned an empty OpenGL owned texture frame" } @@ -143,6 +147,8 @@ internal object OpenGLRenderTarget { "MapLibre owned texture target is ${frame.target()}, expected GL_TEXTURE_2D" } compositor.drawTexture(frame.texture()) + } finally { + frameHandle.release() } return true } @@ -151,7 +157,7 @@ internal object OpenGLRenderTarget { try { compositor.close() } finally { - session.close() + RenderTarget.closeSession(session) } } } @@ -167,7 +173,10 @@ internal object OpenGLRenderTarget { compositor.resize(viewport) val replacement = OpenGLBorrowedTexture(context, viewport) try { - session.setOpenGLBorrowedTextureTarget(borrowedDescriptor(context, viewport, replacement)) + RenderTarget.completeDriverOperation( + session, + session.setOpenGLBorrowedTextureTarget(borrowedDescriptor(context, viewport, replacement)), + ) } catch (error: RuntimeException) { // A failed handover leaves it unknown which target the session holds, so detach before // either is released. @@ -181,9 +190,7 @@ internal object OpenGLRenderTarget { } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false compositor.drawTexture(texture.texture()) return true } @@ -193,7 +200,7 @@ internal object OpenGLRenderTarget { compositor.close() } finally { try { - session.close() + RenderTarget.closeSession(session) } finally { texture.close() } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLTextureCompositor.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLTextureCompositor.kt index 0b6c7d739..c134d759a 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLTextureCompositor.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/OpenGLTextureCompositor.kt @@ -60,6 +60,8 @@ internal class OpenGLTextureCompositor( useProgram(0) checkError("draw OpenGL texture") context.swapBuffers() + // CPU_COMPLETE is supplied when the acquired frame is released, so finish every texture read. + finish() } override fun close() { @@ -369,6 +371,10 @@ internal class OpenGLTextureCompositor( } } + private fun finish() { + if (gles) GLES20.glFinish() else GL11.glFinish() + } + internal companion object { const val TEXTURE_TARGET: Int = GL11.GL_TEXTURE_2D } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/RenderTarget.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/RenderTarget.kt index 9ec66fd18..4858c5849 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/RenderTarget.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/RenderTarget.kt @@ -1,14 +1,18 @@ package org.maplibre.nativeffi.examples.lwjglmap +import kotlinx.coroutines.Deferred import org.maplibre.nativeffi.map.MapHandle +import org.maplibre.nativeffi.render.FrameDemand +import org.maplibre.nativeffi.render.RenderDriver +import org.maplibre.nativeffi.render.RenderFrameResult +import org.maplibre.nativeffi.render.RenderSessionAttachOptions +import org.maplibre.nativeffi.render.RenderSessionAttachment import org.maplibre.nativeffi.render.RenderSessionHandle import org.maplibre.nativeffi.render.RenderTargetExtent /** - * The render session and its mode-specific resources. - * - * Attaching records the calling thread as the session's owner, so every render target is created, - * driven, and closed on the render loop thread, where the host graphics context lives. + * The render loop explicitly services caller-driver work on its graphics thread. Native code owns + * the typed work mailbox and completion state. */ internal interface RenderTarget : AutoCloseable { fun needsMetalAutoreleasePool(): Boolean = false @@ -53,18 +57,48 @@ internal interface RenderTarget : AutoCloseable { */ fun detachSuppressed(error: RuntimeException, session: RenderSessionHandle) { try { - session.detach() + completeDriverOperation(session, session.detach()) } catch (cleanupError: Exception) { error.addSuppressed(cleanupError) } } + val callerDriverOptions: RenderSessionAttachOptions = + RenderSessionAttachOptions(driver = RenderDriver.CALLER_GRAPHICS_THREAD) + + fun completeAttachment(attachment: RenderSessionAttachment): RenderSessionHandle { + try { + completeDriverOperation(attachment.session, attachment.completed) + return attachment.session + } catch (error: Throwable) { + runCatching { attachment.session.abandon() } + runCatching { attachment.session.close() } + throw error + } + } + + fun completeDriverOperation(session: RenderSessionHandle, completed: Deferred) { + while (!completed.isCompleted) session.serviceDriverWork() + runSuspend { completed.await() } + } + + fun renderFrame(session: RenderSessionHandle): RenderFrameResult? { + session.requestFrame(FrameDemand(present = true)) + session.serviceDriverWork() + return session.drainFrameResults().lastOrNull() + } + + fun closeSession(session: RenderSessionHandle) { + completeDriverOperation(session, session.detach()) + session.close() + } + fun closeSuppressed(error: RuntimeException, closeable: AutoCloseable?) { if (closeable == null) { return } try { - closeable.close() + if (closeable is RenderSessionHandle) closeSession(closeable) else closeable.close() } catch (cleanupError: Exception) { error.addSuppressed(cleanupError) } diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Shell.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Shell.kt index 2d02255bb..f4051002f 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Shell.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/Shell.kt @@ -6,17 +6,9 @@ import org.lwjgl.glfw.GLFW.glfwSetWindowContentScaleCallback import org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback import org.lwjgl.glfw.GLFW.glfwWaitEventsTimeout import org.lwjgl.glfw.GLFW.glfwWindowShouldClose -import org.maplibre.nativeffi.map.MapHandle import org.maplibre.nativeffi.render.RenderBackend -/** - * The two loops the example runs on two native threads. - * - * GLFW requires window creation and event polling on the process main thread, so the main thread is - * the render loop: it owns the window, input decoding, the graphics context, and the render session - * it attaches. The spawned thread is the runtime loop: it owns the runtime and the map for their - * whole lifetime. - */ +/** The GLFW-thread shell that owns the window, graphics context, and render session. */ internal object Shell { private const val INITIAL_WIDTH = 960 private const val INITIAL_HEIGHT = 640 @@ -28,89 +20,37 @@ internal object Shell { val initialViewport = Viewport.read(graphics.window()) val viewport = ViewportHolder(initialViewport) initialViewport.log("initial viewport") - - val commands = CommandQueue() val renderRequest = RenderRequest() - val channel = MapChannel() - val runtimeThread = - Thread( - { runtimeLoop(initialViewport, commands, renderRequest, channel) }, - "maplibre-runtime", - ) - runtimeThread.start() - try { - renderLoop(graphics, mode, viewport, commands, renderRequest, channel) - } finally { - // A map with an attached session cannot be destroyed, so the runtime loop tears down only - // after the render loop has closed its session. - channel.requestShutdown() - runtimeThread.join() + MapState.create(initialViewport).use { state -> + renderLoop(graphics, mode, viewport, state, renderRequest) } - channel.failure()?.let { throw it } } } - /** - * Owns the runtime and the map for their whole lifetime, on a thread that is not the one - * presenting. The render loop attaches its own session against the map published here. - */ - private fun runtimeLoop( - viewport: Viewport, - commands: CommandQueue, - renderRequest: RenderRequest, - channel: MapChannel, - ) { - try { - MapState.create(viewport).use { state -> - state.acquireWakeSource().use { wake -> - channel.publish(state.map, wake) - commands.onEnqueue = { channel.wakeRuntimeLoop() } - try { - while (!channel.shutdownRequested()) { - state.step(commands, renderRequest) - } - } catch (error: Throwable) { - // Publish before the wait below, so the render loop sees the failure and closes its - // session rather than stalling until the bound expires. - channel.fail(error) - } - // A map with an attached session cannot be destroyed, so wait for the render loop to - // close its session before `use` tears this down. - channel.awaitShutdown() - } - } - } catch (error: Throwable) { - channel.fail(error) - } - } - - /** The display-paced render loop. Owns the window, input, and the render session it attaches. */ private fun renderLoop( graphics: GraphicsContext, mode: RenderTargetMode, viewport: ViewportHolder, - commands: CommandQueue, + state: MapState, renderRequest: RenderRequest, - channel: MapChannel, ) { - val map = awaitMap(channel) ?: return - val target = RenderTarget.attach(graphics, map, viewport.value, mode) + val target = RenderTarget.attach(graphics, state.map, viewport.value, mode) try { - InputController(graphics.window(), commands, renderRequest) { viewport.value } + InputController(graphics.window(), state, renderRequest) { viewport.value } .use { println("render target: ${mode.cliName()}") println("render target status: ${mode.status()}") InputController.printControls() installResizeCallbacks(graphics.window(), viewport) - // TODO(map-example-spec): Replace poll-and-wait with a display-paced host loop. See Frame - // loop. - while (!glfwWindowShouldClose(graphics.window()) && channel.failure() == null) { + while (!glfwWindowShouldClose(graphics.window())) { glfwPollEvents() + state.pollEvents(renderRequest) if (viewport.consumeChanged()) { viewport.value.log("resized viewport") if (!viewport.value.empty()) { graphics.resize(viewport.value) + state.resize(viewport.value) target.resize(viewport.value) renderRequest.set() } @@ -119,18 +59,12 @@ internal object Shell { glfwWaitEventsTimeout(IDLE_WAIT_SECONDS) continue } - // The map applies a new logical size on the runtime loop's next pump, so a resize is - // followed by frames with nothing to render. var rendered = false if (renderRequest.consume()) { rendered = render(target) - if (!rendered) { - renderRequest.set() - } - } - if (!rendered) { - glfwWaitEventsTimeout(IDLE_WAIT_SECONDS) + if (!rendered) renderRequest.set() } + if (!rendered) glfwWaitEventsTimeout(IDLE_WAIT_SECONDS) } } } finally { @@ -145,24 +79,13 @@ internal object Shell { target.renderUpdate() } - /** Waits for the runtime loop to publish its map, or returns null when it failed first. */ - private fun awaitMap(channel: MapChannel): MapHandle? { - while (channel.failure() == null) { - channel.mapHandle()?.let { - return it - } - Thread.sleep(1) - } - return null - } - private fun installResizeCallbacks(window: Long, viewport: ViewportHolder) { glfwSetWindowSizeCallback(window) { _, _, _ -> viewport.update(window) } glfwSetFramebufferSizeCallback(window) { _, _, _ -> viewport.update(window) } glfwSetWindowContentScaleCallback(window) { _, _, _ -> viewport.update(window) } } - /** Render loop state: GLFW delivers every resize callback on the thread that polls. */ + /** GLFW delivers every resize callback on the thread that polls. */ private class ViewportHolder(var value: Viewport) { private var changed = false diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/SuspendBridge.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/SuspendBridge.kt new file mode 100644 index 000000000..ff22c2578 --- /dev/null +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/SuspendBridge.kt @@ -0,0 +1,23 @@ +package org.maplibre.nativeffi.examples.lwjglmap + +import java.util.concurrent.CountDownLatch +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine + +internal fun runSuspend(block: suspend () -> T): T { + val completed = CountDownLatch(1) + var outcome: Result? = null + block.startCoroutine( + object : Continuation { + override val context = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + outcome = result + completed.countDown() + } + } + ) + completed.await() + return outcome?.getOrThrow() ?: error("suspend call completed without a result") +} diff --git a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/VulkanRenderTarget.kt b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/VulkanRenderTarget.kt index 5920eafe3..df85c9fe8 100644 --- a/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/VulkanRenderTarget.kt +++ b/examples/lwjgl-map/src/main/kotlin/org/maplibre/nativeffi/examples/lwjglmap/VulkanRenderTarget.kt @@ -34,7 +34,11 @@ internal object VulkanRenderTarget { descriptor(context), NativePointer.ofAddress(context.surfaceAddress()), ) - return Surface(map.attachVulkanSurface(descriptor)) + return Surface( + RenderTarget.completeAttachment( + map.attachVulkanSurface(descriptor, RenderTarget.callerDriverOptions) + ) + ) } private fun attachOwnedTexture( @@ -47,7 +51,10 @@ internal object VulkanRenderTarget { var session: RenderSessionHandle? = null var compositor: VulkanTextureCompositor? = null try { - session = map.attachVulkanOwnedTexture(descriptor) + session = + RenderTarget.completeAttachment( + map.attachVulkanOwnedTexture(descriptor, RenderTarget.callerDriverOptions) + ) compositor = VulkanTextureCompositor(context, viewport) return OwnedTexture(session, compositor) } catch (error: RuntimeException) { @@ -67,7 +74,13 @@ internal object VulkanRenderTarget { var compositor: VulkanTextureCompositor? = null try { image = VulkanBorrowedImage.create(context, viewport) - session = map.attachVulkanBorrowedTexture(borrowedDescriptor(context, viewport, image)) + session = + RenderTarget.completeAttachment( + map.attachVulkanBorrowedTexture( + borrowedDescriptor(context, viewport, image), + RenderTarget.callerDriverOptions, + ) + ) compositor = VulkanTextureCompositor(context, viewport) return BorrowedTexture(context, session, compositor, image) } catch (error: RuntimeException) { @@ -108,13 +121,14 @@ internal object VulkanRenderTarget { private class Surface(private val session: RenderSessionHandle) : RenderTarget { override fun resize(viewport: Viewport) { - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } - override fun renderUpdate(): Boolean = session.renderUpdate().result == RenderResult.RENDERED + override fun renderUpdate(): Boolean = + RenderTarget.renderFrame(session)?.disposition == RenderResult.RENDERED override fun close() { - session.close() + RenderTarget.closeSession(session) } } @@ -124,15 +138,15 @@ internal object VulkanRenderTarget { ) : RenderTarget { override fun resize(viewport: Viewport) { compositor.resize(viewport) - session.resize(viewport.width(), viewport.height(), viewport.scaleFactor()) + RenderTarget.completeDriverOperation(session, session.resize(RenderTarget.extent(viewport))) } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } - return session.acquireVulkanOwnedTextureFrame().use { frameHandle -> - val frame = frameHandle.frame() + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false + val frameHandle = + checkNotNull(session.acquireFrame()) { "rendered Vulkan frame was unavailable" } + return try { + val frame = frameHandle.vulkanTexture() check(frame.width() > 0 && frame.height() > 0) { "MapLibre returned an empty Vulkan owned texture frame" } @@ -140,6 +154,8 @@ internal object VulkanRenderTarget { "MapLibre owned texture frame is not shader-readable: layout=${frame.layout()}" } compositor.drawImageView(frame.imageView().address) + } finally { + frameHandle.release() } } @@ -147,7 +163,7 @@ internal object VulkanRenderTarget { try { compositor.close() } finally { - session.close() + RenderTarget.closeSession(session) } } } @@ -163,7 +179,10 @@ internal object VulkanRenderTarget { compositor.resize(viewport) val replacement = VulkanBorrowedImage.create(context, viewport) try { - session.setVulkanBorrowedTextureTarget(borrowedDescriptor(context, viewport, replacement)) + RenderTarget.completeDriverOperation( + session, + session.setVulkanBorrowedTextureTarget(borrowedDescriptor(context, viewport, replacement)), + ) } catch (error: RuntimeException) { // A failed handover leaves it unknown which image the session holds, so detach before // either is released. @@ -177,9 +196,7 @@ internal object VulkanRenderTarget { } override fun renderUpdate(): Boolean { - if (session.renderUpdate().result != RenderResult.RENDERED) { - return false - } + if (RenderTarget.renderFrame(session)?.disposition != RenderResult.RENDERED) return false return compositor.drawImageView(image.view()) } @@ -188,7 +205,7 @@ internal object VulkanRenderTarget { compositor.close() } finally { try { - session.close() + RenderTarget.closeSession(session) } finally { image.close() } diff --git a/examples/rust-map/Cargo.toml b/examples/rust-map/Cargo.toml index 24e8d121e..72f5a44a8 100644 --- a/examples/rust-map/Cargo.toml +++ b/examples/rust-map/Cargo.toml @@ -16,6 +16,9 @@ maplibre-native-ffi = { path = "../../bindings/rust/crates/maplibre-native-ffi" raw-window-handle = "0.6.2" winit = { version = "0.30.13", features = ["rwh_06"] } +[target.'cfg(unix)'.dependencies] +libc = "0.2.180" + [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5.2" objc2-app-kit = { version = "0.2.2", features = ["NSResponder", "NSView", "objc2-quartz-core"] } diff --git a/examples/rust-map/src/app.rs b/examples/rust-map/src/app.rs index 52da624c3..edd399424 100644 --- a/examples/rust-map/src/app.rs +++ b/examples/rust-map/src/app.rs @@ -1,34 +1,26 @@ -//! The render loop: window, input decoding, graphics context, and the render -//! session, all owned by the winit event-loop thread. The runtime and the map -//! live on the spawned runtime loop thread, reached through [`crate::channel`]. +//! The winit event loop owns the window, graphics context, and render session. +//! Runtime and map commands are safe to submit from its callbacks. use std::error::Error; -use std::sync::Arc; -use std::sync::mpsc::{self, Sender}; -use std::thread::{self, JoinHandle}; use winit::event::WindowEvent; use winit::window::{Window, WindowId}; -use crate::channel::{CameraCommand, Shared}; use crate::graphics::GraphicsContext; use crate::input::Controller; -use crate::map_state; +use crate::map_state::MapState; use crate::render_target::{Mode, RenderTarget}; use crate::viewport::Viewport; pub struct App { target: Option, - /// Releases the runtime loop's parked pump so queued work is applied now. - wake: Arc, - runtime_thread: Option>, - commands: Sender, - shared: Arc, + map: Option, graphics: GraphicsContext, window: Window, viewport: Viewport, input: Controller, viewport_dirty: bool, + render_requested: bool, closed: bool, mode: Mode, } @@ -45,48 +37,28 @@ impl App { } viewport.log("initial viewport"); - let shared = Arc::new(Shared::new()); - let (commands, command_queue) = mpsc::channel(); - let (attach_sender, attach_queue) = mpsc::channel(); - let runtime_thread = { - let shared = Arc::clone(&shared); - thread::Builder::new() - .name("maplibre-runtime".into()) - .spawn(move || map_state::run(viewport, command_queue, attach_sender, shared))? - }; - - let handles = match attach_queue.recv() { - Ok(handles) => handles, - Err(_) => { - return Err(stop_runtime_loop( - &shared, - runtime_thread, - "the runtime loop stopped before it published a map".to_string(), - )); - } - }; - let target = match RenderTarget::attach(mode, &handles.attach_ref, &graphics, viewport) { + let map = MapState::new(viewport)?; + let target = match RenderTarget::attach(mode, map.map_handle(), &graphics, viewport) { Ok(target) => target, Err(error) => { - return Err(stop_runtime_loop( - &shared, - runtime_thread, - format!("render target attachment failed: {error}"), - )); + let mut message = format!("render target attachment failed: {error}"); + if let Err(error) = map.close() { + message.push_str(&format!("; map state cleanup failed: {error}")); + } + return Err(message.into()); } }; + window.request_redraw(); Ok(Self { target: Some(target), - wake: handles.wake, - runtime_thread: Some(runtime_thread), - commands, - shared, + map: Some(map), graphics, window, viewport, input: Controller::default(), viewport_dirty: false, + render_requested: true, closed: false, mode, }) @@ -113,29 +85,38 @@ impl App { } WindowEvent::RedrawRequested => self.render_or_exit(), event => { - if self.input.handle(&event, &self.commands, self.viewport) { - // Release the parked pump so the queued command is applied - // on this frame. - let _ = self.wake.signal(); - self.shared.request_render(); - self.window.request_redraw(); + let (input, map) = (&mut self.input, self.map.as_mut().expect("map is open")); + match input.handle(&event, self.viewport, map) { + Ok(true) => { + self.render_requested = true; + self.window.request_redraw(); + } + Ok(false) => {} + Err(error) => { + eprintln!("camera update failed: {error}"); + self.abort_process(1); + } } } } } - /// One render loop iteration. The runtime loop owns `pump` and the event - /// drain. pub fn step(&mut self) { - if let Some(error) = self.shared.failure() { - eprintln!("runtime loop failed: {error}"); - self.abort_process(1); - } if let Err(error) = self.apply_pending_resize() { eprintln!("resize failed: {error}"); self.abort_process(1); } - self.render_or_exit(); + match self.map.as_ref().expect("map is open").drain_events() { + Ok(true) => { + self.render_requested = true; + self.window.request_redraw(); + } + Ok(false) => {} + Err(error) => { + eprintln!("event drain failed: {error}"); + self.abort_process(1); + } + } } fn apply_pending_resize(&mut self) -> Result<(), Box> { @@ -152,12 +133,14 @@ impl App { if next.is_empty() { return Ok(()); } + self.map.as_mut().expect("map is open").resize(next)?; self.graphics.resize(next)?; self.target .as_mut() .expect("render target is open") .resize(&self.graphics, next)?; - self.shared.request_render(); + self.render_requested = true; + self.window.request_redraw(); Ok(()) } @@ -169,22 +152,17 @@ impl App { } fn render(&mut self) -> Result<(), Box> { - if self.closed || self.viewport.is_empty() { - return Ok(()); - } - // Consume first, so a request published during the render survives. - if !self.shared.consume_render_request() { + if self.closed || self.viewport.is_empty() || !self.render_requested { return Ok(()); } + self.render_requested = false; if !self .target .as_mut() .expect("render target is open") .render_update(&self.graphics)? { - // Nothing reached the screen: the map applies a new logical size on - // the runtime loop's next pump, so retry. - self.shared.request_render(); + self.render_requested = true; } Ok(()) } @@ -202,6 +180,7 @@ impl App { } self.closed = true; self.viewport_dirty = false; + self.render_requested = false; let mut first_error = self.graphics.wait_idle().err().map(|error| { format!( @@ -210,26 +189,15 @@ impl App { ) }); - // Close the session before the runtime loop destroys the map, because a - // map with an attached session cannot be destroyed. if let Some(target) = self.target.take() && let Err(error) = target.close(&self.graphics) { append_error(&mut first_error, error.to_string()); } - self.shared.request_shutdown(); - // Release the pump so shutdown is observed now. - let _ = self.wake.signal(); - if let Some(runtime_thread) = self.runtime_thread.take() - && runtime_thread.join().is_err() + if let Some(map) = self.map.take() + && let Err(error) = map.close() { - append_error( - &mut first_error, - "the runtime loop thread panicked".to_string(), - ); - } - if let Some(error) = self.shared.failure() { - append_error(&mut first_error, error); + append_error(&mut first_error, error.to_string()); } match first_error { @@ -244,36 +212,32 @@ impl App { } } -/// Stops a runtime loop that outlived a failed startup, and reports the failure -/// it recorded in preference to the one that stopped it. -fn stop_runtime_loop( - shared: &Shared, - runtime_thread: JoinHandle<()>, - message: String, -) -> Box { - shared.request_shutdown(); - let panicked = runtime_thread.join().is_err(); - let mut reported = shared.failure().unwrap_or(message); - if panicked { - reported.push_str("; the runtime loop thread panicked"); - } - reported.into() -} - fn append_error(message: &mut Option, error: String) { match message { - Some(message) => message.push_str(&format!("; {error}")), + Some(message) => { + message.push_str("; "); + message.push_str(&error); + } None => *message = Some(error), } } +#[cfg(unix)] fn immediate_exit(code: i32) -> ! { - unsafe extern "C" { - fn _exit(status: std::ffi::c_int) -> !; - } + // SAFETY: Abort paths intentionally skip destructors after a fatal native or + // graphics error, and `_exit` terminates the process immediately. + unsafe { libc::_exit(code) } +} - // SAFETY: `_exit` terminates without running native teardown, which the - // macOS Vulkan stack can abort during after the window has closed. The - // operating system reclaims the example's resources. - unsafe { _exit(code) } +#[cfg(windows)] +fn immediate_exit(code: i32) -> ! { + // SAFETY: Abort paths intentionally skip destructors after a fatal native or + // graphics error, and `TerminateProcess` terminates the current process. + unsafe { + windows_sys::Win32::System::Threading::TerminateProcess( + windows_sys::Win32::System::Threading::GetCurrentProcess(), + code as u32, + ); + } + std::process::abort() } diff --git a/examples/rust-map/src/channel.rs b/examples/rust-map/src/channel.rs deleted file mode 100644 index 0c5895eed..000000000 --- a/examples/rust-map/src/channel.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! The cross-thread surface between the render loop, which owns the window and -//! the render session, and the runtime loop, which owns the runtime and map. - -use std::sync::Mutex; -use std::sync::atomic::{AtomicBool, Ordering}; - -use maplibre_native_ffi::ScreenPoint; - -/// A camera change decoded on the render loop and applied on the map's owner -/// thread. Commands carry deltas wherever the current camera is an input, -/// because the read and write have to happen together on the owner thread. -#[derive(Clone, Copy, Debug)] -pub enum CameraCommand { - CancelTransitions, - SetGestureInProgress { - in_progress: bool, - }, - MoveBy { - dx: f64, - dy: f64, - }, - MoveByAnimated { - dx: f64, - dy: f64, - duration_ms: f64, - }, - ScaleBy { - scale: f64, - anchor: ScreenPoint, - }, - ScaleByAnimated { - scale: f64, - anchor: ScreenPoint, - duration_ms: f64, - }, - PitchBy { - delta: f64, - }, - AdjustBearing { - delta: f64, - }, - AdjustBearingAnimated { - delta: f64, - duration_ms: f64, - }, - AdjustPitchAnimated { - delta: f64, - duration_ms: f64, - }, - ResetOrientation { - duration_ms: f64, - }, -} - -/// Render request, shutdown, and first failure, shared by both loops. -#[derive(Debug)] -pub struct Shared { - render_request: AtomicBool, - shutdown: AtomicBool, - failure: Mutex>, -} - -impl Shared { - pub fn new() -> Self { - Self { - render_request: AtomicBool::new(true), - shutdown: AtomicBool::new(false), - failure: Mutex::new(None), - } - } - - /// Announces that a frame is worth drawing. - pub fn request_render(&self) { - self.render_request.store(true, Ordering::Release); - } - - /// Render loop: takes the pending request, leaving it clear. - pub fn consume_render_request(&self) -> bool { - self.render_request.swap(false, Ordering::AcqRel) - } - - /// Render loop: asks the runtime loop to destroy the map and stop. Call - /// only after the render session is closed; a map with an attached session - /// cannot be destroyed. - pub fn request_shutdown(&self) { - self.shutdown.store(true, Ordering::Release); - } - - pub fn shutdown_requested(&self) -> bool { - self.shutdown.load(Ordering::Acquire) - } - - /// Records the first failure seen by either loop. - pub fn fail(&self, message: String) { - let mut failure = self.lock_failure(); - if failure.is_none() { - *failure = Some(message); - } - } - - pub fn failure(&self) -> Option { - self.lock_failure().clone() - } - - /// Recovers a poisoned lock: the stored message stays readable, and the - /// other loop's panic surfaces through the thread join. - fn lock_failure(&self) -> std::sync::MutexGuard<'_, Option> { - self.failure - .lock() - .unwrap_or_else(|error| error.into_inner()) - } -} diff --git a/examples/rust-map/src/input.rs b/examples/rust-map/src/input.rs index b9ddd0cac..78e5fe8dd 100644 --- a/examples/rust-map/src/input.rs +++ b/examples/rust-map/src/input.rs @@ -1,10 +1,10 @@ -use std::sync::mpsc::Sender; - use maplibre_native_ffi::ScreenPoint; use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent}; use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey}; -use crate::channel::CameraCommand; +use std::error::Error; + +use crate::map_state::MapState; use crate::viewport::Viewport; const DRAG_ROTATE_FACTOR: f64 = 0.5; @@ -16,8 +16,7 @@ const KEYBOARD_PITCH: f64 = 5.0; const KEYBOARD_ANIMATION_MS: f64 = 160.0; const RESET_ANIMATION_MS: f64 = 220.0; -/// Decodes host input into camera commands on the render loop, converting to -/// logical map coordinates. The runtime loop applies the commands. +/// Decodes host input and updates the map in logical map coordinates. #[derive(Default)] pub struct Controller { left_down: bool, @@ -42,33 +41,33 @@ impl Controller { println!(" 0: reset pitch and bearing"); } - /// Reports whether the camera changed. + /// Reports whether the event changed the camera. pub fn handle( &mut self, event: &WindowEvent, - commands: &Sender, viewport: Viewport, - ) -> bool { + map: &mut MapState, + ) -> Result> { match event { WindowEvent::CursorMoved { position, .. } => self.cursor( - commands, position.x / viewport.scale_factor, position.y / viewport.scale_factor, + map, ), - WindowEvent::MouseInput { state, button, .. } => self.mouse(commands, *button, *state), - WindowEvent::MouseWheel { delta, .. } => self.wheel(commands, viewport, *delta), + WindowEvent::MouseInput { state, button, .. } => self.mouse(*button, *state, map), + WindowEvent::MouseWheel { delta, .. } => self.wheel(viewport, *delta, map), WindowEvent::KeyboardInput { event, .. } => { - keyboard(commands, viewport, event.physical_key, event.state) + keyboard(viewport, event.physical_key, event.state, map) } WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = modifiers.state(); - false + Ok(false) } - _ => false, + _ => Ok(false), } } - fn cursor(&mut self, commands: &Sender, x: f64, y: f64) -> bool { + fn cursor(&mut self, x: f64, y: f64, map: &mut MapState) -> Result> { let dx = x - self.last_x; let dy = y - self.last_y; self.last_x = x; @@ -78,56 +77,36 @@ impl Controller { if self.right_down || (self.left_down && self.modifiers.control_key()) { if dx != 0.0 { - push( - commands, - CameraCommand::AdjustBearing { - delta: dx * DRAG_ROTATE_FACTOR, - }, - ); + map.adjust_bearing(dx * DRAG_ROTATE_FACTOR, None)?; } if dy != 0.0 { - push( - commands, - CameraCommand::PitchBy { - delta: dy * DRAG_PITCH_FACTOR, - }, - ); + map.adjust_pitch(dy * DRAG_PITCH_FACTOR, None)?; } - dx != 0.0 || dy != 0.0 + Ok(dx != 0.0 || dy != 0.0) } else if self.left_down && (dx != 0.0 || dy != 0.0) { - push(commands, CameraCommand::MoveBy { dx, dy }); - true + map.move_by(dx, dy, None)?; + Ok(true) } else { - false + Ok(false) } } fn mouse( &mut self, - commands: &Sender, button: MouseButton, state: ElementState, - ) -> bool { + map: &mut MapState, + ) -> Result> { let was_dragging = self.dragging(); match button { MouseButton::Left => self.left_down = state == ElementState::Pressed, MouseButton::Right => self.right_down = state == ElementState::Pressed, - _ => return false, - } - if state == ElementState::Pressed { - // Cancel first, so the running transition stops before the first - // delta. - push(commands, CameraCommand::CancelTransitions); + _ => return Ok(false), } if self.dragging() != was_dragging { - push( - commands, - CameraCommand::SetGestureInProgress { - in_progress: self.dragging(), - }, - ); + map.set_gesture_in_progress(self.dragging())?; } - false + Ok(false) } fn dragging(&self) -> bool { @@ -136,94 +115,67 @@ impl Controller { fn wheel( &mut self, - commands: &Sender, viewport: Viewport, delta: MouseScrollDelta, - ) -> bool { + map: &mut MapState, + ) -> Result> { let lines = match delta { MouseScrollDelta::LineDelta(_, y) => f64::from(y), MouseScrollDelta::PixelDelta(position) => position.y / viewport.scale_factor / 120.0, }; if lines == 0.0 { - return false; + return Ok(false); } - push( - commands, - CameraCommand::ScaleBy { - scale: 2.0_f64.powf(lines * 0.25), - anchor: ScreenPoint::new(self.cursor_x, self.cursor_y), - }, - ); - true + map.scale_by( + 2.0_f64.powf(lines * 0.25), + ScreenPoint::new(self.cursor_x, self.cursor_y), + None, + )?; + Ok(true) } } fn keyboard( - commands: &Sender, viewport: Viewport, physical_key: PhysicalKey, state: ElementState, -) -> bool { + map: &mut MapState, +) -> Result> { if state != ElementState::Pressed { - return false; + return Ok(false); } let PhysicalKey::Code(code) = physical_key else { - return false; + return Ok(false); }; let center = ScreenPoint::new( f64::from(viewport.logical_width) / 2.0, f64::from(viewport.logical_height) / 2.0, ); - let command = match code { - KeyCode::ArrowLeft | KeyCode::KeyA => pan(KEYBOARD_PAN, 0.0), - KeyCode::ArrowRight | KeyCode::KeyD => pan(-KEYBOARD_PAN, 0.0), - KeyCode::ArrowUp | KeyCode::KeyW => pan(0.0, KEYBOARD_PAN), - KeyCode::ArrowDown | KeyCode::KeyS => pan(0.0, -KEYBOARD_PAN), - KeyCode::Equal | KeyCode::NumpadAdd => zoom(KEYBOARD_ZOOM, center), - KeyCode::Minus | KeyCode::NumpadSubtract => zoom(1.0 / KEYBOARD_ZOOM, center), - KeyCode::KeyQ => CameraCommand::AdjustBearingAnimated { - delta: -KEYBOARD_BEARING, - duration_ms: KEYBOARD_ANIMATION_MS, - }, - KeyCode::KeyE => CameraCommand::AdjustBearingAnimated { - delta: KEYBOARD_BEARING, - duration_ms: KEYBOARD_ANIMATION_MS, - }, - KeyCode::BracketRight => CameraCommand::AdjustPitchAnimated { - delta: KEYBOARD_PITCH, - duration_ms: KEYBOARD_ANIMATION_MS, - }, - KeyCode::BracketLeft => CameraCommand::AdjustPitchAnimated { - delta: -KEYBOARD_PITCH, - duration_ms: KEYBOARD_ANIMATION_MS, - }, - KeyCode::Digit0 | KeyCode::Numpad0 => CameraCommand::ResetOrientation { - duration_ms: RESET_ANIMATION_MS, - }, - _ => return false, - }; - push(commands, command); - true -} - -fn pan(dx: f64, dy: f64) -> CameraCommand { - CameraCommand::MoveByAnimated { - dx, - dy, - duration_ms: KEYBOARD_ANIMATION_MS, - } -} - -fn zoom(scale: f64, anchor: ScreenPoint) -> CameraCommand { - CameraCommand::ScaleByAnimated { - scale, - anchor, - duration_ms: KEYBOARD_ANIMATION_MS, + match code { + KeyCode::ArrowLeft | KeyCode::KeyA => { + map.move_by(KEYBOARD_PAN, 0.0, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::ArrowRight | KeyCode::KeyD => { + map.move_by(-KEYBOARD_PAN, 0.0, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::ArrowUp | KeyCode::KeyW => { + map.move_by(0.0, KEYBOARD_PAN, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::ArrowDown | KeyCode::KeyS => { + map.move_by(0.0, -KEYBOARD_PAN, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::Equal | KeyCode::NumpadAdd => { + map.scale_by(KEYBOARD_ZOOM, center, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::Minus | KeyCode::NumpadSubtract => { + map.scale_by(1.0 / KEYBOARD_ZOOM, center, Some(KEYBOARD_ANIMATION_MS))? + } + KeyCode::KeyQ => map.adjust_bearing(-KEYBOARD_BEARING, Some(KEYBOARD_ANIMATION_MS))?, + KeyCode::KeyE => map.adjust_bearing(KEYBOARD_BEARING, Some(KEYBOARD_ANIMATION_MS))?, + KeyCode::BracketRight => map.adjust_pitch(KEYBOARD_PITCH, Some(KEYBOARD_ANIMATION_MS))?, + KeyCode::BracketLeft => map.adjust_pitch(-KEYBOARD_PITCH, Some(KEYBOARD_ANIMATION_MS))?, + KeyCode::Digit0 | KeyCode::Numpad0 => map.reset_orientation(RESET_ANIMATION_MS)?, + _ => return Ok(false), } -} - -/// Drops the command if the queue is closed; the runtime loop has stopped and -/// the render loop reports that through the shared failure. -fn push(commands: &Sender, command: CameraCommand) { - let _ = commands.send(command); + Ok(true) } diff --git a/examples/rust-map/src/main.rs b/examples/rust-map/src/main.rs index 0dad9b11c..f4738622e 100644 --- a/examples/rust-map/src/main.rs +++ b/examples/rust-map/src/main.rs @@ -9,7 +9,6 @@ compile_error!("rust-map metal backend is only supported on macOS"); compile_error!("rust-map opengl backend is only supported on Linux and Windows"); mod app; -mod channel; mod graphics; mod input; mod map_state; diff --git a/examples/rust-map/src/map_state.rs b/examples/rust-map/src/map_state.rs index e1d9334df..307eba82d 100644 --- a/examples/rust-map/src/map_state.rs +++ b/examples/rust-map/src/map_state.rs @@ -1,255 +1,199 @@ -//! The runtime loop: runtime and map, owned for their whole lifetime by one -//! spawned thread. The render loop attaches its own session against the map -//! reference published from here. +//! Autonomous runtime and any-thread map state. use std::error::Error; -use std::sync::Arc; -use std::sync::mpsc::{Receiver, Sender}; -use std::thread; use std::time::Duration; use maplibre_native_ffi::{ - AnimationOptions, CameraOptions, LatLng, MapAttachRef, MapHandle, MapMode, MapOptions, - RuntimeEventMask, RuntimeEventPayload, RuntimeEventSource, RuntimeEventType, RuntimeHandle, - RuntimeOptions, + AnimationOptions, CameraDelta, CameraDeltaKind, CameraOptions, CameraUpdate, CameraUpdateMode, + GesturePhase, LatLng, LogicalExtent, MapHandle, MapMode, MapOptions, RuntimeEventMask, + RuntimeEventPayload, RuntimeEventSource, RuntimeEventType, RuntimeHandle, RuntimeOptions, + ScreenPoint, }; -use crate::channel::{CameraCommand, Shared}; use crate::viewport::Viewport; const STYLE_URL: &str = "https://tiles.openfreemap.org/styles/bright"; -// TODO(map-example-spec): Replace fixed pacing with a host condition variable -// woken by the render loop. -const LOOP_PERIOD: Duration = Duration::from_millis(4); - -/// Backstop for the park; the render loop's wake source normally releases it. -const PARK_TIMEOUT: Duration = Duration::from_millis(100); - -/// What the runtime loop hands the render loop once the map exists. -pub struct RuntimeLoopHandles { - pub attach_ref: MapAttachRef, - pub wake: Arc, -} - -/// Runs the runtime loop until the render loop asks for shutdown. Failures land -/// in [`Shared::fail`] for the render loop to report. -pub fn run( - viewport: Viewport, - commands: Receiver, - attach: Sender, - shared: Arc, -) { - let mut state = match MapState::new(viewport) { - Ok(state) => state, - Err(error) => { - shared.fail(error.to_string()); - return; - } - }; - - if let Err(error) = pump(&mut state, &commands, attach, &shared) { - shared.fail(error.to_string()); - } - - // A map with an attached session cannot be destroyed, so wait for the - // render loop to close its session and request shutdown. - while !shared.shutdown_requested() { - thread::sleep(LOOP_PERIOD); - } - if let Err(error) = state.close() { - shared.fail(error.to_string()); - } -} - -fn pump( - state: &mut MapState, - commands: &Receiver, - attach: Sender, - shared: &Shared, -) -> Result<(), Box> { - // The attach reference and wake source are the only handles that leave this - // thread; every other map call stays here. - let handles = RuntimeLoopHandles { - attach_ref: state.map.attach_ref()?, - wake: Arc::new(state.runtime.wake_source()?), - }; - if attach.send(handles).is_err() { - return Ok(()); - } - drop(attach); - - while !shared.shutdown_requested() { - state.apply_commands(commands)?; - // No display paces this thread, so it parks in the pump until the - // runtime has work or the render loop signals the wake source. - state.runtime.pump(Some(PARK_TIMEOUT), None)?; - if state.drain_events()? { - shared.request_render(); - } - } - Ok(()) -} - -struct MapState { - runtime: RuntimeHandle, +pub struct MapState { map: MapHandle, + runtime: RuntimeHandle, } impl MapState { - fn new(viewport: Viewport) -> Result> { + pub fn new(viewport: Viewport) -> Result> { let mut runtime_options = RuntimeOptions::default(); runtime_options.cache_path = Some(":memory:".into()); - let runtime = match RuntimeHandle::with_options(&runtime_options) { - Ok(runtime) => runtime, - Err(error) => { - return Err(startup_error( - format!("runtime creation failed: {error}"), - None, - None, - )); - } - }; + let runtime = RuntimeHandle::with_options(&runtime_options) + .map_err(|error| format!("runtime creation failed: {error}"))?; + let mut map_options = MapOptions::new( viewport.logical_width, viewport.logical_height, viewport.scale_factor, ); map_options.mode = MapMode::Continuous; - let map = match MapHandle::with_options(&runtime, &map_options) { - Ok(map) => map, - Err(error) => { - return Err(startup_error( - format!("map creation failed: {error}"), + let map = match MapHandle::with_options(&runtime, &map_options).and_then(|future| { + if !future.wait(Duration::from_secs(30))? { + return Err(maplibre_native_ffi::Error::new( + maplibre_native_ffi::ErrorKind::NotReady, None, - Some(runtime), + "map creation timed out", )); } + future.take() + }) { + Ok(map) => map, + Err(error) => { + let mut message = format!("map creation failed: {error}"); + append_cleanup_result(&mut message, "runtime", close_runtime(runtime)); + return Err(message.into()); + } }; - if let Err(error) = configure_map(&map) { - return Err(startup_error( - format!("map initialization failed: {error}"), - Some(map), - Some(runtime), - )); + let mut state = Self { map, runtime }; + if let Err(error) = state.configure() { + let mut message = format!("map initialization failed: {error}"); + if let Err(error) = state.close() { + append_error(&mut message, error.to_string()); + } + return Err(message.into()); } - Ok(Self { runtime, map }) + Ok(state) } - /// Applies every queued camera command on the map's owner thread. - fn apply_commands( + pub fn map_handle(&self) -> &MapHandle { + &self.map + } + + pub fn set_gesture_in_progress(&mut self, in_progress: bool) -> Result<(), Box> { + let mut update = CameraUpdate::default(); + update.gesture_phase = if in_progress { + GesturePhase::Begin + } else { + GesturePhase::End + }; + self.map.update_camera(&update)?; + Ok(()) + } + + pub fn move_by( &self, - commands: &Receiver, - ) -> maplibre_native_ffi::Result<()> { - for command in commands.try_iter() { - self.apply(command)?; - } + dx: f64, + dy: f64, + duration_ms: Option, + ) -> Result<(), Box> { + let mut delta = CameraDelta::default(); + delta.offset = ScreenPoint::new(dx, dy); + delta.animation = duration_ms.map(animation).unwrap_or_default(); + self.map.apply_camera_delta(&delta)?; Ok(()) } - /// Applies one decoded camera command on the map's owner thread, where - /// read-modify-write commands also read the current camera. - fn apply(&self, command: CameraCommand) -> maplibre_native_ffi::Result<()> { - let map = &self.map; - match command { - CameraCommand::CancelTransitions => map.cancel_transitions(), - CameraCommand::SetGestureInProgress { in_progress } => { - map.set_gesture_in_progress(in_progress) - } - CameraCommand::MoveBy { dx, dy } => map.move_by(dx, dy), - CameraCommand::MoveByAnimated { - dx, - dy, - duration_ms, - } => map.move_by_animated(dx, dy, Some(&animation(duration_ms))), - CameraCommand::ScaleBy { scale, anchor } => map.scale_by(scale, Some(anchor)), - CameraCommand::ScaleByAnimated { - scale, - anchor, - duration_ms, - } => map.scale_by_animated(scale, Some(anchor), Some(&animation(duration_ms))), - CameraCommand::PitchBy { delta } => map.pitch_by(delta), - CameraCommand::AdjustBearing { delta } => { - map.jump_to(&bearing_camera(self.next_bearing(delta)?)) - } - CameraCommand::AdjustBearingAnimated { delta, duration_ms } => map.ease_to( - &bearing_camera(self.next_bearing(delta)?), - Some(&animation(duration_ms)), - ), - CameraCommand::AdjustPitchAnimated { delta, duration_ms } => { - let pitch = (map.camera()?.pitch.unwrap_or(0.0) + delta).clamp(0.0, 60.0); - let mut camera = CameraOptions::default(); - camera.pitch = Some(pitch); - map.ease_to(&camera, Some(&animation(duration_ms))) - } - CameraCommand::ResetOrientation { duration_ms } => { - let mut camera = CameraOptions::default(); - camera.bearing = Some(0.0); - camera.pitch = Some(0.0); - map.ease_to(&camera, Some(&animation(duration_ms))) - } - } + pub fn scale_by( + &self, + scale: f64, + anchor: ScreenPoint, + duration_ms: Option, + ) -> Result<(), Box> { + let mut delta = CameraDelta::default(); + delta.kind = CameraDeltaKind::Scale; + delta.amount = scale; + delta.anchor = Some(anchor); + delta.animation = duration_ms.map(animation).unwrap_or_default(); + self.map.apply_camera_delta(&delta)?; + Ok(()) + } + + pub fn adjust_pitch(&self, delta: f64, duration_ms: Option) -> Result<(), Box> { + let mut camera_delta = CameraDelta::default(); + camera_delta.kind = CameraDeltaKind::Pitch; + camera_delta.amount = delta; + camera_delta.animation = duration_ms.map(animation).unwrap_or_default(); + self.map.apply_camera_delta(&camera_delta)?; + Ok(()) + } + + pub fn adjust_bearing( + &self, + delta: f64, + duration_ms: Option, + ) -> Result<(), Box> { + let mut camera_delta = CameraDelta::default(); + camera_delta.kind = CameraDeltaKind::Bearing; + camera_delta.amount = delta; + camera_delta.animation = duration_ms.map(animation).unwrap_or_default(); + self.map.apply_camera_delta(&camera_delta)?; + Ok(()) } - fn next_bearing(&self, delta: f64) -> maplibre_native_ffi::Result { - Ok(self.map.camera()?.bearing.unwrap_or(0.0) + delta) + pub fn reset_orientation(&self, duration_ms: f64) -> Result<(), Box> { + let mut update = CameraUpdate::default(); + update.camera.bearing = Some(0.0); + update.camera.pitch = Some(0.0); + update.mode = CameraUpdateMode::Ease; + update.animation = animation(duration_ms); + self.map.update_camera(&update)?; + Ok(()) } - /// Drains one batch of runtime events, reporting whether the map wants - /// another frame. - fn drain_events(&mut self) -> maplibre_native_ffi::Result { + pub fn resize(&mut self, viewport: Viewport) -> Result<(), Box> { + self.map.resize(LogicalExtent { + width: viewport.logical_width, + height: viewport.logical_height, + scale_factor: viewport.scale_factor, + })?; + Ok(()) + } + + pub fn drain_events(&self) -> maplibre_native_ffi::Result { + let mut render_requested = false; let source = RuntimeEventSource::Map(self.map.id()); - let mut render_update_available = false; - // One drain takes every event the pump produced. The batch borrows - // runtime storage, and this loop keeps nothing from it. - let batch = self.runtime.drain_events(0)?; - for event in batch.iter() { - if event.source() != source { - continue; - } + let batch = self.runtime.drain_events()?; + for event in batch.iter().filter(|event| event.source() == source) { match event.event_type() { - RuntimeEventType::MapRenderUpdateAvailable => render_update_available = true, + RuntimeEventType::MapRenderUpdateAvailable => render_requested = true, RuntimeEventType::MapRenderFrameFinished => { if let RuntimeEventPayload::RenderFrame(frame) = event.payload() { - render_update_available |= frame.needs_repaint; + render_requested |= frame.needs_repaint; } } _ => {} } } - Ok(render_update_available) + Ok(render_requested) } - fn close(self) -> Result<(), Box> { - let mut first_error = self - .map - .close() - .err() - .map(|error| format!("map close failed: {error}")); - if let Err(error) = self.runtime.close() { - append_error(&mut first_error, format!("runtime close failed: {error}")); + pub fn close(self) -> Result<(), Box> { + let Self { map, runtime, .. } = self; + let mut first_error = None; + if let Err(error) = map.close() { + append_optional_error(&mut first_error, format!("map close failed: {error}")); + } + if let Err(error) = close_runtime(runtime) { + append_optional_error(&mut first_error, format!("runtime close failed: {error}")); } match first_error { Some(error) => Err(error.into()), None => Ok(()), } } -} -fn configure_map(map: &MapHandle) -> maplibre_native_ffi::Result<()> { - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, so this runs before the style load. - map.set_event_mask( - RuntimeEventMask::MAP_RENDER_UPDATE_AVAILABLE | RuntimeEventMask::MAP_RENDER_FRAME_FINISHED, - )?; - map.set_style_url(STYLE_URL)?; - let mut camera = CameraOptions::default(); - camera.center = Some(LatLng::new(37.7749, -122.4194)); - camera.zoom = Some(13.0); - camera.bearing = Some(12.0); - camera.pitch = Some(30.0); - map.jump_to(&camera)?; - map.request_repaint() + fn configure(&mut self) -> Result<(), Box> { + self.map.set_event_mask( + RuntimeEventMask::MAP_RENDER_UPDATE_AVAILABLE + | RuntimeEventMask::MAP_RENDER_FRAME_FINISHED, + )?; + self.map.set_style_url(STYLE_URL)?; + let mut camera = CameraOptions::default(); + camera.center = Some(LatLng::new(37.7749, -122.4194)); + camera.zoom = Some(13.0); + camera.bearing = Some(12.0); + camera.pitch = Some(30.0); + let mut update = CameraUpdate::default(); + update.camera = camera; + self.map.update_camera(&update)?; + self.map.request_repaint()?; + Ok(()) + } } fn animation(duration_ms: f64) -> AnimationOptions { @@ -258,24 +202,17 @@ fn animation(duration_ms: f64) -> AnimationOptions { animation } -fn bearing_camera(bearing: f64) -> CameraOptions { - let mut camera = CameraOptions::default(); - camera.bearing = Some(bearing); - camera -} - -fn startup_error( - mut message: String, - map: Option, - runtime: Option, -) -> Box { - if let Some(map) = map { - append_cleanup_result(&mut message, "map", map.close()); - } - if let Some(runtime) = runtime { - append_cleanup_result(&mut message, "runtime", runtime.close()); +/// Closes a runtime and waits for native teardown, so the process exits after +/// MapLibre's threads and resources are gone rather than racing them. +fn close_runtime(runtime: RuntimeHandle) -> std::result::Result<(), String> { + let teardown = runtime + .close() + .map_err(|error| error.into_error().to_string())?; + match teardown.wait(Duration::from_secs(30)) { + Ok(true) => teardown.take().map_err(|error| error.to_string()), + Ok(false) => Err("runtime teardown timed out".to_owned()), + Err(error) => Err(error.to_string()), } - message.into() } fn append_cleanup_result( @@ -284,13 +221,18 @@ fn append_cleanup_result( result: std::result::Result<(), E>, ) { if let Err(error) = result { - message.push_str(&format!("; {resource} cleanup failed: {error}")); + append_error(message, format!("{resource} cleanup failed: {error}")); } } -fn append_error(message: &mut Option, error: String) { +fn append_optional_error(message: &mut Option, error: String) { match message { - Some(message) => message.push_str(&format!("; {error}")), + Some(message) => append_error(message, error), None => *message = Some(error), } } + +fn append_error(message: &mut String, error: String) { + message.push_str("; "); + message.push_str(&error); +} diff --git a/examples/rust-map/src/metal.rs b/examples/rust-map/src/metal.rs index 061a36770..f180a3bae 100644 --- a/examples/rust-map/src/metal.rs +++ b/examples/rust-map/src/metal.rs @@ -1,6 +1,4 @@ -use maplibre_native_ffi::{ - Error as MaplibreError, ErrorKind, MetalOwnedTextureFrameHandle, NativePointer, -}; +use maplibre_native_ffi::{AcquiredFrameHandle, Error as MaplibreError, ErrorKind, NativePointer}; use objc2::ClassType; use objc2::rc::Retained; use objc2::runtime::ProtocolObject; @@ -110,16 +108,13 @@ impl MetalTextureCompositor { /// Samples the frame's texture into the layer's next drawable, and reports /// whether it presented. - pub fn draw( - &mut self, - frame: &MetalOwnedTextureFrameHandle, - ) -> maplibre_native_ffi::Result { - let metadata = frame.frame()?; + pub fn draw(&mut self, frame: &AcquiredFrameHandle) -> maplibre_native_ffi::Result { + let (metadata, texture, _) = frame.metal_texture()?; if metadata.width == 0 || metadata.height == 0 { return Err(metal_error("owned Metal frame has an empty extent")); } // SAFETY: The frame keeps this texture pointer valid until frame release. - let texture = unsafe { frame.texture()?.as_ptr::>() }; + let texture = unsafe { texture.as_ptr::>() }; let texture = unsafe { texture.as_ref() } .ok_or_else(|| metal_error("owned Metal frame has a null texture"))?; self.draw_texture(texture) diff --git a/examples/rust-map/src/opengl.rs b/examples/rust-map/src/opengl.rs index 82933b40e..e89d0ed5f 100644 --- a/examples/rust-map/src/opengl.rs +++ b/examples/rust-map/src/opengl.rs @@ -10,7 +10,7 @@ use glutin::display::{GetGlDisplay, GlDisplay}; use glutin::prelude::*; use glutin::surface::{Surface, SurfaceAttributesBuilder, WindowSurface}; use glutin_winit::DisplayBuilder; -use maplibre_native_ffi::{NativePointer, OpenGLContextDescriptor, OpenGLOwnedTextureFrameHandle}; +use maplibre_native_ffi::{AcquiredFrameHandle, NativePointer, OpenGLContextDescriptor}; use raw_window_handle::HasWindowHandle; use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes}; @@ -188,9 +188,9 @@ impl OpenGLTextureCompositor { pub fn draw_frame( &self, context: &OpenGLContext, - frame: &OpenGLOwnedTextureFrameHandle, + frame: &AcquiredFrameHandle, ) -> maplibre_native_ffi::Result<()> { - let metadata = frame.frame()?; + let (metadata, texture) = frame.opengl_texture()?; if metadata.width == 0 || metadata.height == 0 { return Err(compositor_error("owned OpenGL frame has an empty extent")); } @@ -200,7 +200,7 @@ impl OpenGLTextureCompositor { metadata.target ))); } - let texture = unsafe { frame.texture()?.value() }; + let texture = unsafe { texture.value() }; self.draw_texture(context, texture) } @@ -246,6 +246,9 @@ impl OpenGLTextureCompositor { self.gl.bind_texture(TEXTURE_TARGET, None); self.gl.bind_vertex_array(None); self.gl.use_program(None); + // This example releases the acquired texture with CPU-complete + // synchronization, so finish all sampling before that release. + self.gl.finish(); } check_gl_error(&self.gl, "draw OpenGL texture") .map_err(|error| compositor_error(error.to_string()))?; diff --git a/examples/rust-map/src/render_target.rs b/examples/rust-map/src/render_target.rs index a33da8520..527ee8d6d 100644 --- a/examples/rust-map/src/render_target.rs +++ b/examples/rust-map/src/render_target.rs @@ -12,7 +12,10 @@ pub use opengl_target::RenderTarget; #[cfg(maplibre_render_backend = "vulkan")] pub use vulkan_target::RenderTarget; -use maplibre_native_ffi::RenderTargetExtent; +use maplibre_native_ffi::{ + AcquiredFrameHandle, Error, ErrorKind, FrameDemand, FrameDisposition, GpuSyncKind, + RenderSessionHandle, RenderTargetExtent, +}; use crate::viewport::Viewport; @@ -59,3 +62,37 @@ fn extent(viewport: Viewport) -> RenderTargetExtent { viewport.scale_factor, ) } + +fn request_render_frame( + session: &RenderSessionHandle, + present: bool, + service_caller_driver: bool, +) -> maplibre_native_ffi::Result { + session.request_frame(FrameDemand { + present, + ..FrameDemand::default() + })?; + if service_caller_driver { + session.service_driver_work(usize::MAX)?; + } + Ok(session + .drain_frame_results()? + .copy_results()? + .into_iter() + .any(|result| result.disposition == FrameDisposition::Rendered)) +} + +fn require_cpu_complete_producer(frame: &AcquiredFrameHandle) -> maplibre_native_ffi::Result<()> { + let producer = frame.producer_sync()?; + if producer.kind == GpuSyncKind::CpuComplete { + return Ok(()); + } + Err(Error::new( + ErrorKind::InvalidState, + None, + format!( + "rust-map cannot consume a {:?} producer synchronization payload", + producer.kind + ), + )) +} diff --git a/examples/rust-map/src/render_target/metal_target.rs b/examples/rust-map/src/render_target/metal_target.rs index 7f26073f0..c1c5f0f92 100644 --- a/examples/rust-map/src/render_target/metal_target.rs +++ b/examples/rust-map/src/render_target/metal_target.rs @@ -1,10 +1,14 @@ use std::error::Error as StdError; +use std::time::Duration; -use maplibre_native_ffi::{Error, ErrorKind, MapAttachRef, RenderResult, RenderSessionHandle}; +use maplibre_native_ffi::{ + Error, ErrorKind, GpuSync, MapHandle, RenderSessionAttachOptions, RenderSessionAttachment, + RenderSessionHandle, +}; use crate::graphics::GraphicsContext; -use crate::metal::{MetalBorrowedTexture, MetalContext, MetalTextureCompositor}; -use crate::render_target::{Mode, extent}; +use crate::metal::{MetalBorrowedTexture, MetalTextureCompositor}; +use crate::render_target::{Mode, extent, request_render_frame, require_cpu_complete_producer}; use crate::viewport::Viewport; pub enum RenderTarget { @@ -25,20 +29,60 @@ pub enum RenderTarget { impl RenderTarget { pub fn attach( mode: Mode, - map: &MapAttachRef, + map: &MapHandle, graphics: &GraphicsContext, viewport: Viewport, ) -> maplibre_native_ffi::Result { let metal = graphics.metal(); + let options = + RenderSessionAttachOptions::core_worker(if mode == Mode::OwnedTexture { 2 } else { 0 }); match mode { - Mode::OwnedTexture => attach_owned_texture(map, metal, viewport), - Mode::BorrowedTexture => attach_borrowed_texture(map, metal, viewport), - Mode::NativeSurface => attach_surface(map, metal, viewport), + Mode::OwnedTexture => { + let descriptor = maplibre_native_ffi::MetalOwnedTextureDescriptor::new( + extent(viewport), + metal.context_descriptor(), + ); + let session = finish_attach(map.attach_metal_owned_texture(&descriptor, options)?)?; + let compositor = MetalTextureCompositor::new(metal).map_err(|error| { + compositor_error(format!("Metal compositor creation failed: {error:?}")) + })?; + Ok(Self::OwnedTexture { + session, + compositor: Box::new(compositor), + }) + } + Mode::BorrowedTexture => { + let texture = MetalBorrowedTexture::new(metal, viewport)?; + let descriptor = maplibre_native_ffi::MetalBorrowedTextureDescriptor::new( + extent(viewport), + viewport.physical_width, + viewport.physical_height, + texture.pointer(), + ); + let session = + finish_attach(map.attach_metal_borrowed_texture(&descriptor, options)?)?; + let compositor = MetalTextureCompositor::new(metal).map_err(|error| { + compositor_error(format!("Metal compositor creation failed: {error:?}")) + })?; + Ok(Self::BorrowedTexture { + session, + compositor: Box::new(compositor), + texture: Box::new(texture), + }) + } + Mode::NativeSurface => { + let descriptor = maplibre_native_ffi::MetalSurfaceDescriptor::new( + extent(viewport), + metal.context_descriptor(), + metal.layer_pointer(), + ); + Ok(Self::Surface { + session: finish_attach(map.attach_metal_surface(&descriptor, options)?)?, + }) + } } } - /// Resizes without closing the session; a caller-owned texture is replaced - /// with one at the new size and handed over. pub fn resize( &mut self, graphics: &GraphicsContext, @@ -55,21 +99,15 @@ impl RenderTarget { viewport.physical_height, replacement.pointer(), ); - if let Err(error) = session.set_metal_borrowed_texture_target(&descriptor) { - // On failure the session may already hold the replacement, - // so keep it alive rather than hand over a dangling - // texture. - std::mem::forget(replacement); - return Err(error); - } + let operation = session.set_metal_borrowed_texture_target(&descriptor)?; + wait_core(&operation, "Metal target replacement")?; **texture = replacement; Ok(()) } - Self::OwnedTexture { session, .. } | Self::Surface { session } => session.resize( - viewport.logical_width, - viewport.logical_height, - viewport.scale_factor, - ), + Self::OwnedTexture { session, .. } | Self::Surface { session } => { + drop(session.resize(&extent(viewport))?); + Ok(()) + } } } @@ -77,41 +115,35 @@ impl RenderTarget { &mut self, _graphics: &GraphicsContext, ) -> maplibre_native_ffi::Result { + let present = matches!(self, Self::Surface { .. }); + let session = match self { + Self::OwnedTexture { session, .. } + | Self::BorrowedTexture { session, .. } + | Self::Surface { session } => session, + }; + let rendered = request_render_frame(session, present, false)?; + if !rendered { + return Ok(false); + } match self { Self::OwnedTexture { session, compositor, } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - let frame = session.acquire_metal_owned_texture_frame()?; - let draw_result = compositor.draw(&frame); - let close_result = frame.close().map_err(|error| error.into_error()); - match (draw_result, close_result) { - (Ok(presented), Ok(())) => Ok(presented), - (Err(draw_error), Ok(())) => Err(draw_error), - (Ok(_), Err(close_error)) => Err(close_error), - (Err(draw_error), Err(close_error)) => Err(Error::new( - draw_error.kind(), - draw_error.raw_status(), - format!("{draw_error}; frame cleanup failed: {close_error}"), - )), - } + let frame = session.acquire_frame()?; + require_cpu_complete_producer(&frame)?; + let presented = compositor.draw(&frame)?; + frame + .release(GpuSync::CPU_COMPLETE) + .map_err(|error| error.into_error())?; + Ok(presented) } Self::BorrowedTexture { - session, compositor, texture, - } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - compositor.draw_texture(texture.texture()) - } - Self::Surface { session } => { - Ok(session.render_update()?.result == RenderResult::Rendered) - } + .. + } => compositor.draw_texture(texture.texture()), + Self::Surface { .. } => Ok(true), } } @@ -121,9 +153,10 @@ impl RenderTarget { session, compositor, } => { + detach_core(&session)?; drop(compositor); session - .close() + .destroy() .map_err(|error| Box::new(error) as Box) } Self::BorrowedTexture { @@ -131,89 +164,51 @@ impl RenderTarget { compositor, texture, } => { + detach_core(&session)?; drop(compositor); - let result = session - .close() - .map_err(|error| Box::new(error) as Box); drop(texture); - result + session + .destroy() + .map_err(|error| Box::new(error) as Box) + } + Self::Surface { session } => { + detach_core(&session)?; + session + .destroy() + .map_err(|error| Box::new(error) as Box) } - Self::Surface { session } => session - .close() - .map_err(|error| Box::new(error) as Box), } } } -fn attach_owned_texture( - map: &MapAttachRef, - metal: &MetalContext, - viewport: Viewport, -) -> maplibre_native_ffi::Result { - let descriptor = maplibre_native_ffi::MetalOwnedTextureDescriptor::new( - extent(viewport), - metal.context_descriptor(), - ); - let session = map.attach_metal_owned_texture(&descriptor)?; - let compositor = match MetalTextureCompositor::new(metal) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("Metal texture compositor creation failed: {error:?}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - return Err(compositor_error(message)); - } - }; - Ok(RenderTarget::OwnedTexture { - session, - compositor: Box::new(compositor), - }) +fn finish_attach( + attachment: RenderSessionAttachment, +) -> maplibre_native_ffi::Result { + if !attachment.completion.wait(Duration::from_secs(30))? { + return Err(compositor_error("render attachment timed out")); + } + attachment.completion.take()?; + let session = attachment.session; + Ok(session) } -fn attach_borrowed_texture( - map: &MapAttachRef, - metal: &MetalContext, - viewport: Viewport, -) -> maplibre_native_ffi::Result { - let texture = MetalBorrowedTexture::new(metal, viewport)?; - let descriptor = maplibre_native_ffi::MetalBorrowedTextureDescriptor::new( - extent(viewport), - viewport.physical_width, - viewport.physical_height, - texture.pointer(), - ); - let session = map.attach_metal_borrowed_texture(&descriptor)?; - let compositor = match MetalTextureCompositor::new(metal) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("Metal texture compositor creation failed: {error:?}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - return Err(compositor_error(message)); - } - }; - Ok(RenderTarget::BorrowedTexture { - session, - compositor: Box::new(compositor), - texture: Box::new(texture), - }) +fn wait_core( + operation: &maplibre_native_ffi::NativeFuture<()>, + name: &str, +) -> maplibre_native_ffi::Result<()> { + if !operation.wait(Duration::from_secs(30))? { + return Err(compositor_error(format!("{name} timed out"))); + } + operation.take() } -fn attach_surface( - map: &MapAttachRef, - metal: &MetalContext, - viewport: Viewport, -) -> maplibre_native_ffi::Result { - let descriptor = maplibre_native_ffi::MetalSurfaceDescriptor::new( - extent(viewport), - metal.context_descriptor(), - metal.layer_pointer(), - ); - Ok(RenderTarget::Surface { - session: map.attach_metal_surface(&descriptor)?, - }) +fn detach_core(session: &RenderSessionHandle) -> Result<(), Box> { + let operation = session.detach()?; + if !operation.wait(Duration::from_secs(30))? { + return Err("render detach timed out".into()); + } + operation.take()?; + Ok(()) } fn compositor_error(message: impl Into) -> Error { diff --git a/examples/rust-map/src/render_target/opengl_target.rs b/examples/rust-map/src/render_target/opengl_target.rs index 324f2fe00..140a2697a 100644 --- a/examples/rust-map/src/render_target/opengl_target.rs +++ b/examples/rust-map/src/render_target/opengl_target.rs @@ -1,13 +1,14 @@ use std::error::Error as StdError; use maplibre_native_ffi::{ - Error, MapAttachRef, OpenGLBorrowedTextureDescriptor, OpenGLOwnedTextureDescriptor, - OpenGLSurfaceDescriptor, RenderResult, RenderSessionHandle, + Error, GpuSync, MapHandle, NativeFuture, OpenGLBorrowedTextureDescriptor, + OpenGLOwnedTextureDescriptor, OpenGLSurfaceDescriptor, RenderSessionAttachOptions, + RenderSessionAttachment, RenderSessionHandle, }; use crate::graphics::GraphicsContext; -use crate::opengl::{OpenGLBorrowedTexture, OpenGLContext, OpenGLTextureCompositor}; -use crate::render_target::{Mode, extent}; +use crate::opengl::{OpenGLBorrowedTexture, OpenGLTextureCompositor}; +use crate::render_target::{Mode, extent, request_render_frame, require_cpu_complete_producer}; use crate::viewport::Viewport; pub enum RenderTarget { @@ -28,107 +29,67 @@ pub enum RenderTarget { impl RenderTarget { pub fn attach( mode: Mode, - map: &MapAttachRef, + map: &MapHandle, graphics: &GraphicsContext, viewport: Viewport, ) -> maplibre_native_ffi::Result { - let opengl = graphics.opengl(); - match mode { - Mode::OwnedTexture => Self::attach_owned_texture(map, opengl, viewport), - Mode::BorrowedTexture => Self::attach_borrowed_texture(map, opengl, viewport), - Mode::NativeSurface => Self::attach_surface(map, opengl, viewport), - } - } - - pub fn attach_owned_texture( - map: &MapAttachRef, - opengl: &OpenGLContext, - viewport: Viewport, - ) -> maplibre_native_ffi::Result { - let context = opengl.descriptor().map_err(|error| { + let gl = graphics.opengl(); + let context = gl.descriptor().map_err(|error| { compositor_error(format!("OpenGL context descriptor failed: {error}")) })?; - let descriptor = OpenGLOwnedTextureDescriptor::new(extent(viewport), context); - let session = map.attach_opengl_owned_texture(&descriptor)?; - let compositor = match OpenGLTextureCompositor::new(opengl, viewport) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("OpenGL texture compositor creation failed: {error}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - return Err(compositor_error(message)); + let options = + RenderSessionAttachOptions::caller_graphics_thread(if mode == Mode::OwnedTexture { + 2 + } else { + 0 + }); + match mode { + Mode::OwnedTexture => { + let descriptor = OpenGLOwnedTextureDescriptor::new(extent(viewport), context); + let attachment = map.attach_opengl_owned_texture(&descriptor, options)?; + let session = finish_attach(attachment)?; + let compositor = OpenGLTextureCompositor::new(gl, viewport).map_err(|error| { + compositor_error(format!("OpenGL compositor creation failed: {error}")) + })?; + Ok(Self::OwnedTexture { + session, + compositor: Box::new(compositor), + }) } - }; - Ok(Self::OwnedTexture { - session, - compositor: Box::new(compositor), - }) - } - - pub fn attach_borrowed_texture( - map: &MapAttachRef, - opengl: &OpenGLContext, - viewport: Viewport, - ) -> maplibre_native_ffi::Result { - let context = opengl.descriptor().map_err(|error| { - compositor_error(format!("OpenGL context descriptor failed: {error}")) - })?; - let texture = OpenGLBorrowedTexture::new(opengl, viewport).map_err(|error| { - compositor_error(format!("OpenGL borrowed texture creation failed: {error}")) - })?; - let descriptor = OpenGLBorrowedTextureDescriptor::new( - extent(viewport), - viewport.physical_width, - viewport.physical_height, - context, - texture.texture(), - texture.target(), - ); - let session = match map.attach_opengl_borrowed_texture(&descriptor) { - Ok(session) => session, - Err(error) => { - texture.close(Some(opengl)); - return Err(error); + Mode::BorrowedTexture => { + let texture = OpenGLBorrowedTexture::new(gl, viewport).map_err(|error| { + compositor_error(format!("OpenGL texture creation failed: {error}")) + })?; + let descriptor = OpenGLBorrowedTextureDescriptor::new( + extent(viewport), + viewport.physical_width, + viewport.physical_height, + context, + texture.texture(), + texture.target(), + ); + let session = + finish_attach(map.attach_opengl_borrowed_texture(&descriptor, options)?)?; + let compositor = OpenGLTextureCompositor::new(gl, viewport).map_err(|error| { + compositor_error(format!("OpenGL compositor creation failed: {error}")) + })?; + Ok(Self::BorrowedTexture { + session, + compositor: Box::new(compositor), + texture: Box::new(texture), + }) } - }; - let compositor = match OpenGLTextureCompositor::new(opengl, viewport) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("OpenGL texture compositor creation failed: {error}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - texture.close(Some(opengl)); - return Err(compositor_error(message)); + Mode::NativeSurface => { + let surface = gl.surface_pointer().map_err(|error| { + compositor_error(format!("OpenGL surface handle failed: {error}")) + })?; + let descriptor = OpenGLSurfaceDescriptor::new(extent(viewport), context, surface); + let session = finish_attach(map.attach_opengl_surface(&descriptor, options)?)?; + Ok(Self::Surface { session }) } - }; - Ok(Self::BorrowedTexture { - session, - compositor: Box::new(compositor), - texture: Box::new(texture), - }) - } - - pub fn attach_surface( - map: &MapAttachRef, - opengl: &OpenGLContext, - viewport: Viewport, - ) -> maplibre_native_ffi::Result { - let context = opengl.descriptor().map_err(|error| { - compositor_error(format!("OpenGL context descriptor failed: {error}")) - })?; - let surface = opengl - .surface_pointer() - .map_err(|error| compositor_error(format!("OpenGL surface handle failed: {error}")))?; - let descriptor = OpenGLSurfaceDescriptor::new(extent(viewport), context, surface); - Ok(Self::Surface { - session: map.attach_opengl_surface(&descriptor)?, - }) + } } - /// Resizes without closing the session; a caller-owned texture is replaced - /// with one at the new size and handed over. pub fn resize( &mut self, graphics: &GraphicsContext, @@ -140,48 +101,38 @@ impl RenderTarget { compositor, } => { compositor.resize(viewport); - session.resize( - viewport.logical_width, - viewport.logical_height, - viewport.scale_factor, - ) + drop(session.resize(&extent(viewport))?); + Ok(()) } Self::BorrowedTexture { session, compositor, texture, } => { - let opengl = graphics.opengl(); - let replacement = - OpenGLBorrowedTexture::new(opengl, viewport).map_err(|error| { - compositor_error(format!( - "OpenGL borrowed texture creation failed: {error}" - )) - })?; - let context = opengl.descriptor().map_err(|error| { - compositor_error(format!("OpenGL context descriptor failed: {error}")) + let gl = graphics.opengl(); + let replacement = OpenGLBorrowedTexture::new(gl, viewport).map_err(|error| { + compositor_error(format!("OpenGL texture creation failed: {error}")) })?; let descriptor = OpenGLBorrowedTextureDescriptor::new( extent(viewport), viewport.physical_width, viewport.physical_height, - context, + gl.descriptor() + .map_err(|error| compositor_error(error.to_string()))?, replacement.texture(), replacement.target(), ); - // On failure the session may already hold the replacement, so - // leak it rather than hand the session a dangling texture. - session.set_opengl_borrowed_texture_target(&descriptor)?; + let operation = session.set_opengl_borrowed_texture_target(&descriptor)?; + drive(session, &operation)?; compositor.resize(viewport); let outgoing = std::mem::replace(&mut **texture, replacement); - outgoing.close(Some(opengl)); + outgoing.close(Some(gl)); + Ok(()) + } + Self::Surface { session } => { + drop(session.resize(&extent(viewport))?); Ok(()) } - Self::Surface { session } => session.resize( - viewport.logical_width, - viewport.logical_height, - viewport.scale_factor, - ), } } @@ -189,77 +140,101 @@ impl RenderTarget { &mut self, graphics: &GraphicsContext, ) -> maplibre_native_ffi::Result { - let opengl = graphics.opengl(); + let present = matches!(self, Self::Surface { .. }); + let session = match self { + Self::OwnedTexture { session, .. } + | Self::BorrowedTexture { session, .. } + | Self::Surface { session } => session, + }; + let rendered = request_render_frame(session, present, true)?; + if !rendered { + return Ok(false); + } match self { Self::OwnedTexture { session, compositor, } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - let frame = session.acquire_opengl_owned_texture_frame()?; - let draw_result = compositor.draw_frame(opengl, &frame); - let close_result = frame.close().map_err(|error| error.into_error()); - match (draw_result, close_result) { - (Ok(()), Ok(())) => Ok(true), - (Err(draw_error), Ok(())) => Err(draw_error), - (Ok(()), Err(close_error)) => Err(close_error), - (Err(draw_error), Err(close_error)) => Err(Error::new( - draw_error.kind(), - draw_error.raw_status(), - format!("{draw_error}; frame cleanup failed: {close_error}"), - )), - } + let frame = session.acquire_frame()?; + require_cpu_complete_producer(&frame)?; + compositor.draw_frame(graphics.opengl(), &frame)?; + frame + .release(GpuSync::CPU_COMPLETE) + .map_err(|error| error.into_error())?; + Ok(true) } Self::BorrowedTexture { - session, compositor, texture, + .. } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - compositor.draw_texture(opengl, texture.texture())?; + compositor.draw_texture(graphics.opengl(), texture.texture())?; Ok(true) } - Self::Surface { session } => { - Ok(session.render_update()?.result == RenderResult::Rendered) - } + Self::Surface { .. } => Ok(true), } } pub fn close(self, graphics: &GraphicsContext) -> Result<(), Box> { - let opengl = Some(graphics.opengl()); match self { Self::OwnedTexture { session, compositor, } => { - compositor.close(opengl); - session - .close() - .map_err(|error| Box::new(error) as Box) + detach(&session)?; + compositor.close(Some(graphics.opengl())); + destroy(session) } Self::BorrowedTexture { session, compositor, texture, } => { - compositor.close(opengl); - let result = session - .close() - .map_err(|error| Box::new(error) as Box); - texture.close(opengl); - result + detach(&session)?; + compositor.close(Some(graphics.opengl())); + texture.close(Some(graphics.opengl())); + destroy(session) + } + Self::Surface { session } => { + detach(&session)?; + destroy(session) } - Self::Surface { session } => session - .close() - .map_err(|error| Box::new(error) as Box), } } } +fn finish_attach( + attachment: RenderSessionAttachment, +) -> maplibre_native_ffi::Result { + drive(&attachment.session, &attachment.completion)?; + let session = attachment.session; + Ok(session) +} + +fn drive( + session: &RenderSessionHandle, + operation: &NativeFuture<()>, +) -> maplibre_native_ffi::Result<()> { + while !operation.is_ready() { + if session.service_driver_work(usize::MAX)? == 0 { + std::thread::yield_now(); + } + } + operation.take() +} + +fn detach(session: &RenderSessionHandle) -> Result<(), Box> { + let operation = session.detach()?; + drive(session, &operation)?; + Ok(()) +} + +fn destroy(session: RenderSessionHandle) -> Result<(), Box> { + session + .destroy() + .map_err(|error| Box::new(error) as Box) +} + fn compositor_error(message: impl Into) -> Error { Error::new(maplibre_native_ffi::ErrorKind::NativeError, None, message) } diff --git a/examples/rust-map/src/render_target/vulkan_target.rs b/examples/rust-map/src/render_target/vulkan_target.rs index 43b0509bc..e4c71ae29 100644 --- a/examples/rust-map/src/render_target/vulkan_target.rs +++ b/examples/rust-map/src/render_target/vulkan_target.rs @@ -1,13 +1,14 @@ use std::error::Error as StdError; +use std::time::Duration; use maplibre_native_ffi::{ - Error, ErrorKind, MapAttachRef, RenderResult, RenderSessionHandle, - VulkanBorrowedTextureDescriptor, VulkanContextDescriptor, VulkanOwnedTextureDescriptor, - VulkanSurfaceDescriptor, + Error, ErrorKind, GpuSync, MapHandle, RenderSessionAttachOptions, RenderSessionAttachment, + RenderSessionHandle, VulkanBorrowedTextureDescriptor, VulkanContextDescriptor, + VulkanOwnedTextureDescriptor, VulkanSurfaceDescriptor, }; use crate::graphics::GraphicsContext; -use crate::render_target::{Mode, extent}; +use crate::render_target::{Mode, extent, request_render_frame, require_cpu_complete_producer}; use crate::viewport::Viewport; use crate::vulkan::{BorrowedImage, VulkanContext}; use crate::vulkan_texture_compositor::VulkanTextureCompositor; @@ -30,20 +31,56 @@ pub enum RenderTarget { impl RenderTarget { pub fn attach( mode: Mode, - map: &MapAttachRef, + map: &MapHandle, graphics: &GraphicsContext, viewport: Viewport, ) -> maplibre_native_ffi::Result { - let vulkan = graphics.vulkan(); + let vk = graphics.vulkan(); + let options = + RenderSessionAttachOptions::core_worker(if mode == Mode::OwnedTexture { 2 } else { 0 }); match mode { - Mode::OwnedTexture => attach_owned_texture(map, vulkan, viewport), - Mode::BorrowedTexture => attach_borrowed_texture(map, vulkan, viewport), - Mode::NativeSurface => attach_surface(map, vulkan, viewport), + Mode::OwnedTexture => { + let descriptor = + VulkanOwnedTextureDescriptor::new(extent(viewport), context_descriptor(vk)); + let session = + finish_attach(map.attach_vulkan_owned_texture(&descriptor, options)?)?; + let compositor = VulkanTextureCompositor::new(vk, viewport).map_err(|error| { + compositor_error(format!("Vulkan compositor creation failed: {error:?}")) + })?; + Ok(Self::OwnedTexture { + session, + compositor: Box::new(compositor), + }) + } + Mode::BorrowedTexture => { + let image = BorrowedImage::new(vk, viewport).map_err(|error| { + compositor_error(format!("Vulkan image creation failed: {error:?}")) + })?; + let descriptor = borrowed_descriptor(vk, viewport, &image); + let session = + finish_attach(map.attach_vulkan_borrowed_texture(&descriptor, options)?)?; + let compositor = VulkanTextureCompositor::new(vk, viewport).map_err(|error| { + compositor_error(format!("Vulkan compositor creation failed: {error:?}")) + })?; + Ok(Self::BorrowedTexture { + session, + compositor: Box::new(compositor), + image: Box::new(image), + }) + } + Mode::NativeSurface => { + let descriptor = VulkanSurfaceDescriptor::new( + extent(viewport), + context_descriptor(vk), + vk.surface_pointer(), + ); + Ok(Self::Surface { + session: finish_attach(map.attach_vulkan_surface(&descriptor, options)?)?, + }) + } } } - /// Resizes without closing the session; a caller-owned image is replaced - /// with one at the new size and handed over. pub fn resize( &mut self, graphics: &GraphicsContext, @@ -55,57 +92,33 @@ impl RenderTarget { compositor, } => { compositor.resize(viewport).map_err(|error| { - compositor_error(format!( - "Vulkan texture compositor resize failed: {error:?}" - )) + compositor_error(format!("Vulkan resize failed: {error:?}")) })?; - session.resize( - viewport.logical_width, - viewport.logical_height, - viewport.scale_factor, - ) + drop(session.resize(&extent(viewport))?); + Ok(()) } Self::BorrowedTexture { session, compositor, image, } => { - let vulkan = graphics.vulkan(); - let replacement = BorrowedImage::new(vulkan, viewport).map_err(|error| { - compositor_error(format!("Vulkan borrowed image creation failed: {error:?}")) - })?; - let descriptor = VulkanBorrowedTextureDescriptor::new( - extent(viewport), - viewport.physical_width, - viewport.physical_height, - context_descriptor(vulkan), - replacement.image_pointer(), - replacement.view_pointer(), - ash::vk::Format::R8G8B8A8_UNORM.as_raw() as u32, - ash::vk::ImageLayout::UNDEFINED.as_raw() as u32, - ash::vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL.as_raw() as u32, - ); - if let Err(error) = session.set_vulkan_borrowed_texture_target(&descriptor) { - // On failure the session may already hold the replacement, - // so keep it alive rather than hand over a dangling image. - std::mem::forget(replacement); - return Err(error); - } - // Adopt before anything else that can fail: the session renders - // into this image now. + let replacement = + BorrowedImage::new(graphics.vulkan(), viewport).map_err(|error| { + compositor_error(format!("Vulkan image creation failed: {error:?}")) + })?; + let descriptor = borrowed_descriptor(graphics.vulkan(), viewport, &replacement); + let operation = session.set_vulkan_borrowed_texture_target(&descriptor)?; + wait_core(&operation, "Vulkan target replacement")?; **image = replacement; compositor.resize(viewport).map_err(|error| { - compositor_error(format!( - "Vulkan texture compositor resize failed: {error:?}" - )) + compositor_error(format!("Vulkan resize failed: {error:?}")) })?; Ok(()) } - Self::Surface { session } => session.resize( - viewport.logical_width, - viewport.logical_height, - viewport.scale_factor, - ), + Self::Surface { session } => { + drop(session.resize(&extent(viewport))?); + Ok(()) + } } } @@ -113,43 +126,38 @@ impl RenderTarget { &mut self, _graphics: &GraphicsContext, ) -> maplibre_native_ffi::Result { + let present = matches!(self, Self::Surface { .. }); + let session = match self { + Self::OwnedTexture { session, .. } + | Self::BorrowedTexture { session, .. } + | Self::Surface { session } => session, + }; + let rendered = request_render_frame(session, present, false)?; + if !rendered { + return Ok(false); + } match self { Self::OwnedTexture { session, compositor, } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - let frame = session.acquire_vulkan_owned_texture_frame()?; - let draw_result = compositor.draw(&frame); - let close_result = frame.close().map_err(|error| error.into_error()); - match (draw_result, close_result) { - (Ok(presented), Ok(())) => Ok(presented), - (Err(draw_error), Ok(())) => Err(draw_error), - (Ok(_), Err(close_error)) => Err(close_error), - (Err(draw_error), Err(close_error)) => Err(Error::new( - draw_error.kind(), - draw_error.raw_status(), - format!("{draw_error}; frame cleanup failed: {close_error}"), - )), - } + let frame = session.acquire_frame()?; + require_cpu_complete_producer(&frame)?; + let presented = compositor.draw(&frame)?; + compositor.wait_idle().map_err(|error| { + compositor_error(format!("Vulkan consumer wait failed: {error:?}")) + })?; + frame + .release(GpuSync::CPU_COMPLETE) + .map_err(|error| error.into_error())?; + Ok(presented) } Self::BorrowedTexture { - session, - compositor, - image, - } => { - if session.render_update()?.result != RenderResult::Rendered { - return Ok(false); - } - compositor.draw_image_view(image.view()).map_err(|error| { - compositor_error(format!("Vulkan texture compositor draw failed: {error:?}")) - }) - } - Self::Surface { session } => { - Ok(session.render_update()?.result == RenderResult::Rendered) - } + compositor, image, .. + } => compositor + .draw_image_view(image.view()) + .map_err(|error| compositor_error(format!("Vulkan draw failed: {error:?}"))), + Self::Surface { .. } => Ok(true), } } @@ -159,145 +167,95 @@ impl RenderTarget { session, mut compositor, } => { - let mut close_error = compositor - .close() - .err() - .map(|error| format!("Vulkan texture compositor close failed: {error:?}")); - if let Err(error) = session.close() { - append_error( - &mut close_error, - format!("render session close failed: {error}"), - ); - } - match close_error { - Some(error) => Err(Box::new(compositor_error(error))), - None => Ok(()), - } + detach(&session)?; + compositor.close()?; + destroy(session) } Self::BorrowedTexture { session, mut compositor, image, } => { - let mut close_error = compositor - .close() - .err() - .map(|error| format!("Vulkan texture compositor close failed: {error:?}")); - if let Err(error) = session.close() { - append_error( - &mut close_error, - format!("render session close failed: {error}"), - ); - } + detach(&session)?; + compositor.close()?; drop(image); - match close_error { - Some(error) => Err(Box::new(compositor_error(error))), - None => Ok(()), - } + destroy(session) + } + Self::Surface { session } => { + detach(&session)?; + destroy(session) } - Self::Surface { session } => session - .close() - .map_err(|error| Box::new(error) as Box), } } } -fn attach_owned_texture( - map: &MapAttachRef, - vulkan: &VulkanContext, - viewport: Viewport, -) -> maplibre_native_ffi::Result { - let descriptor = - VulkanOwnedTextureDescriptor::new(extent(viewport), context_descriptor(vulkan)); - let session = map.attach_vulkan_owned_texture(&descriptor)?; - let compositor = match VulkanTextureCompositor::new(vulkan, viewport) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("Vulkan texture compositor creation failed: {error:?}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - return Err(compositor_error(message)); - } - }; - Ok(RenderTarget::OwnedTexture { - session, - compositor: Box::new(compositor), - }) +fn finish_attach( + attachment: RenderSessionAttachment, +) -> maplibre_native_ffi::Result { + if !attachment.completion.wait(Duration::from_secs(30))? { + return Err(compositor_error("render attachment timed out")); + } + attachment.completion.take()?; + let session = attachment.session; + Ok(session) +} + +fn wait_core( + operation: &maplibre_native_ffi::NativeFuture<()>, + name: &str, +) -> maplibre_native_ffi::Result<()> { + if !operation.wait(Duration::from_secs(30))? { + return Err(compositor_error(format!("{name} timed out"))); + } + operation.take() +} + +fn detach(session: &RenderSessionHandle) -> Result<(), Box> { + let operation = session.detach()?; + if !operation.wait(Duration::from_secs(30))? { + return Err("render detach timed out".into()); + } + operation.take()?; + Ok(()) } -fn attach_borrowed_texture( - map: &MapAttachRef, - vulkan: &VulkanContext, +fn destroy(session: RenderSessionHandle) -> Result<(), Box> { + session + .destroy() + .map_err(|error| Box::new(error) as Box) +} + +fn borrowed_descriptor( + vk: &VulkanContext, viewport: Viewport, -) -> maplibre_native_ffi::Result { - let image = BorrowedImage::new(vulkan, viewport).map_err(|error| { - compositor_error(format!("Vulkan borrowed image creation failed: {error:?}")) - })?; - let descriptor = VulkanBorrowedTextureDescriptor::new( + image: &BorrowedImage, +) -> VulkanBorrowedTextureDescriptor { + VulkanBorrowedTextureDescriptor::new( extent(viewport), viewport.physical_width, viewport.physical_height, - context_descriptor(vulkan), + context_descriptor(vk), image.image_pointer(), image.view_pointer(), ash::vk::Format::R8G8B8A8_UNORM.as_raw() as u32, ash::vk::ImageLayout::UNDEFINED.as_raw() as u32, ash::vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL.as_raw() as u32, - ); - let session = map.attach_vulkan_borrowed_texture(&descriptor)?; - let compositor = match VulkanTextureCompositor::new(vulkan, viewport) { - Ok(compositor) => compositor, - Err(error) => { - let mut message = format!("Vulkan texture compositor creation failed: {error:?}"); - if let Err(close_error) = session.close() { - message.push_str(&format!("; render session cleanup failed: {close_error}")); - } - return Err(compositor_error(message)); - } - }; - Ok(RenderTarget::BorrowedTexture { - session, - compositor: Box::new(compositor), - image: Box::new(image), - }) + ) } -fn attach_surface( - map: &MapAttachRef, - vulkan: &VulkanContext, - viewport: Viewport, -) -> maplibre_native_ffi::Result { - let descriptor = VulkanSurfaceDescriptor::new( - extent(viewport), - context_descriptor(vulkan), - vulkan.surface_pointer(), - ); - Ok(RenderTarget::Surface { - session: map.attach_vulkan_surface(&descriptor)?, - }) -} - -fn context_descriptor(vulkan: &VulkanContext) -> VulkanContextDescriptor { +fn context_descriptor(vk: &VulkanContext) -> VulkanContextDescriptor { let mut descriptor = VulkanContextDescriptor::new( - vulkan.instance_pointer(), - vulkan.physical_device_pointer(), - vulkan.device_pointer(), - vulkan.graphics_queue_pointer(), - vulkan.graphics_queue_family_index(), + vk.instance_pointer(), + vk.physical_device_pointer(), + vk.device_pointer(), + vk.graphics_queue_pointer(), + vk.graphics_queue_family_index(), ); - descriptor.get_instance_proc_addr = vulkan.get_instance_proc_addr_pointer(); - descriptor.get_device_proc_addr = vulkan.get_device_proc_addr_pointer(); + descriptor.get_instance_proc_addr = vk.get_instance_proc_addr_pointer(); + descriptor.get_device_proc_addr = vk.get_device_proc_addr_pointer(); descriptor } -fn append_error(message: &mut Option, error: String) { - match message { - Some(message) => message.push_str(&format!("; {error}")), - None => *message = Some(error), - } -} - fn compositor_error(message: impl Into) -> Error { Error::new(ErrorKind::NativeError, None, message) } diff --git a/examples/rust-map/src/shell.rs b/examples/rust-map/src/shell.rs index 561623ea3..bbcaafed9 100644 --- a/examples/rust-map/src/shell.rs +++ b/examples/rust-map/src/shell.rs @@ -2,7 +2,7 @@ use std::error::Error; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; -use winit::event::{StartCause, WindowEvent}; +use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; @@ -54,11 +54,9 @@ impl Shell { } impl ApplicationHandler for Shell { - fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: StartCause) { - // TODO(map-example-spec): Replace fixed timer with a display-paced host - // loop. + fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: winit::event::StartCause) { event_loop.set_control_flow(ControlFlow::WaitUntil( - Instant::now() + Duration::from_millis(4), + Instant::now() + Duration::from_millis(16), )); } diff --git a/examples/rust-map/src/vulkan_texture_compositor.rs b/examples/rust-map/src/vulkan_texture_compositor.rs index 945b88116..6baf43734 100644 --- a/examples/rust-map/src/vulkan_texture_compositor.rs +++ b/examples/rust-map/src/vulkan_texture_compositor.rs @@ -2,7 +2,7 @@ use std::io::Cursor; use ash::vk; use ash::vk::Handle; -use maplibre_native_ffi::{Error, ErrorKind, VulkanOwnedTextureFrameHandle}; +use maplibre_native_ffi::{AcquiredFrameHandle, Error, ErrorKind}; use crate::viewport::Viewport; use crate::vulkan::VulkanContext; @@ -108,11 +108,8 @@ impl VulkanTextureCompositor { Ok(()) } - pub fn draw( - &mut self, - frame: &VulkanOwnedTextureFrameHandle, - ) -> maplibre_native_ffi::Result { - let metadata = frame.frame()?; + pub fn draw(&mut self, frame: &AcquiredFrameHandle) -> maplibre_native_ffi::Result { + let (metadata, _, image_view, _) = frame.vulkan_texture()?; if metadata.width == 0 || metadata.height == 0 { return Err(compositor_error("owned Vulkan frame has an empty extent")); } @@ -122,7 +119,7 @@ impl VulkanTextureCompositor { metadata.layout ))); } - let image_view = unsafe { vk::ImageView::from_raw(frame.image_view()?.address() as u64) }; + let image_view = unsafe { vk::ImageView::from_raw(image_view.address() as u64) }; if image_view == vk::ImageView::null() { return Err(compositor_error("owned Vulkan frame has a null image view")); } @@ -603,7 +600,7 @@ impl VulkanTextureCompositor { } } - fn wait_idle(&self) -> Result<(), vk::Result> { + pub fn wait_idle(&self) -> Result<(), vk::Result> { unsafe { self.device.device_wait_idle() } } diff --git a/examples/swift-map/Sources/SwiftMap/Channels.swift b/examples/swift-map/Sources/SwiftMap/Channels.swift deleted file mode 100644 index ddc29ca70..000000000 --- a/examples/swift-map/Sources/SwiftMap/Channels.swift +++ /dev/null @@ -1,183 +0,0 @@ -import Foundation -import MaplibreNativeFFI - -/// A camera change decoded on the render loop and applied on the map's owner -/// thread. Commands carry deltas wherever the current camera is an input, -/// because the read and write have to happen together on the owner thread. -enum CameraCommand { - case cancelTransitions - case setGestureInProgress(Bool) - case moveBy(dx: Double, dy: Double) - case moveByAnimated(dx: Double, dy: Double, animation: AnimationOptions) - case scaleBy(scale: Double, anchor: ScreenPoint) - case scaleByAnimated( - scale: Double, - anchor: ScreenPoint, - animation: AnimationOptions - ) - case adjustBearing(delta: Double) - case adjustBearingAnimated(delta: Double, animation: AnimationOptions) - case adjustPitch(delta: Double) - case adjustPitchAnimated(delta: Double, animation: AnimationOptions) - case resetOrientation(animation: AnimationOptions) -} - -/// The cross-thread surface between the render loop, which owns the view, the -/// Metal objects, and the render session, and the runtime loop, which owns the -/// runtime and the map. -/// -/// The runtime loop that reads these channels MUST be a dedicated `Thread`. -/// Native owner-thread checks are keyed on the OS thread, and a serial -/// `DispatchQueue`, an `actor`, or a `Task` may run successive blocks on -/// different threads, producing `MLN_STATUS_WRONG_THREAD` failures. -final class Channels: @unchecked Sendable { - private let condition = NSCondition() - private var commands: [CameraCommand] = [] - private var renderRequested = true - private var publishedAttachRef: MapAttachRef? - /// Releases the runtime loop's parked pump. Set once the loop has published. - private var wake: WakeSource? - private var shutdown = false - private var failure: String? - private var runtimeLoopFinished = false - - // MARK: - Camera commands (render loop to runtime loop) - - /// Render loop: queues a decoded camera change and wakes the runtime loop. - /// The buffer grows rather than dropping, because deltas and gesture brackets - /// are not recoverable once discarded. - func push(_ command: CameraCommand) { - condition.lock() - commands.append(command) - let source = wake - condition.unlock() - // The runtime loop parks inside the native pump, not on this condition. - // Signal outside the lock so a native call never runs under it. - try? source?.signal() - } - - /// Runtime loop: swaps `batch` in for the pending commands, keeping the - /// locked section to the swap alone. - func drainCommands(into batch: inout [CameraCommand]) { - // Clearing releases the elements just applied, so do it outside the lock. - batch.removeAll(keepingCapacity: true) - condition.lock() - defer { condition.unlock() } - swap(&commands, &batch) - } - - // MARK: - Render request (runtime loop to render loop) - - func setRenderRequest() { - condition.lock() - defer { condition.unlock() } - renderRequested = true - } - - /// Render loop: takes the request, if any. - func consumeRenderRequest() -> Bool { - condition.lock() - defer { condition.unlock() } - let wasRequested = renderRequested - renderRequested = false - return wasRequested - } - - // MARK: - Attach reference (runtime loop to render loop) - - /// Runtime loop: announces the map it just created. - func publish(attachRef: MapAttachRef, wake: WakeSource) { - condition.lock() - defer { condition.unlock() } - self.wake = wake - publishedAttachRef = attachRef - } - - /// Render loop: the reference to attach against, once the runtime loop has a - /// map. - func attachRef() -> MapAttachRef? { - condition.lock() - defer { condition.unlock() } - return publishedAttachRef - } - - // MARK: - Shutdown and failure - - /// Render loop: asks the runtime loop to stop. Called only after the render - /// session is closed, because the map cannot be destroyed before then. - func requestShutdown() { - condition.lock() - shutdown = true - condition.broadcast() - let source = wake - condition.unlock() - // Release the pump so shutdown is observed now. - try? source?.signal() - } - - /// Runtime loop: blocks until the render loop has closed its session. The map - /// cannot be destroyed before then. Bounded so a render loop that died - /// without signalling cannot wedge teardown. - func waitForShutdown(timeout: TimeInterval) { - let deadline = Date(timeIntervalSinceNow: timeout) - condition.lock() - defer { condition.unlock() } - while !shutdown { - if !condition.wait(until: deadline) { return } - } - } - - var isShutdownRequested: Bool { - condition.lock() - defer { condition.unlock() } - return shutdown - } - - /// Records the first failure from either loop. It crosses as text because - /// `any Error` carries no `Sendable` guarantee. - func fail(_ error: Error) { - condition.lock() - defer { condition.unlock() } - if failure == nil { - failure = String(describing: error) - } - condition.broadcast() - } - - var failureMessage: String? { - condition.lock() - defer { condition.unlock() } - return failure - } - - /// Runtime loop: reports that it has closed the map and the runtime and is - /// about to exit. - func markRuntimeLoopFinished() { - condition.lock() - defer { condition.unlock() } - runtimeLoopFinished = true - condition.broadcast() - } - - /// Render loop: waits for the runtime loop to finish. Returns false when the - /// deadline passed first. - func waitForRuntimeLoopExit(timeout: TimeInterval) -> Bool { - let deadline = Date(timeIntervalSinceNow: timeout) - condition.lock() - defer { condition.unlock() } - while !runtimeLoopFinished { - if !condition.wait(until: deadline) { - return runtimeLoopFinished - } - } - return true - } - - /// Render loop: releases the runtime loop's parked pump. - func wakeRuntimeLoop() { - condition.lock() - let source = wake - condition.unlock() - try? source?.signal() - } -} diff --git a/examples/swift-map/Sources/SwiftMap/InputController.swift b/examples/swift-map/Sources/SwiftMap/InputController.swift index 25f97a962..04befe743 100644 --- a/examples/swift-map/Sources/SwiftMap/InputController.swift +++ b/examples/swift-map/Sources/SwiftMap/InputController.swift @@ -6,9 +6,8 @@ private let resetAnimationDurationMS = 220.0 private let preciseScrollDeltaDivisor = 10.0 private let maxScrollDeltaPerEvent = 4.0 -/// Decodes host input into camera commands on the render loop, converting to -/// logical map coordinates. The runtime loop applies the commands. Every -/// handler returns whether the camera changed. +/// Decodes host input into camera updates on the render loop, converting to +/// logical map coordinates. Every handler returns whether the camera changed. @MainActor final class InputController { enum DragMode { @@ -27,51 +26,49 @@ final class InputController { private var dragButton = DragButton.none private var lastLocation = CGPoint.zero - func mouseDown(_ event: NSEvent, commands: Channels) -> Bool { - beginDrag( + func mouseDown(_ event: NSEvent, mapState: MapState) async throws -> Bool { + try await beginDrag( .left, mode: event.modifierFlags.contains(.control) ? .rotate : .pan, at: event.locationInWindow, - commands: commands + mapState: mapState ) - return false } - func rightMouseDown(_ event: NSEvent, commands: Channels) -> Bool { - beginDrag( + func rightMouseDown(_ event: NSEvent, + mapState: MapState) async throws -> Bool + { + try await beginDrag( .right, mode: .rotate, at: event.locationInWindow, - commands: commands + mapState: mapState ) - return false } - func mouseUp(_ event: NSEvent, commands: Channels) -> Bool { - endDrag(.left, at: event.locationInWindow, commands: commands) - return false + func mouseUp(_ event: NSEvent, mapState: MapState) async throws -> Bool { + try await endDrag(.left, at: event.locationInWindow, mapState: mapState) } - func rightMouseUp(_ event: NSEvent, commands: Channels) -> Bool { - endDrag(.right, at: event.locationInWindow, commands: commands) - return false + func rightMouseUp(_ event: NSEvent, mapState: MapState) async throws -> Bool { + try await endDrag(.right, at: event.locationInWindow, mapState: mapState) } private func beginDrag( _ button: DragButton, mode: DragMode, at location: CGPoint, - commands: Channels - ) { + mapState: MapState + ) async throws -> Bool { // A second button pressed during a live drag joins it, leaving the drag // baseline alone. - guard dragMode == .none else { return } + guard dragMode == .none else { return false } lastLocation = location dragMode = mode dragButton = button - // Cancel first, so the running transition stops before the first delta. - commands.push(.cancelTransitions) - commands.push(.setGestureInProgress(true)) + try await mapState.cancelTransitions() + try await mapState.setGestureInProgress(true) + return true } /// Ends the drag only for the button that started it, so the gesture bracket @@ -79,16 +76,17 @@ final class InputController { private func endDrag( _ button: DragButton, at location: CGPoint, - commands: Channels - ) { - guard dragButton == button else { return } + mapState: MapState + ) async throws -> Bool { + guard dragButton == button else { return false } lastLocation = location dragMode = .none dragButton = .none - commands.push(.setGestureInProgress(false)) + try await mapState.setGestureInProgress(false) + return true } - func mouseDragged(_ event: NSEvent, commands: Channels) -> Bool { + func mouseDragged(_ event: NSEvent, mapState: MapState) async throws -> Bool { let location = event.locationInWindow let dx = Double(location.x - lastLocation.x) let dy = Double(lastLocation.y - location.y) @@ -99,18 +97,20 @@ final class InputController { return false case .pan: if dx == 0 && dy == 0 { return false } - commands.push(.moveBy(dx: dx, dy: dy)) + try await mapState.moveBy(dx: dx, dy: dy) case .rotate: if dx == 0 && dy == 0 { return false } - commands.push(.adjustBearing(delta: dx * 0.5)) - commands.push(.adjustPitch(delta: -dy * 0.5)) + try await mapState.adjustBearing(delta: dx * 0.5) + try await mapState.adjustPitch(delta: dy * 0.5) } return true } - func scrollWheel(_ event: NSEvent, commands: Channels, - in view: NSView) -> Bool - { + func scrollWheel( + _ event: NSEvent, + in view: NSView, + mapState: MapState + ) async throws -> Bool { let delta = scrollDelta(event) if delta == 0 { return false } @@ -120,13 +120,15 @@ final class InputController { y: Double(view.bounds.height - location.y) ) let scale = pow(2.0, delta * 0.25) - commands.push(.scaleBy(scale: scale, anchor: anchor)) + try await mapState.scaleBy(scale, anchor: anchor) return true } - func keyDown(_ event: NSEvent, commands: Channels, - viewport: Viewport) -> Bool - { + func keyDown( + _ event: NSEvent, + viewport: Viewport, + mapState: MapState + ) async throws -> Bool { let panStep = 120.0 let zoomStep = 1.25 let bearingStep = 10.0 @@ -140,46 +142,51 @@ final class InputController { switch event.keyCode { case 123, 0: - commands.push(.moveByAnimated(dx: panStep, dy: 0, animation: animation)) + try await mapState.moveBy(dx: panStep, dy: 0, animation: animation) case 124, 2: - commands.push(.moveByAnimated(dx: -panStep, dy: 0, animation: animation)) + try await mapState.moveBy(dx: -panStep, dy: 0, animation: animation) case 126, 13: - commands.push(.moveByAnimated(dx: 0, dy: panStep, animation: animation)) + try await mapState.moveBy(dx: 0, dy: panStep, animation: animation) case 125, 1: - commands.push(.moveByAnimated(dx: 0, dy: -panStep, animation: animation)) + try await mapState.moveBy(dx: 0, dy: -panStep, animation: animation) case 24, 69: - commands.push(.scaleByAnimated( - scale: zoomStep, + try await mapState.scaleBy( + zoomStep, anchor: center, animation: animation - )) + ) case 27, 78: - commands.push(.scaleByAnimated( - scale: 1.0 / zoomStep, + try await mapState.scaleBy( + 1.0 / zoomStep, anchor: center, animation: animation - )) + ) case 12: - commands - .push(.adjustBearingAnimated( - delta: -bearingStep, - animation: animation - )) + try await mapState.adjustBearing( + delta: -bearingStep, + animation: animation + ) case 14: - commands - .push(.adjustBearingAnimated(delta: bearingStep, animation: animation)) + try await mapState.adjustBearing( + delta: bearingStep, + animation: animation + ) case 30: - commands - .push(.adjustPitchAnimated(delta: pitchStep, animation: animation)) + try await mapState.adjustPitch( + delta: pitchStep, + animation: animation + ) case 33: - commands - .push(.adjustPitchAnimated(delta: -pitchStep, animation: animation)) + try await mapState.adjustPitch( + delta: -pitchStep, + animation: animation + ) case 29: - commands.push(.resetOrientation( + try await mapState.resetOrientation( animation: AnimationOptions( durationMilliseconds: resetAnimationDurationMS ) - )) + ) default: return false } diff --git a/examples/swift-map/Sources/SwiftMap/MapState.swift b/examples/swift-map/Sources/SwiftMap/MapState.swift index 719811290..e0cb10e87 100644 --- a/examples/swift-map/Sources/SwiftMap/MapState.swift +++ b/examples/swift-map/Sources/SwiftMap/MapState.swift @@ -26,100 +26,89 @@ struct Viewport: Equatable { } } -/// Runtime and map, owned for their whole lifetime by the runtime loop thread. -/// The render loop thread owns the view, the Metal objects, and the session. +/// Runtime and map state owned by the main render loop. +@MainActor final class MapState { private let runtime: RuntimeHandle private let map: MapHandle private var isClosed = false - init(viewport: Viewport) throws { + init(viewport: Viewport) async throws { precondition( !viewport.isEmpty, "cannot create MapState with an empty viewport" ) - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - var createdMap: MapHandle? - var didInitialize = false - defer { - if !didInitialize { - try? createdMap?.close() - try? runtime.close() - } + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") + ) + let map: MapHandle + do { + map = try await MapHandle( + runtime: runtime, + options: MapOptions( + width: viewport.logicalWidth, + height: viewport.logicalHeight, + scaleFactor: viewport.scaleFactor, + mode: .continuous + ) + ) + } catch { + try? await runtime.close() + throw error } - let map = try MapHandle( - runtime: runtime, - options: MapOptions( - width: viewport.logicalWidth, - height: viewport.logicalHeight, - scaleFactor: viewport.scaleFactor, - mode: .continuous - ) + self.runtime = runtime + self.map = map + try await map.setEventMask([ + .mapRenderUpdateAvailable, .mapRenderFrameFinished, + ]) + _ = try await map.setStyleURL( + "https://tiles.openfreemap.org/styles/bright" ) - createdMap = map - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, so this runs before the style load. - try map.setEventMask([.mapRenderUpdateAvailable, .mapRenderFrameFinished]) - try map.setStyleURL("https://tiles.openfreemap.org/styles/bright") - try map.jump(to: CameraOptions( + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions( center: LatLng(latitude: 37.7749, longitude: -122.4194), zoom: 13.0, bearing: 12.0, pitch: 30.0 - )) - try map.requestRepaint() - - self.runtime = runtime - self.map = map - didInitialize = true + ))) + _ = try await map.requestRepaint() } - /// The `Sendable` reference the render loop attaches its own session against. - /// `MapHandle` itself stays on this thread. - func attachRef() throws -> MapAttachRef { - try map.attachRef() + var mapHandle: MapHandle { + map } - /// Closes the map and then the runtime. The render session must already be - /// closed; a map with an attached session cannot be destroyed. - func close() throws { - guard !isClosed else { return } - isClosed = true - var firstError: Error? - do { - try map.close() - } catch { - firstError = firstError ?? error - } - do { - try runtime.close() - } catch { - firstError = firstError ?? error - } - if let firstError { - throw firstError + func scheduleEventDrains( + onRenderRequested: @escaping @MainActor @Sendable () -> Void, + onFailure: @escaping @MainActor @Sendable (Error) -> Void + ) { + runtime.setEventReadyHandler { [weak self] in + Task { @MainActor in + guard let self, !self.isClosed else { return } + do { + if try self.drainEvents() { onRenderRequested() } + } catch { + onFailure(error) + } + } } } - /// Pumps the runtime, parking up to `timeout` when there is nothing to do. - func pump(timeout: TimeInterval) throws { - try runtime.pump(timeout: timeout) - } - - /// Acquires the wake source the render loop uses to release this loop's park. - func wakeSource() throws -> WakeSource { - try runtime.wakeSource() + func close() async throws { + guard !isClosed else { return } + isClosed = true + runtime.setEventReadyHandler(nil) + try map.close() + // Awaiting the release completion keeps process exit ordered after native + // teardown. + try await runtime.close() } - /// Drains one batch of runtime events, reporting whether the map wants - /// another frame. - func drainEvents() throws -> Bool { + private func drainEvents() throws -> Bool { var renderPending = false - // One drain takes every event the pump produced. - for event in try runtime.drainEvents().events { - guard map.isSource(of: event) else { continue } + for event in try runtime.drainEvents().events + where map.isSource(of: event) + { switch event.type { case .mapRenderUpdateAvailable: renderPending = true @@ -134,51 +123,72 @@ final class MapState { return renderPending } - /// Applies one decoded camera command on the map's owner thread, where - /// read-modify-write commands also read the current camera. - func apply(_ command: CameraCommand) throws { - switch command { - case .cancelTransitions: - try map.cancelTransitions() - case let .setGestureInProgress(inProgress): - try map.setGestureInProgress(inProgress) - case let .moveBy(dx, dy): - try map.moveBy(deltaX: dx, deltaY: dy) - case let .moveByAnimated(dx, dy, animation): - try map.moveBy(deltaX: dx, deltaY: dy, animation: animation) - case let .scaleBy(scale, anchor): - try map.scaleBy(scale, anchor: anchor) - case let .scaleByAnimated(scale, anchor, animation): - try map.scaleBy(scale, anchor: anchor, animation: animation) - case let .adjustBearing(delta): - let current = try map.camera() - try map.jump(to: CameraOptions(bearing: (current.bearing ?? 0) + delta)) - case let .adjustBearingAnimated(delta, animation): - let current = try map.camera() - try map.ease( - to: CameraOptions(bearing: (current.bearing ?? 0) + delta), - animation: animation - ) - case let .adjustPitch(delta): - let current = try map.camera() - try map.jump( - to: CameraOptions(pitch: clampedPitch((current.pitch ?? 0) + delta)) - ) - case let .adjustPitchAnimated(delta, animation): - let current = try map.camera() - try map.ease( - to: CameraOptions(pitch: clampedPitch((current.pitch ?? 0) + delta)), - animation: animation - ) - case let .resetOrientation(animation): - try map.ease( - to: CameraOptions(bearing: 0, pitch: 0), - animation: animation - ) - } + func resize(_ extent: MapLogicalExtent) async throws { + _ = try await map.resize(to: extent) } -} -private func clampedPitch(_ pitch: Double) -> Double { - min(max(pitch, 0.0), 60.0) + func setGestureInProgress(_ inProgress: Bool) async throws { + _ = try await map.updateCamera(CameraUpdate( + camera: CameraOptions(), + gesturePhase: inProgress ? .begin : .end + )) + } + + func cancelTransitions() async throws { + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions())) + } + + func moveBy( + dx: Double, + dy: Double, + animation: AnimationOptions? = nil + ) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + offset: ScreenPoint(x: dx, y: dy), + animation: animation ?? AnimationOptions() + )) + } + + func scaleBy( + _ scale: Double, + anchor: ScreenPoint, + animation: AnimationOptions? = nil + ) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .scale, + amount: scale, + anchor: anchor, + animation: animation ?? AnimationOptions() + )) + } + + func adjustBearing( + delta: Double, + animation: AnimationOptions? = nil + ) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .bearing, + amount: delta, + animation: animation ?? AnimationOptions() + )) + } + + func adjustPitch( + delta: Double, + animation: AnimationOptions? = nil + ) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .pitch, + amount: delta, + animation: animation ?? AnimationOptions() + )) + } + + func resetOrientation(animation: AnimationOptions) async throws { + _ = try await map.updateCamera(CameraUpdate( + mode: .ease, + camera: CameraOptions(bearing: 0, pitch: 0), + animation: animation + )) + } } diff --git a/examples/swift-map/Sources/SwiftMap/MetalMapView.swift b/examples/swift-map/Sources/SwiftMap/MetalMapView.swift index 3a9b4b70b..c200ad7c8 100644 --- a/examples/swift-map/Sources/SwiftMap/MetalMapView.swift +++ b/examples/swift-map/Sources/SwiftMap/MetalMapView.swift @@ -3,23 +3,27 @@ import MaplibreNativeFFI import QuartzCore /// The display-paced render loop. This view runs on the main thread and owns -/// the window, input decoding, the Metal objects, and the render session. The -/// runtime and the map live on the runtime loop thread it starts, reached -/// through ``Channels``. +/// the window, input decoding, the runtime, the map, the Metal objects, and the +/// render session. @MainActor final class MetalMapView: NSView { private let metalLayer = CAMetalLayer() private let input = InputController() + private let mode: RenderTargetMode - private let channels = Channels() private var graphics: MetalGraphicsContext? + private var mapState: MapState? private var renderTarget: MetalRenderTarget? - private var runtimeLoop: RuntimeLoopThread? + private var setupTask: Task? private var timer: Timer? + private var frameTask: Task? + private var shutdownTask: Task? private var currentViewport: Viewport? + private var renderRequested = true private var consecutiveRenderFailures = 0 private var didLogStartupStatus = false private var isShutDown = false + private var terminateAfterShutdown = false private var setupError: Error? private var errorLabel: NSTextField? @@ -68,27 +72,42 @@ final class MetalMapView: NSView { } } - /// Closes the session before the runtime loop closes the map; a map with an + /// Closes the session before closing the map; a map with an /// attached session cannot be destroyed. @objc private func shutdown() { + beginShutdown() + } + + private func beginShutdown(terminate: Bool = false) { + terminateAfterShutdown = terminateAfterShutdown || terminate guard !isShutDown else { return } isShutDown = true timer?.invalidate() timer = nil - do { - try renderTarget?.close() - } catch { - print(error) + if frameTask == nil { + finishShutdown() } + } + + private func finishShutdown() { + guard shutdownTask == nil else { return } + let target = renderTarget renderTarget = nil - if runtimeLoop != nil { - channels.requestShutdown() - if !channels.waitForRuntimeLoopExit(timeout: 5.0) { - print("runtime loop did not finish before the shutdown deadline") + let setup = setupTask + shutdownTask = Task { @MainActor in + await setup?.value + do { + try await target?.close() + try await self.mapState?.close() + } catch { + print(error) + } + self.mapState = nil + NotificationCenter.default.removeObserver(self) + if self.terminateAfterShutdown { + NSApp.terminate(nil) } - runtimeLoop = nil } - NotificationCenter.default.removeObserver(self) } override func layout() { @@ -102,70 +121,108 @@ final class MetalMapView: NSView { } override func mouseDown(with event: NSEvent) { - requestRenderIfCameraChanged(input.mouseDown(event, commands: channels)) + updateMap { try await self.input.mouseDown(event, mapState: $0) } } override func rightMouseDown(with event: NSEvent) { - requestRenderIfCameraChanged( - input.rightMouseDown(event, commands: channels) - ) + updateMap { try await self.input.rightMouseDown(event, mapState: $0) } } override func mouseUp(with event: NSEvent) { - requestRenderIfCameraChanged(input.mouseUp(event, commands: channels)) + updateMap { try await self.input.mouseUp(event, mapState: $0) } } override func rightMouseUp(with event: NSEvent) { - requestRenderIfCameraChanged(input.rightMouseUp(event, commands: channels)) + updateMap { try await self.input.rightMouseUp(event, mapState: $0) } } override func mouseDragged(with event: NSEvent) { - requestRenderIfCameraChanged(input.mouseDragged(event, commands: channels)) + updateMap { try await self.input.mouseDragged(event, mapState: $0) } } override func rightMouseDragged(with event: NSEvent) { - requestRenderIfCameraChanged(input.mouseDragged(event, commands: channels)) + updateMap { try await self.input.mouseDragged(event, mapState: $0) } } override func scrollWheel(with event: NSEvent) { - requestRenderIfCameraChanged( - input.scrollWheel(event, commands: channels, in: self) - ) + updateMap { + try await self.input.scrollWheel(event, in: self, mapState: $0) + } } override func keyDown(with event: NSEvent) { guard let viewport = currentViewport else { return } - requestRenderIfCameraChanged( - input.keyDown(event, commands: channels, viewport: viewport) - ) + updateMap { + try await self.input.keyDown(event, viewport: viewport, mapState: $0) + } } - private func requestRenderIfCameraChanged(_ cameraChanged: Bool) { - if cameraChanged { - channels.setRenderRequest() + private func updateMap( + _ update: @escaping (MapState) async throws -> Bool + ) { + guard !isShutDown, let state = mapState else { return } + Task { @MainActor in + do { + if try await update(state) { + renderRequested = true + } + } catch { + fail(String(describing: error)) + } } } private func startTimerIfNeeded() { guard timer == nil else { return } - // TODO(map-example-spec): Replace fixed NSTimer with a display-paced host - // loop. timer = Timer .scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in - Task { @MainActor in self?.tick() } + Task { @MainActor in self?.scheduleFrame() } } RunLoop.main.add(timer!, forMode: .common) } - /// Starts the runtime loop once a non-empty viewport is known, because the - /// map takes its initial extent from it. - private func startRuntimeLoopIfNeeded(viewport: Viewport) { - guard runtimeLoop == nil, !isShutDown else { return } - let loop = RuntimeLoopThread(channels: channels, viewport: viewport) - runtimeLoop = loop - loop.start() + /// Creates the map once a non-empty viewport is known, because the map takes + /// its initial extent from it. + private func startMapStateIfNeeded(viewport: Viewport) { + guard mapState == nil, setupTask == nil, !isShutDown else { return } + setupTask = Task { @MainActor [weak self] in + guard let self else { return } + var setupFailure: Error? + do { + let state = try await MapState(viewport: viewport) + if self.isShutDown { + try await state.close() + } else { + if let latest = self.currentViewport, !latest.isEmpty, + latest != viewport + { + try await state.resize(MapLogicalExtent( + width: latest.logicalWidth, + height: latest.logicalHeight, + scaleFactor: latest.scaleFactor + )) + } + self.mapState = state + state.scheduleEventDrains( + onRenderRequested: { [weak self] in + self?.renderRequested = true + }, + onFailure: { [weak self] error in + self?.fail(String(describing: error)) + } + ) + self.renderRequested = true + } + } catch { + setupFailure = error + } + self.setupTask = nil + if let setupFailure { + self.fail(String(describing: setupFailure)) + } + } } private func updateViewport() { @@ -181,36 +238,32 @@ final class MetalMapView: NSView { return } - do { - graphics.resize(viewport) - try renderTarget?.resize(graphics: graphics, viewport: viewport) - currentViewport = viewport - channels.setRenderRequest() - startRuntimeLoopIfNeeded(viewport: viewport) - } catch { - // A failed resize leaves the render target detached, so stop the timer - // rather than render through a detached session. - print(error) - timer?.invalidate() - timer = nil - showError(String(describing: error)) + graphics.resize(viewport) + currentViewport = viewport + updateMap { state in + try await state.resize(MapLogicalExtent( + width: viewport.logicalWidth, + height: viewport.logicalHeight, + scaleFactor: viewport.scaleFactor + )) + return true } + startMapStateIfNeeded(viewport: viewport) } - /// Attaches the render session on this thread. Attach records the calling - /// thread as the session's owner, and every later session call runs here. - private func attachIfNeeded() { + /// Attaches the render session on the graphics thread that services it. + private func attachIfNeeded() async { guard renderTarget == nil, let graphics, let viewport = currentViewport, !viewport.isEmpty, - let attachRef = channels.attachRef() + let renderMap = mapState?.mapHandle else { return } do { - renderTarget = try MetalRenderTarget.attach( + renderTarget = try await MetalRenderTarget.attach( mode: mode, - attachRef: attachRef, + map: renderMap, graphics: graphics, viewport: viewport ) @@ -218,33 +271,45 @@ final class MetalMapView: NSView { logStartupStatus(mode: mode) didLogStartupStatus = true } - channels.setRenderRequest() + renderRequested = true } catch { fail(String(describing: error)) } } - private func tick() { - guard !isShutDown else { return } - if let failureMessage = channels.failureMessage { - fail(failureMessage) - return + private func scheduleFrame() { + guard frameTask == nil, !isShutDown else { return } + frameTask = Task { @MainActor in + await tick() + frameTask = nil + if isShutDown { + finishShutdown() + } } - attachIfNeeded() - guard let renderTarget, + } + + private func tick() async { + guard !isShutDown else { return } + await attachIfNeeded() + guard !isShutDown, + var renderTarget, + let graphics, let viewport = currentViewport, !viewport.isEmpty else { return } do { - // Consume first, so a request published during the render survives. - if channels.consumeRenderRequest() { - let rendered = try renderTarget.renderUpdate() + if try renderTargetNeedsResize(renderTarget, viewport: viewport) { + try await renderTarget.resize(graphics: graphics, viewport: viewport) + self.renderTarget = renderTarget + } + if renderRequested { + renderRequested = false + let rendered = try await renderTarget.renderFrame() if !rendered { - channels.setRenderRequest() + renderRequested = true } } - try renderTarget.finishFrame() consecutiveRenderFailures = 0 } catch { print(error) @@ -255,11 +320,22 @@ final class MetalMapView: NSView { } } + private func renderTargetNeedsResize( + _ target: MetalRenderTarget, + viewport: Viewport + ) throws -> Bool { + switch target { + case let .ownedTexture(session, _), + let .borrowedTexture(session, _, _), + let .nativeSurface(session): + return try session.snapshot().extent != viewport.extent + } + } + private func fail(_ message: String) { print(message) showError(message) - shutdown() - NSApp.terminate(nil) + beginShutdown(terminate: true) } private func readViewport() -> Viewport { diff --git a/examples/swift-map/Sources/SwiftMap/MetalRenderTarget.swift b/examples/swift-map/Sources/SwiftMap/MetalRenderTarget.swift index 5dd78a1db..3d099cfaf 100644 --- a/examples/swift-map/Sources/SwiftMap/MetalRenderTarget.swift +++ b/examples/swift-map/Sources/SwiftMap/MetalRenderTarget.swift @@ -41,9 +41,9 @@ final class MetalGraphicsContext { } } -/// The render session and its mode-specific resources. Attach records the -/// calling thread as the session's owner, so every call here runs on the render -/// loop thread that owns the Metal objects. +/// The render session and its mode-specific resources. The host cadence +/// services driver work on the graphics thread that owns the Metal objects. +/// Every operation remains isolated to the main actor. @MainActor enum MetalRenderTarget { case ownedTexture( @@ -57,29 +57,29 @@ enum MetalRenderTarget { ) case nativeSurface(session: RenderSessionHandle) - /// Attaches a session against the map the runtime loop published. + /// Attaches a session against the map owned by the view. static func attach( mode: RenderTargetMode, - attachRef: MapAttachRef, + map: MapHandle, graphics: MetalGraphicsContext, viewport: Viewport - ) throws -> MetalRenderTarget { + ) async throws -> MetalRenderTarget { switch mode { case .ownedTexture: - return try attachOwnedTexture( - attachRef: attachRef, + return try await attachOwnedTexture( + map: map, graphics: graphics, viewport: viewport ) case .borrowedTexture: - return try attachBorrowedTexture( - attachRef: attachRef, + return try await attachBorrowedTexture( + map: map, graphics: graphics, viewport: viewport ) case .nativeSurface: - return try attachNativeSurface( - attachRef: attachRef, + return try await attachNativeSurface( + map: map, graphics: graphics, viewport: viewport ) @@ -91,34 +91,24 @@ enum MetalRenderTarget { mutating func resize( graphics: MetalGraphicsContext, viewport: Viewport - ) throws { + ) async throws { switch self { case let .ownedTexture(session, compositor): + try await session.resize(viewport.extent) compositor.resize(viewport) - try session.resize( - width: viewport.logicalWidth, - height: viewport.logicalHeight, - scaleFactor: viewport.scaleFactor - ) case let .borrowedTexture(session, compositor, _): let replacement = try MetalBorrowedTexture( graphics: graphics, viewport: viewport ) - do { - try session - .setMetalBorrowedTextureTarget(MetalBorrowedTextureDescriptor( - extent: viewport.extent, - physicalWidth: viewport.physicalWidth, - physicalHeight: viewport.physicalHeight, - texture: replacement.pointer - )) - } catch { - // The session may hold either texture, and both are released as this - // scope unwinds, so detach first. - try? session.detach() - throw error - } + try await session.setMetalBorrowedTextureTarget( + MetalBorrowedTextureDescriptor( + extent: viewport.extent, + physicalWidth: viewport.physicalWidth, + physicalHeight: viewport.physicalHeight, + texture: replacement.pointer + ) + ) compositor.resize(viewport) self = .borrowedTexture( session: session, @@ -126,101 +116,109 @@ enum MetalRenderTarget { texture: replacement ) case let .nativeSurface(session): - try session.resize( - width: viewport.logicalWidth, - height: viewport.logicalHeight, - scaleFactor: viewport.scaleFactor - ) + try await session.resize(viewport.extent) } } - /// Renders the latest map update, reporting whether a frame was presented. - /// For a few iterations after attach or resize the session reports a pending - /// size, because the map applies a new logical size on the runtime loop's - /// next pump. - func renderUpdate() throws -> Bool { + /// Services caller-driver work and submits one host-paced frame demand. + func renderFrame() async throws -> Bool { + let session: RenderSessionHandle + switch self { + case let .ownedTexture(value, _), + let .borrowedTexture(value, _, _), + let .nativeSurface(value): + session = value + } + try session.requestFrame(FrameDemand(options: [.ifNeeded, .present])) + try session.serviceDriverWork() + let results = try session.drainFrameResults() + guard let result = results.last else { return false } + if !results.contains(where: { $0.result == .rendered }) { + return result.result != .sizePending && + result.result != .targetNotReady + } + switch self { case let .ownedTexture(session, compositor): - guard try session.renderUpdate().result == .rendered else { - return false - } - let frame = try session.acquireMetalOwnedTextureFrame() - var presented = false - var firstError: Error? + guard let frame = try session.acquireFrame() else { return false } do { - presented = try compositor.draw(frame: frame) + let presented = try compositor.draw(frame: frame) + try frame.release() + return presented } catch { - firstError = error - } - do { - try frame.close() - } catch { - firstError = firstError ?? error - } - if let firstError { - throw firstError - } - return presented - case let .borrowedTexture(session, compositor, texture): - guard try session.renderUpdate().result == .rendered else { - return false + try? frame.release() + throw error } + case let .borrowedTexture(_, compositor, texture): return try compositor.draw(texture: texture.texture) - case let .nativeSurface(session): - return try session.renderUpdate().result == .rendered + case .nativeSurface: + return true } } - func finishFrame() throws { - // Metal surface and texture paths need no per-iteration host upkeep here. - } - - func close() throws { + func close() async throws { + let session: RenderSessionHandle switch self { - case let .ownedTexture(session, _): - try session.close() - case let .borrowedTexture(session, _, _): - try session.close() - case let .nativeSurface(session): + case let .ownedTexture(value, _), + let .borrowedTexture(value, _, _), + let .nativeSurface(value): + session = value + } + do { + try await session.detach() try session.close() + } catch { + _ = try? session.abandon() + try? session.close() + throw error } } private static func attachOwnedTexture( - attachRef: MapAttachRef, + map: MapHandle, graphics: MetalGraphicsContext, viewport: Viewport - ) throws -> MetalRenderTarget { - let session = - try attachRef.attachMetalOwnedTexture(MetalOwnedTextureDescriptor( + ) async throws -> MetalRenderTarget { + let attachment = try map.attachMetalOwnedTexture( + MetalOwnedTextureDescriptor( extent: viewport.extent, context: graphics.contextDescriptor - )) + ), + options: .init( + driver: .callerGraphicsThread, + requestedTextureRingDepth: 3 + ) + ) + let session = try await finishAttachment(attachment) do { let compositor = try MetalTextureCompositor(graphics: graphics) return .ownedTexture(session: session, compositor: compositor) } catch { + try? await session.detach() try? session.close() throw error } } private static func attachBorrowedTexture( - attachRef: MapAttachRef, + map: MapHandle, graphics: MetalGraphicsContext, viewport: Viewport - ) throws -> MetalRenderTarget { + ) async throws -> MetalRenderTarget { let texture = try MetalBorrowedTexture( graphics: graphics, viewport: viewport ) - let session = try attachRef - .attachMetalBorrowedTexture(MetalBorrowedTextureDescriptor( + let attachment = try map.attachMetalBorrowedTexture( + MetalBorrowedTextureDescriptor( extent: viewport.extent, physicalWidth: viewport.physicalWidth, physicalHeight: viewport.physicalHeight, texture: texture.pointer - )) + ), + options: .init(driver: .callerGraphicsThread) + ) + let session = try await finishAttachment(attachment) do { let compositor = try MetalTextureCompositor(graphics: graphics) return .borrowedTexture( @@ -229,22 +227,46 @@ enum MetalRenderTarget { texture: texture ) } catch { + try? await session.detach() try? session.close() throw error } } private static func attachNativeSurface( - attachRef: MapAttachRef, + map: MapHandle, graphics: MetalGraphicsContext, viewport: Viewport - ) throws -> MetalRenderTarget { - let session = try attachRef.attachMetalSurface(MetalSurfaceDescriptor( - extent: viewport.extent, - context: graphics.contextDescriptor, - layer: graphics.layerPointer - )) - return .nativeSurface(session: session) + ) async throws -> MetalRenderTarget { + let attachment = try map.attachMetalSurface( + MetalSurfaceDescriptor( + extent: viewport.extent, + context: graphics.contextDescriptor, + layer: graphics.layerPointer + ), + options: .init(driver: .callerGraphicsThread) + ) + return try .nativeSurface(session: await finishAttachment(attachment)) + } + + private static func finishAttachment( + _ attachment: RenderSessionAttachment + ) async throws -> RenderSessionHandle { + let session = attachment.session + do { + try session.setDriverWorkReadyHandler { [weak session] in + Task { @MainActor in + _ = try? session?.serviceDriverWork(maxWork: 0) + } + } + try session.serviceDriverWork(maxWork: 0) + try await attachment.completion.value + return session + } catch { + _ = try? session.abandon() + try? session.close() + throw error + } } } @@ -274,12 +296,15 @@ final class MetalTextureCompositor { ) } - func draw(frame: MetalOwnedTextureFrameHandle) throws -> Bool { + func draw(frame: AcquiredFrameHandle) throws -> Bool { + let synchronization = try frame.producerSynchronization() var presented = false - try frame.withBackendPointers { view in - let address = try view.texture.addressBitPattern - let texture = try metalTexture(address: address) - presented = try draw(texture: texture) + try frame.withMetalTexture { value in + let texture = try metalTexture(address: value.texture.addressBitPattern) + presented = try draw( + texture: texture, + producerSynchronization: synchronization + ) } return presented } @@ -287,7 +312,10 @@ final class MetalTextureCompositor { /// Samples the texture into the layer's next drawable, reporting whether the /// frame was presented. An occluded window or an empty drawable pool yields /// no drawable, which is reported as false rather than failing the frame. - func draw(texture: any MTLTexture) throws -> Bool { + func draw( + texture: any MTLTexture, + producerSynchronization: GPUSynchronization = .cpuComplete + ) throws -> Bool { guard let drawable = layer.nextDrawable() else { return false } let passDescriptor = MTLRenderPassDescriptor() guard let colorAttachment = passDescriptor.colorAttachments[0] else { @@ -306,6 +334,15 @@ final class MetalTextureCompositor { guard let commandBuffer = queue.makeCommandBuffer() else { throw metalError("Metal command buffer creation failed") } + switch producerSynchronization { + case .cpuComplete: + break + case let .metalSharedEvent(pointer, value): + let event = try metalSharedEvent(address: pointer.addressBitPattern) + commandBuffer.encodeWaitForEvent(event, value: value) + default: + throw metalError("Metal frame returned incompatible GPU synchronization") + } guard let encoder = commandBuffer.makeRenderCommandEncoder( descriptor: passDescriptor ) else { @@ -317,6 +354,9 @@ final class MetalTextureCompositor { encoder.endEncoding() commandBuffer.present(drawable) commandBuffer.commit() + // CPU-complete frame release is valid only after the compositor finishes + // sampling the session-owned texture. + commandBuffer.waitUntilCompleted() return true } @@ -380,6 +420,19 @@ private func metalTexture(address: UInt) throws -> any MTLTexture { return texture } +private func metalSharedEvent(address: UInt) throws -> any MTLSharedEvent { + guard let pointer = UnsafeRawPointer(bitPattern: address) else { + throw metalError("Metal frame has a null shared event") + } + let object = Unmanaged.fromOpaque(pointer).takeUnretainedValue() + guard let event = object as? any MTLSharedEvent else { + throw metalError( + "Metal frame pointer did not contain an MTLSharedEvent" + ) + } + return event +} + private func nativePointer(_ object: AnyObject) -> NativePointer { NativePointer( bitPattern: UInt(bitPattern: Unmanaged.passUnretained(object).toOpaque()) diff --git a/examples/swift-map/Sources/SwiftMap/RuntimeLoop.swift b/examples/swift-map/Sources/SwiftMap/RuntimeLoop.swift deleted file mode 100644 index c2e3af33a..000000000 --- a/examples/swift-map/Sources/SwiftMap/RuntimeLoop.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import MaplibreNativeFFI - -/// The runtime loop, owning the runtime and the map for their whole lifetime. -/// The render loop attaches its own session against the reference published -/// here. -/// -/// This must be a dedicated `Thread`: native owner-thread checks are keyed on -/// the OS thread, and a serial `DispatchQueue`, an `actor`, or a `Task` may run -/// successive blocks on different threads. The view and its window belong to -/// the main thread, which is therefore the render loop. -final class RuntimeLoopThread: Thread { - /// Backstop for this loop's park; the render loop's wake source normally - /// releases it. - private static let parkTimeout = 0.1 - - /// Bound on waiting for the render loop to close its session during teardown. - private static let shutdownWaitTimeout = 5.0 - - private let channels: Channels - private let initialViewport: Viewport - - init(channels: Channels, viewport: Viewport) { - self.channels = channels - initialViewport = viewport - super.init() - name = "org.maplibre.nativeffi.examples.swift-map.runtime-loop" - } - - override func main() { - do { - try run() - } catch { - channels.fail(error) - } - channels.markRuntimeLoopFinished() - } - - private func run() throws { - let state = try MapState(viewport: initialViewport) - // A map with an attached session cannot be destroyed, so wait for the - // render loop to close its session. Failures are published before this - // wait runs, so the render loop sees them and releases the wait. - defer { - channels.waitForShutdown(timeout: Self.shutdownWaitTimeout) - do { - try state.close() - } catch { - print(error) - } - } - - do { - try pumpUntilShutdown(state: state) - } catch { - channels.fail(error) - } - } - - private func pumpUntilShutdown(state: MapState) throws { - let attachRef = try state.attachRef() - let wake = try state.wakeSource() - // A wake source is its own native handle: closing the runtime does not - // release it. - defer { try? wake.close() } - channels.publish(attachRef: attachRef, wake: wake) - - var batch: [CameraCommand] = [] - - while !channels.isShutdownRequested, channels.failureMessage == nil { - channels.drainCommands(into: &batch) - for command in batch { - try state.apply(command) - } - // No display paces this thread, so it parks in the pump until the - // runtime has work or the render loop signals the wake source. - try state.pump(timeout: Self.parkTimeout) - if try state.drainEvents() { - channels.setRenderRequest() - } - } - } -} diff --git a/examples/swift-map/Sources/SwiftMapIOS/Channels.swift b/examples/swift-map/Sources/SwiftMapIOS/Channels.swift deleted file mode 100644 index bd6964199..000000000 --- a/examples/swift-map/Sources/SwiftMapIOS/Channels.swift +++ /dev/null @@ -1,178 +0,0 @@ -import Foundation -import MaplibreNativeFFI - -/// A camera change decoded on the render loop and applied on the map's owner -/// thread. Commands carry deltas wherever the current camera is an input, -/// because the read and write have to happen together on the owner thread. -enum CameraCommand { - case cancelTransitions - case setGestureInProgress(Bool) - case moveBy(dx: Double, dy: Double) - case scaleBy(scale: Double, anchor: ScreenPoint) - /// Adds `delta` degrees to the current bearing, pivoting on `anchor`. - case adjustBearing(delta: Double, anchor: ScreenPoint) - /// Adds `delta` degrees to the current pitch, clamped to `[0, 60]`. - case adjustPitch(delta: Double) - /// Zooms to `round(zoom) + 1` about `anchor`, the double-tap step. - case zoomToNextStep(anchor: ScreenPoint, animation: AnimationOptions) -} - -/// The cross-thread surface between the render loop, which owns the view, the -/// Metal objects, and the render session, and the runtime loop, which owns the -/// runtime and the map. -/// -/// The runtime loop that reads these channels MUST be a dedicated `Thread`. -/// Native owner-thread checks are keyed on the OS thread, and a serial -/// `DispatchQueue`, an `actor`, or a `Task` may run successive blocks on -/// different threads, producing `MLN_STATUS_WRONG_THREAD` failures. -final class Channels: @unchecked Sendable { - private let condition = NSCondition() - private var commands: [CameraCommand] = [] - private var renderRequested = true - private var publishedAttachRef: MapAttachRef? - /// Releases the runtime loop's parked pump. Set once the loop has published. - private var wake: WakeSource? - private var shutdown = false - private var failure: String? - private var runtimeLoopFinished = false - - // MARK: - Camera commands (render loop to runtime loop) - - /// Render loop: queues a decoded camera change and wakes the runtime loop. - /// The buffer grows rather than dropping, because deltas and gesture brackets - /// are not recoverable once discarded. - func push(_ command: CameraCommand) { - condition.lock() - commands.append(command) - let source = wake - condition.unlock() - // The runtime loop parks inside the native pump, not on this condition. - // Signal outside the lock so a native call never runs under it. - try? source?.signal() - } - - /// Runtime loop: swaps `batch` in for the pending commands, keeping the - /// locked section to the swap alone. - func drainCommands(into batch: inout [CameraCommand]) { - // Clearing releases the elements just applied, so do it outside the lock. - batch.removeAll(keepingCapacity: true) - condition.lock() - defer { condition.unlock() } - swap(&commands, &batch) - } - - // MARK: - Render request (runtime loop to render loop) - - func setRenderRequest() { - condition.lock() - defer { condition.unlock() } - renderRequested = true - } - - /// Render loop: takes the request, if any. - func consumeRenderRequest() -> Bool { - condition.lock() - defer { condition.unlock() } - let wasRequested = renderRequested - renderRequested = false - return wasRequested - } - - // MARK: - Attach reference (runtime loop to render loop) - - /// Runtime loop: announces the map it just created. - func publish(attachRef: MapAttachRef, wake: WakeSource) { - condition.lock() - defer { condition.unlock() } - self.wake = wake - publishedAttachRef = attachRef - } - - /// Render loop: the reference to attach against, once the runtime loop has a - /// map. - func attachRef() -> MapAttachRef? { - condition.lock() - defer { condition.unlock() } - return publishedAttachRef - } - - // MARK: - Shutdown and failure - - /// Render loop: asks the runtime loop to stop. Called only after the render - /// session is closed, because the map cannot be destroyed before then. - func requestShutdown() { - condition.lock() - shutdown = true - condition.broadcast() - let source = wake - condition.unlock() - // Release the pump so shutdown is observed now. - try? source?.signal() - } - - /// Runtime loop: blocks until the render loop has closed its session. The map - /// cannot be destroyed before then. Bounded so a render loop that died - /// without signalling cannot wedge teardown. - func waitForShutdown(timeout: TimeInterval) { - let deadline = Date(timeIntervalSinceNow: timeout) - condition.lock() - defer { condition.unlock() } - while !shutdown { - if !condition.wait(until: deadline) { return } - } - } - - var isShutdownRequested: Bool { - condition.lock() - defer { condition.unlock() } - return shutdown - } - - /// Records the first failure from either loop. It crosses as text because - /// `any Error` carries no `Sendable` guarantee. - func fail(_ error: Error) { - condition.lock() - defer { condition.unlock() } - if failure == nil { - failure = String(describing: error) - } - condition.broadcast() - } - - var failureMessage: String? { - condition.lock() - defer { condition.unlock() } - return failure - } - - /// Runtime loop: reports that it has closed the map and the runtime and is - /// about to exit. - func markRuntimeLoopFinished() { - condition.lock() - defer { condition.unlock() } - runtimeLoopFinished = true - condition.broadcast() - } - - /// Render loop: waits for the runtime loop to finish. Returns false when the - /// deadline passed first. - func waitForRuntimeLoopExit(timeout: TimeInterval) -> Bool { - let deadline = Date(timeIntervalSinceNow: timeout) - condition.lock() - defer { condition.unlock() } - while !runtimeLoopFinished { - if !condition.wait(until: deadline) { - return runtimeLoopFinished - } - } - return true - } - - /// Render loop: releases the runtime loop's parked pump. - func wakeRuntimeLoop() { - condition.lock() - let source = wake - condition.unlock() - try? source?.signal() - } -} diff --git a/examples/swift-map/Sources/SwiftMapIOS/MapState.swift b/examples/swift-map/Sources/SwiftMapIOS/MapState.swift index 0c8eef4e0..a8dc4d9dc 100644 --- a/examples/swift-map/Sources/SwiftMapIOS/MapState.swift +++ b/examples/swift-map/Sources/SwiftMapIOS/MapState.swift @@ -30,100 +30,89 @@ struct Viewport: Equatable { } } -/// Runtime and map, owned for their whole lifetime by the runtime loop thread. -/// The render loop thread owns the view, the Metal objects, and the session. +/// Runtime and map state owned by the main render loop. +@MainActor final class MapState { private let runtime: RuntimeHandle private let map: MapHandle private var isClosed = false - init(viewport: Viewport) throws { + init(viewport: Viewport) async throws { precondition( !viewport.isEmpty, "cannot create MapState with an empty viewport" ) - let runtime = - try RuntimeHandle(options: RuntimeOptions(cachePath: ":memory:")) - var createdMap: MapHandle? - var didInitialize = false - defer { - if !didInitialize { - try? createdMap?.close() - try? runtime.close() - } - } - - let map = try MapHandle( - runtime: runtime, - options: MapOptions( - width: viewport.logicalWidth, - height: viewport.logicalHeight, - scaleFactor: viewport.scaleFactor, - mode: .continuous - ) + let runtime = try RuntimeHandle( + options: RuntimeOptions(cachePath: ":memory:") ) - createdMap = map - // The two event types the runtime loop reads. A map queues no event of an - // unselected type, so this runs before the style load. - try map.setEventMask([.mapRenderUpdateAvailable, .mapRenderFrameFinished]) - try map.setStyleURL("https://tiles.openfreemap.org/styles/bright") - try map.jump(to: CameraOptions( - center: LatLng(latitude: 37.7749, longitude: -122.4194), - zoom: 13.0, - bearing: 12.0, - pitch: 30.0 - )) - try map.requestRepaint() + let map: MapHandle + do { + map = try await MapHandle( + runtime: runtime, + options: MapOptions( + width: viewport.logicalWidth, + height: viewport.logicalHeight, + scaleFactor: viewport.scaleFactor, + mode: .continuous + ) + ) + } catch { + try? await runtime.close() + throw error + } self.runtime = runtime self.map = map - didInitialize = true + try await map.setEventMask([ + .mapRenderUpdateAvailable, .mapRenderFrameFinished, + ]) + _ = try await map.setStyleURL( + "https://tiles.openfreemap.org/styles/bright" + ) + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions( + center: LatLng(latitude: 37.7749, longitude: -122.4194), + zoom: 13, + bearing: 12, + pitch: 30 + ))) + _ = try await map.requestRepaint() } - /// The `Sendable` reference the render loop attaches its own session against. - /// `MapHandle` itself stays on this thread. - func attachRef() throws -> MapAttachRef { - try map.attachRef() + var mapHandle: MapHandle { + map } - /// Closes the map and then the runtime. The render session must already be - /// closed; a map with an attached session cannot be destroyed. - func close() throws { - guard !isClosed else { return } - isClosed = true - var firstError: Error? - do { - try map.close() - } catch { - firstError = firstError ?? error - } - do { - try runtime.close() - } catch { - firstError = firstError ?? error - } - if let firstError { - throw firstError + func scheduleEventDrains( + onRenderRequested: @escaping @MainActor @Sendable () -> Void, + onFailure: @escaping @MainActor @Sendable (Error) -> Void + ) { + runtime.setEventReadyHandler { [weak self] in + Task { @MainActor in + guard let self, !self.isClosed else { return } + do { + if try self.drainEvents() { onRenderRequested() } + } catch { + onFailure(error) + } + } } } - /// Pumps the runtime, parking up to `timeout` when there is nothing to do. - func pump(timeout: TimeInterval) throws { - try runtime.pump(timeout: timeout) - } - - /// Acquires the wake source the render loop uses to release this loop's park. - func wakeSource() throws -> WakeSource { - try runtime.wakeSource() + func close() async throws { + guard !isClosed else { return } + isClosed = true + runtime.setEventReadyHandler(nil) + try map.close() + // Awaiting the release completion keeps process exit ordered after native + // teardown. + try await runtime.close() } - /// Drains one batch of runtime events, reporting whether the map wants - /// another frame. - func drainEvents() throws -> Bool { + private func drainEvents() throws -> Bool { var renderPending = false - // One drain takes every event the pump produced. - for event in try runtime.drainEvents().events { - guard map.isSource(of: event) else { continue } + for event in try runtime.drainEvents().events + where map.isSource(of: event) + { switch event.type { case .mapRenderUpdateAvailable: renderPending = true @@ -138,42 +127,59 @@ final class MapState { return renderPending } - /// Applies one decoded camera command on the map's owner thread, where - /// read-modify-write commands also read the current camera. - func apply(_ command: CameraCommand) throws { - switch command { - case .cancelTransitions: - try map.cancelTransitions() - case let .setGestureInProgress(inProgress): - try map.setGestureInProgress(inProgress) - case let .moveBy(dx, dy): - try map.moveBy(deltaX: dx, deltaY: dy) - case let .scaleBy(scale, anchor): - try map.scaleBy(scale, anchor: anchor) - case let .adjustBearing(delta, anchor): - let camera = try map.camera() - try map.jump(to: CameraOptions( - bearing: (camera.bearing ?? 0) + delta, - anchor: anchor - )) - case let .adjustPitch(delta): - let camera = try map.camera() - try map.jump( - to: CameraOptions(pitch: clampedPitch((camera.pitch ?? 0) + delta)) - ) - case let .zoomToNextStep(anchor, animation): - let camera = try map.camera() - let zoom = camera.zoom ?? 0 - let targetZoom = round(zoom) + 1.0 - try map.scaleBy( - pow(2.0, targetZoom - zoom), - anchor: anchor, - animation: animation - ) - } + func resize(_ extent: MapLogicalExtent) async throws { + _ = try await map.resize(to: extent) + } + + func setGestureInProgress(_ inProgress: Bool) async throws { + _ = try await map.updateCamera(CameraUpdate( + camera: CameraOptions(), + gesturePhase: inProgress ? .begin : .end + )) + } + + func cancelTransitions() async throws { + _ = try await map.updateCamera(CameraUpdate(camera: CameraOptions())) + } + + func moveBy(dx: Double, dy: Double) async throws { + _ = try await map.applyCameraDelta( + CameraDelta(offset: ScreenPoint(x: dx, y: dy)) + ) + } + + func scaleBy(_ scale: Double, anchor: ScreenPoint) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .scale, + amount: scale, + anchor: anchor + )) + } + + func adjustBearing(delta: Double, anchor: ScreenPoint) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .bearing, + amount: delta, + anchor: anchor + )) + } + + func adjustPitch(delta: Double) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .pitch, + amount: delta + )) } -} -private func clampedPitch(_ pitch: Double) -> Double { - min(max(pitch, 0.0), 60.0) + func zoomToNextStep( + anchor: ScreenPoint, + animation: AnimationOptions + ) async throws { + _ = try await map.applyCameraDelta(CameraDelta( + kind: .scale, + amount: 2, + anchor: anchor, + animation: animation + )) + } } diff --git a/examples/swift-map/Sources/SwiftMapIOS/MetalMapView.swift b/examples/swift-map/Sources/SwiftMapIOS/MetalMapView.swift index f69fbf938..391f50769 100644 --- a/examples/swift-map/Sources/SwiftMapIOS/MetalMapView.swift +++ b/examples/swift-map/Sources/SwiftMapIOS/MetalMapView.swift @@ -4,10 +4,8 @@ import os import QuartzCore import UIKit -/// The display-paced render loop. This view runs on the main thread and owns -/// the layer, gesture decoding, the Metal objects, and the render session. The -/// runtime and the map live on the runtime loop thread it starts, reached -/// through ``Channels``. +/// The display-paced render loop. This view owns the layer, gesture decoding, +/// runtime, map, Metal objects, and render session on the main thread. @MainActor final class MetalMapView: UIView { static let willTerminateMapViews = Notification @@ -16,16 +14,20 @@ final class MetalMapView: UIView { subsystem: "org.maplibre.nativeffi.examples.swift-map-ios", category: "MapView" ) - private let channels = Channels() private var graphics: MetalGraphicsContext? + private var mapState: MapState? private var renderTarget: MetalRenderTarget? - private var runtimeLoop: RuntimeLoopThread? + private var setupTask: Task? private var displayLink: CADisplayLink? + private var frameTask: Task? + private var shutdownTask: Task? private var currentViewport: Viewport? + private var renderRequested = true private var didLogStartupStatus = false private var viewVisible = false private var appForeground = true private var isShutDown = false + private var pendingResize = false /// The recognizers with a gesture still open. Pinch, rotation, and shove /// recognize simultaneously and report to the map as one gesture. @@ -60,7 +62,7 @@ final class MetalMapView: UIView { NotificationCenter.default.removeObserver(self) MainActor.assumeIsolated { stopHostLoop() - teardown() + renderTarget?.abandon() } } @@ -91,78 +93,91 @@ final class MetalMapView: UIView { @objc private func closeMap() { stopHostLoop() - teardown() + beginTeardown() } - /// Closes the session before the runtime loop closes the map; a map with an + /// Closes the session before closing the map; a map with an /// attached session cannot be destroyed. - private func teardown() { + private func beginTeardown() { guard !isShutDown else { return } isShutDown = true - do { - try renderTarget?.close() - } catch { - log.error("\(String(describing: error), privacy: .public)") + stopHostLoop() + if frameTask == nil { + finishTeardown() } + } + + private func finishTeardown() { + guard shutdownTask == nil else { return } + let target = renderTarget renderTarget = nil - guard runtimeLoop != nil else { return } - channels.requestShutdown() - if !channels.waitForRuntimeLoopExit(timeout: 5.0) { - log.error("runtime loop did not finish before the shutdown deadline") + let setup = setupTask + shutdownTask = Task { @MainActor in + await setup?.value + do { + try await target?.close() + try await self.mapState?.close() + } catch { + self.log.error("\(String(describing: error), privacy: .public)") + } + self.mapState = nil } - runtimeLoop = nil } @objc private func displayLinkTick() { - guard !isShutDown else { return } - if let failureMessage = channels.failureMessage { - log.error("\(failureMessage, privacy: .public)") - // The runtime loop waits for the session to close before destroying the - // map, so tear down rather than only stopping the display link. - stopHostLoop() - teardown() - return + guard frameTask == nil, !isShutDown else { return } + frameTask = Task { @MainActor in + await renderDisplayFrame() + frameTask = nil + if isShutDown { + finishTeardown() + } } - attachIfNeeded() - guard let renderTarget, + } + + private func renderDisplayFrame() async { + guard !isShutDown else { return } + await attachIfNeeded() + guard !isShutDown, + let renderTarget, let viewport = currentViewport, !viewport.isEmpty else { return } do { - // Consume first, so a request published during the render survives. - if channels.consumeRenderRequest() { - let rendered = try renderTarget.renderUpdate() + if pendingResize { + try await renderTarget.resize(viewport) + pendingResize = false + } + if renderRequested { + renderRequested = false + let rendered = try renderTarget.renderFrame() if !rendered { - channels.setRenderRequest() + renderRequested = true } } - try renderTarget.finishFrame() } catch { showError(error) - // Stopping the display link alone leaves the session attached, and the - // runtime loop would hold a map it can never destroy. - stopHostLoop() - teardown() + beginTeardown() } } - /// Attaches the render session on this thread. Attach records the calling - /// thread as the session's owner, and every later session call runs here. - private func attachIfNeeded() { + /// Attaches the render session on the graphics thread that services it. + private func attachIfNeeded() async { guard renderTarget == nil, let graphics, let viewport = currentViewport, !viewport.isEmpty, - let attachRef = channels.attachRef() + let renderMap = mapState?.mapHandle else { return } do { - renderTarget = try MetalRenderTarget.attach( - attachRef: attachRef, + renderTarget = try await MetalRenderTarget.attach( + map: renderMap, graphics: graphics, viewport: viewport ) + pendingResize = false if !didLogStartupStatus { log.info("render target: native-surface") log.info( @@ -170,13 +185,10 @@ final class MetalMapView: UIView { ) didLogStartupStatus = true } - channels.setRenderRequest() + renderRequested = true } catch { showError(error) - // The runtime loop waits for the session to close before destroying the - // map, so tear down rather than only stopping the display link. - stopHostLoop() - teardown() + beginTeardown() } } @@ -184,7 +196,7 @@ final class MetalMapView: UIView { guard !isShutDown else { return } refreshViewport() if viewVisible, appForeground { - channels.setRenderRequest() + renderRequested = true startHostLoop() } } @@ -201,13 +213,48 @@ final class MetalMapView: UIView { displayLink = nil } - /// Starts the runtime loop once a non-empty viewport is known, because the - /// map takes its initial extent from it. - private func startRuntimeLoopIfNeeded(viewport: Viewport) { - guard runtimeLoop == nil else { return } - let loop = RuntimeLoopThread(channels: channels, viewport: viewport) - runtimeLoop = loop - loop.start() + /// Creates the map once a non-empty viewport is known, because the map takes + /// its initial extent from it. + private func startMapStateIfNeeded(viewport: Viewport) { + guard mapState == nil, setupTask == nil, !isShutDown else { return } + setupTask = Task { @MainActor [weak self] in + guard let self else { return } + var setupFailure: Error? + do { + let state = try await MapState(viewport: viewport) + if self.isShutDown { + try await state.close() + } else { + if let latest = self.currentViewport, !latest.isEmpty, + latest != viewport + { + try await state.resize(MapLogicalExtent( + width: latest.logicalWidth, + height: latest.logicalHeight, + scaleFactor: latest.scaleFactor + )) + } + self.mapState = state + state.scheduleEventDrains( + onRenderRequested: { [weak self] in + self?.renderRequested = true + }, + onFailure: { [weak self] error in + self?.showError(error) + self?.beginTeardown() + } + ) + self.renderRequested = true + } + } catch { + setupFailure = error + } + self.setupTask = nil + if let setupFailure { + self.showError(setupFailure) + self.beginTeardown() + } + } } private func refreshViewport() { @@ -221,21 +268,23 @@ final class MetalMapView: UIView { return } - do { - graphics.resize(viewport) - try renderTarget?.resize(viewport) - currentViewport = viewport - channels.setRenderRequest() - startRuntimeLoopIfNeeded(viewport: viewport) - } catch { - showError(error) + graphics.resize(viewport) + currentViewport = viewport + pendingResize = renderTarget != nil + updateMap { state in + try await state.resize(MapLogicalExtent( + width: viewport.logicalWidth, + height: viewport.logicalHeight, + scaleFactor: viewport.scaleFactor + )) } + renderRequested = true + startMapStateIfNeeded(viewport: viewport) } private func readViewport() -> Viewport { - let scale = traitCollection.displayScale > 0 ? traitCollection - .displayScale : - UIScreen.main.scale + let displayScale = traitCollection.displayScale + let scale = displayScale > 0 ? displayScale : UIScreen.main.scale let rawLogicalWidth = bounds.width let rawLogicalHeight = bounds.height let rawPhysicalWidth = rawLogicalWidth * scale @@ -310,145 +359,126 @@ final class MetalMapView: UIView { ) } - /// Queues decoded camera commands and sets the render request when the - /// gesture changed the camera. - private func enqueue(_ decode: (Channels) -> Bool) { - if decode(channels) { - channels.setRenderRequest() + private func updateMap( + _ update: @escaping @MainActor (MapState) async throws -> Void + ) { + guard !isShutDown, let state = mapState else { return } + renderRequested = true + Task { @MainActor in + do { + try await update(state) + } catch { + showError(error) + beginTeardown() + } } } /// Opens the gesture bracket for the first recognizer to begin. - private func beginGesture( - _ recognizer: UIGestureRecognizer, - _ commands: Channels - ) { + private func beginGesture(_ recognizer: UIGestureRecognizer) { if openGestures.isEmpty { - commands.push(.setGestureInProgress(true)) + updateMap { state in + try await state.cancelTransitions() + try await state.setGestureInProgress(true) + } } openGestures.insert(ObjectIdentifier(recognizer)) } /// Closes the bracket once the last recognizer ends or is cancelled, so each /// open is paired with a close. - private func endGesture( - _ recognizer: UIGestureRecognizer, - _ commands: Channels - ) { + private func endGesture(_ recognizer: UIGestureRecognizer) { guard openGestures.remove(ObjectIdentifier(recognizer)) != nil else { return } if openGestures.isEmpty { - commands.push(.setGestureInProgress(false)) + updateMap { try await $0.setGestureInProgress(false) } } } @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) { - enqueue { commands in - switch recognizer.state { - case .began: - commands.push(.cancelTransitions) - self.beginGesture(recognizer, commands) - recognizer.setTranslation(.zero, in: self) - return false - case .changed: - let translation = recognizer.translation(in: self) - recognizer.setTranslation(.zero, in: self) - guard translation != .zero else { return false } - commands.push(.moveBy( + switch recognizer.state { + case .began: + beginGesture(recognizer) + recognizer.setTranslation(.zero, in: self) + case .changed: + let translation = recognizer.translation(in: self) + recognizer.setTranslation(.zero, in: self) + guard translation != .zero else { return } + updateMap { state in + try await state.moveBy( dx: Double(translation.x), dy: Double(translation.y) - )) - return true - default: - self.endGesture(recognizer, commands) - return false + ) } + default: + endGesture(recognizer) } } @objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) { - enqueue { commands in - switch recognizer.state { - case .began: - commands.push(.cancelTransitions) - self.beginGesture(recognizer, commands) - recognizer.scale = 1.0 - return false - case .changed: - let scale = Double(recognizer.scale) - recognizer.scale = 1.0 - guard scale.isFinite, scale > 0 else { return false } - let location = recognizer.location(in: self) - commands.push(.scaleBy( - scale: scale, - anchor: self.screenPoint(location) - )) - return true - default: - self.endGesture(recognizer, commands) - return false - } + switch recognizer.state { + case .began: + beginGesture(recognizer) + recognizer.scale = 1.0 + case .changed: + let scale = Double(recognizer.scale) + recognizer.scale = 1.0 + guard scale.isFinite, scale > 0 else { return } + let anchor = screenPoint(recognizer.location(in: self)) + updateMap { try await $0.scaleBy(scale, anchor: anchor) } + default: + endGesture(recognizer) } } @objc private func handleRotation(_ recognizer: UIRotationGestureRecognizer) { - enqueue { commands in - switch recognizer.state { - case .began: - commands.push(.cancelTransitions) - self.beginGesture(recognizer, commands) - recognizer.rotation = 0 - return false - case .changed: - let deltaRadians = recognizer.rotation - recognizer.rotation = 0 - guard deltaRadians != 0 else { return false } - let location = recognizer.location(in: self) - commands.push(.adjustBearing( + switch recognizer.state { + case .began: + beginGesture(recognizer) + recognizer.rotation = 0 + case .changed: + let deltaRadians = recognizer.rotation + recognizer.rotation = 0 + guard deltaRadians != 0 else { return } + let anchor = screenPoint(recognizer.location(in: self)) + updateMap { + try await $0.adjustBearing( delta: -Double(deltaRadians * 180 / .pi), - anchor: self.screenPoint(location) - )) - return true - default: - self.endGesture(recognizer, commands) - return false + anchor: anchor + ) } + default: + endGesture(recognizer) } } @objc private func handleShove(_ recognizer: UIPanGestureRecognizer) { - enqueue { commands in - switch recognizer.state { - case .began: - guard recognizer.numberOfTouches == 2 else { return false } - commands.push(.cancelTransitions) - self.beginGesture(recognizer, commands) - recognizer.setTranslation(.zero, in: self) - return false - case .changed: - guard recognizer.numberOfTouches == 2 else { return false } - let translation = recognizer.translation(in: self) - recognizer.setTranslation(.zero, in: self) - guard translation.y != 0 else { return false } - commands.push(.adjustPitch(delta: -Double(translation.y) * 0.1)) - return true - default: - self.endGesture(recognizer, commands) - return false + switch recognizer.state { + case .began: + guard recognizer.numberOfTouches == 2 else { return } + beginGesture(recognizer) + recognizer.setTranslation(.zero, in: self) + case .changed: + guard recognizer.numberOfTouches == 2 else { return } + let translation = recognizer.translation(in: self) + recognizer.setTranslation(.zero, in: self) + guard translation.y != 0 else { return } + updateMap { + try await $0.adjustPitch(delta: -Double(translation.y) * 0.1) } + default: + endGesture(recognizer) } } @objc private func handleDoubleTap(_ recognizer: UITapGestureRecognizer) { - enqueue { commands in - commands.push(.cancelTransitions) - let location = recognizer.location(in: self) - commands.push(.zoomToNextStep( - anchor: screenPoint(location), + let anchor = screenPoint(recognizer.location(in: self)) + updateMap { + try await $0.zoomToNextStep( + anchor: anchor, animation: MaplibreNativeFFI.AnimationOptions(durationMilliseconds: 160) - )) - return true + ) } } diff --git a/examples/swift-map/Sources/SwiftMapIOS/MetalRenderTarget.swift b/examples/swift-map/Sources/SwiftMapIOS/MetalRenderTarget.swift index bdc7920a7..dafcf7b4a 100644 --- a/examples/swift-map/Sources/SwiftMapIOS/MetalRenderTarget.swift +++ b/examples/swift-map/Sources/SwiftMapIOS/MetalRenderTarget.swift @@ -40,9 +40,9 @@ final class MetalGraphicsContext { } } -/// The render session for the host `CAMetalLayer`. Attach records the calling -/// thread as the session's owner, so every call here runs on the render loop -/// thread that owns the view and its layer. +/// The render session for the host `CAMetalLayer`. The display link services +/// driver work on the graphics thread. Every operation remains isolated to the +/// main actor. @MainActor final class MetalRenderTarget { private let session: RenderSessionHandle @@ -51,42 +51,64 @@ final class MetalRenderTarget { self.session = session } - /// Attaches a session against the map the runtime loop published. + /// Attaches a session against the map owned by the view. static func attach( - attachRef: MapAttachRef, + map: MapHandle, graphics: MetalGraphicsContext, viewport: Viewport - ) throws -> MetalRenderTarget { - let session = try attachRef.attachMetalSurface(MetalSurfaceDescriptor( - extent: viewport.extent, - context: graphics.contextDescriptor, - layer: graphics.layerPointer - )) - return MetalRenderTarget(session: session) + ) async throws -> MetalRenderTarget { + let attachment = try map.attachMetalSurface( + MetalSurfaceDescriptor( + extent: viewport.extent, + context: graphics.contextDescriptor, + layer: graphics.layerPointer + ), + options: .init(driver: .callerGraphicsThread) + ) + let session = attachment.session + do { + try session.setDriverWorkReadyHandler { [weak session] in + Task { @MainActor in + _ = try? session?.serviceDriverWork(maxWork: 0) + } + } + try session.serviceDriverWork(maxWork: 0) + try await attachment.completion.value + return MetalRenderTarget(session: session) + } catch { + _ = try? session.abandon() + try? session.close() + throw error + } } - func resize(_ viewport: Viewport) throws { - try session.resize( - width: viewport.logicalWidth, - height: viewport.logicalHeight, - scaleFactor: viewport.scaleFactor - ) + func resize(_ viewport: Viewport) async throws { + try await session.resize(viewport.extent) } - /// Renders the latest map update, reporting whether a frame was drawn. For a - /// few iterations after attach or resize the session reports a pending size, - /// because the map applies a new logical size on the runtime loop's next - /// pump. - func renderUpdate() throws -> Bool { - try session.renderUpdate().result == .rendered + /// Services graphics work and submits one display-link-paced frame demand. + func renderFrame() throws -> Bool { + try session.requestFrame(FrameDemand(options: [.ifNeeded, .present])) + try session.serviceDriverWork() + let results = try session.drainFrameResults() + guard let result = results.last else { return false } + return result.result != .sizePending && + result.result != .targetNotReady } - func finishFrame() throws { - // The Metal surface path needs no per-iteration host upkeep here. + func close() async throws { + do { + try await session.detach() + try session.close() + } catch { + abandon() + throw error + } } - func close() throws { - try session.close() + func abandon() { + _ = try? session.abandon() + try? session.close() } } diff --git a/examples/swift-map/Sources/SwiftMapIOS/RuntimeLoop.swift b/examples/swift-map/Sources/SwiftMapIOS/RuntimeLoop.swift deleted file mode 100644 index 7e5c6c544..000000000 --- a/examples/swift-map/Sources/SwiftMapIOS/RuntimeLoop.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import MaplibreNativeFFI -import os - -/// The runtime loop, owning the runtime and the map for their whole lifetime. -/// The render loop attaches its own session against the reference published -/// here. It keeps running across view and app lifecycle transitions, so loading -/// continues while the view is off screen. -/// -/// This must be a dedicated `Thread`: native owner-thread checks are keyed on -/// the OS thread, and a serial `DispatchQueue`, an `actor`, or a `Task` may run -/// successive blocks on different threads. The view and its window belong to -/// the main thread, which is therefore the render loop. -final class RuntimeLoopThread: Thread { - /// Backstop for this loop's park; the render loop's wake source normally - /// releases it. - private static let parkTimeout = 0.1 - - /// Bound on waiting for the render loop to close its session during teardown. - private static let shutdownWaitTimeout = 5.0 - - private let channels: Channels - private let initialViewport: Viewport - private let log = Logger( - subsystem: "org.maplibre.nativeffi.examples.swift-map-ios", - category: "RuntimeLoop" - ) - - init(channels: Channels, viewport: Viewport) { - self.channels = channels - initialViewport = viewport - super.init() - name = "org.maplibre.nativeffi.examples.swift-map-ios.runtime-loop" - } - - override func main() { - do { - try run() - } catch { - channels.fail(error) - } - channels.markRuntimeLoopFinished() - } - - private func run() throws { - let state = try MapState(viewport: initialViewport) - // A map with an attached session cannot be destroyed, so wait for the - // render loop to close its session. Failures are published before this - // wait runs, so the render loop sees them and releases the wait. - defer { - channels.waitForShutdown(timeout: Self.shutdownWaitTimeout) - do { - try state.close() - } catch { - log.error("\(String(describing: error), privacy: .public)") - } - } - - do { - try pumpUntilShutdown(state: state) - } catch { - channels.fail(error) - } - } - - private func pumpUntilShutdown(state: MapState) throws { - let attachRef = try state.attachRef() - let wake = try state.wakeSource() - // A wake source is its own native handle: closing the runtime does not - // release it. - defer { try? wake.close() } - channels.publish(attachRef: attachRef, wake: wake) - - var batch: [CameraCommand] = [] - - while !channels.isShutdownRequested, channels.failureMessage == nil { - channels.drainCommands(into: &batch) - for command in batch { - try state.apply(command) - } - // No display paces this thread, so it parks in the pump until the - // runtime has work or the render loop signals the wake source. - try state.pump(timeout: Self.parkTimeout) - if try state.drainEvents() { - channels.setRenderRequest() - } - } - } -} diff --git a/examples/zig-map/channel.zig b/examples/zig-map/channel.zig deleted file mode 100644 index 9193ab1fe..000000000 --- a/examples/zig-map/channel.zig +++ /dev/null @@ -1,154 +0,0 @@ -//! The cross-thread surface between the render loop, which owns the window and -//! the render session, and the runtime loop, which owns the runtime and the map. - -const std = @import("std"); -const maplibre = @import("maplibre_native_ffi"); - -/// A camera change decoded on the render loop and applied on the map's owner -/// thread. Commands carry deltas rather than absolute targets, because reading -/// the camera and writing the new one has to happen together on that thread. -pub const CameraCommand = union(enum) { - cancel_transitions, - set_gesture_in_progress: struct { in_progress: bool }, - move_by: struct { dx: f64, dy: f64 }, - move_by_animated: struct { dx: f64, dy: f64, duration_ms: f64 }, - scale_by: struct { scale: f64, anchor: maplibre.ScreenPoint }, - scale_by_animated: struct { scale: f64, anchor: maplibre.ScreenPoint, duration_ms: f64 }, - pitch_by: struct { delta: f64 }, - adjust_bearing: struct { delta: f64 }, - adjust_bearing_animated: struct { delta: f64, duration_ms: f64 }, - adjust_pitch_animated: struct { delta: f64, duration_ms: f64 }, - reset_orientation: struct { duration_ms: f64 }, -}; - -/// Pending camera commands, filled by the render loop and drained by the -/// runtime loop. -/// -/// The queue grows rather than dropping: a dropped delta is motion the drag -/// never gets back, and a dropped gesture bracket leaves every delta after it -/// attributed to no gesture. -pub const CommandQueue = struct { - lock: std.Io.Mutex = std.Io.Mutex.init, - allocator: std.mem.Allocator, - items: std.ArrayList(CameraCommand) = .empty, - - pub fn init(allocator: std.mem.Allocator) CommandQueue { - return .{ .allocator = allocator }; - } - - pub fn deinit(self: *CommandQueue) void { - self.items.deinit(self.allocator); - } - - /// Render loop: queues one decoded camera change. Panics on allocation - /// failure. - pub fn push(self: *CommandQueue, command: CameraCommand) void { - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - self.items.append(self.allocator, command) catch - @panic("camera command queue out of memory"); - } - - /// Runtime loop: hands over everything queued so far and takes `out` in - /// exchange. - pub fn drainInto(self: *CommandQueue, out: *std.ArrayList(CameraCommand)) void { - // `out` belongs to the runtime loop alone, so clear it outside the lock. - out.clearRetainingCapacity(); - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - std.mem.swap(std.ArrayList(CameraCommand), &self.items, out); - } -}; - -/// One-bit signal that a frame is worth drawing. -pub const RenderRequest = struct { - value: std.atomic.Value(bool) = .init(true), - - pub fn set(self: *RenderRequest) void { - self.value.store(true, .release); - } - - pub fn consume(self: *RenderRequest) bool { - return self.value.swap(false, .acq_rel); - } -}; - -/// Publishes the map and the runtime's wake source from the runtime loop to the -/// render loop, and carries shutdown and failure the other way. -/// -/// The render loop uses its copy of the map handle only to attach a session, -/// which native serves from any thread; every other map call stays on the -/// runtime loop. -pub const MapChannel = struct { - lock: std.Io.Mutex = std.Io.Mutex.init, - map: ?maplibre.MapHandle = null, - wake: ?maplibre.WakeSourceHandle = null, - published: std.atomic.Value(bool) = .init(false), - shutdown: std.atomic.Value(bool) = .init(false), - failure: ?anyerror = null, - failed: std.atomic.Value(bool) = .init(false), - - /// Runtime loop: announces the map it just created and its wake source. - pub fn publish( - self: *MapChannel, - map: maplibre.MapHandle, - wake: maplibre.WakeSourceHandle, - ) void { - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - self.map = map; - self.wake = wake; - self.published.store(true, .release); - } - - /// Render loop: releases the runtime loop's parked pump. A no-op before the - /// runtime loop has published, when there is nothing parked yet. - pub fn wakeRuntimeLoop(self: *MapChannel) void { - if (!self.published.load(.acquire)) return; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - if (self.wake) |wake| wake.signal() catch {}; - } - - /// Render loop: the map to attach against, once the runtime loop has one. - pub fn mapHandle(self: *MapChannel) ?maplibre.MapHandle { - if (!self.published.load(.acquire)) return null; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - return self.map; - } - - /// Render loop: asks the runtime loop to stop. Called only after the render - /// session is closed, because the map cannot be destroyed before then. - pub fn requestShutdown(self: *MapChannel) void { - self.shutdown.store(true, .release); - // Release the pump so shutdown is observed now. - self.wakeRuntimeLoop(); - } - - pub fn shutdownRequested(self: *MapChannel) bool { - return self.shutdown.load(.acquire); - } - - /// Runtime loop: blocks until the render loop has closed its session. The - /// map cannot be destroyed before then. - pub fn awaitShutdown(self: *MapChannel, io: std.Io) void { - while (!self.shutdownRequested()) { - io.sleep(.fromMilliseconds(1), .awake) catch {}; - } - } - - pub fn fail(self: *MapChannel, err: anyerror) void { - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - if (self.failure == null) self.failure = err; - self.failed.store(true, .release); - } - - pub fn failureValue(self: *MapChannel) ?anyerror { - if (!self.failed.load(.acquire)) return null; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - return self.failure; - } -}; diff --git a/examples/zig-map/input.zig b/examples/zig-map/input.zig index 2e69300c4..93ad7dbe9 100644 --- a/examples/zig-map/input.zig +++ b/examples/zig-map/input.zig @@ -2,7 +2,7 @@ const std = @import("std"); const maplibre = @import("maplibre_native_ffi"); const c = @import("c.zig").c; -const channel = @import("channel.zig"); +const map_state = @import("map_state.zig"); const types = @import("types.zig"); const DragMode = enum { @@ -19,11 +19,9 @@ pub const Result = struct { camera_changed: bool = false, }; -/// Decodes host input into camera commands. Runs on the render loop, which does -/// not own the map, so it only queues commands for the runtime loop to apply. +/// Decodes host input and updates the any-thread map directly. pub const Controller = struct { drag_mode: DragMode = .none, - /// The button that started the live drag; only its release ends the drag. drag_button: u8 = 0, last_x: f64 = 0, last_y: f64 = 0, @@ -31,15 +29,15 @@ pub const Controller = struct { pub fn handleEvent( self: *Controller, event: *const c.SDL_Event, - commands: *channel.CommandQueue, + state: *map_state.MapState, current_viewport: types.Viewport, - ) Result { + ) !Result { return switch (event.type) { - c.SDL_EVENT_MOUSE_BUTTON_DOWN => self.handleMouseButtonDown(event.button, commands, current_viewport), - c.SDL_EVENT_MOUSE_BUTTON_UP => self.handleMouseButtonUp(event.button, commands), - c.SDL_EVENT_MOUSE_MOTION => self.handleMouseMotion(event.motion, commands, current_viewport), - c.SDL_EVENT_MOUSE_WHEEL => handleMouseWheel(event.wheel, commands, current_viewport), - c.SDL_EVENT_KEY_DOWN => handleKeyDown(event.key, commands, current_viewport), + c.SDL_EVENT_MOUSE_BUTTON_DOWN => try self.handleMouseButtonDown(event.button, state, current_viewport), + c.SDL_EVENT_MOUSE_BUTTON_UP => try self.handleMouseButtonUp(event.button, state), + c.SDL_EVENT_MOUSE_MOTION => try self.handleMouseMotion(event.motion, state, current_viewport), + c.SDL_EVENT_MOUSE_WHEEL => try handleMouseWheel(event.wheel, state, current_viewport), + c.SDL_EVENT_KEY_DOWN => try handleKeyDown(event.key, state, current_viewport), else => .{}, }; } @@ -47,23 +45,17 @@ pub const Controller = struct { fn handleMouseButtonDown( self: *Controller, button: c.SDL_MouseButtonEvent, - commands: *channel.CommandQueue, + state: *map_state.MapState, current_viewport: types.Viewport, - ) Result { - // A second button joins the live drag rather than starting one, leaving - // the drag's baseline position alone. + ) !Result { if (self.drag_mode != .none) return .{ .handled = true }; - const mode = dragModeForButton(button.button); if (mode == .none) return .{}; const cursor = logicalPoint(button.x, button.y, current_viewport); self.last_x = cursor.x; self.last_y = cursor.y; - - // Queued first, so any transition stops before the first delta lands. - commands.push(.cancel_transitions); - commands.push(.{ .set_gesture_in_progress = .{ .in_progress = true } }); + try state.setGesture(.begin); self.drag_mode = mode; self.drag_button = button.button; return .{ .handled = true }; @@ -72,34 +64,29 @@ pub const Controller = struct { fn handleMouseButtonUp( self: *Controller, button: c.SDL_MouseButtonEvent, - commands: *channel.CommandQueue, - ) Result { - if (button.button != c.SDL_BUTTON_LEFT and button.button != c.SDL_BUTTON_RIGHT) { - return .{}; - } - // The drag ends once, when the button that started it comes up. + state: *map_state.MapState, + ) !Result { + if (button.button != c.SDL_BUTTON_LEFT and button.button != c.SDL_BUTTON_RIGHT) return .{}; if (button.button != self.drag_button) return .{ .handled = true }; - self.endDrag(commands); + try self.endDrag(state); self.last_x = button.x; self.last_y = button.y; return .{ .handled = true }; } - /// Every path that ends a drag runs through here, so the gesture bracket the - /// drag opened is always closed. - fn endDrag(self: *Controller, commands: *channel.CommandQueue) void { + fn endDrag(self: *Controller, state: *map_state.MapState) !void { if (self.drag_mode == .none) return; self.drag_mode = .none; self.drag_button = 0; - commands.push(.{ .set_gesture_in_progress = .{ .in_progress = false } }); + try state.setGesture(.end); } fn handleMouseMotion( self: *Controller, motion: c.SDL_MouseMotionEvent, - commands: *channel.CommandQueue, + state: *map_state.MapState, current_viewport: types.Viewport, - ) Result { + ) !Result { const cursor = logicalPoint(motion.x, motion.y, current_viewport); const x = cursor.x; const y = cursor.y; @@ -114,14 +101,14 @@ pub const Controller = struct { const dx = x - self.last_x; const dy = y - self.last_y; if (dx == 0 and dy == 0) return .{ .handled = true }; - commands.push(.{ .move_by = .{ .dx = dx, .dy = dy } }); + try state.moveBy(dx, dy); }, .rotate => { const dx = x - self.last_x; const dy = y - self.last_y; if (dx == 0 and dy == 0) return .{ .handled = true }; - commands.push(.{ .adjust_bearing = .{ .delta = dx * 0.5 } }); - commands.push(.{ .pitch_by = .{ .delta = dy / 2.0 } }); + try state.adjustBearing(dx * 0.5); + try state.pitchBy(dy / 2.0); }, } return .{ .handled = true, .camera_changed = true }; @@ -145,23 +132,23 @@ pub fn logControls() void { fn handleMouseWheel( wheel: c.SDL_MouseWheelEvent, - commands: *channel.CommandQueue, + state: *map_state.MapState, current_viewport: types.Viewport, -) Result { +) !Result { const delta: f64 = wheel.y; if (delta == 0) return .{ .handled = true }; const anchor = logicalPoint(wheel.mouse_x, wheel.mouse_y, current_viewport); const scale = std.math.pow(f64, 2.0, delta * 0.25); - commands.push(.{ .scale_by = .{ .scale = scale, .anchor = anchor } }); + try state.scaleBy(scale, anchor); return .{ .handled = true, .camera_changed = true }; } fn handleKeyDown( key: c.SDL_KeyboardEvent, - commands: *channel.CommandQueue, + state: *map_state.MapState, current_viewport: types.Viewport, -) Result { +) !Result { const pan_step = 120.0; const zoom_step = 1.25; const bearing_step = 10.0; @@ -172,42 +159,19 @@ fn handleKeyDown( ); switch (key.scancode) { - scancode(c.SDL_SCANCODE_LEFT), scancode(c.SDL_SCANCODE_A) => { - commands.push(.{ .move_by_animated = .{ .dx = pan_step, .dy = 0, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_RIGHT), scancode(c.SDL_SCANCODE_D) => { - commands.push(.{ .move_by_animated = .{ .dx = -pan_step, .dy = 0, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_UP), scancode(c.SDL_SCANCODE_W) => { - commands.push(.{ .move_by_animated = .{ .dx = 0, .dy = pan_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_DOWN), scancode(c.SDL_SCANCODE_S) => { - commands.push(.{ .move_by_animated = .{ .dx = 0, .dy = -pan_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_EQUALS), scancode(c.SDL_SCANCODE_KP_PLUS) => { - commands.push(.{ .scale_by_animated = .{ .scale = zoom_step, .anchor = center, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_MINUS), scancode(c.SDL_SCANCODE_KP_MINUS) => { - commands.push(.{ .scale_by_animated = .{ .scale = 1.0 / zoom_step, .anchor = center, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_Q) => { - commands.push(.{ .adjust_bearing_animated = .{ .delta = -bearing_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_E) => { - commands.push(.{ .adjust_bearing_animated = .{ .delta = bearing_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_RIGHTBRACKET) => { - commands.push(.{ .adjust_pitch_animated = .{ .delta = pitch_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_LEFTBRACKET) => { - commands.push(.{ .adjust_pitch_animated = .{ .delta = -pitch_step, .duration_ms = keyboard_animation_ms } }); - }, - scancode(c.SDL_SCANCODE_0) => { - commands.push(.{ .reset_orientation = .{ .duration_ms = reset_animation_ms } }); - }, + scancode(c.SDL_SCANCODE_LEFT), scancode(c.SDL_SCANCODE_A) => try state.moveByAnimated(pan_step, 0, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_RIGHT), scancode(c.SDL_SCANCODE_D) => try state.moveByAnimated(-pan_step, 0, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_UP), scancode(c.SDL_SCANCODE_W) => try state.moveByAnimated(0, pan_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_DOWN), scancode(c.SDL_SCANCODE_S) => try state.moveByAnimated(0, -pan_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_EQUALS), scancode(c.SDL_SCANCODE_KP_PLUS) => try state.scaleByAnimated(zoom_step, center, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_MINUS), scancode(c.SDL_SCANCODE_KP_MINUS) => try state.scaleByAnimated(1.0 / zoom_step, center, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_Q) => try state.adjustBearingAnimated(-bearing_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_E) => try state.adjustBearingAnimated(bearing_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_RIGHTBRACKET) => try state.adjustPitchAnimated(pitch_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_LEFTBRACKET) => try state.adjustPitchAnimated(-pitch_step, keyboard_animation_ms), + scancode(c.SDL_SCANCODE_0) => try state.resetOrientation(reset_animation_ms), else => return .{}, } - return .{ .handled = true, .camera_changed = true }; } diff --git a/examples/zig-map/main.zig b/examples/zig-map/main.zig index 806395e78..a96f6d56a 100644 --- a/examples/zig-map/main.zig +++ b/examples/zig-map/main.zig @@ -4,7 +4,6 @@ const build_options = @import("build_options"); const objc = if (build_options.supports_metal) @import("objc") else struct {}; const c = @import("c.zig").c; -const channel = @import("channel.zig"); const diagnostics = @import("diagnostics.zig"); const maplibre = @import("maplibre_native_ffi"); const input = @import("input.zig"); @@ -14,54 +13,23 @@ const types = @import("types.zig"); const viewport = @import("viewport.zig"); const RenderTarget = render.RenderTarget; +const uses_egl = build_options.supports_opengl and + (builtin.os.tag == .linux or builtin.os.tag == .macos); -/// Backstop for a parked pump that nothing signals; the wake source is what -/// normally releases it. -const park_timeout_milliseconds = 100; -const uses_egl = build_options.supports_opengl and (builtin.os.tag == .linux or builtin.os.tag == .macos); +const EventReceiver = struct { + scheduled: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + wake_failed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + event_type: u32, -const RuntimeLoopArgs = struct { - allocator: std.mem.Allocator, - io: std.Io, - initial_viewport: types.Viewport, - commands: *channel.CommandQueue, - render_request: *channel.RenderRequest, - map_channel: *channel.MapChannel, -}; - -/// Owns the runtime and the map for their whole lifetime, on a thread that is -/// not the one presenting. -fn runtimeLoop(args: RuntimeLoopArgs) void { - var state = map_state.MapState.init(args.allocator, args.initial_viewport) catch |err| { - args.map_channel.fail(err); - return; - }; - // A map with an attached session cannot be destroyed, so wait for the render - // loop to close its session first; defers run in reverse. - defer state.deinit(); - defer args.map_channel.awaitShutdown(args.io); - - runtimeLoopBody(args, &state) catch |err| args.map_channel.fail(err); -} + fn schedule(user_data: ?*anyopaque) callconv(.c) void { + const self: *EventReceiver = @ptrCast(@alignCast(user_data.?)); + if (self.scheduled.swap(true, .acq_rel)) return; -fn runtimeLoopBody(args: RuntimeLoopArgs, state: *map_state.MapState) !void { - // The render loop signals this to release the parked pump. - const wake = try state.runtime.wakeSource(); - defer wake.release(); - - var batch: std.ArrayList(channel.CameraCommand) = .empty; - defer batch.deinit(args.allocator); - - args.map_channel.publish(state.map, wake); - - while (!args.map_channel.shutdownRequested() and args.map_channel.failureValue() == null) { - try state.applyCommands(args.commands, &batch); - try state.runtime.pump(park_timeout_milliseconds, null); - if (try map_state.drainEvents(state.allocator, &state.runtime, &state.map)) { - args.render_request.set(); - } + var event = std.mem.zeroes(c.SDL_Event); + event.type = self.event_type; + if (!c.SDL_PushEvent(&event)) self.wake_failed.store(true, .release); } -} +}; pub fn main(init_args: std.process.Init) !void { const target_mode = (try parseRenderTargetMode(init_args)) orelse return; @@ -110,81 +78,74 @@ pub fn main(init_args: std.process.Init) !void { var current_viewport = viewport.get(window_handle); viewport.log("initial viewport", current_viewport); + const event_wake_type = c.SDL_RegisterEvents(1); + if (event_wake_type == 0) return types.AppError.EventDrainFailed; + var event_receiver = EventReceiver{ + .event_type = event_wake_type, + }; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - // The graphics context, the render session, and every presentation resource - // belong to this thread, which owns the window. - var target = try RenderTarget.init(allocator, window_handle, current_viewport, target_mode); - - var commands = channel.CommandQueue.init(allocator); - defer commands.deinit(); - var render_request = channel.RenderRequest{}; - var map_channel = channel.MapChannel{}; + var state = try map_state.MapState.init(allocator, current_viewport); + defer state.deinit(); + try state.runtime.setEventWakeCallback( + EventReceiver.schedule, + &event_receiver, + ); + defer state.runtime.clearEventWakeCallback() catch {}; - const runtime_thread = try std.Thread.spawn(.{}, runtimeLoop, .{RuntimeLoopArgs{ - .allocator = allocator, - .io = init_args.io, - .initial_viewport = current_viewport, - .commands = &commands, - .render_request = &render_request, - .map_channel = &map_channel, - }}); + // The graphics context, render session, and presentation resources remain + // on the window-owning thread. + var target = try RenderTarget.init(allocator, window_handle, current_viewport, target_mode); + defer target.deinit(); + try target.attach(&state.map, current_viewport); - const result = renderLoop( + try renderLoop( init_args.io, window_handle, target_mode, &target, ¤t_viewport, - &commands, - &render_request, - &map_channel, + &state, + &event_receiver, ); - - // Destroy the session before the runtime loop destroys the map: a map with - // an attached session cannot be destroyed. - target.deinit(); - map_channel.requestShutdown(); - runtime_thread.join(); - - try result; - if (map_channel.failureValue()) |err| return err; } -/// The display-paced render loop. Owns the window, input, and the render -/// session once it adopts it. +/// The display-paced loop. Runtime/map calls are any-thread; the render session +/// remains attached to this graphics thread. fn renderLoop( io: std.Io, window_handle: *c.SDL_Window, target_mode: types.RenderTargetMode, target: *RenderTarget, current_viewport: *types.Viewport, - commands: *channel.CommandQueue, - render_request: *channel.RenderRequest, - map_channel: *channel.MapChannel, + state: *map_state.MapState, + event_receiver: *EventReceiver, ) !void { - var map = while (true) { - if (map_channel.failureValue()) |err| return err; - if (map_channel.mapHandle()) |handle| break handle; - try io.sleep(.fromMilliseconds(1), .awake); - }; - try target.attach(&map, current_viewport.*); - printStartupStatus(target_mode); input.logControls(); var running = true; + var render_requested = true; var input_controller = input.Controller{}; while (running) { const pool = if (build_options.supports_metal) objc.AutoreleasePool.init() else {}; defer if (build_options.supports_metal) pool.deinit(); - if (map_channel.failureValue()) |err| return err; + if (event_receiver.wake_failed.swap(false, .acq_rel)) { + event_receiver.scheduled.store(false, .release); + if (try map_state.drainEvents(state)) render_requested = true; + } var event: c.SDL_Event = undefined; while (c.SDL_PollEvent(&event)) { + if (event.type == event_receiver.event_type) { + event_receiver.scheduled.store(false, .release); + if (try map_state.drainEvents(state)) render_requested = true; + continue; + } switch (event.type) { c.SDL_EVENT_QUIT => running = false, c.SDL_EVENT_WINDOW_CLOSE_REQUESTED => running = false, @@ -195,32 +156,31 @@ fn renderLoop( current_viewport.* = viewport.get(window_handle); viewport.log("resized viewport", current_viewport.*); try target.resize(current_viewport.*); - // The resize is queued to the map's owner thread; release - // its pump. - map_channel.wakeRuntimeLoop(); - render_request.set(); + var resize_completion = try state.map.resize( + current_viewport.logical_width, + current_viewport.logical_height, + current_viewport.scale_factor, + ); + resize_completion.deinit(); + render_requested = true; }, else => { - const input_result = input_controller.handleEvent( + const input_result = try input_controller.handleEvent( &event, - commands, + state, current_viewport.*, ); - if (input_result.handled) { - map_channel.wakeRuntimeLoop(); - } - if (input_result.camera_changed) render_request.set(); + if (input_result.camera_changed) render_requested = true; }, } } try target.finishFrame(); - // Consume before rendering, so a request published during the render - // call is not discarded. - if (render_request.consume()) { + if (render_requested) { + render_requested = false; if (!try target.renderUpdate(null, current_viewport.*)) { - render_request.set(); + render_requested = true; } } diff --git a/examples/zig-map/map_state.zig b/examples/zig-map/map_state.zig index 060e76dfb..cd6e7f38a 100644 --- a/examples/zig-map/map_state.zig +++ b/examples/zig-map/map_state.zig @@ -1,11 +1,9 @@ const std = @import("std"); const maplibre = @import("maplibre_native_ffi"); -const channel = @import("channel.zig"); const diagnostics = @import("diagnostics.zig"); const types = @import("types.zig"); -/// Runtime and map, owned for their whole lifetime by the runtime loop thread. pub const MapState = struct { allocator: std.mem.Allocator, diagnostic_store: *maplibre.DiagnosticStore, @@ -24,23 +22,31 @@ pub const MapState = struct { diagnostics.logError("runtime create failed", err, diagnostic_store); return types.AppError.RuntimeCreateFailed; }; - errdefer runtime.close() catch {}; - - var map = maplibre.MapHandle.create(&runtime, .{ - .width = viewport.logical_width, - .height = viewport.logical_height, - .scale_factor = viewport.scale_factor, - .mode = .continuous, - }) catch |err| { + errdefer if (runtime.close()) |future| { + var teardown = future; + _ = teardown.wait(null) catch {}; + teardown.deinit(); + } else |_| {}; + + var map_future = maplibre.MapHandle.create(&runtime, .{ .mode = .continuous }) catch |err| { + diagnostics.logError("map create failed", err, diagnostic_store); + return types.AppError.MapCreateFailed; + }; + defer map_future.deinit(); + var map = map_future.wait(diagnostic_store) catch |err| { diagnostics.logError("map create failed", err, diagnostic_store); return types.AppError.MapCreateFailed; }; errdefer map.close() catch {}; try selectEvents(&map, diagnostic_store); + var resize_completion = map.resize(viewport.logical_width, viewport.logical_height, viewport.scale_factor) catch |err| { + diagnostics.logError("map resize failed", err, diagnostic_store); + return types.AppError.MapCreateFailed; + }; + resize_completion.deinit(); try loadStyle(allocator, &map, diagnostic_store); try setCamera(&map, diagnostic_store); - return .{ .allocator = allocator, .diagnostic_store = diagnostic_store, @@ -51,120 +57,99 @@ pub const MapState = struct { pub fn deinit(self: *MapState) void { self.map.close() catch {}; - self.runtime.close() catch {}; + // Awaiting the release completion keeps process exit ordered after + // native teardown. + if (self.runtime.close()) |future| { + var teardown = future; + _ = teardown.wait(null) catch {}; + teardown.deinit(); + } else |_| {} self.diagnostic_store.deinit(); self.allocator.destroy(self.diagnostic_store); } - /// Applies every queued camera command on the map's owner thread. `batch` - /// is owned by the runtime loop and reused across drains. - pub fn applyCommands( - self: *MapState, - commands: *channel.CommandQueue, - batch: *std.ArrayList(channel.CameraCommand), - ) !void { - commands.drainInto(batch); - for (batch.items) |command| { - try applyCameraCommand(&self.map, command, self.diagnostic_store); - } + pub fn setGesture(self: *MapState, phase: maplibre.GesturePhase) !void { + try self.updateCamera(.{ .gesture_phase = phase }); } -}; -/// Applies one decoded camera command. Runs on the map's owner thread, so the -/// read-modify-write commands read the current camera here. -pub fn applyCameraCommand( - map: *maplibre.MapHandle, - command: channel.CameraCommand, - diagnostic_store: *const maplibre.DiagnosticStore, -) !void { - switch (command) { - .cancel_transitions => try expectCameraStatus( - map.cancelTransitions(), - "cancel camera transitions failed", - diagnostic_store, - ), - .set_gesture_in_progress => |gesture| try expectCameraStatus( - map.setGestureInProgress(gesture.in_progress), - "set gesture in progress failed", - diagnostic_store, - ), - .move_by => |move| try expectCameraStatus( - map.moveBy(move.dx, move.dy), - "camera pan failed", - diagnostic_store, - ), - .move_by_animated => |move| try expectCameraStatus( - map.moveByAnimated(move.dx, move.dy, .{ .duration_ms = move.duration_ms }), - "keyboard pan failed", - diagnostic_store, - ), - .scale_by => |zoom| try expectCameraStatus( - map.scaleBy(zoom.scale, zoom.anchor), - "camera zoom failed", - diagnostic_store, - ), - .scale_by_animated => |zoom| try expectCameraStatus( - map.scaleByAnimated(zoom.scale, zoom.anchor, .{ .duration_ms = zoom.duration_ms }), - "keyboard zoom failed", - diagnostic_store, - ), - .pitch_by => |pitch| try expectCameraStatus( - map.pitchBy(pitch.delta), - "camera pitch failed", - diagnostic_store, - ), - .adjust_bearing => |bearing| { - const camera = try currentCamera(map, diagnostic_store); - try expectCameraStatus( - map.jumpTo(.{ .bearing = (camera.bearing orelse 0) + bearing.delta }), - "camera rotate failed", - diagnostic_store, - ); - }, - .adjust_bearing_animated => |bearing| { - const camera = try currentCamera(map, diagnostic_store); - try expectCameraStatus( - map.easeTo( - .{ .bearing = (camera.bearing orelse 0) + bearing.delta }, - .{ .duration_ms = bearing.duration_ms }, - ), - "keyboard rotate failed", - diagnostic_store, - ); - }, - .adjust_pitch_animated => |pitch| { - const camera = try currentCamera(map, diagnostic_store); - try expectCameraStatus( - map.easeTo( - .{ .pitch = clamp((camera.pitch orelse 0) + pitch.delta, 0.0, 60.0) }, - .{ .duration_ms = pitch.duration_ms }, - ), - "keyboard pitch failed", - diagnostic_store, - ); - }, - .reset_orientation => |reset| try expectCameraStatus( - map.easeTo(.{ .bearing = 0, .pitch = 0 }, .{ .duration_ms = reset.duration_ms }), - "camera reset failed", - diagnostic_store, - ), + pub fn moveBy(self: *MapState, dx: f64, dy: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ .offset = .{ .x = dx, .y = dy } })); } -} -/// Drains one batch of runtime events, reporting whether the map wants another -/// frame. -pub fn drainEvents( - allocator: std.mem.Allocator, - runtime: *maplibre.RuntimeHandle, - map: *maplibre.MapHandle, -) !bool { - const map_id = try map.id(); + pub fn moveByAnimated(self: *MapState, dx: f64, dy: f64, duration_ms: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ + .offset = .{ .x = dx, .y = dy }, + .animation = .{ .duration_ms = duration_ms }, + })); + } + + pub fn scaleBy(self: *MapState, scale: f64, anchor: maplibre.ScreenPoint) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ .kind = .scale, .amount = scale, .anchor = anchor })); + } + + pub fn scaleByAnimated(self: *MapState, scale: f64, anchor: maplibre.ScreenPoint, duration_ms: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ + .kind = .scale, + .amount = scale, + .anchor = anchor, + .animation = .{ .duration_ms = duration_ms }, + })); + } + + pub fn pitchBy(self: *MapState, delta: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ .kind = .pitch, .amount = delta })); + } + + pub fn adjustBearing(self: *MapState, delta: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ .kind = .bearing, .amount = delta })); + } + + pub fn adjustBearingAnimated(self: *MapState, delta: f64, duration_ms: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ + .kind = .bearing, + .amount = delta, + .animation = .{ .duration_ms = duration_ms }, + })); + } + + pub fn adjustPitchAnimated(self: *MapState, delta: f64, duration_ms: f64) !void { + try self.cameraMutation(self.map.applyCameraDelta(.{ + .kind = .pitch, + .amount = delta, + .animation = .{ .duration_ms = duration_ms }, + })); + } + + pub fn resetOrientation(self: *MapState, duration_ms: f64) !void { + try self.updateCamera(.{ + .mode = .ease, + .camera = .{ .bearing = 0, .pitch = 0 }, + .animation = .{ .duration_ms = duration_ms }, + }); + } + + fn updateCamera(self: *MapState, update: maplibre.CameraUpdate) !void { + try self.cameraMutation(self.map.updateCamera(update)); + } + + fn cameraMutation(self: *MapState, result: anytype) !void { + var completion = result catch |err| { + diagnostics.logError("camera update failed", err, self.diagnostic_store); + return types.AppError.CameraUpdateFailed; + }; + completion.deinit(); + } +}; +/// Drains runtime events, reporting whether the map requested another frame. +pub fn drainEvents(self: *MapState) !bool { + const map_id = try self.map.id(); var render_update_available = false; - var batch = try runtime.drainEvents(allocator, 0); + var batch = try self.runtime.drainEvents(self.allocator); defer batch.deinit(); for (0..batch.len()) |index| { const event = try batch.at(index); - if (event.source_type != .map or event.source_id == null or !std.meta.eql(event.source_id.?, map_id)) continue; + if (event.source_type != .map or event.source_id == null or + !std.meta.eql(event.source_id.?, map_id)) continue; switch (event.event_type) { .map_render_update_available => render_update_available = true, .map_render_frame_finished => switch (event.payload) { @@ -177,47 +162,20 @@ pub fn drainEvents( return render_update_available; } -fn currentCamera( - map: *maplibre.MapHandle, - diagnostic_store: *const maplibre.DiagnosticStore, -) !maplibre.CameraOptions { - return map.getCamera() catch |err| { - diagnostics.logError("camera snapshot failed", err, diagnostic_store); - return types.AppError.CameraCommandFailed; - }; -} - -fn expectCameraStatus( - result: maplibre.Error!void, - message: []const u8, - diagnostic_store: *const maplibre.DiagnosticStore, -) !void { - result catch |err| { - diagnostics.logError(message, err, diagnostic_store); - return types.AppError.CameraCommandFailed; - }; -} - -fn clamp(value: f64, min: f64, max: f64) f64 { - if (value < min) return min; - if (value > max) return max; - return value; -} - -/// Selects the two event types the runtime loop reads. The map queues no other -/// type once this returns, and it runs before the style load, because a map -/// keeps the events it has already queued. +/// Selects the two event types that the host drains. This runs before the style +/// load because the map retains events that were already queued. fn selectEvents( map: *maplibre.MapHandle, diagnostic_store: *const maplibre.DiagnosticStore, ) !void { - map.setEventMask(.{ + var completion = map.setEventMask(.{ .map_render_update_available = true, .map_render_frame_finished = true, }) catch |err| { diagnostics.logError("event mask select failed", err, diagnostic_store); return types.AppError.EventMaskFailed; }; + completion.deinit(); } fn loadStyle( @@ -225,23 +183,25 @@ fn loadStyle( map: *maplibre.MapHandle, diagnostic_store: *const maplibre.DiagnosticStore, ) !void { - map.setStyleUrl(allocator, "https://tiles.openfreemap.org/styles/bright") catch |err| { + var completion = map.setStyleUrl(allocator, "https://tiles.openfreemap.org/styles/bright") catch |err| { diagnostics.logError("style load failed", err, diagnostic_store); return types.AppError.StyleLoadFailed; }; + completion.deinit(); } fn setCamera( map: *maplibre.MapHandle, diagnostic_store: *const maplibre.DiagnosticStore, ) !void { - map.jumpTo(.{ + var completion = map.updateCamera(.{ .camera = .{ .center = .{ .latitude = 37.7749, .longitude = -122.4194 }, .zoom = 13.0, .bearing = 12.0, .pitch = 30.0, - }) catch |err| { + } }) catch |err| { diagnostics.logError("camera jump failed", err, diagnostic_store); return types.AppError.CameraJumpFailed; }; + completion.deinit(); } diff --git a/examples/zig-map/render/metal/mod.zig b/examples/zig-map/render/metal/mod.zig index 56583510e..bc3ce437b 100644 --- a/examples/zig-map/render/metal/mod.zig +++ b/examples/zig-map/render/metal/mod.zig @@ -46,7 +46,7 @@ pub const MetalRenderTarget = union(enum) { }; } - /// Attaches the render session on the map owner thread. + /// Attaches the render session on the graphics thread. pub fn attach(self: *MetalRenderTarget, map: *maplibre.MapHandle, viewport: types.Viewport) !void { switch (self.*) { .owned_texture => |*backend| try backend.attach(map, viewport), @@ -150,7 +150,7 @@ const MetalTextureCompositor = struct { self.view.resize(viewport); } - fn drawMetalTexture(self: *MetalTextureCompositor, metal_texture: *anyopaque) !bool { + fn drawMetalTexture(self: *MetalTextureCompositor, metal_texture: *anyopaque, producer_sync: maplibre.GpuSync) !bool { const drawable = self.view.layer.msgSend(objc.Object, "nextDrawable", .{}); // A minimized or occluded window has no drawable, so skip the frame // rather than fail. @@ -172,6 +172,14 @@ const MetalTextureCompositor = struct { const command_buffer = self.queue.msgSend(objc.Object, "commandBuffer", .{}); if (command_buffer.value == null) return types.AppError.BackendDrawFailed; + switch (producer_sync) { + .cpu_complete => {}, + .metal_shared_event => |producer| command_buffer.msgSend(void, "encodeWaitForEvent:value:", .{ + objc.Object.fromId(producer.object.toPtr()), + producer.value, + }), + else => return types.AppError.BackendDrawFailed, + } const encoder = command_buffer.msgSend( objc.Object, "renderCommandEncoderWithDescriptor:", @@ -237,11 +245,11 @@ const MetalOwnedTextureBackend = struct { const texture = maplibre.attachMetalOwnedTexture(map, .{ .extent = render_target.extent(viewport), .context = .{ .device = maplibre.NativePointer.fromPtr(self.compositor.view.device.value.?) }, - }) catch |err| { + }, .{ .driver = .caller_graphics_thread, .requested_texture_ring_depth = 2 }) catch |err| { diagnostics.logError("Metal texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -255,17 +263,21 @@ const MetalOwnedTextureBackend = struct { .texture => |*texture| texture, else => return false, }; - var frame = texture.acquireMetalOwnedTextureFrame() catch |err| switch (err) { + var frame = texture.acquireFrame() catch |err| switch (err) { error.InvalidState => return false, else => { diagnostics.logError("Metal texture acquire failed", err, null); return types.AppError.BackendDrawFailed; }, }; - defer frame.release() catch |err| diagnostics.logError("Metal texture release failed", err, null); - - const info = try frame.info(); - return try self.compositor.drawMetalTexture(info.texture.toPtr()); + var frame_owned = true; + errdefer if (frame_owned) frame.release(.cpu_complete) catch {}; + const producer_sync = try frame.producerSync(); + const info = try frame.metalTexture(); + const drawn = try self.compositor.drawMetalTexture(info.texture.toPtr(), producer_sync); + try frame.release(.cpu_complete); + frame_owned = false; + return drawn; } }; @@ -309,18 +321,20 @@ const MetalBorrowedTextureBackend = struct { const replacement = try createBorrowedTexture(self.compositor.view.device, viewport); errdefer replacement.release(); - session.setMetalBorrowedTextureTarget(.{ + var completion = session.setMetalBorrowedTextureTarget(.{ .extent = render_target.extent(viewport), .physical_width = viewport.physical_width, .physical_height = viewport.physical_height, .texture = maplibre.NativePointer.fromPtr(replacement.value.?), }) catch |err| { - // The session may have taken the replacement before failing, so - // detach before either texture is released. - session.detach() catch {}; diagnostics.logError("Metal borrowed texture set target failed", err, null); return types.AppError.TextureResizeFailed; }; + defer completion.deinit(); + render_target.serviceUntilComplete(session, &completion) catch |err| { + diagnostics.logError("Metal borrowed texture replacement failed", err, null); + return types.AppError.TextureResizeFailed; + }; // Released only once the session has taken the replacement. self.borrowed_texture.release(); self.borrowed_texture = replacement; @@ -336,11 +350,11 @@ const MetalBorrowedTextureBackend = struct { .physical_width = viewport.physical_width, .physical_height = viewport.physical_height, .texture = maplibre.NativePointer.fromPtr(self.borrowed_texture.value.?), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("Metal borrowed texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -350,7 +364,7 @@ const MetalBorrowedTextureBackend = struct { ) !bool { _ = viewport; if (!try self.session.renderUpdate(diagnostic_store)) return false; - return try self.compositor.drawMetalTexture(self.borrowed_texture.value.?); + return try self.compositor.drawMetalTexture(self.borrowed_texture.value.?, .cpu_complete); } }; @@ -402,11 +416,11 @@ const MetalSurfaceBackend = struct { .extent = render_target.extent(viewport), .context = .{ .device = maplibre.NativePointer.fromPtr(self.view.device.value.?) }, .layer = maplibre.NativePointer.fromPtr(self.view.layer.value.?), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("Metal surface attach failed", err, null); return types.AppError.SurfaceAttachFailed; }; - return .{ .surface = surface }; + return render_target.surfaceSession(surface); } }; diff --git a/examples/zig-map/render/opengl/mod.zig b/examples/zig-map/render/opengl/mod.zig index ad17151fb..e634a2210 100644 --- a/examples/zig-map/render/opengl/mod.zig +++ b/examples/zig-map/render/opengl/mod.zig @@ -237,7 +237,7 @@ const PlatformOpenGLRenderTarget = union(enum) { }; } - /// Attaches the render session on the map owner thread. + /// Attaches the render session on the graphics thread. pub fn attach(self: *PlatformOpenGLRenderTarget, map: *maplibre.MapHandle, viewport: types.Viewport) !void { switch (self.*) { .owned_texture => |*backend| try backend.attach(map, viewport), @@ -537,11 +537,11 @@ const OpenGLOwnedTextureBackend = struct { const texture = maplibre.attachOpenGLOwnedTexture(map, .{ .extent = render_target.extent(viewport), .context = self.compositor.context.descriptor(), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread, .requested_texture_ring_depth = 2 }) catch |err| { diagnostics.logError("OpenGL texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -555,17 +555,21 @@ const OpenGLOwnedTextureBackend = struct { .texture => |*texture| texture, else => return false, }; - var frame = texture.acquireOpenGLOwnedTextureFrame() catch |err| switch (err) { + var frame = texture.acquireFrame() catch |err| switch (err) { error.InvalidState => return false, else => { diagnostics.logError("OpenGL texture acquire failed", err, null); return types.AppError.BackendDrawFailed; }, }; - defer frame.release() catch |err| diagnostics.logError("OpenGL texture release failed", err, null); - - const info = try frame.info(); - return try self.compositor.drawTexture(info.texture); + var frame_owned = true; + errdefer if (frame_owned) frame.release(.cpu_complete) catch {}; + const info = try frame.openGLTexture(); + const drawn = try self.compositor.drawTexture(info.texture); + try self.compositor.finishFrame(); + try frame.release(.cpu_complete); + frame_owned = false; + return drawn; } }; @@ -647,7 +651,7 @@ const OpenGLBorrowedTextureBackend = struct { viewport, ); errdefer replacement.deinit(&self.compositor.context, self.compositor.procs); - session.setOpenGLBorrowedTextureTarget(.{ + var completion = session.setOpenGLBorrowedTextureTarget(.{ .extent = render_target.extent(viewport), .physical_width = viewport.physical_width, .physical_height = viewport.physical_height, @@ -655,12 +659,14 @@ const OpenGLBorrowedTextureBackend = struct { .texture = replacement.texture, .target = gl_texture_target, }) catch |err| { - // The session may have taken the replacement before failing, so - // detach before either texture is released. - session.detach() catch {}; diagnostics.logError("OpenGL borrowed texture set target failed", err, null); return types.AppError.TextureResizeFailed; }; + defer completion.deinit(); + render_target.serviceUntilComplete(session, &completion) catch |err| { + diagnostics.logError("OpenGL borrowed texture replacement failed", err, null); + return types.AppError.TextureResizeFailed; + }; // Released only once the session has taken the replacement. self.borrowed_texture.deinit(&self.compositor.context, self.compositor.procs); self.borrowed_texture = replacement; @@ -682,11 +688,11 @@ const OpenGLBorrowedTextureBackend = struct { .context = self.compositor.context.descriptor(), .texture = self.borrowed_texture.texture, .target = gl_texture_target, - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("OpenGL borrowed texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -748,24 +754,28 @@ const OpenGLSurfaceBackend = struct { return; } const handle = try self.session.surfaceHandle(); - handle.setOpenGLSurfaceTarget(.{ + var completion = handle.setOpenGLSurfaceTarget(.{ .extent = render_target.extent(viewport), .context = self.context.descriptor(), .surface = self.context.surface(), }) catch |err| { - // SDL already dropped the outgoing surface and the session may have - // taken the replacement before failing, so detach. - handle.detach() catch {}; diagnostics.logError("OpenGL surface set target failed", err, null); return types.AppError.SurfaceAttachFailed; }; + defer completion.deinit(); + render_target.serviceUntilComplete(handle, &completion) catch |err| { + diagnostics.logError("OpenGL surface replacement failed", err, null); + return types.AppError.SurfaceAttachFailed; + }; } /// Detaches the session, discarding errors, on a path already returning a /// failure. fn detachSuppressed(self: *OpenGLSurfaceBackend) void { const handle = self.session.surfaceHandle() catch return; - handle.detach() catch {}; + var completion = handle.detach() catch return; + defer completion.deinit(); + render_target.serviceUntilComplete(handle, &completion) catch {}; } fn finishFrame(self: *OpenGLSurfaceBackend) !void { @@ -782,11 +792,11 @@ const OpenGLSurfaceBackend = struct { .extent = render_target.extent(viewport), .context = self.context.descriptor(), .surface = self.context.surface(), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("OpenGL surface attach failed", err, null); return types.AppError.SurfaceAttachFailed; }; - return .{ .surface = surface }; + return render_target.surfaceSession(surface); } fn renderUpdate( diff --git a/examples/zig-map/render/vulkan/mod.zig b/examples/zig-map/render/vulkan/mod.zig index 43b582171..c6f42f4cb 100644 --- a/examples/zig-map/render/vulkan/mod.zig +++ b/examples/zig-map/render/vulkan/mod.zig @@ -31,7 +31,7 @@ pub const VulkanRenderTarget = union(enum) { }; } - /// Attaches the render session on the map owner thread. + /// Attaches the render session on the graphics thread. pub fn attach(self: *VulkanRenderTarget, map: *maplibre.MapHandle, viewport: types.Viewport) !void { switch (self.*) { .owned_texture => |*backend| try backend.attach(map, viewport), @@ -253,7 +253,7 @@ const VulkanTextureCompositor = struct { const VulkanOwnedTextureBackend = struct { compositor: VulkanTextureCompositor, session: render_target.Session, - pending_frame: ?maplibre.VulkanOwnedTextureFrameHandle, + pending_frame: ?maplibre.AcquiredFrame, fn init( allocator: std.mem.Allocator, @@ -303,11 +303,11 @@ const VulkanOwnedTextureBackend = struct { const texture = maplibre.attachVulkanOwnedTexture(map, .{ .extent = render_target.extent(viewport), .context = vulkanContextDescriptor(&self.compositor.context), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread, .requested_texture_ring_depth = 2 }) catch |err| { diagnostics.logError("Vulkan texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -321,28 +321,32 @@ const VulkanOwnedTextureBackend = struct { .texture => |*texture| texture, else => return false, }; - var frame = texture.acquireVulkanOwnedTextureFrame() catch |err| switch (err) { + var frame = texture.acquireFrame() catch |err| switch (err) { error.InvalidState => return false, else => { diagnostics.logError("Vulkan texture acquire failed", err, null); return types.AppError.BackendDrawFailed; }, }; - errdefer frame.release() catch |err| diagnostics.logError("Vulkan texture release failed", err, null); - - const info = try frame.info(); + var frame_owned = true; + errdefer if (frame_owned) frame.release(.cpu_complete) catch {}; + const info = try frame.vulkanTexture(); const image_view: c.VkImageView = @ptrCast(info.image_view.toPtr()); if (!try self.compositor.presentImageView(image_view)) { - frame.release() catch |err| diagnostics.logError("Vulkan texture release failed", err, null); + try frame.release(.cpu_complete); + frame_owned = false; return false; } + frame_owned = false; self.pending_frame = frame; return true; } fn releasePendingFrame(self: *VulkanOwnedTextureBackend) void { - if (self.pending_frame) |*frame| frame.release() catch |err| diagnostics.logError("Vulkan texture release failed", err, null); + if (self.pending_frame) |*frame| { + frame.release(.cpu_complete) catch |err| diagnostics.logError("Vulkan texture release failed", err, null); + } self.pending_frame = null; } }; @@ -471,7 +475,7 @@ const VulkanBorrowedTextureBackend = struct { var replacement = try BorrowedImage.init(&self.compositor.context, viewport); errdefer replacement.deinit(self.compositor.context.device); - session.setVulkanBorrowedTextureTarget(.{ + var completion = session.setVulkanBorrowedTextureTarget(.{ .extent = render_target.extent(viewport), .physical_width = viewport.physical_width, .physical_height = viewport.physical_height, @@ -482,12 +486,14 @@ const VulkanBorrowedTextureBackend = struct { .initial_layout = c.VK_IMAGE_LAYOUT_UNDEFINED, .final_layout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, }) catch |err| { - // The session may have taken the replacement before failing, so - // detach before either image is released. - session.detach() catch {}; diagnostics.logError("Vulkan borrowed texture set target failed", err, null); return types.AppError.TextureResizeFailed; }; + defer completion.deinit(); + render_target.serviceUntilComplete(session, &completion) catch |err| { + diagnostics.logError("Vulkan borrowed texture replacement failed", err, null); + return types.AppError.TextureResizeFailed; + }; // Released only once the session has taken the replacement. self.borrowed_image.deinit(self.compositor.context.device); self.borrowed_image = replacement; @@ -512,11 +518,11 @@ const VulkanBorrowedTextureBackend = struct { .format = c.VK_FORMAT_R8G8B8A8_UNORM, .initial_layout = c.VK_IMAGE_LAYOUT_UNDEFINED, .final_layout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("Vulkan borrowed texture attach failed", err, null); return types.AppError.TextureAttachFailed; }; - return .{ .texture = texture }; + return render_target.textureSession(texture); } fn renderUpdate( @@ -581,11 +587,11 @@ const VulkanSurfaceBackend = struct { .extent = render_target.extent(viewport), .context = vulkanContextDescriptor(&self.context), .surface = maplibre.NativePointer.fromPtr(@ptrCast(self.context.surface.?)), - }) catch |err| { + }, .{ .driver = .caller_graphics_thread }) catch |err| { diagnostics.logError("Vulkan surface attach failed", err, null); return types.AppError.SurfaceAttachFailed; }; - return .{ .surface = surface }; + return render_target.surfaceSession(surface); } }; diff --git a/examples/zig-map/render_target.zig b/examples/zig-map/render_target.zig index 0d4c5234c..ee9c4a614 100644 --- a/examples/zig-map/render_target.zig +++ b/examples/zig-map/render_target.zig @@ -10,32 +10,26 @@ pub const Session = union(enum) { pub fn deinit(self: *Session) void { switch (self.*) { .none => {}, - .texture => |*texture| texture.close() catch {}, - .surface => |*surface| surface.close() catch {}, + .texture => |*session| closeSession(session), + .surface => |*session| closeSession(session), } self.* = .none; } - pub fn resize( - self: *Session, - viewport: types.Viewport, - diagnostic_store: ?*const maplibre.DiagnosticStore, - ) !void { - switch (self.*) { + pub fn resize(self: *Session, viewport: types.Viewport, diagnostic_store: ?*const maplibre.DiagnosticStore) !void { + const session = switch (self.*) { .none => return types.AppError.TextureResizeFailed, - .texture => |*texture| texture.resize(extent(viewport)) catch |err| { - diagnostics.logError("texture resize failed", err, diagnostic_store); - return types.AppError.TextureResizeFailed; - }, - .surface => |*surface| surface.resize(extent(viewport)) catch |err| { - diagnostics.logError("surface resize failed", err, diagnostic_store); - return types.AppError.SurfaceResizeFailed; - }, - } + .texture => |*value| value, + .surface => |*value| value, + }; + var completion = session.resize(extent(viewport)) catch |err| { + diagnostics.logError("render target resize failed", err, diagnostic_store); + return types.AppError.TextureResizeFailed; + }; + defer completion.deinit(); + try serviceUntilComplete(session, &completion); } - /// The handle behind an attached texture session, which a caller-owned - /// target needs so it can hand a replacement texture to the live session. pub fn textureHandle(self: *Session) !*maplibre.RenderSessionHandle { return switch (self.*) { .texture => |*texture| texture, @@ -50,40 +44,71 @@ pub const Session = union(enum) { }; } - pub fn renderUpdate( - self: *Session, - diagnostic_store: ?*const maplibre.DiagnosticStore, - ) !bool { - switch (self.*) { + pub fn renderUpdate(self: *Session, diagnostic_store: ?*const maplibre.DiagnosticStore) !bool { + const Target = struct { session: *maplibre.RenderSessionHandle, present: bool }; + const target: Target = switch (self.*) { .none => return false, - .texture => |*texture| { - const update = texture.renderUpdate() catch |err| { - diagnostics.logError("texture render failed", err, diagnostic_store); - return types.AppError.TextureRenderFailed; - }; - return switch (update.result) { - .rendered => true, - else => false, - }; - }, - .surface => |*surface| { - const update = surface.renderUpdate() catch |err| { - diagnostics.logError("surface render failed", err, diagnostic_store); - return types.AppError.SurfaceRenderFailed; - }; - return switch (update.result) { - .rendered => true, - else => false, - }; - }, - } + .texture => |*value| .{ .session = value, .present = false }, + .surface => |*value| .{ .session = value, .present = true }, + }; + target.session.requestFrame(.{ .if_needed = true, .present = target.present }) catch |err| { + diagnostics.logError("frame request failed", err, diagnostic_store); + return types.AppError.TextureRenderFailed; + }; + _ = target.session.serviceDriverWork(0) catch |err| { + diagnostics.logError("render driver service failed", err, diagnostic_store); + return types.AppError.TextureRenderFailed; + }; + var batch = try target.session.drainFrameResults(); + defer batch.release(); + for (0..try batch.count()) |index| switch ((try batch.get(index)).disposition) { + .rendered => return true, + else => {}, + }; + return false; } }; -pub fn extent(viewport: types.Viewport) maplibre.RenderTargetExtent { - return .{ - .width = viewport.logical_width, - .height = viewport.logical_height, - .scale_factor = viewport.scale_factor, +pub fn textureSession(attachment: maplibre.RenderSessionAttachment) !Session { + var owned = attachment; + errdefer { + _ = owned.session.abandon() catch {}; + owned.session.destroy() catch {}; + } + defer owned.completion.deinit(); + try serviceUntilComplete(&owned.session, &owned.completion); + return .{ .texture = owned.session }; +} + +pub fn surfaceSession(attachment: maplibre.RenderSessionAttachment) !Session { + var owned = attachment; + errdefer { + _ = owned.session.abandon() catch {}; + owned.session.destroy() catch {}; + } + defer owned.completion.deinit(); + try serviceUntilComplete(&owned.session, &owned.completion); + return .{ .surface = owned.session }; +} + +pub fn serviceUntilComplete(session: *maplibre.RenderSessionHandle, completion: *maplibre.Future(void)) !void { + while (!try completion.poll()) _ = try session.serviceDriverWork(0); + try completion.wait(null); +} + +fn closeSession(session: *maplibre.RenderSessionHandle) void { + var completion = session.detach() catch { + _ = session.abandon() catch {}; + session.destroy() catch {}; + return; + }; + defer completion.deinit(); + serviceUntilComplete(session, &completion) catch { + _ = session.abandon() catch {}; }; + session.destroy() catch {}; +} + +pub fn extent(viewport: types.Viewport) maplibre.RenderTargetExtent { + return .{ .width = viewport.logical_width, .height = viewport.logical_height, .scale_factor = viewport.scale_factor }; } diff --git a/examples/zig-map/types.zig b/examples/zig-map/types.zig index 7b62b8a93..21e4cc336 100644 --- a/examples/zig-map/types.zig +++ b/examples/zig-map/types.zig @@ -7,10 +7,11 @@ pub const AppError = error{ RuntimeCreateFailed, MapCreateFailed, EventMaskFailed, + EventDrainFailed, TextureAttachFailed, StyleLoadFailed, CameraJumpFailed, - CameraCommandFailed, + CameraUpdateFailed, TextureResizeFailed, TextureRenderFailed, SurfaceAttachFailed, diff --git a/examples/zig-readback/main.zig b/examples/zig-readback/main.zig index 9d54017fc..d71efd9aa 100644 --- a/examples/zig-readback/main.zig +++ b/examples/zig-readback/main.zig @@ -8,6 +8,7 @@ extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; const vk = if (build_options.supports_vulkan) @import("vulkan") else struct {}; const supports_egl = build_options.supports_opengl and (builtin.os.tag == .linux or builtin.os.tag == .macos); +const uses_caller_driver = build_options.supports_opengl and !supports_egl; const egl = if (supports_egl) @import("egl") else struct {}; @@ -16,180 +17,172 @@ const sdl = if (build_options.supports_opengl and builtin.os.tag == .windows) @i const width = 512; const height = 512; const style_url = "https://tiles.openfreemap.org/styles/bright"; -const park_timeout_milliseconds = 100; - -const RuntimeLoopArgs = struct { - allocator: std.mem.Allocator, - context: *OwnedTextureContext, - shared: *Shared, -}; - -/// Owns the runtime and the map for their whole lifetime, on one thread that is -/// not the one presenting. -fn runtimeLoop(args: RuntimeLoopArgs) void { - runtimeLoopFallible(args) catch |err| args.shared.fail(err); +fn waitForFuture(future: *maplibre.Future(void), diagnostic_store: ?*maplibre.DiagnosticStore) !void { + try future.wait(diagnostic_store); +} +fn waitForSessionFuture(session: *maplibre.RenderSessionHandle, future: *maplibre.Future(void), diagnostic_store: ?*maplibre.DiagnosticStore) !void { + if (!uses_caller_driver) return waitForFuture(future, diagnostic_store); + while (!try future.poll()) _ = try session.serviceDriverWork(0); + try future.wait(diagnostic_store); } -fn runtimeLoopFallible(args: RuntimeLoopArgs) !void { - const shared = args.shared; +pub fn main(init_args: std.process.Init) !void { + const allocator = init_args.gpa; + var args = try std.process.Args.Iterator.initAllocator(init_args.minimal.args, allocator); + defer args.deinit(); + _ = args.skip(); + const output_path = args.next() orelse "map.ppm"; + + try maplibre.setAsyncLogSeverityMask(.none, null); + defer maplibre.setAsyncLogSeverityMask(.default, null) catch {}; + try logAndValidateRenderBackend(); - var diagnostic_store = maplibre.DiagnosticStore.init(args.allocator); + var diagnostic_store = maplibre.DiagnosticStore.init(allocator); defer diagnostic_store.deinit(); - var runtime = try maplibre.RuntimeHandle.create(args.allocator, .{ .cache_path = ":memory:" }, &diagnostic_store); - defer runtime.close() catch {}; + var runtime = try maplibre.RuntimeHandle.create(allocator, .{ .cache_path = ":memory:" }, &diagnostic_store); + defer if (runtime.close()) |future| { + var teardown = future; + _ = teardown.wait(null) catch {}; + teardown.deinit(); + } else |_| {}; - var map = try maplibre.MapHandle.create(&runtime, .{ - .width = width, - .height = height, - .scale_factor = 1.0, - .mode = .static, - }); + var map_future = try maplibre.MapHandle.create(&runtime, .{ .mode = .static }); + defer map_future.deinit(); + var map = try map_future.wait(&diagnostic_store); defer map.close() catch {}; - - // The five event types the runtime loop reads. A map queues no event of an - // unselected type, so this runs before the style load. - try map.setEventMask(.{ + var event_mask = try map.setEventMask(.{ .map_render_update_available = true, .map_still_image_finished = true, .map_still_image_failed = true, .map_loading_failed = true, .map_render_error = true, }); - + event_mask.deinit(); + var resize = try map.resize(width, height, 1.0); + resize.deinit(); try setInitialCamera(&map); - try map.setStyleUrl(args.allocator, style_url); - try map.requestStillImage(); - - // The render loop signals this to release the parked pump. - const wake = try runtime.wakeSource(); - defer wake.release(); - - // A map with an attached session cannot be destroyed, so wait for the render - // loop to close its session before the deferred map close. Installing this - // before the setup above would deadlock a setup failure: the render loop - // would still be in awaitMap() with nothing to close. - defer shared.awaitSessionClosed(); - shared.publish(map, wake); - - pumpUntilSessionCloses(args, &runtime, &map, &diagnostic_store) catch |err| { - shared.fail(err); - }; -} + var style = try map.setStyleUrl(allocator, style_url); + style.deinit(); -fn pumpUntilSessionCloses( - args: RuntimeLoopArgs, - runtime: *maplibre.RuntimeHandle, - map: *maplibre.MapHandle, - diagnostic_store: *maplibre.DiagnosticStore, -) !void { - const shared = args.shared; - const map_id = try map.id(); - while (shared.failureValue() == null and !shared.sessionClosed()) { - runtime.pump(park_timeout_milliseconds, null) catch |err| { - logLatestDiagnostic(diagnostic_store); - return err; - }; - var batch = try runtime.drainEvents(args.allocator, 0); - defer batch.deinit(); - for (0..batch.len()) |index| { - const event = try batch.at(index); - if (event.source_type != .map or event.source_id == null or - !std.meta.eql(event.source_id.?, map_id)) continue; - switch (event.event_type) { - .map_render_update_available => shared.requestRender(), - .map_still_image_finished => shared.finishStillImage(), - .map_loading_failed => return error.MapLoadingFailed, - .map_render_error => return error.MapRenderFailed, - .map_still_image_failed => return error.StillImageFailed, - else => {}, - } - } - } -} + var barrier = try runtime.barrier(); + defer barrier.deinit(); + try waitForFuture(&barrier, &diagnostic_store); -pub fn main(init_args: std.process.Init) !void { - const allocator = init_args.gpa; - var args = try std.process.Args.Iterator.initAllocator(init_args.minimal.args, allocator); - defer args.deinit(); - _ = args.skip(); - const output_path = args.next() orelse "map.ppm"; - - try maplibre.setAsyncLogSeverityMask(.none, null); - defer maplibre.setAsyncLogSeverityMask(.default, null) catch {}; - try logAndValidateRenderBackend(); - - // The graphics context belongs to this thread, which attaches the session, - // presents, and reads back. It stays current here for the whole run. var context = try OwnedTextureContext.init(); defer context.deinit(); - - var shared = Shared{ .io = init_args.io }; - const thread = try std.Thread.spawn(.{}, runtimeLoop, .{RuntimeLoopArgs{ - .allocator = allocator, - .context = &context, - .shared = &shared, - }}); - - const render_result = renderOnThisThread(init_args.io, allocator, &context, &shared, output_path); - if (render_result) |_| {} else |err| shared.fail(err); - shared.markSessionClosed(); - thread.join(); - - if (shared.failureValue()) |err| return err; + try renderWithDriver( + init_args.io, + allocator, + &map, + &context, + output_path, + ); } -/// The render loop. Attaches its own session against the runtime loop's map, -/// renders until the still image is both finished and drawn, then reads back -/// and writes the file. -fn renderOnThisThread( +/// Uses a native core worker except for WGL, whose device context stays on this +/// graphics thread. +fn renderWithDriver( io: std.Io, allocator: std.mem.Allocator, + map: *maplibre.MapHandle, context: *OwnedTextureContext, - shared: *Shared, output_path: []const u8, ) !void { - var map = try shared.awaitMap(); - var session = try attachOwnedTexture(context, &map, .{ + var attachment = try attachOwnedTexture(context, map, .{ .extent = .{ .width = width, .height = height, .scale_factor = 1.0 }, }); - // The runtime loop cannot destroy the map until this closes, so it must - // close on every path. - defer session.close() catch {}; - - var rendered_frame = false; - const started = std.Io.Clock.awake.now(io); - while (started.durationTo(std.Io.Clock.awake.now(io)).toNanoseconds() < 5 * std.time.ns_per_s) { - if (shared.failureValue()) |err| return err; - // Attempt a frame every iteration rather than only when the runtime loop - // asks: in a still-image map the render attempts themselves advance - // loading. - _ = shared.consumeRenderRequest(); - switch ((try session.renderUpdate()).result) { - .rendered => rendered_frame = true, - else => {}, + var attachment_needs_cleanup = true; + errdefer if (attachment_needs_cleanup) { + _ = attachment.session.abandon() catch {}; + attachment.session.destroy() catch {}; + }; + defer attachment.completion.deinit(); + try waitForSessionFuture(&attachment.session, &attachment.completion, null); + + var session = attachment.session; + defer { + var detach = session.detach() catch null; + if (detach) |*completion| { + defer completion.deinit(); + waitForSessionFuture(&session, completion, null) catch { + _ = session.abandon() catch {}; + }; + } else { + _ = session.abandon() catch {}; } - // The still-image completion can land before or after the frame that - // satisfied it, so finish only once both have happened. - if (shared.stillImageDone() and rendered_frame) break; - try io.sleep(.fromMilliseconds(2), .awake); - } else { - return error.RenderTimedOut; + session.destroy() catch {}; } + attachment_needs_cleanup = false; - const image_data = try allocator.alloc(u8, @as(usize, width) * @as(usize, height) * 4); - defer allocator.free(image_data); - const image_info = try session.readPremultipliedRgba8Into(image_data); + var still_image = try map.requestStillImage(); + defer still_image.deinit(); + + try session.requestFrame(.{ .token = 1, .if_needed = false }); + try waitForRenderedFrame(io, &session, &still_image, 1); + + const capabilities = try session.capabilities(); + if (capabilities.frame_acquisition) { + var frame = try session.acquireFrame(); + var frame_owned = true; + errdefer if (frame_owned) frame.release(.cpu_complete) catch {}; + _ = try frame.producerSync(); + try frame.release(.cpu_complete); + frame_owned = false; + } - try writePpm(io, allocator, output_path, image_data, image_info); - std.debug.print("wrote {s} ({d}x{d})\n", .{ output_path, image_info.width, image_info.height }); + var readback = try session.readback(allocator); + defer readback.deinit(); + if (uses_caller_driver) { + while (!try readback.poll()) _ = try session.serviceDriverWork(0); + } + var image = try readback.wait(null); + defer image.deinit(); + + try writePpm(io, allocator, output_path, image.data, image.info); + std.debug.print("wrote {s} ({d}x{d})\n", .{ output_path, image.info.width, image.info.height }); } -fn logLatestDiagnostic(diagnostic_store: *const maplibre.DiagnosticStore) void { - const diagnostic = diagnostic_store.get() orelse return; - std.debug.print("native diagnostic", .{}); - if (diagnostic.raw_status) |raw_status| std.debug.print(" ({d})", .{raw_status}); - std.debug.print(": {s}\n", .{diagnostic.message}); +fn waitForRenderedFrame( + io: std.Io, + session: *maplibre.RenderSessionHandle, + still_image: *maplibre.Future(void), + token: u64, +) !void { + var rendered = false; + var demand_pending = true; + for (0..10_000) |_| { + if (uses_caller_driver) { + _ = try session.serviceDriverWork(0); + } + var results = session.drainFrameResults() catch |err| { + if (err != error.NotReady) return err; + try io.sleep(.fromMilliseconds(1), .awake); + continue; + }; + defer results.release(); + for (0..try results.count()) |index| { + const result = try results.get(index); + if (result.token != token) continue; + demand_pending = false; + switch (result.disposition) { + .rendered => rendered = true, + .no_update, .size_pending, .target_not_ready => {}, + else => return error.FrameNotRendered, + } + } + const still_completed = try still_image.poll(); + if (still_completed) { + try still_image.wait(null); + if (rendered) return; + } + if (!demand_pending) { + try session.requestFrame(.{ .token = token, .if_needed = false }); + demand_pending = true; + } + try io.sleep(.fromMilliseconds(1), .awake); + } + return error.FrameResultTimedOut; } fn logAndValidateRenderBackend() !void { @@ -225,117 +218,7 @@ const OwnedTextureDescriptor = struct { extent: maplibre.RenderTargetExtent, }; -/// The cross-thread surface between the render loop, which owns the graphics -/// context and the render session, and the runtime loop, which owns the runtime -/// and the map. -const Shared = struct { - io: std.Io, - - lock: std.Io.Mutex = std.Io.Mutex.init, - /// Published by the runtime loop once it has created the map. The render - /// loop attaches its own session against this. - map: ?maplibre.MapHandle = null, - wake: ?maplibre.WakeSourceHandle = null, - /// First fatal error from either loop. - failure: ?anyerror = null, - - map_published: std.atomic.Value(bool) = .init(false), - /// Set by the render loop once its session is closed, which is what frees - /// the runtime loop to destroy the map. - session_closed: std.atomic.Value(bool) = .init(false), - /// Set by the runtime loop when the map has published a new render update. - render_requested: std.atomic.Value(bool) = .init(false), - /// Set by the runtime loop when the still image completed. The render loop - /// waits for both this and a rendered frame. - still_image_done: std.atomic.Value(bool) = .init(false), - failed: std.atomic.Value(bool) = .init(false), - - fn fail(self: *Shared, err: anyerror) void { - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - if (self.failure == null) self.failure = err; - self.failed.store(true, .release); - } - - fn failureValue(self: *Shared) ?anyerror { - if (!self.failed.load(.acquire)) return null; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - return self.failure; - } - - fn publish( - self: *Shared, - handle: maplibre.MapHandle, - wake: maplibre.WakeSourceHandle, - ) void { - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - self.map = handle; - self.wake = wake; - self.map_published.store(true, .release); - } - - /// Render loop: releases the runtime loop's parked pump. - fn wakeRuntimeLoop(self: *Shared) void { - if (!self.map_published.load(.acquire)) return; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - if (self.wake) |wake| wake.signal() catch {}; - } - - fn tryTakeMap(self: *Shared) ?maplibre.MapHandle { - if (!self.map_published.load(.acquire)) return null; - std.Io.Threaded.mutexLock(&self.lock); - defer std.Io.Threaded.mutexUnlock(&self.lock); - return self.map; - } - - /// Waits for the runtime loop to create the map. - fn awaitMap(self: *Shared) !maplibre.MapHandle { - while (true) { - if (self.failureValue()) |err| return err; - if (self.tryTakeMap()) |handle| return handle; - self.io.sleep(.fromMilliseconds(1), .awake) catch {}; - } - } - - fn requestRender(self: *Shared) void { - self.render_requested.store(true, .release); - } - - /// Consumes the render request before the caller renders, so a request - /// published during the render is not lost. - fn consumeRenderRequest(self: *Shared) bool { - return self.render_requested.swap(false, .acq_rel); - } - - fn finishStillImage(self: *Shared) void { - self.still_image_done.store(true, .release); - } - - fn stillImageDone(self: *Shared) bool { - return self.still_image_done.load(.acquire); - } - - fn markSessionClosed(self: *Shared) void { - self.session_closed.store(true, .release); - // Release the pump so the runtime loop observes this now. - self.wakeRuntimeLoop(); - } - - fn sessionClosed(self: *Shared) bool { - return self.session_closed.load(.acquire); - } - - fn awaitSessionClosed(self: *Shared) void { - while (!self.sessionClosed()) { - self.io.sleep(.fromMilliseconds(1), .awake) catch {}; - } - } -}; - -const OwnedTextureContext = if (build_options.supports_opengl) OpenGLAttachContext else if (build_options.supports_vulkan) VulkanAttachContext else if (build_options.supports_metal) struct { +const OwnedTextureContext = if (build_options.supports_vulkan) VulkanAttachContext else if (build_options.supports_metal) struct { device: *anyopaque, fn init() !@This() { @@ -347,32 +230,35 @@ const OwnedTextureContext = if (build_options.supports_opengl) OpenGLAttachConte fn descriptor(self: *const @This()) maplibre.MetalContextDescriptor { return .{ .device = maplibre.NativePointer.fromPtr(self.device) }; } -} else struct {}; +} else if (build_options.supports_opengl) OpenGLAttachContext else struct {}; -/// Attaches an owned-texture session on the calling thread, which becomes the -/// session's owner thread. +/// Supplies transferable state to a core worker or a WGL context to the caller +/// driver. fn attachOwnedTexture( context: *OwnedTextureContext, map: *maplibre.MapHandle, descriptor: OwnedTextureDescriptor, -) !maplibre.RenderSessionHandle { - return if (build_options.supports_opengl) - try maplibre.attachOpenGLOwnedTexture(map, .{ - .extent = descriptor.extent, - .context = context.descriptor(), - }) - else if (build_options.supports_vulkan) +) !maplibre.RenderSessionAttachment { + return if (build_options.supports_vulkan) try maplibre.attachVulkanOwnedTexture(map, .{ .extent = descriptor.extent, .context = context.descriptor(), - }) + }, .{ .driver = .core_worker, .requested_texture_ring_depth = 1 }) else if (build_options.supports_metal) try maplibre.attachMetalOwnedTexture(map, .{ .extent = descriptor.extent, .context = context.descriptor(), + }, .{ .driver = .core_worker, .requested_texture_ring_depth = 1 }) + else if (build_options.supports_opengl) + try maplibre.attachOpenGLOwnedTexture(map, .{ + .extent = descriptor.extent, + .context = context.descriptor(), + }, .{ + .driver = if (supports_egl) .core_worker else .caller_graphics_thread, + .requested_texture_ring_depth = 1, }) else - unreachable; + return error.RenderBackendUnavailable; } const OpenGLAttachContext = if (build_options.supports_opengl and builtin.os.tag == .windows) struct { @@ -433,15 +319,11 @@ const OpenGLAttachContext = if (build_options.supports_opengl and builtin.os.tag } else if (supports_egl) struct { display: egl.EGLDisplay, config: egl.EGLConfig, - surface: egl.EGLSurface, - share_context: egl.EGLContext, fn init() !@This() { const display = try initDisplay(); errdefer _ = egl.eglTerminate(display); - if (egl.eglBindAPI(egl.EGL_OPENGL_ES_API) == egl.EGL_FALSE) return error.EglUnavailable; - const config_attributes = [_]egl.EGLint{ egl.EGL_SURFACE_TYPE, egl.EGL_PBUFFER_BIT, egl.EGL_RENDERABLE_TYPE, egl.EGL_OPENGL_ES3_BIT, @@ -461,36 +343,13 @@ const OpenGLAttachContext = if (build_options.supports_opengl and builtin.os.tag return error.EglUnavailable; } - const context_attributes = [_]egl.EGLint{ - egl.EGL_CONTEXT_CLIENT_VERSION, 3, - egl.EGL_NONE, - }; - const share_context = egl.eglCreateContext(display, config, egl.EGL_NO_CONTEXT, &context_attributes); - if (share_context == egl.EGL_NO_CONTEXT) return error.EglUnavailable; - errdefer _ = egl.eglDestroyContext(display, share_context); - - const surface_attributes = [_]egl.EGLint{ - egl.EGL_WIDTH, 8, - egl.EGL_HEIGHT, 8, - egl.EGL_NONE, - }; - const surface = egl.eglCreatePbufferSurface(display, config, &surface_attributes); - if (surface == egl.EGL_NO_SURFACE) return error.EglUnavailable; - errdefer _ = egl.eglDestroySurface(display, surface); - - if (egl.eglMakeCurrent(display, surface, surface, share_context) == egl.EGL_FALSE) return error.EglUnavailable; return .{ .display = display, .config = config, - .surface = surface, - .share_context = share_context, }; } fn deinit(self: *@This()) void { - _ = egl.eglMakeCurrent(self.display, egl.EGL_NO_SURFACE, egl.EGL_NO_SURFACE, egl.EGL_NO_CONTEXT); - _ = egl.eglDestroySurface(self.display, self.surface); - _ = egl.eglDestroyContext(self.display, self.share_context); _ = egl.eglTerminate(self.display); } @@ -519,7 +378,9 @@ const OpenGLAttachContext = if (build_options.supports_opengl and builtin.os.tag return .{ .egl = .{ .display = maplibre.NativePointer.fromPtr(@ptrCast(self.display.?)), .config = maplibre.NativePointer.fromPtr(@ptrCast(self.config.?)), - .share_context = maplibre.NativePointer.fromPtr(@ptrCast(self.share_context.?)), + .share_context = null, + .client_api = .gles, + .ownership = .dedicated, .get_proc_address = null, } }; } @@ -708,12 +569,13 @@ fn expectVk(result: if (build_options.supports_vulkan) vk.VkResult else i32) !vo } fn setInitialCamera(map: *maplibre.MapHandle) !void { - try map.jumpTo(.{ + var completion = try map.updateCamera(.{ .camera = .{ .center = .{ .latitude = 37.7749, .longitude = -122.4194 }, .zoom = 13.0, .bearing = 12.0, .pitch = 30.0, - }); + } }); + completion.deinit(); } fn writePpm( @@ -723,14 +585,23 @@ fn writePpm( rgba: []const u8, info: maplibre.TextureImageInfo, ) !void { - const pixel_count = @as(usize, @intCast(info.width)) * @as(usize, @intCast(info.height)); - const rgb = try allocator.alloc(u8, pixel_count * 3); + const image_width: usize = @intCast(info.width); + const image_height: usize = @intCast(info.height); + const stride: usize = @intCast(info.stride); + const row_bytes = image_width * 4; + const required_bytes = stride * image_height; + if (stride < row_bytes or info.byte_length < required_bytes or rgba.len < required_bytes) return error.InvalidReadbackLayout; + const rgb = try allocator.alloc(u8, image_width * image_height * 3); defer allocator.free(rgb); - for (0..pixel_count) |index| { - rgb[index * 3 + 0] = rgba[index * 4 + 0]; - rgb[index * 3 + 1] = rgba[index * 4 + 1]; - rgb[index * 3 + 2] = rgba[index * 4 + 2]; + for (0..image_height) |row| { + const source = rgba[row * stride ..][0..row_bytes]; + const destination = rgb[row * image_width * 3 ..][0 .. image_width * 3]; + for (0..image_width) |column| { + destination[column * 3 + 0] = source[column * 4 + 0]; + destination[column * 3 + 1] = source[column * 4 + 1]; + destination[column * 3 + 2] = source[column * 4 + 2]; + } } var file = try std.Io.Dir.cwd().createFile(io, output_path, .{}); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24871c611..b25949ead 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ agp = "9.1.1" dokka = "2.2.0" maven-publish = "0.37.0" javacpp = "1.5.14" +coroutines = "1.11.0" lwjgl = "3.4.2" android-compileSdk = "36" android-minSdk = "24" @@ -17,6 +18,7 @@ androidx-test-runner = "1.7.0" [libraries] androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } javacpp = { module = "org.bytedeco:javacpp", version.ref = "javacpp" } +coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" } lwjgl = { module = "org.lwjgl:lwjgl" } lwjgl-egl = { module = "org.lwjgl:lwjgl-egl" } diff --git a/include/maplibre_native_c.h b/include/maplibre_native_c.h index 5b31260e8..cf37bfd94 100644 --- a/include/maplibre_native_c.h +++ b/include/maplibre_native_c.h @@ -2,14 +2,15 @@ * @file maplibre_native_c.h * Public C API for low-level MapLibre Native bindings. * - * Functions that operate on thread-affine handles validate the caller thread - * and return MLN_STATUS_WRONG_THREAD for owner-thread mismatches. Functions - * without an explicit owner-thread requirement may be called from any thread. + * Graphics-thread-affine functions validate the caller thread and return + * MLN_STATUS_WRONG_THREAD for mismatches. Functions without an explicit + * graphics-thread requirement may be called from any thread. * * Status-returning functions clear thread-local diagnostics on entry. After a * synchronous failure status is returned, read * mln_thread_last_error_message() on the same thread before making another C - * API call. Asynchronous native failures are reported through runtime events. + * API call. Asynchronous native failures are reported through completions or + * repeated event streams. * * This header targets C23. */ @@ -20,6 +21,7 @@ #include "maplibre_native_c/android.h" // IWYU pragma: export #include "maplibre_native_c/base.h" // IWYU pragma: export #include "maplibre_native_c/camera.h" // IWYU pragma: export +#include "maplibre_native_c/completion.h" // IWYU pragma: export #include "maplibre_native_c/diagnostics.h" // IWYU pragma: export #include "maplibre_native_c/logging.h" // IWYU pragma: export #include "maplibre_native_c/map.h" // IWYU pragma: export @@ -31,5 +33,6 @@ #include "maplibre_native_c/style.h" // IWYU pragma: export #include "maplibre_native_c/surface.h" // IWYU pragma: export #include "maplibre_native_c/texture.h" // IWYU pragma: export +#include "maplibre_native_c/wake.h" // IWYU pragma: export #endif // MAPLIBRE_NATIVE_C_H diff --git a/include/maplibre_native_c/base.h b/include/maplibre_native_c/base.h index e49921157..2133e0177 100644 --- a/include/maplibre_native_c/base.h +++ b/include/maplibre_native_c/base.h @@ -49,6 +49,16 @@ typedef enum mln_status : int32_t { MLN_STATUS_UNSUPPORTED = -4, /** A native MapLibre error or C++ exception was converted to status. */ MLN_STATUS_NATIVE_ERROR = -5, + /** The operation reached its terminal cancelled disposition. */ + MLN_STATUS_CANCELLED = -6, + /** A conflicting driver call or lifecycle transition is in flight. */ + MLN_STATUS_BUSY = -7, + /** The render target or graphics receiver was irreversibly lost. */ + MLN_STATUS_TARGET_LOST = -8, + /** A nonblocking acquisition or service call has no result yet. */ + MLN_STATUS_NOT_READY = -9, + /** A command or operation named an ID with no live object behind it. */ + MLN_STATUS_NOT_FOUND = -10, } mln_status; /** Render backend support flags reported by this native library build. */ @@ -91,7 +101,11 @@ typedef uint64_t mln_offline_region_list; typedef uint64_t mln_buffer; typedef uint64_t mln_resource_request_handle; typedef uint64_t mln_render_session; -typedef uint64_t mln_wake_source; +typedef uint64_t mln_event_batch; +typedef uint64_t mln_adapter_resource_request_queue; +typedef uint64_t mln_adapter_log_queue; +typedef uint64_t mln_acquired_frame; +typedef uint64_t mln_render_frame_batch; /** * Borrowed data. The data pointer may be null only when size is zero. diff --git a/include/maplibre_native_c/callback_adapter.h b/include/maplibre_native_c/callback_adapter.h index 41d4190fc..8b3b344f7 100644 --- a/include/maplibre_native_c/callback_adapter.h +++ b/include/maplibre_native_c/callback_adapter.h @@ -29,8 +29,10 @@ #include #include "maplibre_native_c/base.h" // IWYU pragma: export +#include "maplibre_native_c/logging.h" // IWYU pragma: export #include "maplibre_native_c/runtime.h" // IWYU pragma: export #include "maplibre_native_c/style.h" // IWYU pragma: export +#include "maplibre_native_c/wake.h" // IWYU pragma: export #ifdef __cplusplus extern "C" { @@ -41,6 +43,65 @@ extern "C" { /** Rule kind that matches every resource kind. */ #define MLN_ADAPTER_RESOURCE_KIND_ANY UINT32_MAX +/** How a completion adapter copies the function-specific result value. */ +typedef enum mln_adapter_completion_copy_kind : uint32_t { + /** Copy value_count elements of the element size supplied at creation. */ + MLN_ADAPTER_COMPLETION_COPY_FLAT = 0, + /** Copy one or more mln_buffer_view values and their bytes. */ + MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS = 1, + /** Copy one or more mln_offline_region_info values and nested storage. */ + MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS = 2, + /** Copy one mln_style_source_result and its nested storage. */ + MLN_ADAPTER_COMPLETION_COPY_STYLE_SOURCE = 3, + /** Copy one mln_style_layer_result and its nested storage. */ + MLN_ADAPTER_COMPLETION_COPY_STYLE_LAYER = 4, + /** Copy one mln_style_image_result and its nested storage. */ + MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE = 5, + /** Copy one mln_style_image_stretches_result and its arrays. */ + MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE_STRETCHES = 6, + /** Copy one or more mln_queried_feature values and nested storage. */ + MLN_ADAPTER_COMPLETION_COPY_QUERIED_FEATURES = 7, + /** Copy one mln_texture_readback_result and its pixel bytes. */ + MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK = 8, +} mln_adapter_completion_copy_kind; + +/** Native-owned completion copy delivered to an asynchronous host listener. */ +typedef struct mln_adapter_completion_record { + void* owner; + mln_completion_result result; +} mln_adapter_completion_record; + +/** Receives one native-owned completion record on the host listener context. */ +typedef void (*mln_adapter_completion_listener)( + void* user_data, mln_adapter_completion_record* record +); + +/** + * Creates a completion descriptor that copies its borrowed result before + * notifying an asynchronous host listener. + * + * out_completion must point to a zeroed descriptor. element_size is used only + * with MLN_ADAPTER_COMPLETION_COPY_FLAT and may be zero for resultless calls. + * The caller passes the descriptor to exactly one asynchronous C API call. If + * that call rejects the submission, the caller must pass the descriptor to + * mln_adapter_completion_reject(). + */ +MLN_API mln_status mln_adapter_completion_create( + uint32_t copy_kind, size_t element_size, + mln_adapter_completion_listener listener, void* user_data, + mln_completion* out_completion +) MLN_NOEXCEPT; + +/** Releases adapter state after the submitting C API rejected a completion. */ +MLN_API void mln_adapter_completion_reject( + mln_completion* completion +) MLN_NOEXCEPT; + +/** Releases a completion record after the host copied its result. */ +MLN_API void mln_adapter_completion_record_destroy( + mln_adapter_completion_record* record +) MLN_NOEXCEPT; + // This block uses line comments because its examples contain URL patterns that // a block comment cannot carry. @@ -90,8 +151,8 @@ typedef struct mln_adapter_resource_rewrite_rule { /** * A borrowed table of rewrite rules. * - * The rules pointer is borrowed and must stay valid while the table is - * registered as resource transform user data. + * The rules pointer and every rule string stay valid through the terminal event + * of the command that replaces or clears this transform. */ typedef struct mln_adapter_resource_rewrite_rules { const mln_adapter_resource_rewrite_rule* rules; @@ -112,8 +173,9 @@ typedef struct mln_adapter_http_header { * how url compares against the complete transformed URL. A null url or an * unknown flag bit makes the rule match nothing. * - * The first matching rule supplies its complete header list. Every pointer is - * borrowed and must outlive the registration. + * The first matching rule supplies its complete header list. Every pointer + * stays valid through the terminal event of the command that replaces or + * clears this transform. */ typedef struct mln_adapter_http_header_transform_rule { uint32_t kind; @@ -152,8 +214,8 @@ typedef struct mln_adapter_resource_provider_rule { /** * A borrowed table of provider rules. * - * The rules pointer is borrowed and must stay valid while the table is - * registered as resource provider user data. + * The rules pointer, response buffers, and rule strings stay valid through the + * terminal event of the command that replaces or clears this provider. */ typedef struct mln_adapter_resource_provider_rules { const mln_adapter_resource_provider_rule* rules; @@ -186,7 +248,7 @@ typedef enum mln_adapter_resource_route_flags : uint32_t { * * The url field is a comparison value, read literally or as a glob pattern * according to flags. A null url or an unknown flag bit makes the route match - * nothing. The url pointer is borrowed and must outlive the provider. + * nothing. The url pointer has the lifetime of its queued provider. */ typedef struct mln_adapter_queued_resource_provider_route { uint32_t kind; @@ -195,25 +257,16 @@ typedef struct mln_adapter_queued_resource_provider_route { } mln_adapter_queued_resource_provider_route; /** - * Receives a queued request as a native-owned - * mln_adapter_queued_resource_request, or null when the provider retires. - * - * The listener returns void and may be invoked from any MapLibre thread. It - * takes ownership of the record and releases it with - * mln_adapter_resource_provider_request_destroy() once the host has read it. - */ -typedef void (*mln_adapter_queued_resource_request_listener)(void* request); - -/** - * A provider that hands matching requests to a host listener. + * A provider that copies matching requests into a native queue. * - * The routes pointer is borrowed and must stay valid while the provider is - * registered. + * The routes pointer and every route URL stay valid through the terminal event + * of the command that replaces or clears this provider. queue identifies the + * queue that receives each copied request. */ typedef struct mln_adapter_queued_resource_provider { const mln_adapter_queued_resource_provider_route* routes; size_t route_count; - mln_adapter_queued_resource_request_listener listener; + mln_adapter_resource_request_queue queue; } mln_adapter_queued_resource_provider; /** @@ -254,28 +307,6 @@ typedef struct mln_adapter_queued_resource_request { size_t prior_data_size; } mln_adapter_queued_resource_request; -/** - * Receives a log record as a native-owned mln_adapter_log_record, or null when - * the callback retires. - * - * The listener returns void and may be invoked from any MapLibre logging or - * worker thread. It takes ownership of the record and releases it with - * mln_adapter_log_record_destroy() once the host has read it. - */ -typedef void (*mln_adapter_log_record_listener)(void* record); - -/** - * Registration state for an adapted log callback. - * - * The consume field is the value reported to MapLibre for every dispatched - * record. The address of this struct identifies the registration; it is - * borrowed and must stay valid until the callback is replaced or cleared. - */ -typedef struct mln_adapter_log_callback_state { - mln_adapter_log_record_listener listener; - uint32_t consume; -} mln_adapter_log_callback_state; - /** * A native-owned copy of a log record. * @@ -284,13 +315,28 @@ typedef struct mln_adapter_log_callback_state { */ typedef struct mln_adapter_log_record { void* owner; - bool retire_callback; uint32_t severity; uint32_t event; int64_t code; const char* message; } mln_adapter_log_record; +/** + * Registration state for an adapted log callback. + * + * The callback copies records into queue and reports consume to MapLibre. The + * address of this struct identifies the registration. When release_user_data is + * non-null, a successful install transfers responsibility for release_context + * to the adapter, which releases it after the registration is replaced or + * cleared. The struct must remain valid until that release callback runs. + */ +typedef struct mln_adapter_log_callback_state { + mln_adapter_log_queue queue; + uint32_t consume; + mln_log_callback_release release_user_data; + void* release_context; +} mln_adapter_log_callback_state; + /** * Creates a token describing a handle the host has not closed yet. * @@ -311,11 +357,73 @@ MLN_API void mln_adapter_handle_leak_token_destroy(void* token) MLN_NOEXCEPT; MLN_API void mln_adapter_handle_leak_report(void* token) MLN_NOEXCEPT; /** - * The mln_log_callback implementation this layer registers. + * Creates a resource-request queue with a wake for its receiver. * - * Copies the record, hands it to the registered listener, and reports the - * registration's fixed consume value. The user_data pointer is the - * mln_adapter_log_callback_state passed to mln_adapter_log_set_callback(). + * out_queue must point to the null handle. The association remains immutable + * until the queue is closed. + */ +MLN_API mln_status mln_adapter_resource_request_queue_create( + const mln_wake* wake, mln_adapter_resource_request_queue* out_queue +) MLN_NOEXCEPT; + +/** + * Acquires the oldest queued request, or null when the queue is empty. + * + * out_request must point to null. The caller owns a returned record and + * releases it with mln_adapter_resource_provider_request_destroy(). The queue + * remains ready until this drain confirms it is empty. + */ +MLN_API mln_status mln_adapter_resource_request_queue_acquire( + mln_adapter_resource_request_queue queue, + mln_adapter_queued_resource_request** out_request +) MLN_NOEXCEPT; + +/** + * Closes a resource-request queue. + * + * Pending records and their request handles are released, and the wake is + * detached before this function returns. A null or already released queue is + * a no-op. + */ +MLN_API void mln_adapter_resource_request_queue_close( + mln_adapter_resource_request_queue queue +) MLN_NOEXCEPT; + +/** + * Creates a log-record queue with a wake for its receiver. + * + * out_queue must point to the null handle. The association remains immutable + * until the queue is closed. + */ +MLN_API mln_status mln_adapter_log_queue_create( + const mln_wake* wake, mln_adapter_log_queue* out_queue +) MLN_NOEXCEPT; + +/** + * Acquires the oldest copied log record, or null when the queue is empty. + * + * out_record must point to null. The caller owns a returned record and releases + * it with mln_adapter_log_record_destroy(). + */ +MLN_API mln_status mln_adapter_log_queue_acquire( + mln_adapter_log_queue queue, mln_adapter_log_record** out_record +) MLN_NOEXCEPT; + +/** + * Closes a log queue. + * + * Pending records are released, and the wake is detached before this function + * returns. A null or already released queue is a no-op. + */ +MLN_API void mln_adapter_log_queue_close( + mln_adapter_log_queue queue +) MLN_NOEXCEPT; + +/** + * The mln_log_callback implementation for a log queue. + * + * user_data points to an mln_adapter_log_callback_state. Each record is copied + * into its queue, and the callback reports the state's fixed consume value. */ MLN_API uint32_t mln_adapter_log_callback( void* user_data, uint32_t severity, uint32_t event, int64_t code, @@ -325,19 +433,12 @@ MLN_API uint32_t mln_adapter_log_callback( /** * Installs state as the process-global log callback, or clears the current * callback when state is null. - * - * A registration this call replaces receives one final null record through its - * listener once its in-flight dispatches finish. - * - * Returns: - * - MLN_STATUS_OK on success. - * - The status reported by mln_log_set_callback() or mln_log_clear_callback(). */ MLN_API mln_status mln_adapter_log_set_callback( mln_adapter_log_callback_state* state ) MLN_NOEXCEPT; -/** Releases a log record delivered to a listener. */ +/** Releases a log record acquired from a log queue. */ MLN_API void mln_adapter_log_record_destroy(void* record) MLN_NOEXCEPT; /** @@ -390,31 +491,24 @@ MLN_API uint32_t mln_adapter_resource_provider_rules_callback( /** * The mln_resource_provider_callback implementation for queued providers. * - * The user_data pointer is an mln_adapter_queued_resource_provider. A request - * matching one of the provider's routes is copied and handed to the listener, - * and reports MLN_RESOURCE_PROVIDER_DECISION_HANDLE. Other requests pass - * through unchanged and continue through the native loader. A request that - * cannot be copied is completed with an error response rather than left - * outstanding. + * user_data points to an mln_adapter_queued_resource_provider. A request + * matching one route is copied into the provider's queue and reports + * MLN_RESOURCE_PROVIDER_DECISION_HANDLE. Other requests pass through. A request + * that cannot be copied is completed with an error response. */ MLN_API uint32_t mln_adapter_queued_resource_provider_callback( void* user_data, const mln_resource_request* request, mln_resource_request_handle handle ) MLN_NOEXCEPT; -/** Releases a queued request record delivered to a listener. */ -MLN_API void mln_adapter_resource_provider_request_destroy( - void* request -) MLN_NOEXCEPT; - /** - * Delivers one null record to a queued provider's listener. + * Releases the copied payload of a resource request acquired from a queue. * - * Hosts call this after the provider is no longer registered so the listener - * can release the host-side state that backed it. + * Acquiring the record transfers its request handle to the host. The host + * completes or releases that handle independently. */ -MLN_API void mln_adapter_queued_resource_provider_retire( - mln_adapter_queued_resource_provider* provider +MLN_API void mln_adapter_resource_provider_request_destroy( + void* request ) MLN_NOEXCEPT; /** diff --git a/include/maplibre_native_c/camera.h b/include/maplibre_native_c/camera.h index 6a818fe99..8fbb2a08b 100644 --- a/include/maplibre_native_c/camera.h +++ b/include/maplibre_native_c/camera.h @@ -10,6 +10,7 @@ #include #include "base.h" +#include "completion.h" #include "map.h" #ifdef __cplusplus @@ -23,6 +24,10 @@ MLN_API mln_camera_options mln_camera_options_default(void) MLN_NOEXCEPT; /** Returns empty animation options initialized for this C API version. */ MLN_API mln_animation_options mln_animation_options_default(void) MLN_NOEXCEPT; +/** Returns an empty relative camera update initialized for this API version. */ +MLN_API mln_camera_delta mln_camera_delta_default(void) MLN_NOEXCEPT; +/** Returns an empty atomic camera update initialized for this API version. */ +MLN_API mln_camera_update mln_camera_update_default(void) MLN_NOEXCEPT; /** Returns empty camera fitting options initialized for this C API version. */ MLN_API mln_camera_fit_options @@ -49,756 +54,211 @@ mln_map_viewport_options_default(void) MLN_NOEXCEPT; MLN_API mln_map_tile_options mln_map_tile_options_default(void) MLN_NOEXCEPT; /** - * Applies MapLibre debug overlay mask bits to a map. + * Submits a debug-overlay command. * - * Pass 0 to disable all debug overlays. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or options - * contains unknown bits. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_set_debug_options(mln_map map, uint32_t options) MLN_NOEXCEPT; - -/** - * Copies the current MapLibre debug overlay mask bits. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or out_options is - * null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed mask is visible through mln_map_snapshot_get as + * snapshot.debug_options. */ -MLN_API mln_status -mln_map_get_debug_options(mln_map map, uint32_t* out_options) MLN_NOEXCEPT; - +MLN_API mln_status mln_map_set_debug_options( + mln_map map, uint32_t options, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Enables or disables MapLibre's rendering stats overlay view. + * Submits a rendering-stats visibility command. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed value is visible through mln_map_snapshot_get as + * snapshot.rendering_stats_view_enabled. */ MLN_API mln_status mln_map_set_rendering_stats_view_enabled( - mln_map map, bool enabled + mln_map map, bool enabled, const mln_completion* completion ) MLN_NOEXCEPT; - -/** - * Copies whether MapLibre's rendering stats overlay view is enabled. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or out_enabled is - * null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_rendering_stats_view_enabled( - mln_map map, bool* out_enabled +/** Submits an ordered debug-log command. */ +MLN_API mln_status mln_map_dump_debug_logs( + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; - /** - * Copies whether MapLibre currently considers the map fully loaded. + * Submits a copied viewport-options command. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or out_loaded is - * null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_is_fully_loaded(mln_map map, bool* out_loaded) MLN_NOEXCEPT; - -/** - * Dumps map debug logs through MapLibre Native logging. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_dump_debug_logs(mln_map map) MLN_NOEXCEPT; - -/** - * Copies live map viewport and render-transform controls. - * - * On success, *out_options is overwritten and all known fields are marked. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_options is - * null, or out_options->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_viewport_options( - mln_map map, mln_map_viewport_options* out_options -) MLN_NOEXCEPT; - -/** - * Applies selected live map viewport and render-transform controls. - * - * Only fields indicated by options->fields affect the map. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, options is null, - * options->size is too small, options->fields contains unknown bits, an enum - * value is unknown, or an enabled frustum offset value is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed options are visible through mln_map_snapshot_get as + * snapshot.viewport. */ MLN_API mln_status mln_map_set_viewport_options( - mln_map map, const mln_map_viewport_options* options + mln_map map, const mln_map_viewport_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; - -/** - * Copies tile prefetch and LOD tuning controls. - * - * On success, *out_options is overwritten and all known fields are marked. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_options is - * null, or out_options->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_tile_options( - mln_map map, mln_map_tile_options* out_options -) MLN_NOEXCEPT; - /** - * Applies selected tile prefetch and LOD tuning controls. - * - * Only fields indicated by options->fields affect the map. + * Submits a copied tile-options command. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, options is null, - * options->size is too small, options->fields contains unknown bits, - * prefetch_zoom_delta is greater than 255, a double field is non-finite, or - * lod_mode is unknown. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed options are visible through mln_map_snapshot_get as + * snapshot.tile. */ MLN_API mln_status mln_map_set_tile_options( - mln_map map, const mln_map_tile_options* options -) MLN_NOEXCEPT; - -/** - * Copies the current camera snapshot. - * - * On success, *out_camera is overwritten. MapLibre Native reports no anchor in - * a camera snapshot, so out_camera->fields leaves MLN_CAMERA_OPTION_ANCHOR - * clear; anchor is input-only, as documented on mln_camera_options. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_camera is - * null, or out_camera->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_get_camera(mln_map map, mln_camera_options* out_camera) MLN_NOEXCEPT; - -/** - * Applies a camera jump command. - * - * Only fields indicated by camera->fields affect the map. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, camera is null, - * camera->size is too small, camera->fields contains unknown bits, or an - * enabled camera field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_jump_to(mln_map map, const mln_camera_options* camera) MLN_NOEXCEPT; - -/** - * Applies a camera ease transition command. - * - * Only fields indicated by camera->fields affect the map. - * - * MapLibre Native's default ease duration is zero, so a null animation, or one - * that omits MLN_ANIMATION_OPTION_DURATION, moves the camera to the target - * immediately and reports it on the next snapshot without any pumping. Set - * MLN_ANIMATION_OPTION_DURATION to animate. - * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. A zero-duration ease reports its end - * during this call, so the event is already queued when the call returns. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, camera is null, - * camera->size is too small, camera->fields contains unknown bits, an enabled - * camera field is invalid, animation->size is too small, animation->fields - * contains unknown bits, or an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_ease_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation -) MLN_NOEXCEPT; - -/** - * Applies a camera fly transition command. - * - * Only fields indicated by camera->fields affect the map. - * - * This is the one camera command that animates by default. When the animation - * is null or omits MLN_ANIMATION_OPTION_DURATION, MapLibre Native derives a - * duration from the flight distance and the velocity, which defaults to 1.2 - * screenfuls per second, so the camera reaches the target over several pumped - * frames. - * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, camera is null, - * camera->size is too small, camera->fields contains unknown bits, an enabled - * camera field is invalid, animation->size is too small, animation->fields - * contains unknown bits, or an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_fly_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation -) MLN_NOEXCEPT; - -/** - * Applies a screen-space pan command. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or a delta value - * is non-finite. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_move_by(mln_map map, double delta_x, double delta_y) MLN_NOEXCEPT; - -/** - * Applies an animated screen-space pan command. - * - * This command eases, so it inherits the zero default duration described on - * mln_map_ease_to(). Set MLN_ANIMATION_OPTION_DURATION to animate. - * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, a delta value is - * non-finite, animation->size is too small, animation->fields contains - * unknown bits, or an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_move_by_animated( - mln_map map, double delta_x, double delta_y, - const mln_animation_options* animation + mln_map map, const mln_map_tile_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Applies a screen-space zoom command. + * Copies the camera from the latest immutable map snapshot. * - * Passing a null anchor uses MapLibre Native's default zoom anchor. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, scale is - * non-positive or non-finite, or anchor contains non-finite values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The returned generation identifies the complete map snapshot that supplied + * the camera. This function never reads mutable MapLibre state. */ -MLN_API mln_status mln_map_scale_by( - mln_map map, double scale, const mln_screen_point* anchor +MLN_API mln_status mln_map_camera_snapshot_get( + mln_map map, mln_camera_options* out_camera, uint64_t* out_generation ) MLN_NOEXCEPT; /** - * Applies an animated screen-space zoom command. - * - * Passing a null anchor uses MapLibre Native's default zoom anchor. This - * command eases, so it inherits the zero default duration described on - * mln_map_ease_to(). Set MLN_ANIMATION_OPTION_DURATION to animate. + * Submits one atomic camera update. * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, scale is - * non-positive or non-finite, anchor contains non-finite values, - * animation->size is too small, animation->fields contains unknown bits, or - * an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The update is copied before return. The completion reports its terminal + * disposition and the snapshot generation published by a committed update. */ -MLN_API mln_status mln_map_scale_by_animated( - mln_map map, double scale, const mln_screen_point* anchor, - const mln_animation_options* animation +MLN_API mln_status mln_map_update_camera( + mln_map map, const mln_camera_update* update, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Applies a screen-space rotate command. + * Submits one copied relative camera update. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or a point - * contains non-finite values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion reports its terminal disposition and the snapshot generation + * published by a committed update. */ -MLN_API mln_status mln_map_rotate_by( - mln_map map, mln_screen_point first, mln_screen_point second +MLN_API mln_status mln_map_apply_camera_delta( + mln_map map, const mln_camera_delta* delta, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Applies an animated screen-space rotate command. - * - * This command eases, so it inherits the zero default duration described on - * mln_map_ease_to(). Set MLN_ANIMATION_OPTION_DURATION to animate. + * Starts an ordered camera read. * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, a point contains - * non-finite values, animation->size is too small, animation->fields contains - * unknown bits, or an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion result observes every command accepted before this query. */ -MLN_API mln_status mln_map_rotate_by_animated( - mln_map map, mln_screen_point first, mln_screen_point second, - const mln_animation_options* animation +MLN_API mln_status mln_map_camera_query( + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Applies a pitch delta command. + * Starts an ordered query for a camera that fits geographic bounds. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or pitch is - * non-finite. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_pitch_by(mln_map map, double pitch) MLN_NOEXCEPT; - -/** - * Applies an animated pitch delta command. - * - * This command eases, so it inherits the zero default duration described on - * mln_map_ease_to(). Set MLN_ANIMATION_OPTION_DURATION to animate. - * - * Set MLN_ANIMATION_OPTION_TRANSITION_ID to have this transition report its end - * once through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, as described - * on mln_animation_options.transition_id. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, pitch is - * non-finite, animation->size is too small, animation->fields contains - * unknown bits, or an enabled animation field is invalid. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_pitch_by_animated( - mln_map map, double pitch, const mln_animation_options* animation -) MLN_NOEXCEPT; - -/** - * Cancels active camera transitions. - * - * A cancelled transition that carried MLN_ANIMATION_OPTION_TRANSITION_ID - * reports its end through MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, the - * same way a completed one does. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_cancel_transitions(mln_map map) MLN_NOEXCEPT; - -/** - * Marks whether a host-driven gesture is in progress. - * - * A host that decodes its own pointer gestures sets this to true when a gesture - * starts and back to false when it ends, so the camera commands issued in - * between belong to one live gesture. MapLibre folds the flag into the - * transform's changing state. - * - * The flag stays set until the host clears it, so pair every true with a false, - * including on a cancelled gesture. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_set_gesture_in_progress(mln_map map, bool in_progress) MLN_NOEXCEPT; - -/** - * Copies whether a host-driven gesture is currently in progress. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or - * out_in_progress is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_is_gesture_in_progress(mln_map map, bool* out_in_progress) MLN_NOEXCEPT; - -/** - * Computes a camera that fits geographic bounds in the current viewport. - * - * Passing null fit_options uses zero padding with no bearing or pitch override. - * On success, *out_camera is overwritten. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, bounds are - * invalid, fit_options is invalid, out_camera is null, or out_camera->size is - * too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Inputs are copied before this function returns. The query observes every + * map command accepted before it. Pass null fit_options for default fitting + * controls. */ MLN_API mln_status mln_map_camera_for_lat_lng_bounds( mln_map map, mln_lat_lng_bounds bounds, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera + const mln_camera_fit_options* fit_options, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Computes a camera that fits geographic coordinates in the current viewport. - * - * The coordinates array is borrowed for the duration of this call and is not - * retained. Passing null fit_options uses zero padding with no bearing or pitch - * override. On success, *out_camera is overwritten. + * Starts an ordered query for a camera that fits geographic coordinates. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, coordinates is - * null, coordinate_count is 0, any coordinate is invalid, fit_options is - * invalid, out_camera is null, or out_camera->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The query owns a copy of coordinates and fit_options. */ MLN_API mln_status mln_map_camera_for_lat_lngs( mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera + const mln_camera_fit_options* fit_options, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Computes a camera that fits a geometry in the current viewport. + * Starts an ordered query for a camera that fits a GeoJSON geometry. * - * The UTF-8 GeoJSON Geometry bytes are borrowed for this call and are not - * retained. Empty geometry objects and geometry collections with no - * coordinates are invalid for camera fitting. Passing null fit_options uses - * zero padding with no bearing or pitch override. On success, *out_camera is - * overwritten. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, geometry is empty - * or invalid, geometry contains no coordinates, fit_options is invalid, - * out_camera is null, or out_camera->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The query owns a copy of the geometry bytes and fit_options. */ MLN_API mln_status mln_map_camera_for_geometry( mln_map map, mln_buffer_view geometry, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera + const mln_camera_fit_options* fit_options, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Computes geographic bounds for a camera from two viewport corners. - * - * The box is the hull of the top-left and bottom-right screen corners for that - * camera in the current viewport. When bearing and pitch are zero, the box - * equals the visible area. Those corners are the northwest and southeast of - * the viewport. Longitudes stay in -180 to 180. On success, *out_bounds is - * overwritten. + * Starts an ordered wrapped-bounds query for a copied camera. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, camera is null or - * invalid, or out_bounds is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The result is the hull of the top-left and bottom-right screen corners for + * that camera in the current viewport. When bearing and pitch are zero, the + * box equals the visible area. Those corners are the northwest and southeast + * of the viewport. Longitudes stay in -180 to 180. */ MLN_API mln_status mln_map_lat_lng_bounds_for_camera( - mln_map map, const mln_camera_options* camera, mln_lat_lng_bounds* out_bounds + mln_map map, const mln_camera_options* camera, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Computes geographic bounds for a camera from the four viewport corners. - * - * The axis-aligned hull of all four screen corners and the center encompasses - * the projected viewport. Longitudes unwrap onto the shortest path through the - * center. A viewport that crosses the antimeridian reports values outside -180 - * to 180. On success, *out_bounds is overwritten. + * Starts an ordered unwrapped-bounds query for a copied camera. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, camera is null or - * invalid, or out_bounds is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The result is the axis-aligned hull of all four screen corners and the + * center, which encompasses the projected viewport. Longitudes unwrap onto + * the shortest path through the center. A viewport that crosses the + * antimeridian reports values outside -180 to 180. */ MLN_API mln_status mln_map_lat_lng_bounds_for_camera_unwrapped( - mln_map map, const mln_camera_options* camera, mln_lat_lng_bounds* out_bounds + mln_map map, const mln_camera_options* camera, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies map camera constraint options. - * - * On success, *out_options is overwritten and all known fields are marked. - * The geographic constraint is reported as exactly one of - * MLN_BOUND_OPTION_BOUNDS, which also fills out_options->bounds, or - * MLN_BOUND_OPTION_UNBOUNDED, which leaves out_options->bounds at its default. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_options is - * null, or out_options->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_get_bounds(mln_map map, mln_bound_options* out_options) MLN_NOEXCEPT; - -/** - * Applies selected map camera constraint options. - * - * Only fields indicated by options->fields affect the map. Set - * MLN_BOUND_OPTION_BOUNDS to constrain the camera center to options->bounds, - * or MLN_BOUND_OPTION_UNBOUNDED to release the geographic constraint entirely. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, options is null, - * options->size is too small, options->fields contains unknown bits, - * options->fields contains both MLN_BOUND_OPTION_BOUNDS and - * MLN_BOUND_OPTION_UNBOUNDED, bounds are invalid, a numeric field is - * non-finite, or paired min/max fields are inconsistent. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_set_bounds(mln_map map, const mln_bound_options* options) MLN_NOEXCEPT; - -/** - * Copies the current free camera position and orientation. - * - * On success, *out_options is overwritten. + * Submits a copied camera-constraint command. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_options is - * null, or out_options->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed constraints are visible through mln_map_snapshot_get as + * snapshot.bounds. */ -MLN_API mln_status mln_map_get_free_camera_options( - mln_map map, mln_free_camera_options* out_options +MLN_API mln_status mln_map_set_bounds( + mln_map map, const mln_bound_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; - /** - * Applies selected free camera position and orientation fields. + * Submits a copied free-camera command. * - * Position uses MapLibre Native's modified Web Mercator camera space. - * Orientation is a quaternion stored as x, y, z, w. Only fields indicated by - * options->fields affect the map. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, options is null, - * options->size is too small, options->fields contains unknown bits, position - * contains non-finite values, or orientation contains non-finite values or is - * zero length. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The committed options are visible through mln_map_snapshot_get as + * snapshot.free_camera. */ MLN_API mln_status mln_map_set_free_camera_options( - mln_map map, const mln_free_camera_options* options + mln_map map, const mln_free_camera_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies the current axonometric rendering options. - * - * On success, *out_mode is overwritten. MapLibre currently reports all fields. + * Submits copied axonometric rendering option fields. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_mode is null, - * or out_mode->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_projection_mode( - mln_map map, mln_projection_mode* out_mode -) MLN_NOEXCEPT; - -/** - * Applies axonometric rendering option fields to a map. - * - * Only fields indicated by mode->fields affect the map. Unspecified fields keep - * their current native values. These options mutate the live map render - * transform and do not change coordinate conversion units or formulas. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, mode is null, - * mode->size is too small, mode->fields contains unknown bits, or an enabled - * skew value is non-finite. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Only fields selected by mode->fields affect the map. Unspecified fields keep + * their current native values. The completion reports the terminal + * disposition and published snapshot generation. */ MLN_API mln_status mln_map_set_projection_mode( - mln_map map, const mln_projection_mode* mode + mln_map map, const mln_projection_mode* mode, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Converts a geographic world coordinate to a screen point for the current map. + * Starts an ordered conversion from a geographic coordinate to a screen point. * - * The output point uses logical map pixels with an origin at the top-left of - * the map viewport. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_point is - * null, or coordinate contains invalid latitude or longitude values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Each conversion queues one map query. Hot paths such as per-pointer-move + * conversion use a standalone projection, whose conversions are synchronous. */ MLN_API mln_status mln_map_pixel_for_lat_lng( - mln_map map, mln_lat_lng coordinate, mln_screen_point* out_point + mln_map map, mln_lat_lng coordinate, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Converts a screen point to a geographic world coordinate for the current map. - * - * The input point uses logical map pixels with an origin at the top-left of the - * map viewport. + * Starts an ordered conversion from a screen point to a geographic coordinate. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_coordinate is - * null, or point contains non-finite values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Each conversion queues one map query. Hot paths such as per-pointer-move + * conversion use a standalone projection, whose conversions are synchronous. */ MLN_API mln_status mln_map_lat_lng_for_pixel( - mln_map map, mln_screen_point point, mln_lat_lng* out_coordinate + mln_map map, mln_screen_point point, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Converts geographic world coordinates to screen points for the current map. - * - * The caller owns both arrays. On success, out_points receives coordinate_count - * entries. coordinates and out_points may be null only when coordinate_count is - * 0. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, a required array - * is null, or any coordinate contains invalid latitude or longitude values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Starts an ordered conversion of copied coordinates to screen points. */ MLN_API mln_status mln_map_pixels_for_lat_lngs( mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, - mln_screen_point* out_points + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Converts screen points to geographic world coordinates for the current map. - * - * The caller owns both arrays. On success, out_coordinates receives point_count - * entries. points and out_coordinates may be null only when point_count is 0. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, a required array - * is null, or any point contains non-finite values. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Starts an ordered conversion of copied screen points to coordinates. */ MLN_API mln_status mln_map_lat_lngs_for_pixels( mln_map map, const mln_screen_point* points, size_t point_count, - mln_lat_lng* out_coordinates + const mln_completion* completion ) MLN_NOEXCEPT; #ifdef __cplusplus diff --git a/include/maplibre_native_c/completion.h b/include/maplibre_native_c/completion.h new file mode 100644 index 000000000..d23ac8dd2 --- /dev/null +++ b/include/maplibre_native_c/completion.h @@ -0,0 +1,87 @@ +/** + * @file maplibre_native_c/completion.h + * Public C API declarations for one-shot asynchronous completion. + */ + +#ifndef MAPLIBRE_NATIVE_C_COMPLETION_H +#define MAPLIBRE_NATIVE_C_COMPLETION_H + +#include +#include + +#include "base.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Terminal dispositions reported by command completions. */ +typedef enum mln_command_disposition : uint32_t { + MLN_COMMAND_DISPOSITION_COMMITTED = 0, + MLN_COMMAND_DISPOSITION_SUPERSEDED = 1, + MLN_COMMAND_DISPOSITION_FAILED = 2, + MLN_COMMAND_DISPOSITION_CANCELLED = 3, +} mln_command_disposition; + +/** + * Borrowed terminal result for one accepted asynchronous submission. + * + * The submitting function defines the type behind value and the meaning of + * value_count. Every pointer remains valid only for the completion callback. + * A binding copies any result or diagnostic that it keeps. + * + * disposition and generation are meaningful for commands. Other one-shot + * functions set disposition to MLN_COMMAND_DISPOSITION_COMMITTED and + * generation to zero. + */ +typedef struct mln_completion_result { + uint32_t size; + /** Terminal status. */ + mln_status status; + /** One of mln_command_disposition. */ + uint32_t disposition; + uint32_t reserved; + /** Map snapshot generation published by a committed command, or zero. */ + uint64_t generation; + /** Borrowed diagnostic bytes, empty on success. */ + mln_buffer_view diagnostic; + /** Borrowed function-specific result, or null when the function has none. */ + const void* value; + /** Function-specific element or byte count for value. */ + size_t value_count; +} mln_completion_result; + +/** Receives one terminal result for an accepted asynchronous submission. */ +typedef void (*mln_completion_callback)( + void* user_data, const mln_completion_result* result +); + +/** Releases user_data after its completion can no longer run. */ +typedef void (*mln_completion_release)(void* user_data); + +/** + * Callback state for one asynchronous submission. + * + * The struct itself is borrowed for the submission call. MLN_STATUS_OK + * transfers callback, user_data, and release_user_data to the C API. The + * completion runs exactly once and may run before the submission returns. The + * release callback runs after the completion returns. A non-OK submission + * invokes neither callback and leaves user_data owned by the caller. + * + * The completion runs on whichever thread finishes the work, including the + * submitting thread for inline completion. It may call any-thread C APIs, but + * must return promptly and must not wait for runtime or graphics-driver work + * that can only progress on its current thread. + */ +typedef struct mln_completion { + uint32_t size; + mln_completion_callback callback; + void* user_data; + mln_completion_release release_user_data; +} mln_completion; + +#ifdef __cplusplus +} +#endif + +#endif // MAPLIBRE_NATIVE_C_COMPLETION_H diff --git a/include/maplibre_native_c/diagnostics.h b/include/maplibre_native_c/diagnostics.h index 22c62db79..5184fce2d 100644 --- a/include/maplibre_native_c/diagnostics.h +++ b/include/maplibre_native_c/diagnostics.h @@ -18,23 +18,10 @@ extern "C" { * The returned string is empty when no diagnostic is available. The pointer is * owned by the C API and remains valid until the next C API call on the same * thread that writes a thread-local diagnostic. - */ -MLN_API const char* mln_thread_last_error_message(void) MLN_NOEXCEPT; - -/** - * Returns an opaque token identifying the calling native thread. * - * The token is stable for the life of the thread and differs between live - * threads. It carries no meaning beyond comparison, and a token from a thread - * that has exited may be reused by a later one. - * - * Owner-thread checks in this API key on the native thread. A host whose unit - * of execution is not pinned to one, such as a Dart isolate or an unlocked - * goroutine, compares this token across calls to detect that it moved. - * - * Callable from any thread. + * This Immediate function is callable from any thread. */ -MLN_API uint64_t mln_thread_token(void) MLN_NOEXCEPT; +MLN_API const char* mln_thread_last_error_message(void) MLN_NOEXCEPT; #ifdef __cplusplus } diff --git a/include/maplibre_native_c/logging.h b/include/maplibre_native_c/logging.h index f2423ce10..51a58ee09 100644 --- a/include/maplibre_native_c/logging.h +++ b/include/maplibre_native_c/logging.h @@ -66,12 +66,20 @@ typedef uint32_t (*mln_log_callback)( const char* message ); +/** Releases user_data from a log callback registration. */ +typedef void (*mln_log_callback_release)(void* user_data); + /** * Installs a process-global MapLibre Native log callback. * - * Passing null clears the current callback. The callback and user_data are - * stored by reference and must remain valid until the callback is replaced or - * cleared. + * Passing a null callback clears the current callback. callback and user_data + * are stored by reference. + * + * With a non-null release_user_data, MLN_STATUS_OK transfers responsibility for + * releasing user_data to the C API. The C API invokes release_user_data after + * the final callback returns, when the registration is replaced or cleared. + * With a null release_user_data, the caller keeps that responsibility and must + * keep user_data valid until the registration is replaced or cleared. * * The callback is a low-level native callback: * @@ -80,13 +88,17 @@ typedef uint32_t (*mln_log_callback)( * - MapLibre may invoke it while holding internal logging locks. * - The callback must be thread-safe, return quickly, and must not call this C * API or MapLibre Native APIs. + * - release_user_data may run on the caller that replaces or clears the + * registration. It must not call this C API or MapLibre Native APIs. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ -MLN_API mln_status -mln_log_set_callback(mln_log_callback callback, void* user_data) MLN_NOEXCEPT; +MLN_API mln_status mln_log_set_callback( + mln_log_callback callback, void* user_data, + mln_log_callback_release release_user_data +) MLN_NOEXCEPT; /** * Clears the process-global log callback. diff --git a/include/maplibre_native_c/map.h b/include/maplibre_native_c/map.h index 3812302c6..323052120 100644 --- a/include/maplibre_native_c/map.h +++ b/include/maplibre_native_c/map.h @@ -15,6 +15,7 @@ #include #include "base.h" +#include "completion.h" #include "runtime.h" #ifdef __cplusplus @@ -151,33 +152,24 @@ typedef enum mln_map_mode : uint32_t { /** Produces one-off still images for a single tile. */ MLN_MAP_MODE_TILE = 2, } mln_map_mode; +/** Logical map extent in UI pixels and device-pixel scale. */ +typedef struct mln_logical_extent { + uint32_t width; + uint32_t height; + double scale_factor; +} mln_logical_extent; /** Options used when creating a map. */ typedef struct mln_map_options { uint32_t size; /** - * Initial logical map width in UI pixels. Must be positive. + * Initial logical extent. Width and height must be positive. The scale + * factor must be positive and finite. * - * Attaching a render session replaces this with the render target extent, and - * mln_render_session_resize() replaces it again. Until the first attach it is - * the viewport that camera and projection queries such as - * mln_map_camera_for_lat_lng_bounds() and mln_map_pixel_for_lat_lng() are - * answered against. + * After creation, mln_map_resize() is the only function that changes this + * extent. */ - uint32_t width; - /** Initial logical map height in UI pixels. See width. */ - uint32_t height; - /** - * UI-to-device pixel scale. Must be positive and finite. - * - * Unlike width and height, this is fixed for the lifetime of the map and - * selects sprites, glyphs, and raster tiles for every frame the map renders. - * Render targets carry their own scale factor for geometry and shaders, so - * create the map with the scale factor you intend to render at. A render - * session attached or resized with a different scale factor logs a warning - * and renders styled imagery chosen for the map's density. - */ - double scale_factor; + mln_logical_extent initial_extent; /** One of mln_map_mode. Defaults to MLN_MAP_MODE_CONTINUOUS. */ uint32_t map_mode; /** @@ -220,7 +212,7 @@ typedef struct mln_edge_insets { double right; } mln_edge_insets; -/** Camera fields used for snapshots and camera commands. */ +/** Camera fields used by snapshots and camera updates. */ typedef struct mln_camera_options { uint32_t size; uint32_t fields; @@ -228,14 +220,7 @@ typedef struct mln_camera_options { double longitude; double center_altitude; mln_edge_insets padding; - /** - * Screen-space focal point for a camera command. This field is input-only. - * - * MapLibre Native applies it to camera commands and leaves it out of every - * camera snapshot it reports, so MLN_CAMERA_OPTION_ANCHOR is set only by the - * caller. mln_map_get_camera(), mln_map_projection_get_camera(), and the - * mln_map_camera_for_* family leave it clear. - */ + /** Optional screen-space focal point in logical map pixels. */ mln_screen_point anchor; double zoom; double bearing; @@ -260,14 +245,13 @@ typedef struct mln_animation_options { * Duration in milliseconds. Must be finite and non-negative. Values that * would overflow MapLibre Native's internal duration are invalid. * - * When this field is omitted, ease, pan, zoom, rotate, and pitch transitions - * default to zero and apply instantly, while mln_map_fly_to() derives a - * duration from velocity instead. + * When omitted, ease transitions apply immediately. Fly transitions derive + * their duration from velocity. */ double duration_ms; /** - * Average flyTo velocity in screenfuls per second. Must be positive. - * Defaults to 1.2 when omitted. Applies to mln_map_fly_to(). + * Average fly velocity in screenfuls per second. Must be positive and + * defaults to 1.2 when omitted. */ double velocity; /** Peak zoom for flyTo transitions. */ @@ -282,14 +266,10 @@ typedef struct mln_animation_options { * passes the value through without interpreting it, so callers pick their own * scheme, such as a monotonically increasing counter. * - * Each transition emits that event exactly once, whichever way it ends: - * running to completion, being superseded by a later camera command, being - * cancelled by mln_map_cancel_transitions(), or completing instantly as a - * zero-duration jump. A command this API rejects -- one carrying a non-finite - * enabled camera field, for example -- starts no transition and emits no such - * event. The event carries no completion reason, so a host that needs to tell - * completion from cancellation compares the resulting camera against the - * requested one, or tracks which transition ID is current. + * Each transition emits that event exactly once when it completes, is + * superseded, is cancelled, or applies instantly. The event carries no + * completion reason, so a host that needs to distinguish outcomes compares + * the resulting camera against the requested one. * * The event is queued on the runtime that owns the map and is drained by * mln_runtime_drain_events(). For a transition that runs to completion, it is @@ -303,6 +283,61 @@ typedef struct mln_animation_options { uint64_t transition_id; } mln_animation_options; +/** Relative camera operation carried by mln_camera_delta. */ +typedef enum mln_camera_delta_kind : uint32_t { + MLN_CAMERA_DELTA_MOVE = 0, + MLN_CAMERA_DELTA_SCALE = 1, + MLN_CAMERA_DELTA_BEARING = 2, + MLN_CAMERA_DELTA_PITCH = 3, +} mln_camera_delta_kind; + +/** + * One relative camera operation. + * + * MOVE reads offset. SCALE reads amount as a positive factor. BEARING and + * PITCH read amount as degrees. SCALE and BEARING apply anchor when has_anchor + * is true. Every operation reads animation. + */ +typedef struct mln_camera_delta { + uint32_t size; + uint32_t kind; + mln_screen_point offset; + double amount; + bool has_anchor; + mln_screen_point anchor; + mln_animation_options animation; +} mln_camera_delta; + +/** Camera transition behavior for mln_camera_update. */ +typedef enum mln_camera_update_mode : uint32_t { + MLN_CAMERA_UPDATE_MODE_JUMP = 0, + MLN_CAMERA_UPDATE_MODE_EASE = 1, + MLN_CAMERA_UPDATE_MODE_FLY = 2, +} mln_camera_update_mode; + +/** Gesture boundary carried atomically with a camera update. */ +typedef enum mln_gesture_phase : uint32_t { + MLN_GESTURE_PHASE_NONE = 0, + MLN_GESTURE_PHASE_BEGIN = 1, + MLN_GESTURE_PHASE_UPDATE = 2, + MLN_GESTURE_PHASE_END = 3, + MLN_GESTURE_PHASE_CANCEL = 4, +} mln_gesture_phase; + +/** + * One atomic absolute camera update. + * + * The command copies this struct before returning. + */ +typedef struct mln_camera_update { + uint32_t size; + uint32_t mode; + mln_camera_options camera; + mln_animation_options animation; + uint32_t gesture_phase; + uint32_t reserved; +} mln_camera_update; + /** Optional fitting controls for camera-for-viewport queries. */ typedef struct mln_camera_fit_options { uint32_t size; @@ -443,350 +478,109 @@ typedef struct mln_offline_region_info { * Starts creating an offline region. * * Input strings, GeoJSON geometry bytes, and metadata are copied before this - * call returns. Completion is reported through - * MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. On successful completion, call - * mln_runtime_offline_region_create_take_result() to take the snapshot. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, definition is - * null or invalid, metadata is null with a non-zero size, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * call returns. A successful completion borrows one + * mln_offline_region_snapshot value. */ -MLN_API mln_status mln_runtime_offline_region_create_start( +MLN_API mln_status mln_runtime_offline_region_create( mln_runtime runtime, const mln_offline_region_definition* definition, const uint8_t* metadata, size_t metadata_size, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Starts getting an offline region snapshot by ID. * - * Completion is reported through MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * On successful completion, call - * mln_runtime_offline_region_get_take_result(). - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * A successful completion borrows zero or one mln_offline_region_snapshot + * value, depending on whether the region exists. */ -MLN_API mln_status mln_runtime_offline_region_get_start( +MLN_API mln_status mln_runtime_offline_region_get( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Starts listing offline region snapshots in the runtime database. * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * A successful completion borrows one mln_offline_region_list value. */ -MLN_API mln_status mln_runtime_offline_regions_list_start( - mln_runtime runtime, mln_offline_operation_id* out_operation_id +MLN_API mln_status mln_runtime_offline_regions_list( + mln_runtime runtime, const mln_completion* completion ) MLN_NOEXCEPT; /** * Starts merging offline regions from another database path. * * The side database may be upgraded in place by native code and must be - * writable when native merge requires it. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, - * side_database_path is null, or out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * writable when native merge requires it. A successful completion borrows one + * mln_offline_region_list value. */ -MLN_API mln_status mln_runtime_offline_regions_merge_database_start( +MLN_API mln_status mln_runtime_offline_regions_merge_database( mln_runtime runtime, const char* side_database_path, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Starts updating opaque binary metadata for an offline region. * - * On successful completion, call - * mln_runtime_offline_region_update_metadata_take_result(). - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, metadata is - * null with a non-zero size, or out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * A successful completion borrows one mln_offline_region_snapshot value. */ -MLN_API mln_status mln_runtime_offline_region_update_metadata_start( +MLN_API mln_status mln_runtime_offline_region_update_metadata( mln_runtime runtime, mln_offline_region_id region_id, const uint8_t* metadata, - size_t metadata_size, mln_offline_operation_id* out_operation_id + size_t metadata_size, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Starts getting the current completed/download status for an offline region. + * Starts getting the current download status for an offline region. * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * A successful completion borrows one mln_offline_region_status value. */ -MLN_API mln_status mln_runtime_offline_region_get_status_start( +MLN_API mln_status mln_runtime_offline_region_get_status( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Enables or disables runtime events for an offline region. * - * Observer callbacks are copied into runtime events. Disabling observation also - * discards queued events for this region. - * - * Completion is reported through MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Observer callbacks are copied into runtime events. Disabling observation + * prevents future events for this region and leaves queued events unchanged. + * The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_offline_region_set_observed_start( +MLN_API mln_status mln_runtime_offline_region_set_observed( mln_runtime runtime, mln_offline_region_id region_id, bool observed, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Sets an offline region's native download state. * * Register observation separately with - * mln_runtime_offline_region_set_observed_start() to receive progress and error - * events. - * - * Completion is reported through MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, state is not - * a mln_offline_region_download_state value, or out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * mln_runtime_offline_region_set_observed() to receive progress and error + * events. The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_offline_region_set_download_state_start( +MLN_API mln_status mln_runtime_offline_region_set_download_state( mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Invalidates cached resources for an offline region. * - * Completion is reported through MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_offline_region_invalidate_start( +MLN_API mln_status mln_runtime_offline_region_invalidate( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) MLN_NOEXCEPT; /** * Deletes an offline region. * - * Completion is reported through MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_offline_region_delete_start( +MLN_API mln_status mln_runtime_offline_region_delete( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id -) MLN_NOEXCEPT; - -/** - * Takes the snapshot result from a completed offline region create operation. - * - * Must only be called after the matching - * mln_runtime_offline_region_create_start() operation has completed - * successfully (MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED with result - * status MLN_STATUS_OK). The caller owns the returned snapshot handle and must - * destroy it with mln_offline_region_snapshot_destroy(). - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken and out_region was set. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match a region snapshot. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or out_region - * is null. - */ -MLN_API mln_status mln_runtime_offline_region_create_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) MLN_NOEXCEPT; - -/** - * Takes the snapshot result from a completed offline region get operation. - * - * Must only be called after the matching - * mln_runtime_offline_region_get_start() operation has completed successfully. - * The caller owns the returned snapshot handle and must destroy it with - * mln_offline_region_snapshot_destroy(). - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken; out_found indicates whether a - * region existed for the requested ID. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match an optional region snapshot. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, out_region is - * null, or out_found is null. - */ -MLN_API mln_status mln_runtime_offline_region_get_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region, bool* out_found -) MLN_NOEXCEPT; - -/** - * Takes the region list from a completed offline regions list operation. - * - * Must only be called after the matching - * mln_runtime_offline_regions_list_start() operation has completed - * successfully. The caller owns the returned list handle and must destroy it - * with mln_offline_region_list_destroy(). - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken and out_regions was set. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match a region list. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_regions is null. - */ -MLN_API mln_status mln_runtime_offline_regions_list_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) MLN_NOEXCEPT; - -/** - * Takes the region list from a completed offline database merge operation. - * - * Must only be called after the matching - * mln_runtime_offline_regions_merge_database_start() operation has completed - * successfully. The caller owns the returned list handle and must destroy it - * with mln_offline_region_list_destroy(). - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken and out_regions was set. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match a region list. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_regions is null. - */ -MLN_API mln_status mln_runtime_offline_regions_merge_database_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) MLN_NOEXCEPT; - -/** - * Takes the snapshot result from a completed offline region update-metadata - * operation. - * - * Must only be called after the matching - * mln_runtime_offline_region_update_metadata_start() operation has completed - * successfully. The caller owns the returned snapshot handle and must destroy - * it with mln_offline_region_snapshot_destroy(). - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken and out_region was set. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match a region snapshot. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or out_region - * is null. - */ -MLN_API mln_status mln_runtime_offline_region_update_metadata_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) MLN_NOEXCEPT; - -/** - * Takes the status struct from a completed offline region get-status operation. - * - * Must only be called after the matching - * mln_runtime_offline_region_get_status_start() operation has completed - * successfully. The caller provides a pre-allocated mln_offline_region_status - * struct which is filled by this function. - * - * On success, the operation entry is consumed. On failure, it remains live so - * the caller may retry this call or discard the operation with - * mln_runtime_offline_operation_discard(). Taking or discarding a result also - * removes that operation's undrained completion event. The thread diagnostic - * carries a failed operation's error text. - * - * Returns: - * - MLN_STATUS_OK when the result was taken and out_status was filled. - * - MLN_STATUS_INVALID_STATE when the operation has not completed or its result - * kind does not match a region status. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or out_status - * is null. - */ -MLN_API mln_status mln_runtime_offline_region_get_status_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_status* out_status + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -900,6 +694,42 @@ typedef struct mln_map_tile_options { /** One of mln_tile_lod_mode. */ uint32_t lod_mode; } mln_map_tile_options; +/** + * Immutable map state copied from the latest published generation. + * + * Every field is unkeyed, fixed-size map state that changes only through this + * map's own commands or through load progress. Each committed map command + * publishes a new generation and reports it through its completion, so a + * snapshot whose generation is at or past a completion observes that commit. + */ +typedef struct mln_map_snapshot { + uint32_t size; + /** Debug overlay mask of mln_map_debug_option values. */ + uint32_t debug_options; + uint64_t generation; + mln_camera_options camera; + mln_logical_extent logical_extent; + mln_projection_mode projection_mode; + mln_map_viewport_options viewport; + /** True once every requested style and tile resource finished loading. */ + bool fully_loaded; + bool rendering_stats_view_enabled; + bool repaint_demand; + uint8_t reserved_flags; + uint64_t event_mask; + uint64_t latest_render_update_generation; + mln_map_tile_options tile; + mln_bound_options bounds; + mln_free_camera_options free_camera; +} mln_map_snapshot; + +/** Camera result borrowed for an ordered camera-query completion. */ +typedef struct mln_camera_query_result { + uint32_t size; + uint32_t reserved; + uint64_t generation; + mln_camera_options camera; +} mln_camera_query_result; /** * Returns map options initialized for this C API version. @@ -907,45 +737,28 @@ typedef struct mln_map_tile_options { MLN_API mln_map_options mln_map_options_default(void) MLN_NOEXCEPT; /** - * Creates a map handle on the runtime owner thread. - * - * On success, the runtime owner thread becomes the map owner thread. + * Creates a map on the runtime worker. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, out_map is - * null, *out_map is not null, or options are invalid, which includes an - * event_mask bit outside MLN_RUNTIME_EVENT_MASK_ALL. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Input is copied before this function returns. The completion value points to + * one mln_map handle when status is MLN_STATUS_OK. */ MLN_API mln_status mln_map_create( - mln_runtime runtime, const mln_map_options* options, mln_map* out_map + mln_runtime runtime, const mln_map_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; +/** Copies the latest immutable state published by the map worker. */ +MLN_API mln_status +mln_map_snapshot_get(mln_map map, mln_map_snapshot* out_snapshot) MLN_NOEXCEPT; + /** - * Copies the map's current logical viewport size and its pixel ratio. + * Submits the sole post-creation logical extent update. * - * The size starts at mln_map_options.width and height, and follows the - * attach and resize rules documented there. The scale factor is - * mln_map_options.scale_factor, fixed for the lifetime of the map and - * independent of any render target's scale factor; compare the two before - * attaching or resizing a render session to keep them in agreement. - * - * This is a state snapshot. All three out-parameters are required. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or out_width, - * out_height, or out_scale_factor is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion reports terminal disposition and the snapshot generation + * published by a committed resize. */ -MLN_API mln_status mln_map_get_size( - mln_map map, uint32_t* out_width, uint32_t* out_height, - double* out_scale_factor +MLN_API mln_status mln_map_resize( + mln_map map, mln_logical_extent extent, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -962,20 +775,20 @@ MLN_API mln_status mln_map_get_size( * * Returns: * - MLN_STATUS_OK when the request was accepted. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. - * - MLN_STATUS_INVALID_STATE when map is not in MLN_MAP_MODE_CONTINUOUS. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_INVALID_ARGUMENT when map is not live or completion is invalid. + * - MLN_STATUS_INVALID_STATE when map is not continuous or is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status mln_map_request_repaint(mln_map map) MLN_NOEXCEPT; +MLN_API mln_status mln_map_request_repaint( + mln_map map, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Sets per-feature state on this map. + * Submits a copied per-feature-state command. * - * selector->source_id and selector->feature_id are borrowed for the duration of - * the call. state must contain one UTF-8 JSON object and is parsed before - * return. The accepted command requests a map repaint. + * selector->source_id, selector->feature_id, and state are copied before + * return. state must contain one UTF-8 JSON object and is validated before + * return. The committed command requests a map repaint. * * Feature state belongs to the map. A render session pushes it into the * renderer on the next render update, including the first presented frame that @@ -983,38 +796,39 @@ MLN_API mln_status mln_map_request_repaint(mln_map map) MLN_NOEXCEPT; * the last rendered frame. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the command was accepted. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, selector is * null or invalid, selector lacks MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, - * state is empty, invalid JSON, or not an object. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * state is empty, invalid JSON, or not an object, or completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ MLN_API mln_status mln_map_set_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer_view state + mln_map map, const mln_feature_state_selector* selector, + mln_buffer_view state, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies per-feature state from this map. + * Starts an ordered read of per-feature state from this map. * - * selector->source_id and selector->feature_id are borrowed for the duration of - * the call. On success, *out_state receives an owned buffer containing a UTF-8 - * JSON object. Destroy it with mln_buffer_destroy(). Missing feature state is - * reported as an empty object. The copy does not require a render session or a - * loaded source. + * selector->source_id and selector->feature_id are copied before return. The + * read observes every map command accepted before it and copies the map + * store, not the last rendered frame, so it does not require a render session + * or a loaded source. The completion borrows UTF-8 bytes holding one JSON + * object for the callback. Missing feature state is reported as an empty + * object. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the read was accepted. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, selector is - * null or invalid, selector lacks MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, - * out_state is null, or *out_state is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * null or invalid, selector lacks MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, or + * completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer* out_state + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1024,182 +838,112 @@ MLN_API mln_status mln_map_get_feature_state( * are optional. Passing both removes one state key from one feature. Passing * only feature_id removes all state for that feature. Passing neither removes * all feature state for the source/source-layer. The accepted command requests + * a map repaint. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the command was accepted. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, selector is - * null or invalid, or selector has MLN_FEATURE_STATE_SELECTOR_STATE_KEY - * without MLN_FEATURE_STATE_SELECTOR_FEATURE_ID. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * null or invalid, selector has MLN_FEATURE_STATE_SELECTOR_STATE_KEY + * without MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, or completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ MLN_API mln_status mln_map_remove_feature_state( - mln_map map, const mln_feature_state_selector* selector + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Requests one still image for a static or tile map. * - * Pump the runtime and drain runtime events for this map until - * MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED or - * MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED is reported. Those two types are the - * only completion reports, so select both in the map's event mask. While the - * request is pending, process each - * MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE event from this map. Render - * targets use mln_render_session_render_update(). Surface targets present - * directly. A render-update - * call can report a result other than MLN_RENDER_RESULT_RENDERED before the - * next update is available; keep pumping and draining in that case. After - * MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED, use the latest successful texture - * update when the host needs image bytes or a backend texture. + * Keep servicing the selected render driver while this request is pending. + * Submit frame demands with mln_render_session_request_frame() and drain their + * terminal results with mln_render_session_drain_frame_results(). A + * caller-graphics-thread driver also requires calls to + * mln_render_session_service_driver_work() on its graphics thread. A rendered + * frame may be acquired or read back after the completion runs. * * Returns: * - MLN_STATUS_OK when the request was accepted. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live. * - MLN_STATUS_INVALID_STATE when map is not in MLN_MAP_MODE_STATIC or * MLN_MAP_MODE_TILE, or when a still-image request is already pending. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ -MLN_API mln_status mln_map_request_still_image(mln_map map) MLN_NOEXCEPT; - -/** - * Destroys a map handle on its owner thread. - * - * The map must not have an attached render session. - * - * Destruction also discards this map's queued events, including queued style - * loading failures. There is no flush and no terminal event, so the last state - * a host mirrored from events can stay behind the map's final state. Snapshot - * whatever state the host needs while the map is still live, and let teardown - * proceed without awaiting an event for this map. A batch that a host already - * drained holds copies, so it stays readable after this call. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not a live map handle. - * - MLN_STATUS_INVALID_STATE when map still has an attached render target - * session. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_destroy(mln_map map) MLN_NOEXCEPT; +MLN_API mln_status mln_map_request_still_image( + mln_map map, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Loads a style URL through MapLibre Native style APIs. + * Releases a map after synchronous state preflight. * - * This is a map command. The return status reports synchronous acceptance or - * failure. Later native success and failure are reported through runtime - * events. A URL that is unreachable, malformed, or serves invalid style - * content is still accepted synchronously; every such failure arrives later as - * a style loading-failed event, so hosts report style URL errors from the event - * stream rather than from this return status. + * A successful call consumes the public handle before returning. The + * completion runs after previously accepted work is terminal, the native map + * can no longer call the host, and map-owned callback state has been released. + * Backend worker and graphics-resource cleanup may continue after completion. + * The completion state follows the one-shot ownership contract. * * Returns: - * - MLN_STATUS_OK when the load request was accepted. - * - MLN_STATUS_INVALID_ARGUMENT when map is null, not live, or url is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when a synchronous native error is reported or an - * internal exception is converted to status. + * - MLN_STATUS_OK when release was accepted and the handle was consumed. + * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or completion is + * invalid. + * - MLN_STATUS_INVALID_STATE when the map is already closing or still has an + * attached render session. + * - MLN_STATUS_NATIVE_ERROR when teardown could not be scheduled. */ MLN_API mln_status -mln_map_set_style_url(mln_map map, const char* url) MLN_NOEXCEPT; +mln_map_release(mln_map map, const mln_completion* completion) MLN_NOEXCEPT; /** - * Loads inline style JSON through MapLibre Native style APIs. + * Queues a style URL command. * - * This is a map command. The return status reports synchronous acceptance or - * failure. Later native success and failure are reported through runtime - * events. Malformed JSON can fail synchronously and still enqueue a - * loading-failed event. + * The function copies url before returning acceptance. The completion reports + * committed or failed application. Style loading events continue to report + * network, decode, and parse results after a committed command. * * Returns: - * - MLN_STATUS_OK when the load request was accepted. - * - MLN_STATUS_INVALID_ARGUMENT when map is null, not live, or json is empty or - * has a nonzero size with a null data pointer. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when a synchronous native error is reported or an - * internal exception is converted to status. + * - MLN_STATUS_OK when the command was accepted. + * - MLN_STATUS_INVALID_ARGUMENT when map is not live, url is null, or + * completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status -mln_map_set_style_json(mln_map map, mln_buffer_view json) MLN_NOEXCEPT; +MLN_API mln_status mln_map_set_style_url( + mln_map map, const char* url, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Copies the style document this map's style was last parsed from. - * - * This is a state snapshot of the loaded document, not a serialization of the - * live style. The bytes are the document the style loader last parsed - * successfully: the bytes passed to mln_map_set_style_json(), or the response - * body fetched for mln_map_set_style_url(). Runtime mutations through the - * style APIs, such as adding a layer or setting a paint property, do not change - * it, and a failed parse leaves the previously parsed document in place. - * - * A copy of the document is byte-for-byte identical to the bytes that were - * passed to mln_map_set_style_json(), so a host may hand it back to that - * function unchanged. - * - * out_json may be null only when json_capacity is 0, which is a size probe that - * reports the required length and succeeds. *out_json_size receives the byte - * length before the capacity is checked, so a caller learns the size from a - * call that could not fit the document. The bytes are not null-terminated, so - * an exact-length buffer is sufficient. + * Queues an inline style JSON command. * - * A reported size of 0 means no document has been parsed: no style has been - * loaded yet, or every load so far failed to parse. A parsed document is never - * empty. + * The function copies json before returning acceptance. The completion reports + * committed or failed application. Style loading events continue to report + * later resource and parse results after a committed command. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_json is null - * with non-zero capacity, json_capacity is too small for a non-null buffer, - * or out_json_size is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the command was accepted. + * - MLN_STATUS_INVALID_ARGUMENT when map is not live, json is invalid, or + * completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status mln_map_copy_loaded_style_json( - mln_map map, uint8_t* out_json, size_t json_capacity, size_t* out_json_size +MLN_API mln_status mln_map_set_style_json( + mln_map map, mln_buffer_view json, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies the URL this map's style was last requested from. - * - * Unlike mln_map_copy_loaded_style_json(), this is live rather than load-time - * state: mln_map_set_style_url() records the URL when the request is made, - * before the response arrives or the document parses, and - * mln_map_set_style_json() clears it. The document reports what was last parsed - * while the URL reports what was last requested, so the two can disagree while - * a load is in flight or after one fails. - * - * out_url may be null only when url_capacity is 0, which is a size probe that - * reports the required length and succeeds. *out_url_size receives the byte - * length before the capacity is checked. The bytes are not null-terminated, so - * an exact-length buffer is sufficient. + * Starts an ordered copy of the last successfully parsed style document. * - * A reported size of 0 means no URL bytes are available. That covers a style - * loaded from inline JSON, a map that has loaded no style, and a URL load - * requested with an empty string, which mln_map_set_style_url() accepts. These - * cases are not distinguishable through this entry point. - * - * Returns: - * - MLN_STATUS_OK on success, including a size probe. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_url is null - * with non-zero capacity, url_capacity is too small for a non-null buffer, or - * out_url_size is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion borrows the copied UTF-8 bytes for the callback. */ -MLN_API mln_status mln_map_copy_style_url( - mln_map map, char* out_url, size_t url_capacity, size_t* out_url_size +MLN_API mln_status mln_map_loaded_style_json( + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; +/** Starts an ordered copy of the last requested style URL. */ +MLN_API mln_status +mln_map_style_url(mln_map map, const mln_completion* completion) MLN_NOEXCEPT; + /** * Selects which map-originated event types this map queues. * @@ -1210,8 +954,7 @@ MLN_API mln_status mln_map_copy_style_url( * * This call reads the bits in MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS and ignores * the rest, so MLN_RUNTIME_EVENT_MASK_ALL selects every map-originated type. - * mln_map_get_event_mask() reports the value last set, so a host reads it, - * changes one bit, and writes it back. + * mln_map_snapshot_get() reports the last committed mask. * * A map that has not been narrowed selects every map-originated event type this * library reports, which covers types a caller's header may not declare. A new @@ -1228,8 +971,8 @@ MLN_API mln_status mln_map_copy_style_url( * - MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE is the map's only * invalidation report. See mln_map_request_repaint(). * - MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FINISHED and - * MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FAILED are the only reports that a - * still-image request finished. See mln_map_request_still_image(). + * MLN_RUNTIME_EVENT_MASK_MAP_STILL_IMAGE_FAILED report observer completion + * in addition to the still-image completion. * - MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_TRANSITION_FINISHED carries the * transition identity a caller set on an animation, and * MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_DID_CHANGE distinguishes a completed @@ -1237,39 +980,19 @@ MLN_API mln_status mln_map_copy_style_url( * - MLN_RUNTIME_EVENT_MASK_MAP_LOADING_FAILED and * MLN_RUNTIME_EVENT_MASK_MAP_RENDER_ERROR carry native failure text. * - * mln_map_set_style_url() and mln_map_set_style_json() report a style failure - * that MapLibre raises inside the call through their return status and a thread - * diagnostic, whatever this mask selects. + * Style commands report application failures through their completions + * regardless of this mask. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not a live map handle, or - * mask holds a bit outside MLN_RUNTIME_EVENT_MASK_ALL. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the command is accepted. + * - MLN_STATUS_INVALID_ARGUMENT when map is not live, mask contains an unknown + * bit, or completion is invalid. + * - MLN_STATUS_INVALID_STATE when the map is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status -mln_map_set_event_mask(mln_map map, uint64_t mask) MLN_NOEXCEPT; - -/** - * Reports which map-originated event types this map queues. - * - * The value is the mask last set, including bits outside - * MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS that this map ignores. A map that has - * not been narrowed reports MLN_RUNTIME_EVENT_MASK_ALL as this library defines - * it. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not a live map handle, or - * out_mask is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status -mln_map_get_event_mask(mln_map map, uint64_t* out_mask) MLN_NOEXCEPT; +MLN_API mln_status mln_map_set_event_mask( + mln_map map, uint64_t mask, const mln_completion* completion +) MLN_NOEXCEPT; #ifdef __cplusplus } diff --git a/include/maplibre_native_c/projection.h b/include/maplibre_native_c/projection.h index 1412cf0df..5f4cfb4b3 100644 --- a/include/maplibre_native_c/projection.h +++ b/include/maplibre_native_c/projection.h @@ -7,8 +7,10 @@ #define MAPLIBRE_NATIVE_C_PROJECTION_H #include +#include #include "base.h" +#include "completion.h" #include "map.h" #ifdef __cplusplus @@ -16,92 +18,62 @@ extern "C" { #endif /** - * Creates a standalone projection helper from the current map transform. - * - * The helper owns projection and camera transform state only. It does not own - * style, resources, render targets, or runtime events. Use it to convert - * coordinates or compute camera fitting without changing the source map. - * - * Creation snapshots the map's transform. Later map camera or projection - * changes do not update the helper. Every later projection call is synchronous, - * runs on the calling thread, and is internally serialized. The helper may be - * used from any thread. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_projection is - * null, or *out_projection is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Starts creation of a standalone projection from the map's ordered transform + * state. + * + * The completion borrows an independent projection handle that copies the + * map's transform state after every earlier map command. A binding takes + * ownership of that handle before the callback returns. The projection remains + * usable after its source map and runtime close. This function may be called + * from any thread. + * + * Every later projection call is synchronous, runs on the calling thread, and + * is internally serialized. A projection never observes map changes made after + * its creation. */ MLN_API mln_status mln_map_projection_create( - mln_map map, mln_map_projection* out_projection + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Destroys a standalone projection helper. - * - * Destruction retires the handle and waits for projection calls already in - * progress before it destroys the helper. This function may be called from any - * thread. + * Closes a standalone projection. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The close retires the handle, waits for projection calls already running on + * other threads, and destroys the projection before it returns. A later call + * with the retired handle returns MLN_STATUS_INVALID_ARGUMENT. This function + * may be called from any thread. */ MLN_API mln_status -mln_map_projection_destroy(mln_map_projection projection) MLN_NOEXCEPT; +mln_map_projection_close(mln_map_projection projection) MLN_NOEXCEPT; /** - * Copies the current camera snapshot from a standalone projection helper. - * - * On success, *out_camera is overwritten. MapLibre Native reports no anchor in - * a camera snapshot, so out_camera->fields leaves MLN_CAMERA_OPTION_ANCHOR - * clear; anchor is input-only, as documented on mln_camera_options. This - * function may be called from any thread. + * Copies the projection camera into out_camera. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, out_camera - * is null, or out_camera->size is too small. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * out_camera->size must be at least sizeof(mln_camera_options). The result + * observes every earlier projection setter. This function may be called from + * any thread. */ MLN_API mln_status mln_map_projection_get_camera( mln_map_projection projection, mln_camera_options* out_camera ) MLN_NOEXCEPT; /** - * Applies camera fields to a standalone projection helper. + * Applies a camera update to a standalone projection. * - * Only fields indicated by camera->fields affect the helper. This function may - * be called from any thread. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, camera is - * null, camera->size is too small, or camera->fields contains unknown bits. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Only fields selected by camera->fields affect the projection. The update is + * applied before this function returns, so a later read or conversion observes + * it. The map's camera is unaffected. This function may be called from any + * thread. */ MLN_API mln_status mln_map_projection_set_camera( mln_map_projection projection, const mln_camera_options* camera ) MLN_NOEXCEPT; /** - * Updates a projection helper camera so coordinates are visible within padding. - * - * The coordinates array is borrowed for the duration of this call and is not - * retained. Use mln_map_projection_get_camera() after this call to read the - * computed camera. This function may be called from any thread. + * Applies a camera fit for geographic coordinates. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, - * coordinates is null, coordinate_count is 0, padding contains negative or - * non-finite values, or any coordinate contains invalid latitude or longitude - * values. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The fitted camera is applied before this function returns, so a later read + * or conversion observes it. This function may be called from any thread. */ MLN_API mln_status mln_map_projection_set_visible_coordinates( mln_map_projection projection, const mln_lat_lng* coordinates, @@ -109,20 +81,12 @@ MLN_API mln_status mln_map_projection_set_visible_coordinates( ) MLN_NOEXCEPT; /** - * Updates a projection helper camera so geometry coordinates are visible. + * Applies a camera fit for GeoJSON Geometry bytes. * - * The UTF-8 GeoJSON Geometry bytes are borrowed for this call and are not - * retained. Use - * mln_map_projection_get_camera() after this call to read the computed camera. * Empty geometry objects and geometry collections with no coordinates are - * invalid for camera fitting. This function may be called from any thread. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, geometry - * is empty or invalid, padding contains negative or non-finite values, or the - * geometry contains no coordinates. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * invalid. The fitted camera is applied before this function returns, so a + * later read or conversion observes it. This function may be called from any + * thread. */ MLN_API mln_status mln_map_projection_set_visible_geometry( mln_map_projection projection, mln_buffer_view geometry, @@ -130,16 +94,11 @@ MLN_API mln_status mln_map_projection_set_visible_geometry( ) MLN_NOEXCEPT; /** - * Converts a geographic world coordinate using a standalone projection helper. - * - * The output point uses logical map pixels with an origin at the top-left of - * the helper viewport. This function may be called from any thread. + * Converts a geographic coordinate to a screen point. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, out_point - * is null, or coordinate contains invalid latitude or longitude values. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The output uses logical map pixels with an origin at the top-left of the + * projection viewport. The result observes every earlier projection setter. + * This function may be called from any thread. */ MLN_API mln_status mln_map_projection_pixel_for_lat_lng( mln_map_projection projection, mln_lat_lng coordinate, @@ -147,16 +106,11 @@ MLN_API mln_status mln_map_projection_pixel_for_lat_lng( ) MLN_NOEXCEPT; /** - * Converts a screen point using a standalone projection helper. + * Converts a screen point to a geographic coordinate. * - * The input point uses logical map pixels with an origin at the top-left of the - * helper viewport. This function may be called from any thread. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when projection is null or not live, - * out_coordinate is null, or point contains non-finite values. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The input uses logical map pixels with an origin at the top-left of the + * projection viewport. The result observes every earlier projection setter. + * This function may be called from any thread. */ MLN_API mln_status mln_map_projection_lat_lng_for_pixel( mln_map_projection projection, mln_screen_point point, diff --git a/include/maplibre_native_c/query.h b/include/maplibre_native_c/query.h index 6710e5ecd..e25026825 100644 --- a/include/maplibre_native_c/query.h +++ b/include/maplibre_native_c/query.h @@ -10,6 +10,7 @@ #include #include "base.h" +#include "completion.h" #include "map.h" #ifdef __cplusplus @@ -171,96 +172,43 @@ MLN_API void mln_queried_feature_list_destroy( ) MLN_NOEXCEPT; /** - * Queries rendered features from the latest render session state. + * Starts a rendered-feature query against the session's latest driver state. * - * The session renderer must already exist. geometry and options are borrowed - * for the duration of the call. Passing null for options uses default options. - * On success, *out_result receives an owned queried-feature list. Destroy it - * with mln_queried_feature_list_destroy(). + * All inputs are copied before return. Core-worker sessions execute on their + * worker. Caller-driver sessions publish driver work and complete only after + * the host services it on the graphics thread. The completion value is an + * owned mln_queried_feature_list handle. * * Box geometry is normalized and clipped to the viewport, so a box that * over-covers the viewport queries everything visible. A box that lies entirely * outside the viewport yields an empty result. Point and line-string geometry * are queried as given. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, geometry is - * null or invalid, options are invalid, out_result is null, or *out_result is - * not null. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. */ MLN_API mln_status mln_render_session_query_rendered_features( mln_render_session session, const mln_rendered_query_geometry* geometry, const mln_rendered_feature_query_options* options, - mln_queried_feature_list* out_result + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Queries source features from the latest render session state. - * - * The session renderer must already exist. source_id and options are borrowed - * for the duration of the call. Passing null for options uses default options. - * On success, *out_result receives an owned queried-feature list. Destroy it - * with mln_queried_feature_list_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, source_id is - * invalid or empty, options are invalid, out_result is null, or *out_result - * is not null. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Starts a source-feature query against the session's latest driver state. + * The completion value is an owned mln_queried_feature_list handle. */ MLN_API mln_status mln_render_session_query_source_features( mln_render_session session, mln_buffer_view source_id, const mln_source_feature_query_options* options, - mln_queried_feature_list* out_result + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Queries a feature extension from the latest render session state. - * - * The session renderer must already exist. source_id, feature, extension, - * extension_field, and arguments are borrowed for the duration of the call. - * feature contains one UTF-8 GeoJSON Feature. arguments may be null; when - * present, it contains a UTF-8 JSON object. On success, *out_result receives an - * owned buffer containing either a JSON value or a GeoJSON Feature - * Collection. Destroy it with mln_buffer_destroy(). - * - * The "supercluster" extension requires "cluster_id" feature properties and - * "limit" and "offset" arguments to be nonnegative integer JSON literals. - * Floating-point or negative representations are treated as absent and still - * produce MLN_STATUS_OK. An absent cluster ID produces JSON null. Absent limit - * and offset values use the native defaults of ten leaves at offset zero. The - * result buffer preserves integer JSON representations. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, source_id, - * feature, extension, extension_field, or arguments are invalid, out_result - * is null, or *out_result is not null. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Starts a feature-extension query against the latest driver state. The + * completion value is an owned mln_buffer handle containing JSON. */ MLN_API mln_status mln_render_session_query_feature_extensions( mln_render_session session, mln_buffer_view source_id, mln_buffer_view feature, mln_buffer_view extension, mln_buffer_view extension_field, const mln_buffer_view* arguments, - mln_buffer* out_result + const mln_completion* completion ) MLN_NOEXCEPT; #ifdef __cplusplus diff --git a/include/maplibre_native_c/render_session.h b/include/maplibre_native_c/render_session.h index c1ffaa706..2d39ec981 100644 --- a/include/maplibre_native_c/render_session.h +++ b/include/maplibre_native_c/render_session.h @@ -1,212 +1,285 @@ /** * @file maplibre_native_c/render_session.h - * Public C API declarations for render sessions. + * Public C API declarations for asynchronous render sessions. */ #ifndef MAPLIBRE_NATIVE_C_RENDER_SESSION_H #define MAPLIBRE_NATIVE_C_RENDER_SESSION_H #include +#include #include #include "base.h" -#include "map.h" +#include "completion.h" +#include "render_target.h" #ifdef __cplusplus extern "C" { #endif +/** Terminal disposition of one accepted frame demand. */ +typedef enum mln_render_result : uint32_t { + /** A frame was rendered for acquisition, presentation, or ordered readback. + */ + MLN_RENDER_RESULT_RENDERED = 0, + /** No newer map update was available. */ + MLN_RENDER_RESULT_NO_UPDATE = 1, + /** An ordered extent change had not reached the driver. */ + MLN_RENDER_RESULT_SIZE_PENDING = 2, + /** The target could not produce a frame. */ + MLN_RENDER_RESULT_TARGET_NOT_READY = 3, + /** A newer demand in the same coalescing boundary replaced this demand. */ + MLN_RENDER_RESULT_SUPERSEDED = 4, + /** The demand's timeout elapsed before driver work began. */ + MLN_RENDER_RESULT_DEADLINE_MISSED = 5, +} mln_render_result; + +/** Render-session lifecycle visible in snapshots. */ +typedef enum mln_render_session_state : uint32_t { + MLN_RENDER_SESSION_STATE_ATTACHING = 1U, + MLN_RENDER_SESSION_STATE_ATTACHED = 2U, + MLN_RENDER_SESSION_STATE_DETACHING = 3U, + MLN_RENDER_SESSION_STATE_DETACHED = 4U, + MLN_RENDER_SESSION_STATE_TARGET_LOST = 5U, + MLN_RENDER_SESSION_STATE_ABANDONED = 6U, +} mln_render_session_state; + +/** Frame-demand policy bits. */ +typedef enum mln_frame_demand_flag : uint32_t { + /** Render only when a newer map update exists. */ + MLN_FRAME_DEMAND_IF_NEEDED = 1U << 0U, + /** The target should present the rendered frame when it supports + presentation. */ + MLN_FRAME_DEMAND_PRESENT = 1U << 1U, +} mln_frame_demand_flag; + +/** One nonblocking request for a frame. */ +typedef struct mln_frame_demand { + uint32_t size; + /** A bitwise OR of mln_frame_demand_flag values. */ + uint32_t flags; + /** Host identity returned with the terminal frame result. */ + uint64_t token; + /** Demands coalesce only when this value and their flags match. */ + uint64_t coalescing_boundary; + /** Positive time allowed before driver work begins, in nanoseconds; zero has + * no limit. */ + uint64_t timeout_ns; +} mln_frame_demand; + +/** Immutable result record copied into an owned frame-result batch. */ +typedef struct mln_render_frame_result { + uint32_t size; + /** One mln_render_result value. */ + uint32_t disposition; + uint64_t token; + uint64_t map_update_generation; + uint64_t extent_generation; + /** Zero unless disposition is MLN_RENDER_RESULT_RENDERED. */ + uint64_t frame_generation; + /** + * Whether the map asked for another frame while it rendered this one, as + * during an ongoing camera transition. Set only when disposition is + * MLN_RENDER_RESULT_RENDERED, and false for every other outcome. This is the + * same signal that MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED carries in + * its needs_repaint field, delivered with the frame result so a host can + * re-arm its frame loop without the runtime event round trip. + */ + bool needs_repaint; +} mln_render_frame_result; + +/** Any-thread render-session snapshot. */ +typedef struct mln_render_session_snapshot { + uint32_t size; + /** One mln_render_session_state value. */ + uint32_t state; + /** One mln_render_driver_kind value. */ + uint32_t driver; + /** Most recent terminal mln_render_result value. */ + uint32_t latest_result; + mln_render_target_extent extent; + uint64_t generation; + uint64_t map_update_generation; + uint64_t rendered_update_generation; + uint64_t extent_generation; + uint64_t frame_generation; + uint64_t latest_demand_token; + uint32_t pending_demand_count; + uint32_t acquired_frame_count; + bool target_ready; + bool pending_changes; +} mln_render_session_snapshot; + +/** Result of irreversible CPU-side target abandonment. */ +typedef enum mln_render_abandon_disposition : uint32_t { + /** No graphics resources remained when control was abandoned. */ + MLN_RENDER_ABANDON_DISPOSITION_CLEAN = 0U, + /** Graphics resources could not be destroyed and were quarantined. */ + MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED = 1U, +} mln_render_abandon_disposition; + +typedef struct mln_render_abandon_result { + uint32_t size; + /** One mln_render_abandon_disposition value. */ + uint32_t disposition; + /** Backend resource groups intentionally retained until process exit. */ + uint32_t quarantined_resource_count; + uint32_t reserved; +} mln_render_abandon_result; + +/** Returns a zero-token, render-if-needed frame demand. */ +MLN_API mln_frame_demand mln_frame_demand_default(void) MLN_NOEXCEPT; + +/** Returns the immutable capabilities fixed during attachment. */ +MLN_API mln_status mln_render_session_get_capabilities( + mln_render_session session, mln_render_session_capabilities* out_capabilities +) MLN_NOEXCEPT; + +/** Copies the latest render-session snapshot from any native thread. */ +MLN_API mln_status mln_render_session_get_snapshot( + mln_render_session session, mln_render_session_snapshot* out_snapshot +) MLN_NOEXCEPT; + /** - * Resizes an attached render session. - * - * Width and height are logical map dimensions. The scale_factor value maps - * them to physical backend pixels. Resizing sets the map size, so the map - * viewport and the render target extent stay the same value. - * - * Surface and session-owned texture sessions resize in place. Caller-owned - * borrowed texture targets return MLN_STATUS_UNSUPPORTED because the texture is - * sized by its owner; hand a replacement over with the - * mln_*_borrowed_texture_set_target() function for the backend. See texture.h. - * - * The session renderer survives a resize, carrying the tile pyramid, glyph and - * image atlases, and symbol placement across to the new size. A scale_factor - * that differs from the session's current value retires the renderer instead, - * because its shaders are compiled for a fixed pixel ratio, and renderer-held - * tile and atlas state starts empty on the next - * mln_render_session_render_update(). Map state such as camera, style, sources, - * and feature state survives either way. The session pushes map feature state - * into a replacement renderer on the next render update. - * - * Passing a scale_factor that differs from the map's mln_map_options - * scale_factor logs a warning; see mln_map_options. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, dimensions - * are zero, scale_factor is non-positive or non-finite, or scaled dimensions - * are too large. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame is - * currently acquired. - * - MLN_STATUS_UNSUPPORTED when resizing is not supported by the session kind - * or mode, such as a caller-owned borrowed texture target. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Requests a frame without waiting. Every accepted demand produces one terminal + * result record. A core worker wakes itself; a caller driver publishes its + * driver-work endpoint. */ -MLN_API mln_status mln_render_session_resize( - mln_render_session session, uint32_t width, uint32_t height, - double scale_factor +MLN_API mln_status mln_render_session_request_frame( + mln_render_session session, const mln_frame_demand* demand ) MLN_NOEXCEPT; -/** Outcome of a successful mln_render_session_render_update() call. */ -typedef enum mln_render_result : uint32_t { - /** The call rendered a frame into the render target. */ - MLN_RENDER_RESULT_RENDERED = 0, - /** The call produced no frame. */ - MLN_RENDER_RESULT_NO_UPDATE, - /** The map has not applied the session's current size yet. */ - MLN_RENDER_RESULT_SIZE_PENDING, - /** The render target had no frame to draw into. */ - MLN_RENDER_RESULT_TARGET_NOT_READY, -} mln_render_result; +/** + * Drains every currently queued terminal frame result into an independently + * owned batch. The records remain stable until the batch is released. + */ +MLN_API mln_status mln_render_session_drain_frame_results( + mln_render_session session, mln_render_frame_batch* out_batch +) MLN_NOEXCEPT; + +/** Returns the number of records in an owned frame-result batch. */ +MLN_API mln_status mln_render_frame_batch_count( + mln_render_frame_batch batch, size_t* out_count +) MLN_NOEXCEPT; + +/** Copies one frame-result record. */ +MLN_API mln_status mln_render_frame_batch_get( + mln_render_frame_batch batch, size_t index, + mln_render_frame_result* out_result +) MLN_NOEXCEPT; + +/** Releases a frame-result batch. */ +MLN_API void mln_render_frame_batch_release( + mln_render_frame_batch batch +) MLN_NOEXCEPT; /** - * Renders the map's latest render update into the session's render target. - * - * A surface session presents the frame. A texture session writes it into the - * target texture. - * - * *out_result reports which of these outcomes the call reached, and each one - * names the wake that a host waits for before it calls again: - * - * - MLN_RENDER_RESULT_RENDERED means the target holds a new frame. The map - * retains its latest update, so a host redraws on demand after a resize or a - * surface expose and gates a frame loop on - * MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE. - * - MLN_RENDER_RESULT_NO_UPDATE means the call produced no frame. The map - * either has no update yet, or the Metal backend has not created an owned - * texture because content is not ready. Wait for - * MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE. - * - MLN_RENDER_RESULT_SIZE_PENDING means the session resized and the map, - * which applies its size on its own thread, is still behind. The map - * publishes an update for the new size on its own, so wait for the next - * MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE. - * - MLN_RENDER_RESULT_TARGET_NOT_READY means the render target had no frame - * available, such as a Metal surface whose next drawable is nil. No map - * update resolves this, so wait for a host event that changes the target, - * or back off and retry. - * - * In MLN_MAP_MODE_STATIC, pump a resize through the map before requesting the - * still image. The session applies its extent on the map's owner thread, and a - * still image requested before that lands reports - * MLN_RENDER_RESULT_SIZE_PENDING. - * - * *out_needs_repaint reports whether the map asked for another frame while it - * rendered this one, as during an ongoing camera transition. It is set only - * when *out_result is MLN_RENDER_RESULT_RENDERED, and reads false for every - * other outcome. This is the same signal that - * MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED carries in its needs_repaint - * field, delivered here without the event round trip, so a host can re-arm its - * frame loop before it drains events. - * - * Returns: - * - MLN_STATUS_OK on success, with *out_result and *out_needs_repaint set. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, or - * out_result or out_needs_repaint is null. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame - * is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Acquires the oldest rendered frame that is not already acquired. The frame + * owns its slot until release. The call is nonblocking. */ -MLN_API mln_status mln_render_session_render_update( - mln_render_session session, mln_render_result* out_result, - bool* out_needs_repaint +MLN_API mln_status mln_render_session_acquire_frame( + mln_render_session session, mln_acquired_frame* out_frame +) MLN_NOEXCEPT; + +/** Copies common metadata for an acquired frame. */ +MLN_API mln_status mln_acquired_frame_get_result( + mln_acquired_frame frame, mln_render_frame_result* out_result +) MLN_NOEXCEPT; + +/** Copies the producer synchronization for an acquired texture frame. */ +MLN_API mln_status mln_acquired_frame_get_producer_sync( + mln_acquired_frame frame, mln_gpu_sync* out_sync ) MLN_NOEXCEPT; /** - * Detaches backend-bound render resources from the map while keeping the - * session handle live for destruction. + * Releases an acquired frame after optional consumer GPU work. * - * After detach, resize, render, readback, acquire, and renderer maintenance - * operations return MLN_STATUS_INVALID_STATE. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live. - * - MLN_STATUS_INVALID_STATE when already detached or a texture frame is - * acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The call consumes *frame on success and sets it to MLN_HANDLE_NULL. The + * session retires the ring slot through its selected driver before reusing it. + * A synchronization kind the backend does not support fails with + * MLN_STATUS_UNSUPPORTED before the handle is consumed, so the caller keeps + * frame ownership. After abandonment the call closes the handle without + * graphics work. */ -MLN_API mln_status -mln_render_session_detach(mln_render_session session) MLN_NOEXCEPT; +MLN_API mln_status mln_acquired_frame_release( + mln_acquired_frame* frame, const mln_gpu_sync* consumer_completion +) MLN_NOEXCEPT; /** - * Destroys a render session handle. - * - * If the session is still attached, this function detaches it first. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live. - * - MLN_STATUS_INVALID_STATE when a texture frame is acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Starts an ordered logical resize. The completion runs after the selected + * driver applies the extent and updates the map viewport. */ -MLN_API mln_status -mln_render_session_destroy(mln_render_session session) MLN_NOEXCEPT; +MLN_API mln_status mln_render_session_resize( + mln_render_session session, const mln_render_target_extent* extent, + const mln_completion* completion +) MLN_NOEXCEPT; /** - * Asks the session renderer to release cached resources where possible. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Starts a barrier that completes after all render work accepted before it has + * a terminal result. A barrier does not request a frame. */ -MLN_API mln_status -mln_render_session_reduce_memory_use(mln_render_session session) MLN_NOEXCEPT; +MLN_API mln_status mln_render_session_barrier( + mln_render_session session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** Starts best-effort release of renderer caches. */ +MLN_API mln_status mln_render_session_reduce_memory_use( + mln_render_session session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** Starts asynchronous renderer-data clearing. */ +MLN_API mln_status mln_render_session_clear_data( + mln_render_session session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** Starts asynchronous renderer diagnostic-log emission. */ +MLN_API mln_status mln_render_session_dump_debug_logs( + mln_render_session session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** + * Services up to max_work items for a caller-graphics-thread driver; zero + * services every item currently queued. The first successful service call + * fixes the session's graphics-thread identity; later calls from another native + * thread return MLN_STATUS_WRONG_THREAD. The target context must be current. + * Core-worker sessions return MLN_STATUS_INVALID_STATE. + */ +MLN_API mln_status mln_render_session_service_driver_work( + mln_render_session session, size_t max_work, size_t* out_serviced +) MLN_NOEXCEPT; /** - * Clears renderer data for the session. + * Starts normal graphics-owner teardown and map detachment. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Outstanding acquired frames fail preflight with MLN_STATUS_INVALID_STATE and + * leave the session attached. Once accepted, earlier mailbox operations reach + * a terminal result before graphics resources are destroyed. */ -MLN_API mln_status -mln_render_session_clear_data(mln_render_session session) MLN_NOEXCEPT; +MLN_API mln_status mln_render_session_detach( + mln_render_session session, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Dumps renderer debug logs for the session through MapLibre Native logging. + * Irreversibly closes control and mailboxes without graphics calls. The call + * returns MLN_STATUS_BUSY while a driver call is in flight. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live. - * - MLN_STATUS_INVALID_STATE when the session is detached or no renderer has - * been created for the session yet. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no renderer - * backend, or when an internal exception is converted to status. + * Before returning, the call waits for the map's in-flight tile work, which + * can still reference quarantined renderer resources and through them the + * host's graphics objects. After it returns, no library thread touches the + * session's target or device, so the host may destroy them immediately. Do + * not call from a MapLibre worker callback. + */ +MLN_API mln_status mln_render_session_abandon( + mln_render_session session, mln_render_abandon_result* out_result +) MLN_NOEXCEPT; + +/** + * Retires a detached or abandoned session handle. The call is CPU-only and may + * run on any native thread. */ MLN_API mln_status -mln_render_session_dump_debug_logs(mln_render_session session) MLN_NOEXCEPT; +mln_render_session_destroy(mln_render_session session) MLN_NOEXCEPT; #ifdef __cplusplus } diff --git a/include/maplibre_native_c/render_target.h b/include/maplibre_native_c/render_target.h index 806a6ceed..e412289c6 100644 --- a/include/maplibre_native_c/render_target.h +++ b/include/maplibre_native_c/render_target.h @@ -9,6 +9,7 @@ #include #include "base.h" +#include "wake.h" #ifdef __cplusplus extern "C" { @@ -25,6 +26,94 @@ typedef struct mln_render_target_extent { double scale_factor; } mln_render_target_extent; +/** Execution placement for one render session. */ +typedef enum mln_render_driver_kind : uint32_t { + /** + * Native code owns a serial worker that initializes, drives, and tears down + * transferable graphics state. + */ + MLN_RENDER_DRIVER_CORE_WORKER = 1U, + /** + * The host explicitly calls the narrow driver API from the thread or realm + * where its graphics context is current. + */ + MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD = 2U, +} mln_render_driver_kind; + +/** Optional render-session capabilities. */ +typedef enum mln_render_session_capability_flag : uint32_t { + MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION = 1U << 0U, + MLN_RENDER_SESSION_CAPABILITY_READBACK = 1U << 1U, + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC = 1U << 2U, + MLN_RENDER_SESSION_CAPABILITY_PRESENTATION = 1U << 3U, +} mln_render_session_capability_flag; + +/** + * Common attachment policy copied before an attach call returns. + * + * A successful backend attach-start call publishes a session in ATTACHING + * state and accepts its completion together. Hosts may service caller-driver + * work through that session before attachment completes. A failed completion + * still requires normal detach or abandonment before session destruction. + */ +typedef struct mln_render_session_attach_options { + uint32_t size; + /** One mln_render_driver_kind value. */ + uint32_t driver; + /** + * Requested host-acquirable owned-texture slot count. Private targets grant + * one slot regardless of this value. Ignored by other targets. + */ + uint32_t requested_texture_ring_depth; + uint32_t reserved; + /** Wakes the receiver when the frame-result queue becomes nonempty. */ + mln_wake frame_wake; + /** Wakes the graphics receiver when caller-driver work is available. */ + mln_wake driver_work_wake; +} mln_render_session_attach_options; + +/** Driver and target capabilities fixed for one attached render session. */ +typedef struct mln_render_session_capabilities { + uint32_t size; + /** One mln_render_driver_kind value. */ + uint32_t driver; + /** Granted owned-texture slot count, or zero for a target without a ring. */ + uint32_t texture_ring_depth; + /** A bitwise OR of mln_render_session_capability_flag values. */ + uint32_t flags; +} mln_render_session_capabilities; + +/** Synchronization payload kind for acquired texture frames. */ +typedef enum mln_gpu_sync_kind : uint32_t { + /** The producer or consumer has completed before the API call returns. */ + MLN_GPU_SYNC_CPU_COMPLETE = 0U, + /** id plus a monotonically increasing signal value. */ + MLN_GPU_SYNC_METAL_SHARED_EVENT = 1U, + /** VkSemaphore plus a timeline value. */ + MLN_GPU_SYNC_VULKAN_TIMELINE_SEMAPHORE = 2U, + /** GLsync, used only by a caller-graphics-thread driver. */ + MLN_GPU_SYNC_OPENGL_FENCE = 3U, + /** A backend-defined WebGPU completion token. */ + MLN_GPU_SYNC_WEBGPU_TOKEN = 4U, +} mln_gpu_sync_kind; + +/** + * Backend synchronization copied by frame access and release calls. + * + * object is retained only for Metal. Vulkan, OpenGL, and WebGPU objects remain + * borrowed until a later session barrier or detach completes. + */ +typedef struct mln_gpu_sync { + uint32_t size; + /** One mln_gpu_sync_kind value. */ + uint32_t kind; + void* object; + uint64_t value; +} mln_gpu_sync; + +/** Returns CPU-complete synchronization for this C API version. */ +MLN_API mln_gpu_sync mln_gpu_sync_default(void) MLN_NOEXCEPT; + /** Metal backend context fields shared by Metal render targets. */ typedef struct mln_metal_context_descriptor { uint32_t size; @@ -85,17 +174,18 @@ typedef enum mln_opengl_context_platform : uint32_t { } mln_opengl_context_platform; /** - * How a session's OpenGL context relates to the thread that attached it. + * How a session's OpenGL context relates to its driver thread and host graphics + * state. * * A shared session leaves the thread as it found it: every render makes the * session context current and restores whatever was current before. The session * context joins the host share group named by the descriptor, so a host may * hand the session a texture and sample it from its own context. * - * A dedicated session owns the thread. It makes its context current once and - * keeps it current between renders, and it joins no share group. Use this when - * a thread exists to drive one render session and runs no other graphics work, - * such as an Android host that renders into a SurfaceView. + * A dedicated session owns its driver thread's context. It makes its context + * current once, keeps it current between renders, and joins no share group. A + * core worker can own transferable private graphics state. A host thread can + * own a surface context, such as an Android SurfaceView context. */ typedef enum mln_opengl_context_ownership : uint32_t { /** The session shares its thread with host graphics work. */ @@ -132,10 +222,10 @@ typedef struct mln_wgl_context_descriptor { /** EGL context fields shared by OpenGL render targets. */ typedef struct mln_egl_context_descriptor { uint32_t size; - /** Borrowed EGLDisplay. Required. */ + /** Borrowed EGLDisplay. Required and kept initialized through teardown. */ void* display; /** - * Borrowed EGLConfig used to create a shared session context. Required. + * Borrowed EGLConfig used to create the session context. Required. * OpenGL texture sessions require EGL_SURFACE_TYPE to include * EGL_PBUFFER_BIT. */ @@ -157,23 +247,41 @@ typedef struct mln_egl_context_descriptor { void* get_proc_address; } mln_egl_context_descriptor; +/** WebGL context placement. */ +typedef enum mln_webgl_context_kind : uint32_t { + /** Use a host-created context on its current browser agent. */ + MLN_WEBGL_CONTEXT_EXISTING = 0U, + /** + * Create a WebGL 2 context on a native worker whose pthread creation claims + * canvas_selector through Emscripten's transferred-canvases attribute. + */ + MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS = 1U, +} mln_webgl_context_kind; + /** WebGL context fields shared by OpenGL render targets in the browser. */ typedef struct mln_webgl_context_descriptor { uint32_t size; - /** Borrowed EMSCRIPTEN_WEBGL_CONTEXT_HANDLE. Must be positive. */ + /** One mln_webgl_context_kind value. */ + uint32_t kind; + /** Borrowed EMSCRIPTEN_WEBGL_CONTEXT_HANDLE for EXISTING. Must be positive. + */ int32_t context; + /** + * Copied UTF-8 Emscripten target selector for TRANSFERRED_CANVAS. The HTML + * canvas must still be transferable when attachment starts. + */ + mln_buffer_view canvas_selector; } mln_webgl_context_descriptor; /** OpenGL backend context fields shared by OpenGL render targets. */ typedef struct mln_opengl_context_descriptor { uint32_t size; - /** WGL or EGL context provider. */ + /** WGL, EGL, or WebGL context provider. */ mln_opengl_context_platform platform; /** - * Whether the session shares its thread with host graphics work. WGL and EGL - * surface sessions support both. A WebGL session renders through the host's - * own context and a texture session hands its texture to the host, so both - * are shared only. + * Whether the session shares its driver thread and graphics objects with the + * host. A private EGL owned texture and a transferred WebGL canvas are + * dedicated to their core worker. */ mln_opengl_context_ownership ownership; union { @@ -183,6 +291,10 @@ typedef struct mln_opengl_context_descriptor { } data; } mln_opengl_context_descriptor; +/** Returns default core-worker attachment policy with inherited completions. */ +MLN_API mln_render_session_attach_options +mln_render_session_attach_options_default(void) MLN_NOEXCEPT; + /** * Computes the physical device-pixel size of a logical render target extent. * diff --git a/include/maplibre_native_c/runtime.h b/include/maplibre_native_c/runtime.h index 834c61eb3..a6dc6db59 100644 --- a/include/maplibre_native_c/runtime.h +++ b/include/maplibre_native_c/runtime.h @@ -14,6 +14,8 @@ #include #include "base.h" +#include "completion.h" +#include "wake.h" #ifdef __cplusplus extern "C" { @@ -43,34 +45,6 @@ typedef enum mln_offline_region_download_state : uint32_t { MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE = 1, } mln_offline_region_download_state; -/** Offline database operation token. Zero is never a valid operation ID. */ -typedef uint64_t mln_offline_operation_id; - -/** Offline database operation kinds reported by completion events. */ -typedef enum mln_offline_operation_kind : uint32_t { - MLN_OFFLINE_OPERATION_AMBIENT_CACHE = 1, - MLN_OFFLINE_OPERATION_REGION_CREATE = 2, - MLN_OFFLINE_OPERATION_REGION_GET = 3, - MLN_OFFLINE_OPERATION_REGIONS_LIST = 4, - MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE = 5, - MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA = 6, - MLN_OFFLINE_OPERATION_REGION_GET_STATUS = 7, - MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED = 8, - MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE = 9, - MLN_OFFLINE_OPERATION_REGION_INVALIDATE = 10, - MLN_OFFLINE_OPERATION_REGION_DELETE = 11, - MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE = 12, -} mln_offline_operation_kind; - -/** Offline database operation result kinds reported by completion events. */ -typedef enum mln_offline_operation_result_kind : uint32_t { - MLN_OFFLINE_OPERATION_RESULT_NONE = 0, - MLN_OFFLINE_OPERATION_RESULT_REGION = 1, - MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION = 2, - MLN_OFFLINE_OPERATION_RESULT_REGION_LIST = 3, - MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS = 4, -} mln_offline_operation_result_kind; - /** Offline region status snapshot. */ typedef struct mln_offline_region_status { uint32_t size; @@ -127,10 +101,6 @@ typedef struct mln_offline_region_status { * error text; payload OFFLINE_REGION_RESPONSE_ERROR. * - OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED: code is 0; payload * OFFLINE_REGION_TILE_COUNT_LIMIT. - * - OFFLINE_OPERATION_COMPLETED: code is the operation result as an mln_status - * value, the same value the payload reports in result_status; message carries - * the failure text when the operation failed; payload - * OFFLINE_OPERATION_COMPLETED. */ typedef enum mln_runtime_event_type : uint32_t { MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE = 1, @@ -154,8 +124,7 @@ typedef enum mln_runtime_event_type : uint32_t { MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED = 19, MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR = 20, MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = 21, - MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED = 22, - MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 23, + MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED = 22, } mln_runtime_event_type; /** @@ -217,8 +186,6 @@ typedef enum mln_runtime_event_mask : uint64_t { 1ULL << MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR, MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED = 1ULL << MLN_RUNTIME_EVENT_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED, - MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED = - 1ULL << MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED, /** Selects every map-originated event type this version defines. */ MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS = MLN_RUNTIME_EVENT_MASK_MAP_CAMERA_WILL_CHANGE | @@ -244,14 +211,18 @@ typedef enum mln_runtime_event_mask : uint64_t { MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS = MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_STATUS_CHANGED | MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_RESPONSE_ERROR | - MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED | - MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED, + MLN_RUNTIME_EVENT_MASK_OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED, /** Selects every event type this version defines. */ MLN_RUNTIME_EVENT_MASK_ALL = MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS | MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS, } mln_runtime_event_mask; -/** Source kinds used by mln_runtime_event.source_type. */ +/** + * Source kinds used by mln_runtime_event.source_type. + * + * Value 2 is retired and no version reuses it. It named a projection source; + * projection calls are now synchronous and emit no events. + */ typedef enum mln_runtime_event_source_type : uint32_t { MLN_RUNTIME_EVENT_SOURCE_RUNTIME = 0, MLN_RUNTIME_EVENT_SOURCE_MAP = 1, @@ -260,8 +231,10 @@ typedef enum mln_runtime_event_source_type : uint32_t { /** * Payload kinds used by mln_runtime_event.payload_type. * - * Value 3 is retired and no version reuses it. It was a style-image-missing - * payload whose only content was the image ID that the event message carries. + * Values 3 and 8 are retired and no version reuses them. Value 3 was a + * style-image-missing payload whose only content was the image ID that the + * event message carries. Value 8 was an offline-operation completion payload; + * one-shot work now reports completion directly. */ typedef enum mln_runtime_event_payload_type : uint32_t { MLN_RUNTIME_EVENT_PAYLOAD_NONE = 0, @@ -271,8 +244,7 @@ typedef enum mln_runtime_event_payload_type : uint32_t { MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS = 5, MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR = 6, MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_TILE_COUNT_LIMIT = 7, - MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED = 8, - MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 9, + MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED = 8, } mln_runtime_event_payload_type; /** Camera change kinds reported by camera will-change and did-change events. */ @@ -397,7 +369,7 @@ typedef struct mln_runtime_options { /** Cache database path. Copied during runtime creation. */ const char* cache_path; /** - * Runtime-originated event types this runtime queues, as a bitwise OR of + * Runtime-scoped event types this runtime queues, as a bitwise OR of * mln_runtime_event_mask values. * * This field is always read, so set it explicitly. @@ -407,13 +379,15 @@ typedef struct mln_runtime_options { * mln_runtime_set_event_mask(). */ uint64_t event_mask; + /** Wakes the receiver when the runtime event queue becomes nonempty. */ + mln_wake event_wake; } mln_runtime_options; /** * Rendering statistics reported in MLN_RUNTIME_EVENT_PAYLOAD_RENDER_FRAME. * * This struct has no size field, because it is a member of - * mln_runtime_event_payload. mln_runtime_event_batch.event_size covers the + * mln_runtime_event_payload. mln_runtime_event_batch_view.event_size covers the * whole event, including its payload. */ typedef struct mln_rendering_stats { @@ -484,9 +458,8 @@ typedef struct mln_runtime_event_camera_transition_finished { typedef struct mln_runtime_event_offline_region_status { mln_offline_region_id region_id; /** - * Region status. This member keeps its own size field, because - * mln_offline_region_status is also the output struct of - * mln_runtime_offline_region_get_status_take_result(). + * Region status. This member keeps its own size field because the same struct + * is also returned by mln_runtime_offline_region_get_status(). */ mln_offline_region_status status; } mln_runtime_event_offline_region_status; @@ -504,19 +477,6 @@ typedef struct mln_runtime_event_offline_region_tile_count_limit { uint64_t limit; } mln_runtime_event_offline_region_tile_count_limit; -/** Payload for MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. */ -typedef struct mln_runtime_event_offline_operation_completed { - mln_offline_operation_id operation_id; - /** One of mln_offline_operation_kind. */ - uint32_t operation_kind; - /** One of mln_offline_operation_result_kind. */ - uint32_t result_kind; - /** Async result status as a mln_status value. */ - int32_t result_status; - /** Meaningful for MLN_OFFLINE_OPERATION_REGION_GET. */ - bool found; -} mln_runtime_event_offline_operation_completed; - /** * Typed event payload carried inline by every event. * @@ -526,7 +486,7 @@ typedef struct mln_runtime_event_offline_operation_completed { * A host that decodes a payload type this version does not define treats the * payload as opaque bytes and forwards them unchanged. Those bytes run from the * payload's offset within mln_runtime_event to - * mln_runtime_event_batch.event_size. + * mln_runtime_event_batch_view.event_size. */ typedef union mln_runtime_event_payload { mln_runtime_event_render_frame render_frame; @@ -536,7 +496,6 @@ typedef union mln_runtime_event_payload { mln_runtime_event_offline_region_response_error offline_region_response_error; mln_runtime_event_offline_region_tile_count_limit offline_region_tile_count_limit; - mln_runtime_event_offline_operation_completed offline_operation_completed; mln_runtime_event_camera_transition_finished camera_transition_finished; } mln_runtime_event_payload; @@ -546,8 +505,9 @@ typedef union mln_runtime_event_payload { * Events have a fixed stride and hold no pointers, so a host can copy a whole * batch with one memory copy. * - * Step through an array of these by mln_runtime_event_batch.event_size rather - * than by the size of this struct: a later version may add a member to + * Step through an array of these by + * mln_runtime_event_batch_view.event_size rather than by the size of this + * struct: a later version may add a member to * mln_runtime_event_payload and widen the stride. Every field below, payload * included, keeps its offset across versions. */ @@ -557,9 +517,8 @@ typedef struct mln_runtime_event { /** One of mln_runtime_event_source_type. */ uint32_t source_type; /** - * Source handle for this event: the mln_map for map-originated events, the - * mln_runtime for runtime-originated events, selected by source_type. Every - * handle type is uint64_t, so this needs no cast. + * Source handle selected by source_type: an mln_runtime or an mln_map. + * Every handle type is uint64_t, so this needs no cast. * * The value names one object for the life of the process, so a host may * compare it against a handle it holds even after that handle is released. @@ -575,9 +534,9 @@ typedef struct mln_runtime_event { uint32_t payload_type; /** * Byte offset of this event's message inside - * mln_runtime_event_batch.messages. Zero when message_size is 0. + * mln_runtime_event_batch_view.messages. Zero when message_size is 0. */ - uint32_t message_offset; + uint64_t message_offset; /** * Number of message bytes, excluding the trailing null terminator that * follows them in the arena. Zero when this event carries no message. @@ -588,23 +547,19 @@ typedef struct mln_runtime_event { } mln_runtime_event; /** - * A drained batch of runtime events. + * A borrowed view of one owned runtime-event batch. * - * A caller zero-initializes this struct, sets size, and passes it to - * mln_runtime_drain_events(). mln_runtime_event_batch_default() returns such a - * struct. + * Step through events by event_size. The event and message pointers remain + * valid until the event-batch handle is released. */ -typedef struct mln_runtime_event_batch { +typedef struct mln_runtime_event_batch_view { uint32_t size; /** * Stride of one event in bytes, at least sizeof(mln_runtime_event) in the * header a caller compiled against. Index events with this value. */ uint32_t event_size; - /** - * Borrowed array of event_count events in queue order. Null when event_count - * is 0. - */ + /** Borrowed array of event_count events in queue order. */ const mln_runtime_event* events; /** Number of events in events. */ size_t event_count; @@ -615,12 +570,7 @@ typedef struct mln_runtime_event_batch { const char* messages; /** Number of bytes in messages, including every terminator. */ size_t messages_size; - /** - * Events still queued for this runtime after this batch. A nonzero value - * means another drain reports more events. - */ - size_t remaining_count; -} mln_runtime_event_batch; +} mln_runtime_event_batch_view; typedef struct mln_resource_transform_response { uint32_t size; @@ -657,8 +607,7 @@ MLN_API mln_status mln_resource_transform_response_set_url( * * Callback invocations follow these rules: * - * - MapLibre may invoke the callback on a worker or network thread instead of - * the runtime owner thread. + * - MapLibre may invoke the callback on a runtime, worker, or network thread. * - The callback must be thread-safe, return quickly, and must not call C API * functions other than mln_resource_transform_response_set_url(). * - url and out_response are borrowed for the callback duration. @@ -667,19 +616,34 @@ MLN_API mln_status mln_resource_transform_response_set_url( * API-managed storage for the current transform invocation. * - A non-OK return status is treated as no rewrite and does not fail the * resource request. - * - The callback and user_data must remain valid until the transform is - * replaced, cleared, or the runtime is destroyed. Those calls wait for - * in-flight transform callbacks before returning. + * - The C API invokes release_user_data after the final callback returns. */ typedef mln_status (*mln_resource_transform_callback)( void* user_data, uint32_t kind, const char* url, mln_resource_transform_response* out_response ); +/** + * Releases callback user data after its final possible invocation. + * + * For each accepted registration with a non-null release callback, the C API + * invokes the callback exactly once after replacement, clear, or runtime + * teardown has retired the registration and every in-flight callback has + * returned. It + * may run on a runtime, worker, network, or closing thread. The C API never + * invokes it for a rejected registration. Another thread may retire an + * accepted registration before its registration function returns, so the + * caller transfers user_data ownership before entering that function and + * reclaims it only when registration is rejected. + */ +typedef void (*mln_runtime_callback_release)(void* user_data); + typedef struct mln_resource_transform { uint32_t size; mln_resource_transform_callback callback; void* user_data; + /** Optional. Invoked exactly once for each accepted registration. */ + mln_runtime_callback_release release_user_data; } mln_resource_transform; typedef struct mln_http_header_transform_response { @@ -718,13 +682,12 @@ MLN_API mln_status mln_http_header_transform_response_set( * transport starts the attempt. kind is one mln_resource_kind value and url is * the transformed URL that the transport will request. * - * The callback and user_data must be thread-safe and remain valid until the - * transform is replaced, cleared, or the runtime is destroyed. Those calls - * wait for in-flight callbacks before returning. url and out_response are + * The callback and user_data must be thread-safe. The C API invokes + * release_user_data after the final callback returns. url and out_response are * borrowed for the callback duration. Callback implementations call only - * mln_http_header_transform_response_set() and return promptly. A non-OK - * result discards every header collected during the invocation and lets the - * request proceed unchanged. + * mln_http_header_transform_response_set() and return promptly. A non-OK result + * discards every header collected during the invocation and lets the request + * proceed unchanged. */ typedef mln_status (*mln_http_header_transform_callback)( void* user_data, uint32_t kind, const char* url, @@ -735,6 +698,8 @@ typedef struct mln_http_header_transform { uint32_t size; mln_http_header_transform_callback callback; void* user_data; + /** Optional. Invoked exactly once for each accepted registration. */ + mln_runtime_callback_release release_user_data; } mln_http_header_transform; typedef struct mln_resource_request { @@ -799,9 +764,8 @@ typedef struct mln_resource_response { /** * Intercepts a network resource request. * - * The callback runs synchronously on the thread that reaches the C API network - * file source. That thread may be a MapLibre worker or network thread instead - * of the runtime owner thread. + * The callback runs synchronously on the MapLibre thread that reaches the C API + * network file source. * * Request handling follows these rules: * @@ -824,6 +788,7 @@ typedef struct mln_resource_response { * runtime C API functions. * - The callback may call resource request handle functions for the provided * handle. + * - The C API invokes release_user_data after the final callback returns. */ typedef uint32_t (*mln_resource_provider_callback)( void* user_data, const mln_resource_request* request, @@ -834,6 +799,8 @@ typedef struct mln_resource_provider { uint32_t size; mln_resource_provider_callback callback; void* user_data; + /** Optional. Invoked exactly once for each accepted registration. */ + mln_runtime_callback_release release_user_data; } mln_resource_provider; /** @@ -842,19 +809,10 @@ typedef struct mln_resource_provider { MLN_API mln_runtime_options mln_runtime_options_default(void) MLN_NOEXCEPT; /** - * Creates a runtime handle. - * - * The creating thread becomes the runtime owner thread. Each owner thread may - * hold one live runtime. + * Creates a runtime with a new core-owned worker. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when out_runtime is null, *out_runtime is not - * null, or options has an unsupported size, a nonzero flags value, or an - * event_mask bit outside MLN_RUNTIME_EVENT_MASK_ALL. - * - MLN_STATUS_INVALID_STATE when the current thread already owns a live - * runtime. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The function returns after worker initialization completes. The output must + * point to the null handle and receives ownership of the runtime on success. */ MLN_API mln_status mln_runtime_create( const mln_runtime_options* options, mln_runtime* out_runtime @@ -867,47 +825,53 @@ MLN_API mln_status mln_runtime_create( * non-network schemes such as file, asset, mbtiles, and pmtiles are handled by * native MainResourceLoader before this extension point. * - * This call may replace an existing provider while maps exist. The callback and - * user_data are stored by reference and must remain valid until this call - * returns having replaced them, mln_runtime_clear_resource_provider() returns, - * or the runtime is destroyed. When this call returns, no in-flight request can - * still invoke the previous provider. Requests the previous provider already - * took a handle for keep that handle: complete and release each one as usual. - * Native OnlineFileSource claims every remaining scheme, so a URL with a scheme - * MapLibre does not recognize, such as jar:file:, reaches this callback and - * completes as an HTTP error when the provider passes it through. + * The function copies the provider shape and accepts the change from any + * thread. Execution is ordered with every other command for this runtime. + * The completion reports the terminal command disposition. + * + * With a non-null release_user_data, MLN_STATUS_OK transfers responsibility for + * releasing user_data to the C API. With a null release_user_data, the caller + * keeps user_data valid until a committed replacement or clear command, or + * until native runtime teardown finishes. Requests that the previous provider + * already handled retain their request handles. + * + * Native OnlineFileSource claims every remaining scheme. A URL with an + * unrecognized scheme, such as jar:file:, reaches this callback and completes + * as an HTTP error when the provider passes it through. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the command is accepted. * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, provider is - * null, provider->size is too small, or callback is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * null, provider->size is too small, callback is null, or completion is + * invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ MLN_API mln_status mln_runtime_set_resource_provider( - mln_runtime runtime, const mln_resource_provider* provider + mln_runtime runtime, const mln_resource_provider* provider, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Clears the runtime-scoped network resource provider. * - * After this call succeeds, requests that reach the C API network file source - * go to MapLibre's online file source. When it returns, no in-flight request - * can still invoke the previous provider, and the C API holds no further - * reference to its callback or user_data. Requests the previous provider - * already took a handle for keep that handle: complete and release each one as - * usual. + * The function accepts the change from any thread and orders it with every + * other command for this runtime. The completion reports the terminal command + * disposition. The C API invokes the previous provider's release_user_data + * after every in-flight callback returns. Later requests use MapLibre's online + * file source. Requests that the previous provider already handled retain their + * handles. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the command is accepted. + * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or completion + * is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status -mln_runtime_clear_resource_provider(mln_runtime runtime) MLN_NOEXCEPT; +MLN_API mln_status mln_runtime_clear_resource_provider( + mln_runtime runtime, const mln_completion* completion +) MLN_NOEXCEPT; /** * Completes a C API resource provider request. @@ -983,36 +947,46 @@ MLN_API mln_status mln_resource_request_wait_until_retired( * requests. It does not apply to file, asset, database, MBTiles, or registered * C API provider responses intercepted before OnlineFileSource. * - * This call may replace an existing transform while maps exist. When it - * returns, no in-flight request can still invoke the previous transform. + * The function copies the transform shape and accepts the change from any + * thread. Execution is ordered with every other command for this runtime. + * The completion reports the terminal command disposition. + * With a non-null release_user_data, MLN_STATUS_OK transfers responsibility for + * releasing user_data to the C API. With a null release_user_data, the caller + * keeps user_data valid until a committed replacement or clear command, or + * until native runtime teardown finishes. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the command is accepted. * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, transform is - * null, transform->size is too small, or callback is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * null, transform->size is too small, callback is null, or completion is + * invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ MLN_API mln_status mln_runtime_set_resource_transform( - mln_runtime runtime, const mln_resource_transform* transform + mln_runtime runtime, const mln_resource_transform* transform, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Clears the runtime-scoped URL transform for network resources. * - * After this call succeeds, network resource URLs pass through unchanged. When - * it returns, no in-flight request can still invoke the previous transform. + * The function accepts the change from any thread and orders it with every + * other command for this runtime. The completion reports the terminal command + * disposition. The C API invokes the previous transform's release_user_data + * after every in-flight callback returns. Network resource URLs then pass + * through unchanged. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the command is accepted. + * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or completion + * is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status -mln_runtime_clear_resource_transform(mln_runtime runtime) MLN_NOEXCEPT; +MLN_API mln_status mln_runtime_clear_resource_transform( + mln_runtime runtime, const mln_completion* completion +) MLN_NOEXCEPT; /** * Registers or replaces the runtime-scoped outgoing HTTP header transform. @@ -1020,59 +994,60 @@ mln_runtime_clear_resource_transform(mln_runtime runtime) MLN_NOEXCEPT; * The transform applies only to requests that reach the built-in HTTP client, * including online and offline requests and nested network-backed PMTiles * range requests. Cache hits, non-HTTP schemes, and provider-handled requests - * do not invoke it. Replacement waits for in-flight callbacks before the old - * callback and user_data become unreferenced. + * do not invoke it. + * + * The function copies the transform shape and accepts the change from any + * thread. Execution is ordered with every other command for this runtime. + * The completion reports the terminal command disposition. + * With a non-null release_user_data, MLN_STATUS_OK transfers responsibility for + * releasing user_data to the C API. With a null release_user_data, the caller + * keeps user_data valid until a committed replacement or clear command, or + * until native runtime teardown finishes. * * Returns: - * - MLN_STATUS_OK on success. + * - MLN_STATUS_OK when the command is accepted. * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, transform is - * null, transform->size is too small, or callback is null. + * null, transform->size is too small, callback is null, or completion is + * invalid. * - MLN_STATUS_UNSUPPORTED on OpenHarmony and in the browser, whose HTTP * clients cannot prevent transformed headers from following a cross-origin * redirect. A resource provider serves those requests instead. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ MLN_API mln_status mln_runtime_set_http_header_transform( - mln_runtime runtime, const mln_http_header_transform* transform + mln_runtime runtime, const mln_http_header_transform* transform, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Clears the runtime-scoped outgoing HTTP header transform. * - * When this call returns, no in-flight request can still invoke the previous - * callback and the C API holds no reference to its user_data. + * The function accepts the change from any thread and orders it with every + * other command for this runtime. The completion reports the terminal command + * disposition. The C API invokes the previous transform's release_user_data + * after every in-flight callback returns. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the command is accepted. + * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or completion + * is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime is closing. + * - MLN_STATUS_NATIVE_ERROR when command acceptance fails. */ -MLN_API mln_status -mln_runtime_clear_http_header_transform(mln_runtime runtime) MLN_NOEXCEPT; +MLN_API mln_status mln_runtime_clear_http_header_transform( + mln_runtime runtime, const mln_completion* completion +) MLN_NOEXCEPT; /** * Starts a MapLibre ambient cache maintenance operation for this runtime. * * When runtime options omit cache_path, this operates on MapLibre's default * in-memory database and its effects are not durable beyond the native database - * lifetime. Completion is reported through - * MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or operation - * is not a mln_ambient_cache_operation value, or out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * lifetime. The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_run_ambient_cache_operation_start( - mln_runtime runtime, uint32_t operation, - mln_offline_operation_id* out_operation_id +MLN_API mln_status mln_runtime_run_ambient_cache_operation( + mln_runtime runtime, uint32_t operation, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1084,274 +1059,114 @@ MLN_API mln_status mln_runtime_run_ambient_cache_operation_start( * * When runtime options omit cache_path, this operates on MapLibre's default * in-memory database and its effects are not durable beyond the native database - * lifetime. Completion is reported through - * MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED. - * - * Returns: - * - MLN_STATUS_OK when the operation was accepted and out_operation_id was set. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * out_operation_id is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * lifetime. The completion reports the terminal status. */ -MLN_API mln_status mln_runtime_set_maximum_ambient_cache_size_start( - mln_runtime runtime, uint64_t size, mln_offline_operation_id* out_operation_id +MLN_API mln_status mln_runtime_set_maximum_ambient_cache_size( + mln_runtime runtime, uint64_t size, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Discards runtime-owned state for an offline database operation. - * - * Discarding does not cancel native database work. It drops stored results, - * removes queued completion events for the operation, and suppresses later - * completion delivery when the native operation is still pending. + * Starts an ordered runtime barrier. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or - * operation_id is zero or unknown. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The completion runs after every earlier accepted runtime submission has + * reached a terminal disposition. */ -MLN_API mln_status mln_runtime_offline_operation_discard( - mln_runtime runtime, mln_offline_operation_id operation_id +MLN_API mln_status mln_runtime_barrier( + mln_runtime runtime, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Destroys a runtime handle. - * - * The runtime must no longer own live maps. + * Releases a runtime after synchronous child preflight. * - * This call waits for in-flight resource transform, HTTP header transform, and - * resource provider callbacks before returning, the same way the corresponding - * set and clear functions do. Each callback and its user_data stay valid until - * that point and are unreferenced once this call returns, so a host frees - * callback-owned state only after it does. + * The call rejects a runtime that has live or pending children and leaves it + * open. A successful call consumes the public handle before returning. + * Previously accepted work and native teardown continue in submission order; + * callback user data remains native-owned until its release callback runs. + * This function may be called from any thread. * - * Do not call this while holding a host lock that one of those callbacks also - * acquires; the callback cannot finish, and this call cannot return. + * The completion runs after every earlier accepted submission, including + * released maps' teardown, has finished and the runtime's threads and + * resources are gone. The invoking thread touches no library state after the + * callback returns, so a host that waits for it may exit the process without + * racing native teardown. A host that outlives its runtimes may pass a + * discarding completion. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime - * handle. - * - MLN_STATUS_INVALID_STATE when runtime still owns live maps. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the creating - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * - MLN_STATUS_OK when the handle was consumed. + * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or + * completion is invalid. + * - MLN_STATUS_INVALID_STATE when the runtime has a live or pending child or is + * already closing. + * - MLN_STATUS_NATIVE_ERROR when teardown could not be scheduled. */ -MLN_API mln_status mln_runtime_destroy(mln_runtime runtime) MLN_NOEXCEPT; +MLN_API mln_status mln_runtime_release( + mln_runtime runtime, const mln_completion* completion +) MLN_NOEXCEPT; /** - * Advances this runtime. - * - * The call parks the owner thread when timeout_ms allows it, then drains the - * owner-thread task queues. Drain the queued runtime events with - * mln_runtime_drain_events() afterwards. - * - * timeout_ms sets the park bound: - * - * - Zero drains and returns. Hosts pumping from a frame callback pass zero. - * - A positive value parks for up to that many milliseconds, then drains. - * - A negative value parks until a wake arrives, then drains. - * - * The drain runs every task queued when it begins plus every task those tasks - * enqueue, and services expired timers and ready file descriptors for the - * runtime's own network and database work. - * - * budget_ms bounds the drain: - * - * - A negative value drains without a bound. One unbounded drain can span a - * full style parse, so budget for it as variable work. - * - Zero or a positive value stops the drain at the first task boundary after - * that many milliseconds, measured from the start of the drain. The first - * queued task always runs, so a bounded pump always makes progress. Tasks - * left behind set the wake flag, so the next pump returns without parking - * and continues them. - * - * The budget bounds the task queues alone. Expired timers and ready file - * descriptors are serviced regardless, and a single task runs to completion - * once started, so one long task can overrun the budget. + * Drains this runtime's queued events into a new owned batch. * - * The runtime holds a wake flag. These set it: + * The drain transfers every event that the queue holds, in queue order. Events + * that arrive later enter the next batch. The returned handle owns the event + * records and their message arena. Later drains and runtime destruction leave + * the batch readable. Release each batch with mln_event_batch_release(). * - * - the owner-thread run loop receiving queued work from any thread, which - * covers style, tile, offline database, and resource responses; - * - the runtime queueing a runtime event that a subscription mask selects; - * - mln_wake_source_signal() from any thread. + * This queue operation may be called from any thread. Concurrent drains are + * serialized and each event enters exactly one returned batch. * - * A parking call returns as soon as the flag is set, and clears the flag before - * it returns. Work that arrives during the drain sets the flag again, so the - * next call returns right away and may find that work already done. - * - * A call also returns without parking while unread runtime events are queued. A - * narrowed subscription leaves the queue empty more often, so a parking call - * parks where it previously returned immediately. - * - * Timers and file descriptors set the flag only when they queue owner-thread - * work, and the runtime registers none of its own on the owner-thread run loop. - * Pass a positive timeout_ms so a call returns even when nothing sets the flag. - * - * A non-zero timeout_ms makes this a blocking query. Call it outside any host - * lock that a thread signalling a wake source acquires, and outside C API - * callbacks. Acquire a wake source with mln_runtime_wake_source_acquire() to - * release the owner thread for host-driven work such as submitted tasks or - * shutdown. + * The map and runtime subscription masks suppress unselected events before + * their payloads, messages, queue records, and wakeups are produced. * * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime - * handle. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. + * - MLN_STATUS_OK when out_batch receives an owned batch, including an empty + * batch. + * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not live, or out_batch + * is null or does not point to the null handle. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ -MLN_API mln_status mln_runtime_pump( - mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms +MLN_API mln_status mln_runtime_drain_events( + mln_runtime runtime, mln_event_batch* out_batch ) MLN_NOEXCEPT; -/** - * Acquires a wake source that releases this runtime's parked owner thread. - * - * Each call returns a distinct handle the host destroys with - * mln_wake_source_destroy(). A wake source holds its own reference to the - * runtime's wake state, so it stays valid after the runtime is destroyed and - * hosts tear the two down in either order. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime - * handle, out_source is null, or *out_source is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_runtime_wake_source_acquire( - mln_runtime runtime, mln_wake_source* out_source +/** Borrows the event and message view stored by an owned event batch. */ +MLN_API mln_status mln_event_batch_get( + mln_event_batch batch, mln_runtime_event_batch_view* out_view ) MLN_NOEXCEPT; -/** - * Sets the runtime's wake flag and releases the parked owner thread. - * - * This function may be called from any thread. - * - * A signal raised while the owner thread is running sets the wake flag, so the - * next mln_runtime_pump() call returns without parking. Signalling a wake - * source whose runtime is destroyed succeeds and does nothing, so hosts shut - * the two down in either order. - * - * Returns: - * - MLN_STATUS_OK on success, including after the runtime is destroyed. - * - MLN_STATUS_INVALID_ARGUMENT when source is null. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_wake_source_signal(mln_wake_source source) MLN_NOEXCEPT; - -/** - * Destroys a wake source. - * - * This function may be called from any thread. Null is a no-op. Destroy each - * handle exactly once, once every thread that signals it has finished. - */ -MLN_API void mln_wake_source_destroy(mln_wake_source source) MLN_NOEXCEPT; - -/** Returns a zeroed mln_runtime_event_batch with size filled in. */ -MLN_API mln_runtime_event_batch -mln_runtime_event_batch_default(void) MLN_NOEXCEPT; - -/** - * Drains this runtime's queued runtime events into one borrowed batch. - * - * Events arrive in queue order. Map-originated events set source_type to - * MLN_RUNTIME_EVENT_SOURCE_MAP and source to the source map; - * runtime-originated events set source_type to - * MLN_RUNTIME_EVENT_SOURCE_RUNTIME and source to this runtime. - * - * max_events bounds the drain. Zero drains every queued event. A positive value - * drains at most that many events and reports the number that stayed queued in - * out_batch->remaining_count. A drain also stops when one more message would - * take the message arena past 4 GiB, so read out_batch->remaining_count after - * an unbounded drain too. - * - * Read a payload as the mln_runtime_event_payload member that - * event.payload_type selects. Read a message as event.message_size bytes at - * out_batch->messages plus event.message_offset. Every offset and size pair - * this call writes lies inside the arena. - * - * Copy any value a host keeps, because out_batch->events and - * out_batch->messages point at runtime-owned storage that stays readable only - * until the next mln_runtime_drain_events() call for the same runtime or until - * the runtime is destroyed. Every other C API call leaves the batch readable, - * including calls on the maps this runtime owns and mln_map_destroy() for a map - * whose events the batch carries. Every drain invalidates the batch before it, - * including a drain that finds no events. - * - * Destroying a map discards that map's queued events, so this call reports - * events only for maps that were live when the events were queued. Read the - * state a host mirrors from events before destroying the map that produces - * them. - * - * The map and runtime subscription masks decide which events reach the queue. - * An event of an unselected type is never built and never queued, so it reaches - * no batch and raises no wake flag. See mln_map_set_event_mask() and - * mln_runtime_set_event_mask(). - * - * Draining is a queue operation: it runs no owner-thread work and never parks. - * Call mln_runtime_pump() to advance the runtime, then drain the events that - * pump produced. - * - * This function clears the calling thread's diagnostic message, so read - * mln_thread_last_error_message() for a failed call before draining. - * - * Returns: - * - MLN_STATUS_OK when the drain completed, including when it found no events. - * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime - * handle, out_batch is null, or out_batch->size is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_runtime_drain_events( - mln_runtime runtime, size_t max_events, mln_runtime_event_batch* out_batch -) MLN_NOEXCEPT; +/** Releases an owned event batch. A null handle is a no-op. */ +MLN_API void mln_event_batch_release(mln_event_batch batch) MLN_NOEXCEPT; /** - * Selects which runtime-originated event types this runtime queues. + * Selects which runtime-scoped event types this runtime queues. * * A runtime queues an offline event when this mask selects its type. Region * status, response error, and tile count limit events also require the region - * to be observed with mln_runtime_offline_region_set_observed_start(), so this + * to be observed with mln_runtime_offline_region_set_observed(), so this * mask narrows that subscription rather than replacing it. * - * A runtime that has not been narrowed selects every runtime-originated event - * type this library reports, which covers types a caller's header may not + * A runtime that has not been narrowed selects every runtime-scoped event type + * this library reports, which covers types a caller's header may not * declare. A new mask applies to later events and keeps the events already * queued. * * This call reads the bits in MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS and - * ignores the rest, so MLN_RUNTIME_EVENT_MASK_ALL selects every - * runtime-originated type. mln_runtime_get_event_mask() reports the value last + * ignores the rest, so MLN_RUNTIME_EVENT_MASK_ALL selects every runtime-scoped + * type. mln_runtime_get_event_mask() reports the value last * set, so a host reads it, changes one bit, and writes it back. * - * A host that clears MLN_RUNTIME_EVENT_MASK_OFFLINE_OPERATION_COMPLETED still - * takes each result with the matching take-result entry point, and still reads - * a failed operation's error text from that call's thread diagnostic. An - * offline operation records its result before this mask is consulted. + * Changing this mask does not affect one-shot completion or result ownership. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime * handle, or mask holds a bit outside MLN_RUNTIME_EVENT_MASK_ALL. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_runtime_set_event_mask(mln_runtime runtime, uint64_t mask) MLN_NOEXCEPT; /** - * Reports which runtime-originated event types this runtime queues. + * Reports which runtime-scoped event types this runtime queues. * * The value is the mask last set, including bits outside * MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS that this runtime ignores. A @@ -1362,8 +1177,6 @@ mln_runtime_set_event_mask(mln_runtime runtime, uint64_t mask) MLN_NOEXCEPT; * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when runtime is null or not a live runtime * handle, or out_mask is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the runtime - * owner thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_runtime_get_event_mask( diff --git a/include/maplibre_native_c/style.h b/include/maplibre_native_c/style.h index 414297d4e..fe59ec93d 100644 --- a/include/maplibre_native_c/style.h +++ b/include/maplibre_native_c/style.h @@ -14,6 +14,7 @@ #include #include "base.h" +#include "completion.h" #include "map.h" #ifdef __cplusplus @@ -24,7 +25,30 @@ typedef uint64_t mln_style_id_list; typedef uint64_t mln_style_string_list; typedef uint64_t mln_geojson_source_data; -/** Style source type values returned by mln_map_get_style_source_type(). */ +/** + * @name Style execution contract + * + * Every mutation is a command. It validates and copies all input bytes, arrays, + * option structs, and image pixels before returning. Application runs later in + * runtime order. Its completion reports committed, superseded, failed, or + * cancelled disposition. Committed completions carry the map snapshot + * generation they published, so a caller can fence a later + * mln_map_snapshot_get() on it. + * + * Every query is ordered. It copies its inputs before returning and observes + * all commands accepted earlier by the runtime. Its completion borrows the + * typed result for the duration of the callback; owned handle values inside a + * successful result transfer to the receiver. + * + * All declarations in this header are callable from any thread. Submission + * return values describe validation and acceptance failures. + * State-dependent style errors described in the per-function Returns sections + * are delivered through the completion. Command inputs described below as + * borrowed remain caller-owned; accepted calls have already copied them when + * they return. + */ + +/** Style source type values returned by source metadata queries. */ typedef enum mln_style_source_type : uint32_t { MLN_STYLE_SOURCE_TYPE_UNKNOWN = 0, MLN_STYLE_SOURCE_TYPE_VECTOR = 1, @@ -40,7 +64,7 @@ typedef enum mln_style_source_type : uint32_t { /** Fields available in mln_style_source_info. */ typedef enum mln_style_source_info_field : uint32_t { - /** The source retains a URL. Copy it with mln_map_copy_style_source_url(). */ + /** The source retains a URL. */ MLN_STYLE_SOURCE_INFO_URL = 1U << 0U, /** The tile source was defined with an inline TileJSON description. */ MLN_STYLE_SOURCE_INFO_TILEJSON = 1U << 1U, @@ -54,6 +78,14 @@ typedef enum mln_style_source_info_field : uint32_t { MLN_STYLE_SOURCE_INFO_RASTER_ENCODING = 1U << 5U, } mln_style_source_info_field; +/** Fields available in mln_style_layer_info. */ +typedef enum mln_style_layer_info_field : uint32_t { + /** The layer carries a source ID. */ + MLN_STYLE_LAYER_INFO_SOURCE_ID = 1U << 0U, + /** The layer carries a source-layer ID. */ + MLN_STYLE_LAYER_INFO_SOURCE_LAYER = 1U << 1U, +} mln_style_layer_info_field; + /** Field mask values for mln_style_tile_source_options. */ typedef enum mln_style_tile_source_option_field : uint32_t { MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM = 1U << 0U, @@ -98,6 +130,16 @@ typedef struct mln_image_stretch { float to; } mln_image_stretch; +/** Borrowed image-stretch arrays available during a completion callback. */ +typedef struct mln_style_image_stretches_result { + uint32_t size; + uint32_t reserved; + const mln_image_stretch* stretch_x; + size_t stretch_x_count; + const mln_image_stretch* stretch_y; + size_t stretch_y_count; +} mln_style_image_stretches_result; + /** * Content-box insets in image pixels, measured from the image's top-left. * @@ -117,7 +159,7 @@ typedef enum mln_style_image_text_fit : uint32_t { MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL = 2, } mln_style_image_text_fit; -/** Layer visibility values used by the layer visibility accessors. */ +/** Layer visibility values used by the visibility setter and layer info. */ typedef enum mln_style_layer_visibility : uint32_t { MLN_STYLE_LAYER_VISIBILITY_VISIBLE = 0, MLN_STYLE_LAYER_VISIBILITY_NONE = 1, @@ -181,7 +223,7 @@ typedef enum mln_location_indicator_image_kind : uint32_t { MLN_LOCATION_INDICATOR_IMAGE_KIND_SHADOW = 2, } mln_location_indicator_image_kind; -/** Fixed source metadata returned by mln_map_get_style_source_info(). */ +/** Fixed source metadata included in mln_style_source_result. */ typedef struct mln_style_source_info { uint32_t size; /** One of mln_style_source_type. */ @@ -214,6 +256,48 @@ typedef struct mln_style_source_info { uint32_t raster_encoding; } mln_style_source_info; +/** Complete source metadata borrowed for a completion callback. */ +typedef struct mln_style_source_result { + uint32_t size; + uint32_t reserved; + mln_style_source_info info; + mln_buffer_view attribution; + mln_buffer_view url; + const mln_buffer_view* tile_urls; + size_t tile_url_count; +} mln_style_source_result; + +/** Fixed layer metadata included in mln_style_layer_result. */ +typedef struct mln_style_layer_info { + uint32_t size; + /** Bitwise combination of mln_style_layer_info_field values. */ + uint32_t fields; + /** View of a static style-spec layer type string. It stays valid for the + life of the process. */ + mln_buffer_view type; + /** Lowest zoom at which the layer draws; -INFINITY with no lower bound. */ + double min_zoom; + /** Highest zoom at which the layer draws; INFINITY with no upper bound. */ + double max_zoom; + /** One of mln_style_layer_visibility. */ + uint32_t visibility; + /** Source ID byte length, excluding any null terminator; 0 when fields + lacks SOURCE_ID. */ + size_t source_id_size; + /** Source-layer byte length, excluding any null terminator; 0 when fields + lacks SOURCE_LAYER. */ + size_t source_layer_size; +} mln_style_layer_info; + +/** Complete layer metadata borrowed for a completion callback. */ +typedef struct mln_style_layer_result { + uint32_t size; + uint32_t reserved; + mln_style_layer_info info; + mln_buffer_view source_id; + mln_buffer_view source_layer; +} mln_style_layer_result; + /** Options for vector and raster tile sources. */ typedef struct mln_style_tile_source_options { uint32_t size; @@ -324,18 +408,21 @@ typedef struct mln_custom_geometry_source_options { bool clip; bool wrap; /** - * Optional. Invoked once when this API stops referencing user_data. + * Optional. Invoked once after an accepted command stops referencing + * user_data. * - * The call runs on the map owner thread when the source is removed - * explicitly, when a style load replaces the style that held the source, or - * when the map is destroyed. It runs at most once, and it does not run when - * adding the source failed. A host frees its callback state here. Null means - * the host needs no release. + * The call runs on the runtime execution thread when the source is removed + * explicitly, when a style command replaces the style that held the source, + * when the map is destroyed, or when asynchronous application rejects the + * source. It runs at most once for each accepted command. A synchronous + * rejection before command acceptance invokes no release. A host transfers + * its callback state to each accepted command and frees that state here. Null + * means the host needs no release. * - * This callback must not destroy its map, because a release that a style load - * drives runs inside MapLibre's style-load dispatch. Free callback state and - * return. A host that wants to destroy the map does so from its own call - * after the pump that reported the load returns. + * This callback must not destroy its map, because a release that style + * processing drives may run inside MapLibre's dispatch. Free callback state + * and return. A host that wants to destroy the map does so from its own call + * after the release returns. */ mln_custom_geometry_source_release_callback release_user_data; } mln_custom_geometry_source_options; @@ -361,18 +448,21 @@ typedef struct mln_custom_mvt_vector_source_options { double min_zoom; double max_zoom; /** - * Optional. Invoked once when this API stops referencing user_data. + * Optional. Invoked once after an accepted command stops referencing + * user_data. * - * The call runs on the map owner thread when the source is removed - * explicitly, when a style load replaces the style that held the source, or - * when the map is destroyed. It runs at most once, and it does not run when - * adding the source failed. A host frees its callback state here. Null means - * the host needs no release. + * The call runs on the runtime execution thread when the source is removed + * explicitly, when a style command replaces the style that held the source, + * when the map is destroyed, or when asynchronous application rejects the + * source. It runs at most once for each accepted command. A synchronous + * rejection before command acceptance invokes no release. A host transfers + * its callback state to each accepted command and frees that state here. + * Null means the host needs no release. * - * This callback must not destroy its map, because a release that a style load - * drives runs inside MapLibre's style-load dispatch. Free callback state and - * return. A host that wants to destroy the map does so from its own call - * after the pump that reported the load returns. + * This callback must not destroy its map, because a release that style + * processing drives may run inside MapLibre's dispatch. Free callback state + * and return. A host that wants to destroy the map does so from its own call + * after the release returns. */ mln_custom_mvt_vector_source_release_callback release_user_data; } mln_custom_mvt_vector_source_options; @@ -427,8 +517,7 @@ typedef struct mln_style_image_info { uint32_t stride; size_t byte_length; /** - * Interval counts for the stretchable axes. Read the intervals themselves - * with mln_map_copy_style_image_stretches(). + * Interval counts for the stretchable axes. */ size_t stretch_x_count; size_t stretch_y_count; @@ -445,6 +534,18 @@ typedef struct mln_style_image_info { bool has_text_fit_height; } mln_style_image_info; +/** Complete style image borrowed for a completion callback. */ +typedef struct mln_style_image_result { + uint32_t size; + uint32_t reserved; + mln_style_image_info info; + mln_buffer_view pixels; + const mln_image_stretch* stretch_x; + size_t stretch_x_count; + const mln_image_stretch* stretch_y; + size_t stretch_y_count; +} mln_style_image_result; + /** * Global style transition options. * @@ -603,188 +704,111 @@ MLN_API void mln_style_string_list_destroy( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, source_json is empty or invalid, the source ID already - * exists, or the source JSON cannot be converted. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, source_json is empty or invalid, the source JSON cannot + * be converted. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_style_source_json( - mln_map map, mln_buffer_view source_id, mln_buffer_view source_json + mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Removes one style source by ID. * - * source_id is borrowed for the call. On success, out_removed reports whether a - * source existed and was removed. + * source_id is borrowed for the call. The command commits when a source with + * that ID existed and was removed. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, or out_removed is null. + * invalid or empty, or completion is invalid. + * - MLN_STATUS_NOT_FOUND when no style source has that ID. * - MLN_STATUS_INVALID_STATE when the source exists but a layer still uses it. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_remove_style_source( - mln_map map, mln_buffer_view source_id, bool* out_removed + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Reports whether a style source ID exists. + * Copies complete metadata for one style source. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, or out_exists is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_style_source_exists( - mln_map map, mln_buffer_view source_id, bool* out_exists -) MLN_NOEXCEPT; - -/** - * Gets one style source type. - * - * On success, out_found reports whether source_id exists. When found, - * out_source_type receives one of mln_style_source_type. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_source_type is null, or out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_style_source_type( - mln_map map, mln_buffer_view source_id, uint32_t* out_source_type, - bool* out_found -) MLN_NOEXCEPT; - -/** - * Copies fixed metadata for one style source. - * - * The returned struct contains string lengths and fixed inline TileJSON - * fields, not string contents. Use - * mln_map_copy_style_source_attribution() and - * mln_map_copy_style_source_url() to copy individual strings, and - * mln_map_get_style_source_tile_urls() to copy inline tile URLs. The source ID - * is the lookup key and is also available through style source ID lists. + * A found source completes with one borrowed mln_style_source_result. A missing + * source completes successfully with no value. The binding must copy strings + * and tile URL views before the callback returns. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_info is null, out_info->size is too small, or - * out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_source_info( - mln_map map, mln_buffer_view source_id, mln_style_source_info* out_info, - bool* out_found + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies one style source attribution string into caller-owned memory. + * Copies one style source attribution string. * - * source_id is borrowed for the call. On success, out_attribution_size receives - * the byte length of the attribution, excluding any null terminator. When - * out_found is false or the source has no attribution, out_attribution_size - * receives 0. - * - * Passing null for out_attribution with a capacity of 0 is a size probe: it - * reports the required byte length and succeeds, so a caller can size a buffer - * without treating the result as a failure. With a non-null out_attribution, a - * capacity smaller than the required length still reports that length and - * returns MLN_STATUS_INVALID_ARGUMENT. + * A found attribution completes with one borrowed mln_buffer_view. A missing + * source or attribution completes successfully with no value. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_attribution is null with non-zero capacity, - * attribution_capacity is too small for a non-null buffer, - * out_attribution_size is null, or out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_style_source_attribution( - mln_map map, mln_buffer_view source_id, char* out_attribution, - size_t attribution_capacity, size_t* out_attribution_size, bool* out_found + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies one style source URL into caller-owned memory. - * - * source_id is borrowed for the call. On success, out_url_size receives the URL - * byte length, excluding any null terminator. When out_found is false or the - * source has no URL, out_url_size receives 0. + * Copies one style source URL. * - * Passing null for out_url with a capacity of 0 is a size probe. It reports the - * required byte length and succeeds. With a non-null out_url, a capacity - * smaller than the required length still reports that length and returns - * MLN_STATUS_INVALID_ARGUMENT. + * A found URL completes with one borrowed mln_buffer_view. A missing source or + * URL completes successfully with no value. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_url is null with non-zero capacity, url_capacity is - * too small for a non-null buffer, out_url_size is null, or out_found is - * null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_style_source_url( - mln_map map, mln_buffer_view source_id, char* out_url, size_t url_capacity, - size_t* out_url_size, bool* out_found + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies one style source's inline TileJSON tile URLs into an owned list. + * Copies one style source's inline TileJSON tile URLs. * - * On success, out_found reports whether source_id exists. When found, - * *out_tile_urls receives an owned list. A URL-backed tile source and every - * source without inline TileJSON return an empty list. Loading a URL-backed - * source does not change this result. Destroy the list with - * mln_style_string_list_destroy(). + * A found source completes with a borrowed array of mln_buffer_view values. A + * URL-backed source or source without inline TileJSON returns an empty array. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_tile_urls is null, *out_tile_urls is not null, or - * out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_source_tile_urls( - mln_map map, mln_buffer_view source_id, mln_style_string_list* out_tile_urls, - bool* out_found + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** * Copies style source IDs in style order. * - * On success, *out_source_ids receives an owned list handle. Destroy it with - * mln_style_id_list_destroy(). + * The completion borrows an array of mln_buffer_view values. * * Returns: * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_source_ids is - * null, or *out_source_ids is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or completion is + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_list_style_source_ids( - mln_map map, mln_style_id_list* out_source_ids + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -797,14 +821,12 @@ MLN_API mln_status mln_map_list_style_source_ids( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url - * is invalid or empty, options is invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * is invalid or empty, options is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_geojson_source_url( mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_geojson_source_options* options + const mln_geojson_source_options* options, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -865,14 +887,12 @@ MLN_API void mln_geojson_source_data_destroy( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, data is null or not live, or the source ID already - * exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or data is null or not live. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data + mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -886,12 +906,11 @@ MLN_API mln_status mln_map_add_geojson_source_data( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url * is invalid or empty, the source does not exist, or the source is not a * GeoJSON source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_geojson_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -916,12 +935,11 @@ MLN_API mln_status mln_map_set_geojson_source_url( * invalid or empty, data is null or not live, the source does not exist, the * source is not a GeoJSON source, or the data was prepared with options that * do not match the source's options. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data + mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -930,7 +948,7 @@ MLN_API mln_status mln_map_set_geojson_source_data( * source_id is borrowed for the call. While enabled is true, the source slices * requested tiles inline during the update pass, as if the source's options * had set synchronous_tiling; false restores the option the source was added - * with. The override applies to update passes after this call returns. Hosts + * with. The override applies to update passes after the command commits. Hosts * enable it around high-frequency small updates, such as a tracked position, * so each installed update reaches the next rendered frame. * @@ -939,12 +957,11 @@ MLN_API mln_status mln_map_set_geojson_source_data( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, the source does not exist, or the source is not a * GeoJSON source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_geojson_source_synchronous_tiling( - mln_map map, mln_buffer_view source_id, bool enabled + mln_map map, mln_buffer_view source_id, bool enabled, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -957,14 +974,12 @@ MLN_API mln_status mln_map_set_geojson_source_synchronous_tiling( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url - * is invalid or empty, options is invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * is invalid or empty, options is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_vector_source_url( mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options + const mln_style_tile_source_options* options, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -978,14 +993,13 @@ MLN_API mln_status mln_map_add_vector_source_url( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile URLs are null, empty, or invalid, options is - * invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_vector_source_tiles( mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options + size_t tile_count, const mln_style_tile_source_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -997,14 +1011,12 @@ MLN_API mln_status mln_map_add_vector_source_tiles( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url - * is invalid or empty, options is invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * is invalid or empty, options is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_raster_source_url( mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options + const mln_style_tile_source_options* options, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1018,14 +1030,13 @@ MLN_API mln_status mln_map_add_raster_source_url( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile URLs are null, empty, or invalid, options is - * invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_raster_source_tiles( mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options + size_t tile_count, const mln_style_tile_source_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1038,14 +1049,12 @@ MLN_API mln_status mln_map_add_raster_source_tiles( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url - * is invalid or empty, options is invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * is invalid or empty, options is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_raster_dem_source_url( mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options + const mln_style_tile_source_options* options, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1059,14 +1068,13 @@ MLN_API mln_status mln_map_add_raster_dem_source_url( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile URLs are null, empty, or invalid, options is - * invalid, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_raster_dem_source_tiles( mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options + size_t tile_count, const mln_style_tile_source_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1082,9 +1090,9 @@ MLN_API mln_status mln_map_add_raster_dem_source_tiles( * mln_map_set_style_json() returns successfully. * * fetch_tile and cancel_tile may run on arbitrary native worker threads, may be - * concurrent with owner-thread map calls, and must not call thread-affine map - * APIs directly. Queue work back to the map owner thread before calling - * mln_map_set_custom_geometry_source_tile_data() or invalidation functions. + * concurrent with runtime and map calls. The style command functions in this + * header may be called directly from these callbacks. Callbacks must not block + * waiting for command execution. * Callbacks must not throw, panic, longjmp, or otherwise unwind through the C * ABI. cancel_tile is best-effort and may be repeated or race with fetch_tile. * @@ -1098,15 +1106,13 @@ MLN_API mln_status mln_map_add_raster_dem_source_tiles( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, options is null or invalid, fetch_tile is null, or the - * source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, options is null or invalid, fetch_tile is null. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_custom_geometry_source( mln_map map, mln_buffer_view source_id, - const mln_custom_geometry_source_options* options + const mln_custom_geometry_source_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1120,13 +1126,11 @@ MLN_API mln_status mln_map_add_custom_geometry_source( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile_id is invalid, data is empty or invalid, the source * does not exist, or the source is not a custom geometry source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_custom_geometry_source_tile_data( mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data + mln_buffer_view data, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1137,12 +1141,11 @@ MLN_API mln_status mln_map_set_custom_geometry_source_tile_data( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile_id is invalid, the source does not exist, or the * source is not a custom geometry source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_invalidate_custom_geometry_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1153,12 +1156,11 @@ MLN_API mln_status mln_map_invalidate_custom_geometry_source_tile( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, bounds is invalid, the source does not exist, or the * source is not a custom geometry source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_invalidate_custom_geometry_source_region( - mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds + mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1174,13 +1176,12 @@ MLN_API mln_status mln_map_invalidate_custom_geometry_source_region( * mln_map_set_style_json() returns successfully. * * fetch_tile and cancel_tile may run on arbitrary native worker threads, may be - * concurrent with owner-thread map calls, and must not call thread-affine map - * APIs directly. Queue work back to the map owner thread before calling - * mln_map_set_custom_mvt_vector_source_tile_data(), - * mln_map_set_custom_mvt_vector_source_tile_error(), or - * mln_map_invalidate_custom_mvt_vector_source_tile(). Callbacks must not throw, - * panic, longjmp, or otherwise unwind through the C ABI. cancel_tile is - * best-effort and may be repeated or race with fetch_tile. + * concurrent with runtime and map calls. The style command functions in this + * header, including the tile-delivery commands below, may be called directly + * from these callbacks. Callbacks must not block waiting for command + * execution, and must not throw, panic, longjmp, or otherwise unwind through + * the C ABI. cancel_tile is best-effort and may be repeated or race with + * fetch_tile. * * Custom MVT vector sources belong to the current style. Replacing the style * drops sources that were added to the previous style. A layer that draws this @@ -1194,15 +1195,13 @@ MLN_API mln_status mln_map_invalidate_custom_geometry_source_region( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, options is null or invalid, fetch_tile is null, or the - * source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, options is null or invalid, fetch_tile is null. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_custom_mvt_vector_source( mln_map map, mln_buffer_view source_id, - const mln_custom_mvt_vector_source_options* options + const mln_custom_mvt_vector_source_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1222,13 +1221,11 @@ MLN_API mln_status mln_map_add_custom_mvt_vector_source( * invalid or empty, tile_id is invalid, data is a null pointer with a * nonzero size, the source does not exist, or the source is not a custom MVT * vector source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_custom_mvt_vector_source_tile_data( mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data + mln_buffer_view data, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1247,13 +1244,11 @@ MLN_API mln_status mln_map_set_custom_mvt_vector_source_tile_data( * invalid or empty, tile_id is invalid, message is a null pointer with a * nonzero size, the source does not exist, or the source is not a custom MVT * vector source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_custom_mvt_vector_source_tile_error( mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view message + mln_buffer_view message, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1264,12 +1259,11 @@ MLN_API mln_status mln_map_set_custom_mvt_vector_source_tile_error( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, tile_id is invalid, the source does not exist, or the * source is not a custom MVT vector source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_invalidate_custom_mvt_vector_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1287,123 +1281,78 @@ MLN_API mln_status mln_map_invalidate_custom_mvt_vector_source_tile( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is * invalid or empty, image or options is invalid, image pixels are null, image * dimensions or stride are invalid, or image byte_length is too small. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_style_image( mln_map map, mln_buffer_view image_id, const mln_premultiplied_rgba8_image* image, - const mln_style_image_options* options + const mln_style_image_options* options, const mln_completion* completion ) MLN_NOEXCEPT; /** * Removes one runtime style image by ID. * - * image_id is borrowed for the call. On success, out_removed reports whether an - * image existed and was removed. + * image_id is borrowed for the call. The command commits when an image with + * that ID existed and was removed. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is - * invalid or empty, or out_removed is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. + * - MLN_STATUS_NOT_FOUND when no runtime style image has that ID. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_remove_style_image( - mln_map map, mln_buffer_view image_id, bool* out_removed + mln_map map, mln_buffer_view image_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Reports whether a runtime style image ID exists. + * Copies one complete runtime style image. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is - * invalid or empty, or out_exists is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_style_image_exists( - mln_map map, mln_buffer_view image_id, bool* out_exists -) MLN_NOEXCEPT; - -/** - * Copies fixed metadata for one runtime style image. - * - * On success, out_found reports whether image_id exists. When not found, - * out_info receives default image metadata. + * A found image completes with one borrowed mln_style_image_result containing + * metadata, pixels, and stretch intervals. A missing image completes + * successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is - * invalid or empty, out_info is null, out_info->size is too small, or - * out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_image_info( - mln_map map, mln_buffer_view image_id, mln_style_image_info* out_info, - bool* out_found + mln_map map, mln_buffer_view image_id, const mln_completion* completion ) MLN_NOEXCEPT; /** * Copies one runtime style image as tightly packed premultiplied RGBA8 pixels. * - * image_id is borrowed for the call. On success, out_byte_length receives the - * required byte length. When out_found is false, out_byte_length receives 0. - * - * Passing null for out_pixels with a capacity of 0 is a size probe: it reports - * the required byte length and succeeds, so a caller can size a buffer without - * treating the result as a failure. With a non-null out_pixels, a capacity - * smaller than the required length still reports that length and returns - * MLN_STATUS_INVALID_ARGUMENT. + * A found image completes with one borrowed mln_buffer_view. A missing image + * completes successfully with no value. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is - * invalid or empty, out_pixels is null with non-zero capacity, pixel_capacity - * is too small for a non-null buffer, out_byte_length is null, or out_found - * is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_style_image_premultiplied_rgba8( - mln_map map, mln_buffer_view image_id, uint8_t* out_pixels, - size_t pixel_capacity, size_t* out_byte_length, bool* out_found + mln_map map, mln_buffer_view image_id, const mln_completion* completion ) MLN_NOEXCEPT; /** * Copies one runtime style image's stretchable intervals. * - * image_id is borrowed for the call. Each output array may be null only when - * its capacity is 0. On success, out_stretch_x_count and out_stretch_y_count - * receive the interval counts, and both receive 0 when out_found is false. - * - * Passing null for both arrays with both capacities 0 is a size probe: it - * reports the required counts and succeeds. With a non-null array, a capacity - * smaller than that axis's count still reports the counts and returns - * MLN_STATUS_INVALID_ARGUMENT. + * A found image completes with one borrowed mln_style_image_stretches_result. + * A missing image completes successfully with no value. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, image_id is - * invalid or empty, an output array is null with non-zero capacity, a - * capacity is too small for a non-null array, or an output count or out_found - * is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_style_image_stretches( - mln_map map, mln_buffer_view image_id, mln_image_stretch* out_stretch_x, - size_t stretch_x_capacity, size_t* out_stretch_x_count, - mln_image_stretch* out_stretch_y, size_t stretch_y_capacity, - size_t* out_stretch_y_count, bool* out_found + mln_map map, mln_buffer_view image_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1422,14 +1371,12 @@ MLN_API mln_status mln_map_copy_style_image_stretches( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url * is invalid or empty, coordinates is null or invalid, coordinate_count is - * not 4, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * not 4. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_image_source_url( mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, mln_buffer_view url + size_t coordinate_count, mln_buffer_view url, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1448,14 +1395,13 @@ MLN_API mln_status mln_map_add_image_source_url( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, coordinates is null or invalid, coordinate_count is not * 4, image is invalid, image pixels are null, image dimensions or stride are - * invalid, image byte_length is too small, or the source ID already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid, image byte_length is too small. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_image_source_image( mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, const mln_premultiplied_rgba8_image* image + size_t coordinate_count, const mln_premultiplied_rgba8_image* image, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1469,12 +1415,11 @@ MLN_API mln_status mln_map_add_image_source_image( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id or url * is invalid or empty, the source does not exist, or the source is not an * image source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_image_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1489,13 +1434,11 @@ MLN_API mln_status mln_map_set_image_source_url( * invalid or empty, image is invalid, image pixels are null, image dimensions * or stride are invalid, image byte_length is too small, the source does not * exist, or the source is not an image source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_image_source_image( mln_map map, mln_buffer_view source_id, - const mln_premultiplied_rgba8_image* image + const mln_premultiplied_rgba8_image* image, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1510,36 +1453,28 @@ MLN_API mln_status mln_map_set_image_source_image( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is * invalid or empty, coordinates is null or invalid, coordinate_count is not * 4, the source does not exist, or the source is not an image source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_image_source_coordinates( mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count + size_t coordinate_count, const mln_completion* completion ) MLN_NOEXCEPT; /** * Copies image source coordinates. * - * On success, out_found reports whether source_id exists. When found, - * out_coordinate_count receives 4. If coordinate_capacity is less than 4, - * out_coordinate_count still receives 4 and the function returns - * MLN_STATUS_INVALID_ARGUMENT. + * A found image source completes with four borrowed mln_lat_lng values. A + * missing source completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, source_id is - * invalid or empty, out_coordinates is null with non-zero capacity, - * coordinate_capacity is too small for a found source, out_coordinate_count - * is null, out_found is null, or the source exists and is not an image source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, completion is invalid, or the source exists and is not + * an image source. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_image_source_coordinates( - mln_map map, mln_buffer_view source_id, mln_lat_lng* out_coordinates, - size_t coordinate_capacity, size_t* out_coordinate_count, bool* out_found + mln_map map, mln_buffer_view source_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1553,15 +1488,12 @@ MLN_API mln_status mln_map_get_image_source_coordinates( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id or * source_id is invalid or empty, before_layer_id is invalid or does not - * exist, layer_id already exists, source_id does not exist, or source_id is not - * a raster DEM source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * exist, source_id does not exist, or source_id is not a raster DEM source. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_hillshade_layer( mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id + mln_buffer_view before_layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1576,15 +1508,12 @@ MLN_API mln_status mln_map_add_hillshade_layer( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id or * source_id is invalid or empty, before_layer_id is invalid or does not - * exist, layer_id already exists, source_id does not exist, or source_id is not - * a raster DEM source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * exist, source_id does not exist, or source_id is not a raster DEM source. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_color_relief_layer( mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id + mln_buffer_view before_layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1597,14 +1526,12 @@ MLN_API mln_status mln_map_add_color_relief_layer( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, before_layer_id is invalid or does not exist, or layer_id - * already exists. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or before_layer_id is invalid or does not exist. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_location_indicator_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1619,12 +1546,11 @@ MLN_API mln_status mln_map_add_location_indicator_layer( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, coordinate or altitude is invalid, the layer does not * exist, or the layer is not a location indicator layer. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_location_indicator_location( - mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude + mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, + double altitude, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1635,12 +1561,11 @@ MLN_API mln_status mln_map_set_location_indicator_location( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, bearing is not finite float32, the layer does not exist, * or the layer is not a location indicator layer. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_location_indicator_bearing( - mln_map map, mln_buffer_view layer_id, double bearing + mln_map map, mln_buffer_view layer_id, double bearing, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1651,12 +1576,11 @@ MLN_API mln_status mln_map_set_location_indicator_bearing( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, radius is negative or not finite float32, the layer does * not exist, or the layer is not a location indicator layer. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_location_indicator_accuracy_radius( - mln_map map, mln_buffer_view layer_id, double radius + mln_map map, mln_buffer_view layer_id, double radius, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1670,13 +1594,11 @@ MLN_API mln_status mln_map_set_location_indicator_accuracy_radius( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id or * image_id is invalid or empty, image_kind is invalid, the layer does not * exist, or the layer is not a location indicator layer. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_location_indicator_image_name( mln_map map, mln_buffer_view layer_id, uint32_t image_kind, - mln_buffer_view image_id + mln_buffer_view image_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1695,84 +1617,61 @@ MLN_API mln_status mln_map_set_location_indicator_image_name( * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_json is - * empty or invalid, the layer ID already exists, before_layer_id is invalid - * or does not exist, or the layer JSON cannot be converted. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * empty or invalid, before_layer_id is invalid or does not exist, or the + * layer JSON cannot be converted. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_add_style_layer_json( - mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id + mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Removes one style layer by ID. * - * layer_id is borrowed for the call. On success, out_removed reports whether a - * layer existed and was removed. + * layer_id is borrowed for the call. The command commits when a layer with + * that ID existed and was removed. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, or out_removed is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. + * - MLN_STATUS_NOT_FOUND when no style layer has that ID. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_remove_style_layer( - mln_map map, mln_buffer_view layer_id, bool* out_removed -) MLN_NOEXCEPT; - -/** - * Reports whether a style layer ID exists. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, or out_exists is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_style_layer_exists( - mln_map map, mln_buffer_view layer_id, bool* out_exists + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Borrows one style layer type string. + * Copies complete metadata for one style layer. * - * On success, out_found reports whether layer_id exists. When found, - * out_layer_type receives a view of a static style-spec layer type string. + * A found layer completes with one borrowed mln_style_layer_result. A missing + * layer completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_layer_type is null, or out_found is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ -MLN_API mln_status mln_map_get_style_layer_type( - mln_map map, mln_buffer_view layer_id, mln_buffer_view* out_layer_type, - bool* out_found +MLN_API mln_status mln_map_get_style_layer_info( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** * Copies style layer IDs in style order. * - * On success, *out_layer_ids receives an owned list handle. Destroy it with - * mln_style_id_list_destroy(). + * The completion borrows an array of mln_buffer_view values. * * Returns: * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, out_layer_ids is - * null, or *out_layer_ids is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or completion is + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_list_style_layer_ids( - mln_map map, mln_style_id_list* out_layer_ids + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1786,32 +1685,27 @@ MLN_API mln_status mln_map_list_style_layer_ids( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, before_layer_id is invalid, layer_id does not exist, or * before_layer_id is non-empty and does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_move_style_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Serializes one style layer as a full style-spec layer JSON object. * - * On success, out_found reports whether layer_id exists. When found, - * *out_layer receives an owned UTF-8 JSON buffer. Destroy it with - * mln_buffer_destroy(). + * A found layer completes with one borrowed mln_buffer_view. A missing layer + * completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_layer is null, *out_layer is not null, or out_found - * is null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_layer_json( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_layer, bool* out_found + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1824,12 +1718,10 @@ MLN_API mln_status mln_map_get_style_layer_json( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, light_json is * empty or invalid, or the light JSON cannot be converted. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_style_light_json( - mln_map map, mln_buffer_view light_json + mln_map map, mln_buffer_view light_json, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1844,31 +1736,27 @@ MLN_API mln_status mln_map_set_style_light_json( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, property_name is * invalid or empty, value is empty or invalid, the property name is unknown, * or the property value cannot be converted for that property. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer_view value + mln_map map, mln_buffer_view property_name, mln_buffer_view value, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Serializes one style light property as a style-spec JSON value. * - * On success, *out_value receives an owned UTF-8 JSON buffer. Destroy it - * with mln_buffer_destroy(). Undefined native style light properties - * return null buffers. + * A defined property completes with one borrowed mln_buffer_view. An undefined + * property completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, property_name is - * invalid or empty, out_value is null, or *out_value is not null. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, or completion is invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer* out_value + mln_map map, mln_buffer_view property_name, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1888,19 +1776,18 @@ MLN_API mln_status mln_map_get_style_light_property( * or undersized, options->fields contains unknown bits, or an enabled * duration or delay is negative, non-finite, or out of the native duration * range. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_style_transition_options( - mln_map map, const mln_style_transition_options* options + mln_map map, const mln_style_transition_options* options, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Reads the style's global transition options. * - * On success, *out_options receives the last-known transition configuration. - * Duration and delay report through their field-mask bits, because MapLibre + * The completion borrows the last-known transition configuration. Duration + * and delay report through their field-mask bits, because MapLibre * Native leaves either one unset until a style or a host sets it. * * A map that has loaded no style yet reports duration and delay unset. A style @@ -1914,14 +1801,12 @@ MLN_API mln_status mln_map_set_style_transition_options( * * Returns: * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or out_options is - * null or undersized. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, or completion is + * invalid. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_style_transition_options( - mln_map map, mln_style_transition_options* out_options + mln_map map, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1938,34 +1823,29 @@ MLN_API mln_status mln_map_get_style_transition_options( * property_name is invalid or empty, value is empty or invalid, the layer * does not exist, the property name is unknown for that layer, or the property * value cannot be converted for that property. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_property( mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer_view value + mln_buffer_view value, const mln_completion* completion ) MLN_NOEXCEPT; /** * Serializes one layer property as a style-spec JSON value. * - * On success, *out_value receives an owned UTF-8 JSON buffer. Destroy it - * with mln_buffer_destroy(). Undefined native style properties return - * null buffers. + * A defined property completes with one borrowed mln_buffer_view. An undefined + * property completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id or - * property_name is invalid or empty, out_value is null, *out_value is not - * null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * property_name is invalid or empty, completion is invalid, or the layer does + * not exist. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_layer_property( mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer* out_value + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -1981,31 +1861,27 @@ MLN_API mln_status mln_map_get_layer_property( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, filter is invalid, the layer does not exist, or the * filter cannot be converted. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_filter( - mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter + mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter, + const mln_completion* completion ) MLN_NOEXCEPT; /** * Serializes one layer filter as a style-spec JSON value. * - * On success, *out_filter receives an owned UTF-8 JSON buffer. Destroy it - * with mln_buffer_destroy(). Missing filters return null buffers. + * A present filter completes with one borrowed mln_buffer_view. A missing + * filter completes successfully with no value. * * Returns: * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_filter is null, *out_filter is not null, or the layer - * does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, completion is invalid, or the layer does not exist. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_get_layer_filter( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_filter + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -2023,40 +1899,27 @@ MLN_API mln_status mln_map_get_layer_filter( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, source_layer is invalid, the layer does not exist, or the * layer's type does not take a source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_source_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies one layer's source-layer ID into caller-owned memory. + * Copies one layer's source-layer ID. * - * layer_id is borrowed for the call. On success, out_source_layer_size receives - * the byte length excluding any null terminator, and 0 when the layer carries - * no source-layer. - * - * Passing null for out_source_layer with a capacity of 0 is a size probe: it - * reports the required byte length and succeeds, so a caller can size a buffer - * without treating the result as a failure. With a non-null out_source_layer, a - * capacity smaller than the required length still reports that length and - * returns MLN_STATUS_INVALID_ARGUMENT. + * The completion borrows one mln_buffer_view, which is empty when the layer + * carries no source-layer ID. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_source_layer is null with non-zero capacity, - * source_layer_capacity is too small for a non-null buffer, - * out_source_layer_size is null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, completion is invalid, or the layer does not exist. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_layer_source_layer( - mln_map map, mln_buffer_view layer_id, char* out_source_layer, - size_t source_layer_capacity, size_t* out_source_layer_size + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -2074,40 +1937,27 @@ MLN_API mln_status mln_map_copy_layer_source_layer( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, source_id is invalid or empty, the layer does not exist, * or the layer's type does not take a source. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_source_id( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, + const mln_completion* completion ) MLN_NOEXCEPT; /** - * Copies one layer's source ID into caller-owned memory. - * - * layer_id is borrowed for the call. On success, out_source_id_size receives - * the byte length excluding any null terminator, and 0 when the layer carries - * no source. + * Copies one layer's source ID. * - * Passing null for out_source_id with a capacity of 0 is a size probe: it - * reports the required byte length and succeeds, so a caller can size a buffer - * without treating the result as a failure. With a non-null out_source_id, a - * capacity smaller than the required length still reports that length and - * returns MLN_STATUS_INVALID_ARGUMENT. + * The completion borrows one mln_buffer_view, which is empty when the layer + * carries no source ID. * * Returns: - * - MLN_STATUS_OK on success, including a size probe. + * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_source_id is null with non-zero capacity, - * source_id_capacity is too small for a non-null buffer, out_source_id_size - * is null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. + * invalid or empty, completion is invalid, or the layer does not exist. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_copy_layer_source_id( - mln_map map, mln_buffer_view layer_id, char* out_source_id, - size_t source_id_capacity, size_t* out_source_id_size + mln_map map, mln_buffer_view layer_id, const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -2120,29 +1970,11 @@ MLN_API mln_status mln_map_copy_layer_source_id( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, min_zoom is NaN, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double min_zoom -) MLN_NOEXCEPT; - -/** - * Reads the lowest zoom at which one layer draws. - * - * A layer with no lower bound reports -INFINITY. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_min_zoom is null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double* out_min_zoom + mln_map map, mln_buffer_view layer_id, double min_zoom, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -2155,29 +1987,11 @@ MLN_API mln_status mln_map_get_layer_min_zoom( * - MLN_STATUS_OK on success. * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, max_zoom is NaN, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double max_zoom -) MLN_NOEXCEPT; - -/** - * Reads the highest zoom at which one layer draws. - * - * A layer with no upper bound reports INFINITY. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_max_zoom is null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double* out_max_zoom + mln_map map, mln_buffer_view layer_id, double max_zoom, + const mln_completion* completion ) MLN_NOEXCEPT; /** @@ -2190,29 +2004,11 @@ MLN_API mln_status mln_map_get_layer_max_zoom( * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is * invalid or empty, visibility is not an mln_style_layer_visibility value, or * the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. */ MLN_API mln_status mln_map_set_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t visibility -) MLN_NOEXCEPT; - -/** - * Reads whether one layer draws. - * - * On success, *out_visibility receives an mln_style_layer_visibility value. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, layer_id is - * invalid or empty, out_visibility is null, or the layer does not exist. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the map owner - * thread. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_map_get_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t* out_visibility + mln_map map, mln_buffer_view layer_id, uint32_t visibility, + const mln_completion* completion ) MLN_NOEXCEPT; #ifdef __cplusplus diff --git a/include/maplibre_native_c/surface.h b/include/maplibre_native_c/surface.h index 30ca23c4d..be211129c 100644 --- a/include/maplibre_native_c/surface.h +++ b/include/maplibre_native_c/surface.h @@ -9,6 +9,7 @@ #include #include "base.h" +#include "completion.h" #include "render_target.h" #ifdef __cplusplus @@ -100,270 +101,74 @@ MLN_API mln_webgpu_surface_descriptor mln_webgpu_surface_descriptor_default(void) MLN_NOEXCEPT; /** - * Attaches a Metal native surface render target to a map. + * Starts attachment of a Metal surface target. * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every surface-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session retains - * descriptor->layer and optional descriptor->context.device, and renders into - * and presents through the layer. On success, *out_session receives a handle - * the caller destroys with mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Metal surface sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The descriptor and options are copied before return. The completion runs + * only after the selected driver initializes the target. A core-worker driver + * retains the Metal layer and device on its worker. A caller driver performs + * initialization when the host services driver work with the Metal context + * usable on that thread. */ MLN_API mln_status mln_metal_surface_attach( mln_map map, const mln_metal_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a Vulkan native surface render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every surface-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session renders to - * descriptor->surface and presents through it. The Vulkan device must support - * VK_KHR_swapchain, and the queue family must support graphics and presentation - * to descriptor->surface. Vulkan handles are borrowed and must remain valid - * until the session is detached or destroyed. On success, *out_session receives - * a handle the caller destroys with mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Vulkan surface sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts attachment of a Vulkan surface target. */ MLN_API mln_status mln_vulkan_surface_attach( mln_map map, const mln_vulkan_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Attaches an OpenGL native surface render target to a map. + * Starts attachment of an OpenGL surface target. * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every surface-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. - * - * descriptor->context.ownership decides what the session does with the thread's - * current context. A shared session builds its context in the share group of - * the descriptor's share context, which must be current on this thread, and - * every render restores whatever was current before it. A dedicated session - * builds its context from the display or device context alone, makes it - * current, and keeps it current between renders, so this session holds the - * thread's context from attach until detach or destroy, either of which - * releases it. - * - * The session renders to descriptor->surface and presents through the selected - * context provider. WGL surfaces present with SwapBuffers(HDC), and EGL - * surfaces present with eglSwapBuffers(EGLDisplay, EGLSurface). OpenGL context - * handles are borrowed and must remain valid until detach or destroy. On - * success, *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when OpenGL surface sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * WGL, EGL, and existing WebGL contexts require + * MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD. A transferred WebGL canvas supports + * MLN_RENDER_DRIVER_CORE_WORKER. Context ownership remains independent. */ MLN_API mln_status mln_opengl_surface_attach( mln_map map, const mln_opengl_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Attaches a WebGPU native surface render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every surface-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there; a WebGPU object belongs to the - * agent that created it. The session configures descriptor->surface for this - * device and extent, takes its current texture each frame, and presents through - * it. The surface, device, and instance are borrowed and must remain valid - * until detach or destroy. On success, *out_session receives a handle the - * caller destroys with mln_render_session_destroy(). + * Starts attachment of a WebGPU surface target. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when WebGPU surface sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Browser targets require MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD because + * WebGPU objects remain in their creating agent. */ MLN_API mln_status mln_webgpu_surface_attach( mln_map map, const mln_webgpu_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Presents an attached Metal surface session through a new surface. - * - * Use this when a host destroys and recreates its surface while the map lives - * on. The presentation surface is replaced in place, so the session keeps its - * renderer along with the tile pyramid, glyph and image atlases, and symbol - * placement. Map-owned feature state is unchanged. - * - * descriptor->context must name the graphics context or device the session - * attached with; a null Metal device names none and is accepted. A target on a - * different context, or a lost graphics context, requires destroying the - * session with mln_render_session_destroy() and attaching again. - * - * The new extent applies exactly as mln_render_session_resize() applies one, - * including how the next mln_render_session_render_update() waits for the map - * to catch up to it. A scale_factor that differs from the session's current - * value rebuilds the renderer, whose shaders are compiled for a fixed pixel - * ratio; the surface is replaced either way. - * - * Every failure status but MLN_STATUS_NATIVE_ERROR is reported before the - * target is touched and leaves the session rendering into the one it had. - * MLN_STATUS_NATIVE_ERROR may mean a replacement was already under way, which - * cannot be unwound; destroy the session with mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names a device other than the - * session's. - * - MLN_STATUS_UNSUPPORTED when descriptor->format differs from the session's; - * destroy the session and attach again to change it. - * - MLN_STATUS_INVALID_STATE when the session is detached. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render through a Metal - * surface, or when Metal surface sessions are not supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts an ordered Metal surface replacement. */ MLN_API mln_status mln_metal_surface_set_target( - mln_render_session session, const mln_metal_surface_descriptor* descriptor + mln_render_session session, const mln_metal_surface_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Presents an attached Vulkan surface session through a new surface. - * - * See mln_metal_surface_set_target() for what replacing a surface preserves, - * when a host reaches for it, and how failures are reported. - * descriptor->context must name the same instance, physical device, device, and - * graphics queue the session attached with, and the new descriptor->surface - * must be presentable from that queue family. - * - * The outgoing VkSurfaceKHR must still be valid when this is called, because - * Vulkan requires the session's swapchain to be destroyed before its surface. A - * host that has to release its surface first destroys the session with - * mln_render_session_destroy() and attaches again afterward. Metal and OpenGL - * carry no such requirement. - * - * The replacement must report the color format and the surface-transform - * support this session already compiled a render pass and shaders for. - * MLN_STATUS_UNSUPPORTED reports one that does not, with the session still - * rendering into the surface it has; destroy the session and attach again to - * change either. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, descriptor->context names handles other than the - * session's, or the surface is not usable by this session. - * - MLN_STATUS_INVALID_STATE when the session is detached. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render through a Vulkan - * surface, when the replacement's color format or surface-transform support - * differs from the session's, or when Vulkan surface sessions are not - * supported by this build. - * - MLN_STATUS_NATIVE_ERROR when a Vulkan query about the replacement fails, or - * when an internal exception is converted to status. - */ +/** Starts an ordered Vulkan surface replacement. */ MLN_API mln_status mln_vulkan_surface_set_target( - mln_render_session session, const mln_vulkan_surface_descriptor* descriptor + mln_render_session session, const mln_vulkan_surface_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Presents an attached OpenGL surface session through a new surface. - * - * See mln_metal_surface_set_target() for what replacing a surface preserves, - * when a host reaches for it, and how failures are reported. - * descriptor->context must name the context provider data the session attached - * with. The new surface is made current on the next render, so a host may - * replace a surface it has already destroyed. Under dedicated ownership the - * session's context stays current on the previous surface until that render. - * - * Because nothing is made current here, a surface this call accepts can still - * turn out to be unusable. An HDC whose pixel format does not match the - * session's context, or an EGLSurface from another display, is reported by the - * next mln_render_session_render_update() as MLN_STATUS_NATIVE_ERROR rather - * than by this function. The session stays destroyable in that state. - * - * A lost OpenGL context requires destroying the session and attaching again. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names context provider data other - * than the session's. - * - MLN_STATUS_INVALID_STATE when the session is detached. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render through an OpenGL - * surface, or when OpenGL surface sessions are not supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts an ordered OpenGL surface replacement. */ MLN_API mln_status mln_opengl_surface_set_target( - mln_render_session session, const mln_opengl_surface_descriptor* descriptor + mln_render_session session, const mln_opengl_surface_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Presents an attached WebGPU surface session through a new surface. - * - * The replacement must name the device and format the session attached with. - * The session unconfigures the surface it had and configures the replacement - * for this extent. - * - * Every failure status but MLN_STATUS_NATIVE_ERROR is reported before the - * target is touched and leaves the session rendering into the one it had. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names a device other than the - * session's. - * - MLN_STATUS_INVALID_STATE when the session is detached. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render through a WebGPU - * surface, or when WebGPU surface sessions are not supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts an ordered WebGPU surface replacement. */ MLN_API mln_status mln_webgpu_surface_set_target( - mln_render_session session, const mln_webgpu_surface_descriptor* descriptor + mln_render_session session, const mln_webgpu_surface_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; #ifdef __cplusplus diff --git a/include/maplibre_native_c/texture.h b/include/maplibre_native_c/texture.h index 31ea87419..9c1937446 100644 --- a/include/maplibre_native_c/texture.h +++ b/include/maplibre_native_c/texture.h @@ -10,6 +10,7 @@ #include #include "base.h" +#include "completion.h" #include "render_target.h" #ifdef __cplusplus @@ -158,8 +159,9 @@ typedef struct mln_opengl_owned_texture_descriptor { /** Logical texture extent. */ mln_render_target_extent extent; /** - * Borrowed OpenGL context provider data. The session creates and owns a - * context that shares texture objects with the host context. + * Borrowed OpenGL context provider data. Shared ownership creates a context + * whose texture frames the host can acquire. Dedicated EGL or transferred + * WebGL ownership creates a private core-worker context for CPU readback. */ mln_opengl_context_descriptor context; } mln_opengl_owned_texture_descriptor; @@ -298,6 +300,15 @@ typedef struct mln_texture_image_info { size_t byte_length; } mln_texture_image_info; +/** Texture readback borrowed for a completion callback. */ +typedef struct mln_texture_readback_result { + uint32_t size; + uint32_t reserved; + /** Borrowed pixel bytes, valid only during the callback. */ + mln_buffer_view data; + mln_texture_image_info info; +} mln_texture_readback_result; + /** * Returns Metal owned-texture descriptor defaults for this C API version. */ @@ -353,623 +364,136 @@ MLN_API mln_texture_image_info mln_texture_image_info_default(void) MLN_NOEXCEPT; /** - * Attaches a Metal texture render target owned by the session to a map. + * Starts attachment of a session-owned Metal texture ring. * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session renders into a - * session-owned texture created on descriptor->context.device. On success, - * *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Metal texture sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The common options select driver placement and requested ring depth. The + * descriptor and options are copied before return. */ MLN_API mln_status mln_metal_owned_texture_attach( mln_map map, const mln_metal_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a Metal caller-owned texture render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session renders into - * descriptor->texture. The caller owns the texture, keeps it valid until detach - * or destroy, and synchronizes any use outside this session. On success, - * *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * mln_render_session_resize() returns MLN_STATUS_UNSUPPORTED for this target, - * which the host owns and sizes. Follow a resized host by allocating a texture - * at the new size and handing it over with - * mln_metal_borrowed_texture_set_target(), which keeps the session. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Metal borrowed texture sessions are not - * supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts attachment of a caller-owned Metal texture target. */ MLN_API mln_status mln_metal_borrowed_texture_attach( mln_map map, const mln_metal_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a Vulkan texture render target owned by the session to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session renders into a - * session-owned image created on descriptor->context.device. Vulkan handles are - * borrowed and must remain valid until detach or destroy. On success, - * *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Vulkan texture sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts attachment of a session-owned Vulkan texture ring. */ MLN_API mln_status mln_vulkan_owned_texture_attach( mln_map map, const mln_vulkan_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a Vulkan caller-owned texture render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session renders into - * descriptor->image through descriptor->image_view. The caller owns the image - * and view, keeps them valid until detach or destroy, and handles queue-family - * ownership and synchronization outside this session. On success, *out_session - * receives a handle the caller destroys with mln_render_session_destroy(). - * - * Before each mln_render_session_render_update(), make the image available on - * descriptor->context.graphics_queue in descriptor->initial_layout and keep it - * out of concurrent use. The session submits rendering on that queue, waits for - * the submitted work to finish, and leaves the image in - * descriptor->final_layout before mln_render_session_render_update() returns. - * - * mln_render_session_resize() returns MLN_STATUS_UNSUPPORTED for this target, - * which the host owns and sizes. Follow a resized host by allocating an image - * and view at the new size and handing them over with - * mln_vulkan_borrowed_texture_set_target(), which keeps the session. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when Vulkan borrowed texture sessions are not - * supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts attachment of a caller-owned Vulkan texture target. */ MLN_API mln_status mln_vulkan_borrowed_texture_attach( mln_map map, const mln_vulkan_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Attaches an OpenGL texture render target owned by the session to a map. + * Starts attachment of a session-owned OpenGL texture ring. * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there, so the host context must be current - * on this thread. The session creates an OpenGL texture in a context that - * shares objects with descriptor->context. Host sampling may - * use the acquired texture from a context in the same share group after acquire - * succeeds and before release. On success, *out_session receives a handle the - * caller destroys with mln_render_session_destroy(). + * Shared WGL, EGL, and existing WebGL contexts require the caller driver and + * grant frame acquisition, readback, and consumer synchronization. Dedicated + * EGL and transferred WebGL contexts require the core-worker driver and grant + * readback only with a ring depth of one. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when OpenGL texture sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The host keeps every descriptor-named backend handle valid through completed + * detach. In particular, it keeps an EGLDisplay initialized while its session + * is live. */ MLN_API mln_status mln_opengl_owned_texture_attach( mln_map map, const mln_opengl_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches an OpenGL caller-owned texture render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there, so the host context must be current - * on this thread. The session renders into descriptor->texture. The caller owns - * the texture, keeps it valid until detach or destroy, and synchronizes any use - * outside this session. Each render completes before - * mln_render_session_render_update() returns, so the caller reads or samples - * the texture from any context in the share group of descriptor->context - * without adding synchronization of its own. On success, - * *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * mln_render_session_resize() returns MLN_STATUS_UNSUPPORTED for this target, - * which the host owns and sizes. Follow a resized host by allocating a texture - * at the new size and handing it over with - * mln_opengl_borrowed_texture_set_target(), which keeps the session. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when OpenGL borrowed texture sessions are not - * supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts attachment of a caller-owned OpenGL texture target. */ MLN_API mln_status mln_opengl_borrowed_texture_attach( mln_map map, const mln_opengl_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) MLN_NOEXCEPT; /** - * Renders an attached Metal texture session into a new caller-owned texture. + * Starts attachment of a session-owned WebGPU texture ring. * - * A caller-owned texture is sized by its owner, so mln_render_session_resize() - * reports MLN_STATUS_UNSUPPORTED for these targets and a host that follows a - * resize hands over a replacement here instead. The texture is replaced in - * place, so the session keeps its renderer along with the tile pyramid, glyph - * and image atlases, and symbol placement. Map-owned feature state is - * unchanged. - * - * descriptor->texture must belong to the device the session attached with. A - * texture on a different device requires destroying this session with - * mln_render_session_destroy() and attaching again. - * - * The caller owns the replacement, keeps it valid until the next replacement, - * detach, or destroy, and synchronizes any use outside this session, exactly as - * for mln_metal_borrowed_texture_attach(). The outgoing texture is neither read - * nor released here and need not still be valid. - * - * The new extent applies exactly as mln_render_session_resize() applies one, - * including how the next mln_render_session_render_update() waits for the map - * to catch up to it. A scale_factor that differs from the session's current - * value rebuilds the renderer, whose shaders are compiled for a fixed pixel - * ratio. A pixel format that differs from the one this session attached with is - * reported as MLN_STATUS_UNSUPPORTED, with the session still rendering into the - * texture it has; destroy the session and attach again to change the format. - * - * Every failure status but MLN_STATUS_NATIVE_ERROR is reported before the - * target is touched and leaves the session rendering into the one it had. - * MLN_STATUS_NATIVE_ERROR may mean a replacement was already under way, which - * cannot be unwound; destroy the session with mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->texture belongs to another device. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame is - * currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render into a caller-owned - * Metal texture, when descriptor->texture has a different pixel format from - * the session's, or when Metal borrowed texture sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * Browser targets require MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD. */ +MLN_API mln_status mln_webgpu_owned_texture_attach( + mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** Starts attachment of a caller-owned WebGPU texture target. */ +MLN_API mln_status mln_webgpu_borrowed_texture_attach( + mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion +) MLN_NOEXCEPT; + +/** Starts an ordered caller-owned Metal texture replacement. */ MLN_API mln_status mln_metal_borrowed_texture_set_target( mln_render_session session, - const mln_metal_borrowed_texture_descriptor* descriptor + const mln_metal_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Renders an attached Vulkan texture session into a new caller-owned image. - * - * See mln_metal_borrowed_texture_set_target() for what replacing a target - * preserves, when a host reaches for it, and how failures are reported. - * descriptor->context must name the same instance, physical device, device, and - * graphics queue the session attached with. - * - * The replacement must carry the format and both layouts this session built its - * render pass around. MLN_STATUS_UNSUPPORTED reports one that does not, with - * the session still rendering into the image it has; destroy the session and - * attach again to change them. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names handles other than the - * session's. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame is - * currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render into a caller-owned - * Vulkan image, when descriptor->format, descriptor->initial_layout, or - * descriptor->final_layout differs from the session's, or when Vulkan - * borrowed texture sessions are not supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts an ordered caller-owned Vulkan texture replacement. */ MLN_API mln_status mln_vulkan_borrowed_texture_set_target( mln_render_session session, - const mln_vulkan_borrowed_texture_descriptor* descriptor + const mln_vulkan_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Renders an attached OpenGL texture session into a new caller-owned texture. - * - * See mln_metal_borrowed_texture_set_target() for what replacing a target - * preserves, when a host reaches for it, and how failures are reported. - * descriptor->context must name the context provider data the session attached - * with. The replacement belongs to that context or one in the same share group, - * and the host context must be current on the calling thread. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names context provider data other - * than the session's. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame is - * currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render into a caller-owned - * OpenGL texture, or when OpenGL borrowed texture sessions are not supported - * by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ +/** Starts an ordered caller-owned OpenGL texture replacement. */ MLN_API mln_status mln_opengl_borrowed_texture_set_target( mln_render_session session, - const mln_opengl_borrowed_texture_descriptor* descriptor + const mln_opengl_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Reads the most recently rendered session-owned texture frame into - * caller-owned storage. - * - * The copied image is premultiplied RGBA8 in physical pixels. The function - * fills out_info with the required byte length and image layout metadata. - * - * Passing null for out_data with a capacity of 0 is a size probe: out_info is - * filled and the call succeeds, so a caller can size a buffer without treating - * the result as a failure. Otherwise out_info is still filled when out_data is - * null or out_data_capacity is too small, and the function returns - * MLN_STATUS_INVALID_ARGUMENT. - * - * A backend that cannot read pixels back answers MLN_STATUS_UNSUPPORTED without - * filling out_info, so a size probe is not a way to ask whether readback works. - * - * Returns: - * - MLN_STATUS_OK on success, including a size probe. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, out_info is - * null, out_info->size is too small, out_data is null with non-zero capacity, - * or out_data_capacity is too small for a non-null buffer. - * - MLN_STATUS_INVALID_STATE when no rendered frame is available, the session - * is detached, or a frame is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session is not a texture session, when the - * texture session uses a caller-owned target, or when the session's render - * backend cannot read pixels back. - * - MLN_STATUS_NATIVE_ERROR when readback produces no image or an image whose - * layout does not match the session, when the render backend reports no - * renderer backend, or when an internal exception is converted to status. - */ -MLN_API mln_status mln_texture_read_premultiplied_rgba8( - mln_render_session session, uint8_t* out_data, size_t out_data_capacity, - mln_texture_image_info* out_info -) MLN_NOEXCEPT; - -/** - * Acquires the most recently rendered Metal texture frame. - * - * Use this function with sessions created by mln_metal_owned_texture_attach(). - * - * The returned texture and device pointers are borrowed and remain valid only - * until mln_metal_owned_texture_release_frame() is called for the same frame. - * While acquired, resize, render update, detach, destroy, and a - * second acquire return MLN_STATUS_INVALID_STATE. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, out_frame is - * null, or out_frame->size is too small. - * - MLN_STATUS_INVALID_STATE when the session is detached, no rendered frame is - * available, or a texture frame is already acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot expose a Metal texture frame. - * - MLN_STATUS_NATIVE_ERROR when the render backend reports no rendered Metal - * texture, or when an internal exception is converted to status. - */ -MLN_API mln_status mln_metal_owned_texture_acquire_frame( - mln_render_session session, mln_metal_owned_texture_frame* out_frame -) MLN_NOEXCEPT; - -/** - * Releases a Metal texture frame acquired from a session-owned texture target. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, frame is - * null, frame->size is too small, or frame identity does not match the - * acquired frame. - * - MLN_STATUS_INVALID_STATE when no texture frame is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot release a Metal texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_metal_owned_texture_release_frame( - mln_render_session session, const mln_metal_owned_texture_frame* frame -) MLN_NOEXCEPT; - -/** - * Acquires the most recently rendered Vulkan texture frame. - * - * Use this function with sessions created by mln_vulkan_owned_texture_attach(). - * - * The returned image, image view, and device pointers are borrowed and remain - * valid only until mln_vulkan_owned_texture_release_frame() is called for the - * same frame. While acquired, resize, render update, detach, destroy, and a - * second acquire return MLN_STATUS_INVALID_STATE. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, out_frame is - * null, or out_frame->size is too small. - * - MLN_STATUS_INVALID_STATE when the session is detached, no rendered frame is - * available, or a texture frame is already acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot expose a Vulkan texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_vulkan_owned_texture_acquire_frame( - mln_render_session session, mln_vulkan_owned_texture_frame* out_frame -) MLN_NOEXCEPT; - -/** - * Releases a Vulkan texture frame acquired from a session-owned texture target. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, frame is - * null, frame->size is too small, or frame identity does not match the - * acquired frame. - * - MLN_STATUS_INVALID_STATE when no texture frame is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot release a Vulkan texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_vulkan_owned_texture_release_frame( - mln_render_session session, const mln_vulkan_owned_texture_frame* frame -) MLN_NOEXCEPT; - -/** - * Acquires the most recently rendered OpenGL texture frame. - * - * Use this function with sessions created by mln_opengl_owned_texture_attach(). - * - * The returned texture object is borrowed and remains valid only until - * mln_opengl_owned_texture_release_frame() is called for the same frame. - * While acquired, resize, render update, detach, destroy, and a second acquire - * return MLN_STATUS_INVALID_STATE. - * - * Acquiring completes the session rendering for the frame, so the caller reads - * or samples the texture from any context in the share group of the context - * passed to mln_opengl_owned_texture_attach() without adding synchronization - * of its own. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, out_frame is - * null, or out_frame->size is too small. - * - MLN_STATUS_INVALID_STATE when the session is detached, no rendered frame is - * available, or a texture frame is already acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot expose an OpenGL texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_opengl_owned_texture_acquire_frame( - mln_render_session session, mln_opengl_owned_texture_frame* out_frame -) MLN_NOEXCEPT; - -/** - * Releases an OpenGL texture frame acquired from a session-owned texture - * target. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, frame is - * null, frame->size is too small, or frame identity does not match the - * acquired frame. - * - MLN_STATUS_INVALID_STATE when no texture frame is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot release an OpenGL texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_opengl_owned_texture_release_frame( - mln_render_session session, const mln_opengl_owned_texture_frame* frame +/** Starts an ordered caller-owned WebGPU texture replacement. */ +MLN_API mln_status mln_webgpu_borrowed_texture_set_target( + mln_render_session session, + const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a WebGPU texture render target owned by the session to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. Attach creates - * the session's graphics resources on the calling thread, so the host resources - * named by descriptor must be usable there. The session creates a WebGPU - * texture on descriptor->context.device. The caller owns that device and queue - * and keeps them valid until detach or destroy. Host sampling or copying may - * use the acquired texture after acquire succeeds and before release. On - * success, *out_session receives a handle the caller destroys with - * mln_render_session_destroy(). - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when WebGPU texture sessions are not supported by - * this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_webgpu_owned_texture_attach( - mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, - mln_render_session* out_session +/** Starts readback of the latest rendered texture frame. */ +MLN_API mln_status mln_texture_read_premultiplied_rgba8( + mln_render_session session, const mln_completion* completion ) MLN_NOEXCEPT; -/** - * Attaches a WebGPU caller-owned texture render target to a map. - * - * The map may have at most one live render session. The calling thread becomes - * the session's owner thread, and every texture-session call is affine to it. - * The map need only be live, so a host may attach on the thread that drives its - * render loop while the map stays on the runtime loop thread. - * - * The session renders into descriptor->texture_view. The caller owns the - * texture, view, device, and queue, keeps them valid until detach or destroy, - * and synchronizes any use outside this session. - * - * Before each mln_render_session_render_update(), make the texture available on - * descriptor->context.queue and keep it out of concurrent use. The session - * submits rendering on that queue before mln_render_session_render_update() - * returns. - * - * mln_render_session_resize() returns MLN_STATUS_UNSUPPORTED for this target, - * which the host owns and sizes. Follow a resized host by allocating a texture - * and view at the new size and handing them over with - * mln_webgpu_borrowed_texture_set_target(), which keeps the session. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when map is null or not live, descriptor is - * null or invalid, out_session is null, or *out_session is not null. - * - MLN_STATUS_INVALID_STATE when the map already has a render session. - * - MLN_STATUS_UNSUPPORTED when WebGPU borrowed texture sessions are not - * supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_webgpu_borrowed_texture_attach( - mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session +/** Copies Metal-native metadata from an acquired frame. */ +MLN_API mln_status mln_acquired_frame_get_metal_texture( + mln_acquired_frame frame, mln_metal_owned_texture_frame* out_frame ) MLN_NOEXCEPT; -/** - * Renders an attached WebGPU texture session into a new caller-owned texture. - * - * See mln_metal_borrowed_texture_set_target() for what replacing a target - * preserves, when a host reaches for it, and how failures are reported. - * descriptor->context must name the device and queue the session attached with. - * A null queue names that device's default queue here exactly as it does at - * attach. descriptor->context.instance is not compared, because a texture - * session never uses it. - * - * The replacement must carry the format this session built its render pipelines - * around. MLN_STATUS_UNSUPPORTED reports one that does not, with the session - * still rendering into the texture it has; destroy the session and attach again - * to change it. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, descriptor is - * null or invalid, or descriptor->context names a device or queue other than - * the session's. - * - MLN_STATUS_INVALID_STATE when the session is detached or a texture frame is - * currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when the session does not render into a caller-owned - * WebGPU texture, when descriptor->format differs from the session's, or when - * WebGPU borrowed texture sessions are not supported by this build. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_webgpu_borrowed_texture_set_target( - mln_render_session session, - const mln_webgpu_borrowed_texture_descriptor* descriptor +/** Copies Vulkan-native metadata from an acquired frame. */ +MLN_API mln_status mln_acquired_frame_get_vulkan_texture( + mln_acquired_frame frame, mln_vulkan_owned_texture_frame* out_frame ) MLN_NOEXCEPT; /** - * Acquires the most recently rendered WebGPU texture frame. - * - * Use this function with sessions created by mln_webgpu_owned_texture_attach(). - * - * The returned texture, texture view, and device pointers are borrowed and - * remain valid only until mln_webgpu_owned_texture_release_frame() is called - * for the same frame. While acquired, resize, render update, detach, destroy, - * and a second acquire return MLN_STATUS_INVALID_STATE. + * Copies OpenGL-native metadata from an acquired frame. * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, out_frame is - * null, or out_frame->size is too small. - * - MLN_STATUS_INVALID_STATE when the session is detached, no rendered frame is - * available, or a texture frame is already acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot expose a WebGPU texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. + * The caller driver's context must be current on this thread. */ -MLN_API mln_status mln_webgpu_owned_texture_acquire_frame( - mln_render_session session, mln_webgpu_owned_texture_frame* out_frame +MLN_API mln_status mln_acquired_frame_get_opengl_texture( + mln_acquired_frame frame, mln_opengl_owned_texture_frame* out_frame ) MLN_NOEXCEPT; -/** - * Releases a WebGPU texture frame acquired from a session-owned texture target. - * - * Returns: - * - MLN_STATUS_OK on success. - * - MLN_STATUS_INVALID_ARGUMENT when session is null or not live, frame is - * null, frame->size is too small, or frame identity does not match the - * acquired frame. - * - MLN_STATUS_INVALID_STATE when no texture frame is currently acquired. - * - MLN_STATUS_WRONG_THREAD when called from a thread other than the session - * owner thread. - * - MLN_STATUS_UNSUPPORTED when session cannot release a WebGPU texture frame. - * - MLN_STATUS_NATIVE_ERROR when an internal exception is converted to status. - */ -MLN_API mln_status mln_webgpu_owned_texture_release_frame( - mln_render_session session, const mln_webgpu_owned_texture_frame* frame +/** Copies WebGPU-native metadata from an acquired frame. */ +MLN_API mln_status mln_acquired_frame_get_webgpu_texture( + mln_acquired_frame frame, mln_webgpu_owned_texture_frame* out_frame ) MLN_NOEXCEPT; #ifdef __cplusplus diff --git a/include/maplibre_native_c/wake.h b/include/maplibre_native_c/wake.h new file mode 100644 index 000000000..00c9acc1d --- /dev/null +++ b/include/maplibre_native_c/wake.h @@ -0,0 +1,45 @@ +/** + * @file maplibre_native_c/wake.h + * Public C API declarations for receiver wake callbacks. + */ + +#ifndef MAPLIBRE_NATIVE_C_WAKE_H +#define MAPLIBRE_NATIVE_C_WAKE_H + +#include + +#include "base.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Schedules service by the receiver that owns a queue or driver. */ +typedef void (*mln_wake_callback)(void* user_data); + +/** Releases wake callback state after native code can no longer invoke it. */ +typedef void (*mln_wake_release)(void* user_data); + +/** + * Receiver wake callback copied by a successful owning call. + * + * Native code may invoke callback from any thread, and calls may coalesce or + * overlap. The callback should only schedule receiver work and return. It must + * not destroy the object that owns the wake. Native code calls + * release_user_data after all callback invocations have returned. + * + * A zeroed descriptor disables waking. Polling remains valid when the owning + * API permits an omitted wake. + */ +typedef struct mln_wake { + uint32_t size; + mln_wake_callback callback; + void* user_data; + mln_wake_release release_user_data; +} mln_wake; + +#ifdef __cplusplus +} +#endif + +#endif // MAPLIBRE_NATIVE_C_WAKE_H diff --git a/mise.toml b/mise.toml index bd673e7d8..5db136bf7 100644 --- a/mise.toml +++ b/mise.toml @@ -313,11 +313,12 @@ depends = [{ task = "//:build", args = ["{{usage.preset}}"] }] # The Emscripten suite runs as a page in headless Chromium; see # scripts/run-browser-test.mjs for how a browser is located. run = ''' +cmake --build --preset "$usage_preset" --target mln_ffi_c_api_tests if [[ "$usage_preset" == ios-simulator-* ]]; then mise run //:apple-simulator:boot iOS elif [[ "$usage_preset" == tvos-simulator-* ]]; then mise run //:apple-simulator:boot tvOS -elif [[ "$usage_preset" == ohos-x64-egl || "$usage_preset" == android-x64-* ]]; then +elif [[ "$usage_preset" == ohos-x64-egl || "$usage_preset" == android-arm64-* || "$usage_preset" == android-x64-* ]]; then # An emulator target runs the suite through a device shell rather than ctest, # so the tile fixtures ctest would name by absolute source path are staged # into a directory the runner pushes to the device. @@ -338,8 +339,13 @@ elif [[ "$usage_preset" == ohos-x64-egl || "$usage_preset" == android-x64-* ]]; "$OHOS_SDK_NATIVE/llvm/lib/x86_64-linux-ohos/libc++_shared.so" \ -- "$MISE_MONOREPO_ROOT/build/$usage_preset/mln_ffi_c_api_tests" fi + case "$usage_preset" in + android-arm64-*) abi=arm64-v8a ;; + android-x64-*) abi=x86_64 ;; + esac exec "$MISE_MONOREPO_ROOT/scripts/run-android-emulator-test.sh" \ 300 \ + "$abi" \ "$MISE_MONOREPO_ROOT/build/$usage_preset/install/lib/libmaplibre-native-c.so" \ -- "$MISE_MONOREPO_ROOT/build/$usage_preset/mln_ffi_c_api_tests" fi diff --git a/patches/maplibre-native/0003-run-loop-process-gate.patch b/patches/maplibre-native/0003-run-loop-process-gate.patch deleted file mode 100644 index 3c5d55698..000000000 --- a/patches/maplibre-native/0003-run-loop-process-gate.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/include/mbgl/util/run_loop.hpp b/include/mbgl/util/run_loop.hpp -index 88b3a35..968c0aa 100644 ---- a/include/mbgl/util/run_loop.hpp -+++ b/include/mbgl/util/run_loop.hpp -@@ -56,6 +56,12 @@ public: - /// same thread as the Map object lives. - void setPlatformCallback(std::function callback) { platformCallback = std::move(callback); } - -+ /// Optional gate consulted before each queued task is dequeued. When it -+ /// returns false, process() stops and leaves the remaining tasks queued. -+ /// Called on the loop's own thread. Set it before the loop processes work, -+ /// from the same thread; the assignment is not synchronized. -+ void setProcessGate(std::function gate) { processGate = std::move(gate); } -+ - // So far only needed by the libcurl backend. - void addWatch(int fd, Event, std::function&& callback); - void removeWatch(int fd); -@@ -119,6 +125,12 @@ private: - std::shared_ptr task; - std::unique_lock lock(mutex); - while (true) { -+ if ((!highPriorityQueue.empty() || !defaultQueue.empty()) && processGate && !processGate()) { -+ // The platform wake that led here is spent, so the remaining -+ // tasks re-arm it; otherwise they wait for an unrelated push. -+ wake(); -+ break; -+ } - if (!highPriorityQueue.empty()) { - task = std::move(highPriorityQueue.front()); - highPriorityQueue.pop(); -@@ -136,6 +148,7 @@ private: - } - - std::function platformCallback; -+ std::function processGate; - - Queue defaultQueue; - Queue highPriorityQueue; diff --git a/patches/maplibre-native/README.md b/patches/maplibre-native/README.md index 186d32056..0de2ca44d 100644 --- a/patches/maplibre-native/README.md +++ b/patches/maplibre-native/README.md @@ -10,11 +10,6 @@ canonical Windows drive paths and opens UTF-8 filenames through wide filesystem APIs. This lets the local file source load percent-encoded `file:///C:/...` resources whose paths contain spaces or non-ASCII characters. -`0003-run-loop-process-gate.patch` adds an optional gate callback to -`RunLoop::process()`, consulted before each queued task is dequeued. The C API -uses it to bound one pump's drain; the budget logic stays on the C API side, and -an unset gate keeps upstream behavior. - Drop a patch once the pin moves to a commit that carries it. The sync checks out the pinned commit with `--force`, so it discards whatever the last sync applied before applying the list again. A pin bump, an edit to a patch, and a dropped diff --git a/scripts/boot-ohos-emulator.sh b/scripts/boot-ohos-emulator.sh index 40ada26e3..f068bcc4e 100755 --- a/scripts/boot-ohos-emulator.sh +++ b/scripts/boot-ohos-emulator.sh @@ -5,7 +5,7 @@ set -euo pipefail ohos_sdk_version=${1:?usage: boot-ohos-emulator.sh } -connect_key=127.0.0.1:55555 +connect_key=${MLN_FFI_OHOS_EMULATOR_CONNECT_KEY:-127.0.0.1:55555} state_dir="$MISE_MONOREPO_ROOT/build/ohos-emulator" pid_file="$state_dir/qemu.pid" log_file="$state_dir/qemu.log" diff --git a/scripts/check-execution-conventions.py b/scripts/check-execution-conventions.py new file mode 100755 index 000000000..98d3a21c8 --- /dev/null +++ b/scripts/check-execution-conventions.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Checks execution categories derived from public C declarations. + +A ``const mln_completion* completion`` parameter identifies one-shot +asynchronous work. Drains transfer queued stream records, driver service keeps +its graphics-thread contract, published snapshots are synchronous copies, and +everything else is immediate. Public operation handles, result-taking +accessors, and command IDs are forbidden. +""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +FUNCTION = re.compile( + r"MLN_API\s+[^;]*?\b(mln_[A-Za-z0-9_]+)\s*\([^;]*?\)\s*MLN_NOEXCEPT\s*;", + re.DOTALL, +) + + +def header_files(paths: list[pathlib.Path]) -> list[pathlib.Path]: + files: list[pathlib.Path] = [] + for path in paths: + if path.is_dir(): + files.extend(sorted(path.glob("*.h"))) + else: + files.append(path) + return files + + +def exported_functions( + headers: list[pathlib.Path], +) -> tuple[dict[str, str], dict[str, str]]: + declarations: dict[str, str] = {} + documentation: dict[str, str] = {} + for header in headers: + contents = header.read_text() + for match in FUNCTION.finditer(contents): + name = match.group(1) + if name in declarations: + raise ValueError(f"{name} is declared more than once") + declarations[name] = match.group(0) + preceding = contents[: match.start()] + comments = list(re.finditer(r"/\*\*.*?\*/", preceding, re.DOTALL)) + comment = comments[-1] if comments else None + documentation[name] = ( + comment.group(0) + if comment and not preceding[comment.end() :].strip() + else "" + ) + return declarations, documentation + + +def derive_category(name: str, declaration: str) -> str: + if re.search(r"\bconst\s+mln_completion\s*\*\s*completion\b", declaration): + return "completion" + if "_drain_" in name: + return "event_batch" + if name.endswith("_service_driver_work"): + return "render_driver_call" + if "snapshot" in name and re.search( + r"\b(mln_map|mln_render_session)\s+\w+", declaration + ): + return "published_snapshot" + return "immediate" + + +def convention_errors( + name: str, category: str, declaration: str, documentation: str +) -> list[str]: + errors: list[str] = [] + legacy = re.search( + r"\bmln_operation\b|\bout_(?:command_id|operation)\b", declaration + ) + if legacy or name.endswith(("_start", "_take_result")): + errors.append(f"legacy asynchronous shape remains public: {name}") + if category == "completion": + if not documentation: + errors.append(f"completion function has no boundary documentation: {name}") + elif category == "render_driver_call": + if not re.search(r"\bmln_render_session\s+session\b", declaration): + errors.append(f"render-driver call has no session boundary: {name}") + thread_contract = documentation.lower() + if not ( + "graphics thread" in thread_contract + or "graphics-thread" in thread_contract + or "context must be current" in thread_contract + ): + errors.append(f"render-driver call does not document its thread: {name}") + return errors + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("headers", nargs="+", type=pathlib.Path) + arguments = parser.parse_args() + + errors: list[str] = [] + try: + declarations, documentation = exported_functions( + header_files(arguments.headers) + ) + except (OSError, ValueError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + counts: dict[str, int] = {} + for name in sorted(declarations): + category = derive_category(name, declarations[name]) + counts[category] = counts.get(category, 0) + 1 + errors.extend( + convention_errors(name, category, declarations[name], documentation[name]) + ) + + if errors: + print("\n".join(f"error: {error}" for error in errors), file=sys.stderr) + return 1 + summary = ", ".join(f"{count} {name}" for name, count in sorted(counts.items())) + print(f"classified {len(declarations)} exports by convention: {summary}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/go-cross-env.sh b/scripts/go-cross-env.sh index 03a44158f..89eeb9304 100644 --- a/scripts/go-cross-env.sh +++ b/scripts/go-cross-env.sh @@ -17,7 +17,21 @@ case "$1" in goarch=amd64 compiler_prefix=x86_64-linux-android fi - ndk_bin="$ANDROID_HOME/ndk/$MLN_FFI_ANDROID_NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64/bin" + ndk_prebuilt_root="$ANDROID_HOME/ndk/$MLN_FFI_ANDROID_NDK_VERSION/toolchains/llvm/prebuilt" + ndk_bin= + for host_prebuilt in "$ndk_prebuilt_root"/*; do + if [[ -d "$host_prebuilt/bin" ]]; then + if [[ -n "$ndk_bin" ]]; then + echo "The pinned Android NDK has multiple host prebuilts under $ndk_prebuilt_root." >&2 + return 2 + fi + ndk_bin="$host_prebuilt/bin" + fi + done + if [[ -z "$ndk_bin" ]]; then + echo "The pinned Android NDK has no host prebuilt under $ndk_prebuilt_root." >&2 + return 2 + fi export GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=1 export CC="$ndk_bin/${compiler_prefix}24-clang" export CXX="$ndk_bin/${compiler_prefix}24-clang++" @@ -36,5 +50,8 @@ case "$1" in export GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=1 export CC="$OHOS_SDK_NATIVE/llvm/bin/clang $target_flags" export CXX="$OHOS_SDK_NATIVE/llvm/bin/clang++ $target_flags" + # The OHOS loader leaves Go's internal function-pointer relocations + # unresolved in PIE executables. Link device binaries as ET_EXEC instead. + export CGO_LDFLAGS="${CGO_LDFLAGS:-} -no-pie" ;; esac diff --git a/scripts/run-android-emulator-test.sh b/scripts/run-android-emulator-test.sh index 71a0b4e99..a6d97abf2 100755 --- a/scripts/run-android-emulator-test.sh +++ b/scripts/run-android-emulator-test.sh @@ -4,14 +4,15 @@ # stops the batch. set -euo pipefail -if [[ $# -lt 3 ]]; then - echo "usage: $0 [test-argument ...] -- " >&2 +if [[ $# -lt 4 ]]; then + echo "usage: $0 [test-argument ...] -- " >&2 exit 2 fi timeout_seconds=$1 -native_library=$2 -shift 2 +abi=$2 +native_library=$3 +shift 3 test_arguments=() while (($#)) && [[ $1 != -- ]]; do test_arguments+=("$1") @@ -25,6 +26,13 @@ if (($# == 0)); then exit 2 fi test_executables=("$@") +case "$abi" in + arm64-v8a | x86_64) ;; + *) + echo "Unsupported Android emulator ABI: $abi" >&2 + exit 2 + ;; +esac serial=emulator-5554 remote_dir=/data/local/tmp/maplibre-native-ffi @@ -50,8 +58,10 @@ fi # failure. if [[ ! -x "$adb" ]] || ! "$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | - tr -d '\r' | grep -qx 1; then - mise run //:android-emulator:boot + tr -d '\r' | grep -qx 1 || + ! "$adb" -s "$serial" shell getprop ro.product.cpu.abi 2>/dev/null | + tr -d '\r' | grep -qx "$abi"; then + mise run //:android-emulator:boot "$abi" fi # The shell user may execute what it owns under /data/local/tmp. The Android diff --git a/scripts/run-browser-test.mjs b/scripts/run-browser-test.mjs index adaeaa641..481709a92 100644 --- a/scripts/run-browser-test.mjs +++ b/scripts/run-browser-test.mjs @@ -36,15 +36,22 @@ const FIXTURE_STYLE_LAYER_IDS = { "/__fixture/original-after-clear.json": "original-after-clear", }; -// Enable software graphics in headless Chromium. +// Headless Chromium can use Metal on macOS. Forcing its Linux SwiftShader +// Vulkan stack there prevents WebGPU device creation. +const WEBGPU_BROWSER_ARGS = + process.platform === "darwin" + ? ["--enable-unsafe-webgpu"] + : [ + "--enable-unsafe-webgpu", + "--enable-features=Vulkan", + "--enable-unsafe-swiftshader", + "--use-angle=swiftshader", + "--use-vulkan=swiftshader", + ]; + +// Enable software graphics where the host cannot use its native graphics API. const BACKEND_BROWSER_ARGS = { - webgpu: [ - "--enable-unsafe-webgpu", - "--enable-features=Vulkan", - "--enable-unsafe-swiftshader", - "--use-angle=swiftshader", - "--use-vulkan=swiftshader", - ], + webgpu: WEBGPU_BROWSER_ARGS, opengl: ["--enable-unsafe-swiftshader"], }; diff --git a/scripts/run-ohos-emulator-test.sh b/scripts/run-ohos-emulator-test.sh index 64b088e5f..8542db498 100755 --- a/scripts/run-ohos-emulator-test.sh +++ b/scripts/run-ohos-emulator-test.sh @@ -27,7 +27,7 @@ if (($# == 0)); then fi test_executables=("$@") -connect_key=127.0.0.1:55555 +connect_key=${MLN_FFI_OHOS_EMULATOR_CONNECT_KEY:-127.0.0.1:55555} remote_dir=/data/local/tmp/maplibre-native-ffi fixture_dir=${MLN_FFI_TEST_FIXTURE_DIR:-} @@ -65,12 +65,16 @@ if [[ -n "$fixture_dir" ]]; then done < <(find "$fixture_dir" -type f -print0) fixture_environment="MLN_FFI_TEST_FIXTURE_DIR='$remote_dir/fixtures' " fi +# The Oniro guest's virtio GPU does not expose a reliable accelerated EGL +# screen under QEMU. Mesa's surfaceless llvmpipe path provides deterministic +# offscreen rendering for the test executables. +graphics_environment="EGL_PLATFORM=surfaceless LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe LIBGL_DRIVERS_PATH=/vendor/lib64/chipsetsdk " for test_executable in "${test_executables[@]}"; do echo "Running $(basename "$test_executable") in the OpenHarmony emulator." hdc -t "$connect_key" file send "$test_executable" "$remote_dir/test-executable" - remote_command="cd '$remote_dir' && chmod 755 test-executable && ${fixture_environment}LD_LIBRARY_PATH='$remote_dir' ./test-executable" + remote_command="cd '$remote_dir' && chmod 755 test-executable && ${fixture_environment}${graphics_environment}LD_LIBRARY_PATH='$remote_dir' ./test-executable" for argument in ${test_arguments[@]+"${test_arguments[@]}"}; do printf -v quoted_argument '%q' "$argument" remote_command+=" $quoted_argument" diff --git a/scripts/run-ohos-emulator.sh b/scripts/run-ohos-emulator.sh index 21830d267..404b3766c 100755 --- a/scripts/run-ohos-emulator.sh +++ b/scripts/run-ohos-emulator.sh @@ -7,6 +7,13 @@ if [[ $# -ne 1 ]]; then fi image_dir=$1 +connect_key=${MLN_FFI_OHOS_EMULATOR_CONNECT_KEY:-127.0.0.1:55555} +host_address=${connect_key%:*} +host_port=${connect_key##*:} +if [[ -z "$host_address" || ! "$host_port" =~ ^[0-9]+$ ]]; then + echo "Invalid OpenHarmony emulator connect key: $connect_key" >&2 + exit 2 +fi qemu_bin=${QEMU_BIN:-qemu-system-x86_64} for image in bzImage ramdisk.img updater.img system.img vendor.img userdata.img; do if [[ ! -f "$image_dir/$image" ]]; then @@ -67,5 +74,5 @@ exec "$qemu_bin" \ -append "ip=dhcp loglevel=4 console=ttyS0,115200 init=init root=/dev/ram0 rw ohos.boot.hardware=x86_general ohos.required_mount.system=/dev/block/vdb@/usr@ext4@ro,barrier=1@wait,required ohos.required_mount.vendor=/dev/block/vdc@/vendor@ext4@ro,barrier=1@wait,required ohos.required_mount.misc=/dev/block/vda@/misc@none@none=@wait,required" \ "${acceleration[@]}" \ "${cpu[@]}" \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:55555-:55555 \ + -netdev "user,id=net0,hostfwd=tcp:$host_address:$host_port-:55555" \ -device virtio-net-pci,netdev=net0 diff --git a/scripts/rust-cross-env.sh b/scripts/rust-cross-env.sh index 134844d3e..e614d7abe 100644 --- a/scripts/rust-cross-env.sh +++ b/scripts/rust-cross-env.sh @@ -16,7 +16,21 @@ esac case "$1" in android-*64-*) - ndk_prebuilt="$ANDROID_HOME/ndk/$MLN_FFI_ANDROID_NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64" + ndk_prebuilt_root="$ANDROID_HOME/ndk/$MLN_FFI_ANDROID_NDK_VERSION/toolchains/llvm/prebuilt" + ndk_prebuilt= + for host_prebuilt in "$ndk_prebuilt_root"/*; do + if [[ -d "$host_prebuilt/bin" ]]; then + if [[ -n "$ndk_prebuilt" ]]; then + echo "The pinned Android NDK has multiple host prebuilts under $ndk_prebuilt_root." >&2 + return 2 + fi + ndk_prebuilt="$host_prebuilt" + fi + done + if [[ -z "$ndk_prebuilt" ]]; then + echo "The pinned Android NDK has no host prebuilt under $ndk_prebuilt_root." >&2 + return 2 + fi target_env="${cargo_target//-/_}" export "BINDGEN_EXTRA_CLANG_ARGS_$target_env=--target=$cargo_target --sysroot=$ndk_prebuilt/sysroot" export "CC_$target_env=$ndk_prebuilt/bin/${cargo_target}24-clang" diff --git a/scripts/stop-ohos-emulator.sh b/scripts/stop-ohos-emulator.sh index 961768ad8..62706243e 100755 --- a/scripts/stop-ohos-emulator.sh +++ b/scripts/stop-ohos-emulator.sh @@ -3,10 +3,14 @@ set -euo pipefail pid_file="$MISE_MONOREPO_ROOT/build/ohos-emulator/qemu.pid" +connect_key=${MLN_FFI_OHOS_EMULATOR_CONNECT_KEY:-127.0.0.1:55555} if [[ ! -f "$pid_file" ]]; then echo "No mise-managed OpenHarmony emulator is running." exit 0 fi +# Disconnect hdc before QEMU closes the forwarded socket. This keeps the +# listener port reusable when another suite starts the emulator immediately. +hdc tconn "$connect_key" -remove >/dev/null 2>&1 || true pid=$(<"$pid_file") if [[ "$pid" =~ ^[0-9]+$ ]] && @@ -22,4 +26,3 @@ if [[ "$pid" =~ ^[0-9]+$ ]] && fi fi rm -f "$pid_file" -hdc tconn 127.0.0.1:55555 -remove >/dev/null 2>&1 || true diff --git a/scripts/test-go-device.sh b/scripts/test-go-device.sh index 423e8c92f..37017ebf6 100755 --- a/scripts/test-go-device.sh +++ b/scripts/test-go-device.sh @@ -1,18 +1,24 @@ #!/usr/bin/env bash -# Cross-compiles the Go binding tests for an Android or OpenHarmony x64 preset -# and runs them in the emulator, one executable per package. +# Cross-compiles the Go binding tests for an Android preset or OpenHarmony x64 +# preset and runs them in the matching emulator, one executable per package. set -euo pipefail preset=${1:?usage: test-go-device.sh } case "$preset" in - android-x64-* | ohos-x64-egl) ;; + android-arm64-*) + abi=arm64-v8a + ;; + android-x64-*) + abi=x86_64 + ;; + ohos-x64-egl) ;; ohos-x64-*) echo "The OpenHarmony emulator runs EGL only; check $preset with //bindings/go:build and test ohos-x64-egl instead." >&2 exit 2 ;; - android-* | ohos-*) - echo "The emulators run x64 guests only; check $preset with //bindings/go:build and run an x64 preset's tests instead." >&2 + ohos-*) + echo "The OpenHarmony emulator runs an x64 guest; check $preset with //bindings/go:build and test ohos-x64-egl instead." >&2 exit 2 ;; *) @@ -39,6 +45,7 @@ test_binaries=("$test_dir"/*.test) if [[ "$platform" == android ]]; then exec "$MISE_MONOREPO_ROOT/scripts/run-android-emulator-test.sh" \ 180 \ + "$abi" \ "$native_install_dir/lib/libmaplibre-native-c.so" \ -test.v -- ${test_binaries[@]+"${test_binaries[@]}"} fi diff --git a/scripts/test-python-android-device.sh b/scripts/test-python-android-device.sh index 74ef90e62..afb28be91 100755 --- a/scripts/test-python-android-device.sh +++ b/scripts/test-python-android-device.sh @@ -4,16 +4,32 @@ set -euo pipefail preset=${1:?usage: test-python-android-device.sh } case "$preset" in + android-arm64-egl) + abi=arm64-v8a + cibw_arch=arm64_v8a + system_graphics_library=libGLESv3.so + test_requirements='pytest>=9,<10 pyopengl>=3.1.10,<4' + ;; + android-arm64-vulkan) + abi=arm64-v8a + cibw_arch=arm64_v8a + system_graphics_library=libvulkan.so + test_requirements='pytest>=9,<10' + ;; android-x64-egl) + abi=x86_64 + cibw_arch=x86_64 system_graphics_library=libGLESv3.so test_requirements='pytest>=9,<10 pyopengl>=3.1.10,<4' ;; android-x64-vulkan) + abi=x86_64 + cibw_arch=x86_64 system_graphics_library=libvulkan.so test_requirements='pytest>=9,<10' ;; *) - echo "The Python Android device suite tests Android x64 presets only, not $preset." >&2 + echo "The Python Android device suite does not support $preset." >&2 exit 2 ;; esac @@ -25,7 +41,7 @@ if [[ ! -d "$native_install_dir" ]]; then fi mise run //:android-sdk-packages -mise run //:android-emulator:boot x86_64 +mise run //:android-emulator:boot "$abi" # cibuildwheel pins its own NDK and removes other NDK versions from the SDK it # manages. Give it an isolated SDK view so later repository tasks retain the @@ -45,12 +61,15 @@ for entry_name in build-tools cmake emulator licenses platform-tools platforms s done export ANDROID_HOME="$python_android_home" export ANDROID_SDK_ROOT="$python_android_home" +cmake_bin=$(mise which cmake) +export PATH="${cmake_bin%/*}:$PATH" -export CIBW_BUILD=cp314-android_x86_64 -export CIBW_ARCHS_ANDROID=x86_64 +export CIBW_BUILD="cp314-android_$cibw_arch" +export CIBW_ARCHS_ANDROID="$cibw_arch" export CIBW_BUILD_FRONTEND='build[uv]' export CIBW_CONFIG_SETTINGS_ANDROID='build-args=--no-default-features' -export CIBW_ENVIRONMENT_ANDROID="MAPLIBRE_NATIVE_C_INSTALL_DIR=$native_install_dir" +host_cc=$(command -v cc) +export CIBW_ENVIRONMENT_ANDROID="MAPLIBRE_NATIVE_C_INSTALL_DIR=$native_install_dir HOST_CC=$host_cc" # The graphics loader comes from Android, so wheel repair leaves it external. export CIBW_REPAIR_WHEEL_COMMAND_ANDROID="auditwheel repair --exclude $system_graphics_library --ldpaths {ldpaths} -w {dest_dir} {wheel}" export CIBW_TEST_COMMAND_ANDROID='python -m pytest tests' diff --git a/scripts/test-rust-browser.sh b/scripts/test-rust-browser.sh index 4df08f5bd..95721b284 100755 --- a/scripts/test-rust-browser.sh +++ b/scripts/test-rust-browser.sh @@ -53,13 +53,58 @@ export CARGO_ENCODED_RUSTFLAGS="$encoded" # Shared memory requires an atomics-enabled standard library. export RUSTC_BOOTSTRAP=1 export CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER="$MISE_MONOREPO_ROOT/scripts/run-browser-cargo-test.sh" -cargo test \ - -p maplibre-native-ffi-sys \ - -p maplibre-native-ffi-core \ - -p maplibre-native-ffi \ - --target wasm32-unknown-emscripten \ - -Zbuild-std=std,panic_abort \ - -- --test-threads=1 +cargo_support_tests() { + cargo test \ + -p maplibre-native-ffi-sys \ + -p maplibre-native-ffi-core \ + --target wasm32-unknown-emscripten \ + -Zbuild-std=std,panic_abort \ + -- "$@" --test-threads=1 +} + +cargo_binding_test() { + cargo test \ + -p maplibre-native-ffi \ + --target wasm32-unknown-emscripten \ + -Zbuild-std=std,panic_abort \ + -- "$@" --test-threads=1 +} + +# Chromium retains GPU and pthread resources longer than their native handles. +# Give every integration test a fresh process so otherwise independent tests +# cannot inherit a context budget or worker-pool state from their predecessors. +run_file_tests() { + local prefix=$1 + local source=$2 + while IFS= read -r test_name; do + cargo_binding_test "$prefix$test_name" --exact + done < <( + awk ' + /#\[test\]/ { test = 1; next } + test && /^fn / { + sub(/^fn /, "") + sub(/\(.*/, "") + print + test = 0 + } + ' "$source" + ) +} + +binding_source="$MISE_MONOREPO_ROOT/bindings/rust/crates/maplibre-native-ffi/src" +run_file_tests 'completion::tests::' "$binding_source/completion.rs" +run_file_tests 'custom_geometry::tests::' "$binding_source/custom_geometry.rs" +run_file_tests 'custom_mvt_vector::tests::' "$binding_source/custom_mvt_vector.rs" +run_file_tests 'events::tests::' "$binding_source/events.rs" +run_file_tests 'handle::tests::' "$binding_source/handle.rs" +run_file_tests 'logging::tests::' "$binding_source/logging.rs" +run_file_tests 'map::tests::' "$binding_source/map/tests.rs" +run_file_tests 'projection::tests::' "$binding_source/projection.rs" +run_file_tests 'render::tests::' "$binding_source/render/tests.rs" +run_file_tests 'resource::tests::' "$binding_source/resource.rs" +run_file_tests 'runtime::tests::' "$binding_source/runtime.rs" +run_file_tests 'tests::' "$binding_source/lib.rs" +cargo_support_tests cargo clippy \ -p maplibre-native-ffi-sys \ -p maplibre-native-ffi-core \ diff --git a/scripts/test-rust-device.sh b/scripts/test-rust-device.sh index ac78ca965..b73c1853d 100755 --- a/scripts/test-rust-device.sh +++ b/scripts/test-rust-device.sh @@ -1,18 +1,24 @@ #!/usr/bin/env bash -# Cross-compiles the Rust binding tests for an Android or OpenHarmony x64 -# preset and runs them in the emulator, one executable per crate. +# Cross-compiles the Rust binding tests for an Android preset or OpenHarmony x64 +# preset and runs them in the matching emulator, one executable per crate. set -euo pipefail preset=${1:?usage: test-rust-device.sh } case "$preset" in - android-x64-* | ohos-x64-egl) ;; + android-arm64-*) + abi=arm64-v8a + ;; + android-x64-*) + abi=x86_64 + ;; + ohos-x64-egl) ;; ohos-x64-*) echo "The OpenHarmony emulator runs EGL only; check $preset with //bindings/rust:build and test ohos-x64-egl instead." >&2 exit 2 ;; - android-* | ohos-*) - echo "The emulators run x64 guests only; check $preset with //bindings/rust:build and run an x64 preset's tests instead." >&2 + ohos-*) + echo "The OpenHarmony emulator runs an x64 guest; check $preset with //bindings/rust:build and test ohos-x64-egl instead." >&2 exit 2 ;; *) @@ -53,12 +59,13 @@ while IFS= read -r test_binary || [[ -n "$test_binary" ]]; do done <"$test_manifest" if [[ "$preset" == android-* ]]; then exec "$MISE_MONOREPO_ROOT/scripts/run-android-emulator-test.sh" \ - 180 \ + 600 \ + "$abi" \ "$native_install_dir/lib/libmaplibre-native-c.so" \ --test-threads=1 -- ${test_binaries[@]+"${test_binaries[@]}"} fi exec "$MISE_MONOREPO_ROOT/scripts/run-ohos-emulator-test.sh" \ - 180 \ + 600 \ "$native_install_dir/lib/libmaplibre-native-c.so" \ "$OHOS_SDK_NATIVE/llvm/lib/$compiler_target/libc++_shared.so" \ --test-threads=1 -- ${test_binaries[@]+"${test_binaries[@]}"} diff --git a/src/c_api/autorelease_pool.hpp b/src/c_api/autorelease_pool.hpp index 9f82528ed..9f6105f8f 100644 --- a/src/c_api/autorelease_pool.hpp +++ b/src/c_api/autorelease_pool.hpp @@ -11,10 +11,9 @@ namespace mln::c_api { auto run_in_autorelease_pool(void* context, mln_status (*body)(void*)) -> mln_status; -// A host frame loop that never returns to a run loop has no pool to drain -// autoreleased objects, and Metal blocks the next frame until an in-flight -// command buffer is deallocated. Drain one pool per call. Objects that cross -// the C boundary are retained, not autoreleased. +// A native worker or host frame loop may run without an autorelease pool. +// Drain one pool per C entry point or queued runtime submission. Objects that +// cross the C boundary are retained, not autoreleased. template auto with_autorelease_pool(Function& function) -> mln_status { return run_in_autorelease_pool(&function, [](void* context) -> mln_status { diff --git a/src/c_api/callback_adapter.cpp b/src/c_api/callback_adapter.cpp index fce02c59e..880381636 100644 --- a/src/c_api/callback_adapter.cpp +++ b/src/c_api/callback_adapter.cpp @@ -3,70 +3,400 @@ // Adapts synchronous MapLibre callback contracts to hosts that can only receive // callbacks asynchronously through void listener functions. // -// Everything here runs on MapLibre's own threads. +// Native callbacks enqueue on MapLibre threads; hosts drain and close queues +// from their own execution contexts. #include -#include #include #include #include #include +#include #include #include #include +#include #include +#include #include #include -#include #include #include "maplibre_native_c/callback_adapter.h" +#include "c_api/boundary.hpp" +#include "diagnostics/diagnostics.hpp" +#include "handles/handle_table.hpp" #include "maplibre_native_c.h" #include "runtime/runtime.hpp" +#include "wake/wake.hpp" -namespace { - -using AdapterResourceRewriteRules = mln_adapter_resource_rewrite_rules; -using AdapterHttpHeaderTransformRules = mln_adapter_http_header_transform_rules; -using AdapterResourceProviderRules = mln_adapter_resource_provider_rules; -using AdapterQueuedResourceProviderRoute = - mln_adapter_queued_resource_provider_route; -using AdapterQueuedResourceProvider = mln_adapter_queued_resource_provider; -using AdapterQueuedResourceRequestView = mln_adapter_queued_resource_request; +namespace mln::core { struct AdapterQueuedResourceRequest { - AdapterQueuedResourceRequestView view{}; + mln_adapter_queued_resource_request view{}; std::string requested_url; std::string resolved_url; std::string prior_etag; std::vector prior_data; }; -using AdapterLogCallbackState = mln_adapter_log_callback_state; -using AdapterLogRecordView = mln_adapter_log_record; - struct AdapterLogRecord { - AdapterLogRecordView view{}; + mln_adapter_log_record view{}; std::string message; }; -struct AdapterLogCallbackEntry { - mln_adapter_log_record_listener listener = nullptr; - std::uint32_t consume = 0; - std::size_t in_flight = 0; - bool retired = false; +struct AdapterCompletionRecord { + mln_adapter_completion_record view{}; + std::vector flat; + std::deque strings; + std::vector views; + std::vector offline_regions; + mln_style_source_result style_source{}; + mln_style_layer_result style_layer{}; + mln_style_image_result style_image{}; + mln_style_image_stretches_result image_stretches{}; + std::vector stretch_x; + std::vector stretch_y; + std::vector queried_features; + mln_texture_readback_result texture_readback{}; +}; + +struct AdapterResourceRequestQueueObject { + std::mutex mutex; + std::mutex drain_mutex; + std::deque> records; + std::shared_ptr wake; + bool wake_pending = false; + bool closed = false; + + auto close() noexcept -> void { + auto discarded = decltype(records){}; + auto detached_wake = std::shared_ptr{}; + { + const std::scoped_lock lock(mutex); + if (closed) { + return; + } + closed = true; + discarded.swap(records); + detached_wake = std::move(wake); + } + detached_wake.reset(); + for (const auto& record : discarded) { + mln_resource_request_release(record->view.handle); + } + } + + ~AdapterResourceRequestQueueObject() { close(); } +}; + +struct AdapterLogQueueObject { + std::mutex mutex; + std::mutex drain_mutex; + std::deque> records; + std::shared_ptr wake; + bool wake_pending = false; + bool closed = false; + + auto close() noexcept -> void { + auto discarded = decltype(records){}; + auto detached_wake = std::shared_ptr{}; + { + const std::scoped_lock lock(mutex); + if (closed) { + return; + } + closed = true; + discarded.swap(records); + detached_wake = std::move(wake); + } + detached_wake.reset(); + } + + ~AdapterLogQueueObject() { close(); } +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::AdapterResourceRequestQueue; + static constexpr auto leasable = true; +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::AdapterLogQueue; + static constexpr auto leasable = true; }; +} // namespace mln::core + +namespace { + +using AdapterResourceRewriteRules = mln_adapter_resource_rewrite_rules; +using AdapterHttpHeaderTransformRules = mln_adapter_http_header_transform_rules; +using AdapterResourceProviderRules = mln_adapter_resource_provider_rules; +using AdapterQueuedResourceProviderRoute = + mln_adapter_queued_resource_provider_route; +using AdapterQueuedResourceProvider = mln_adapter_queued_resource_provider; +using AdapterQueuedResourceRequest = mln::core::AdapterQueuedResourceRequest; +using AdapterQueuedResourceRequestView = mln_adapter_queued_resource_request; +using AdapterLogCallbackState = mln_adapter_log_callback_state; +using AdapterLogRecord = mln::core::AdapterLogRecord; +using AdapterCompletionRecord = mln::core::AdapterCompletionRecord; + struct AdapterHandleLeakToken { std::string type_name; std::uint64_t handle = 0; }; +using AdapterLogRecordView = mln_adapter_log_record; std::mutex log_setter_mutex; -std::mutex log_state_mutex; -std::unordered_map log_callbacks; -void* active_log_callback = nullptr; + +struct AdapterCompletionState { + std::uint32_t copy_kind = MLN_ADAPTER_COMPLETION_COPY_FLAT; + std::size_t element_size = 0; + mln_adapter_completion_listener listener = nullptr; + void* user_data = nullptr; +}; + +auto copy_bytes( + AdapterCompletionRecord& record, const void* data, std::size_t size +) -> mln_buffer_view { + if (size == 0) return {}; + if (data == nullptr) throw std::invalid_argument{"completion view is null"}; + const auto* first = static_cast(data); + record.strings.emplace_back(first, size); + const auto& copy = record.strings.back(); + return {.data = copy.data(), .size = copy.size()}; +} + +auto copy_c_string(AdapterCompletionRecord& record, const char* value) -> const + char* { + if (value == nullptr) return nullptr; + record.strings.emplace_back(value); + return record.strings.back().c_str(); +} + +auto copy_view(AdapterCompletionRecord& record, mln_buffer_view value) + -> mln_buffer_view { + return copy_bytes(record, value.data, value.size); +} + +auto copy_offline_region( + AdapterCompletionRecord& record, mln_offline_region_info value +) -> mln_offline_region_info { + switch (value.definition.type) { + case MLN_OFFLINE_REGION_DEFINITION_TILE_PYRAMID: + value.definition.data.tile_pyramid.style_url = + copy_c_string(record, value.definition.data.tile_pyramid.style_url); + break; + case MLN_OFFLINE_REGION_DEFINITION_GEOMETRY: + value.definition.data.geometry.style_url = + copy_c_string(record, value.definition.data.geometry.style_url); + value.definition.data.geometry.geometry = + copy_view(record, value.definition.data.geometry.geometry); + break; + default: + throw std::invalid_argument{"completion has an unknown offline region"}; + } + const auto metadata = copy_bytes(record, value.metadata, value.metadata_size); + value.metadata = static_cast(metadata.data); + value.metadata_size = metadata.size; + return value; +} + +auto copy_completion_value( + AdapterCompletionRecord& record, const mln_completion_result& result, + const AdapterCompletionState& state +) -> void { + if (result.value_count == 0) return; + if (result.value == nullptr) { + throw std::invalid_argument{"completion value is null"}; + } + switch (state.copy_kind) { + case MLN_ADAPTER_COMPLETION_COPY_FLAT: { + if ( + state.element_size == 0 || + result.value_count > + std::numeric_limits::max() / state.element_size + ) { + throw std::invalid_argument{"completion element size is invalid"}; + } + const auto size = result.value_count * state.element_size; + record.flat.resize(size); + std::memcpy(record.flat.data(), result.value, size); + record.view.result.value = record.flat.data(); + break; + } + case MLN_ADAPTER_COMPLETION_COPY_BUFFER_VIEWS: { + const auto values = std::span{ + static_cast(result.value), result.value_count + }; + record.views.reserve(values.size()); + for (const auto& value : values) { + record.views.push_back(copy_view(record, value)); + } + record.view.result.value = record.views.data(); + break; + } + case MLN_ADAPTER_COMPLETION_COPY_OFFLINE_REGIONS: { + const auto values = std::span{ + static_cast(result.value), + result.value_count + }; + record.offline_regions.reserve(values.size()); + for (const auto& value : values) { + record.offline_regions.push_back(copy_offline_region(record, value)); + } + record.view.result.value = record.offline_regions.data(); + break; + } + case MLN_ADAPTER_COMPLETION_COPY_STYLE_SOURCE: { + if (result.value_count != 1) + throw std::invalid_argument{"invalid source result"}; + record.style_source = + *static_cast(result.value); + record.style_source.attribution = + copy_view(record, record.style_source.attribution); + record.style_source.url = copy_view(record, record.style_source.url); + const auto urls = std::span{ + record.style_source.tile_urls, record.style_source.tile_url_count + }; + record.views.reserve(urls.size()); + for (const auto url : urls) + record.views.push_back(copy_view(record, url)); + record.style_source.tile_urls = record.views.data(); + record.view.result.value = &record.style_source; + break; + } + case MLN_ADAPTER_COMPLETION_COPY_STYLE_LAYER: { + if (result.value_count != 1) + throw std::invalid_argument{"invalid layer result"}; + record.style_layer = + *static_cast(result.value); + record.style_layer.info.type = + copy_view(record, record.style_layer.info.type); + record.style_layer.source_id = + copy_view(record, record.style_layer.source_id); + record.style_layer.source_layer = + copy_view(record, record.style_layer.source_layer); + record.view.result.value = &record.style_layer; + break; + } + case MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE: { + if (result.value_count != 1) + throw std::invalid_argument{"invalid image result"}; + record.style_image = + *static_cast(result.value); + record.style_image.pixels = copy_view(record, record.style_image.pixels); + if (record.style_image.stretch_x_count != 0) { + record.stretch_x.assign( + record.style_image.stretch_x, + record.style_image.stretch_x + record.style_image.stretch_x_count + ); + } + if (record.style_image.stretch_y_count != 0) { + record.stretch_y.assign( + record.style_image.stretch_y, + record.style_image.stretch_y + record.style_image.stretch_y_count + ); + } + record.style_image.stretch_x = record.stretch_x.data(); + record.style_image.stretch_y = record.stretch_y.data(); + record.view.result.value = &record.style_image; + break; + } + case MLN_ADAPTER_COMPLETION_COPY_STYLE_IMAGE_STRETCHES: { + if (result.value_count != 1) + throw std::invalid_argument{"invalid stretch result"}; + record.image_stretches = + *static_cast(result.value); + if (record.image_stretches.stretch_x_count != 0) { + record.stretch_x.assign( + record.image_stretches.stretch_x, + record.image_stretches.stretch_x + + record.image_stretches.stretch_x_count + ); + } + if (record.image_stretches.stretch_y_count != 0) { + record.stretch_y.assign( + record.image_stretches.stretch_y, + record.image_stretches.stretch_y + + record.image_stretches.stretch_y_count + ); + } + record.image_stretches.stretch_x = record.stretch_x.data(); + record.image_stretches.stretch_y = record.stretch_y.data(); + record.view.result.value = &record.image_stretches; + break; + } + case MLN_ADAPTER_COMPLETION_COPY_QUERIED_FEATURES: { + const auto values = std::span{ + static_cast(result.value), + result.value_count + }; + record.queried_features.reserve(values.size()); + for (auto value : values) { + value.feature = copy_view(record, value.feature); + value.source_id = copy_view(record, value.source_id); + value.source_layer_id = copy_view(record, value.source_layer_id); + value.state = copy_view(record, value.state); + record.queried_features.push_back(value); + } + record.view.result.value = record.queried_features.data(); + break; + } + case MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK: { + if (result.value_count != 1) + throw std::invalid_argument{"invalid readback result"}; + record.texture_readback = + *static_cast(result.value); + record.texture_readback.data = + copy_view(record, record.texture_readback.data); + record.view.result.value = &record.texture_readback; + break; + } + default: + throw std::invalid_argument{"unknown completion copy kind"}; + } +} + +auto adapter_completion_callback( + void* user_data, const mln_completion_result* result +) noexcept -> void { + const auto* state = static_cast(user_data); + if (state == nullptr || result == nullptr || state->listener == nullptr) + return; + auto record = + std::unique_ptr{new (std::nothrow) + AdapterCompletionRecord{}}; + if (!record) { + state->listener(state->user_data, nullptr); + return; + } + record->view.owner = record.get(); + record->view.result = *result; + record->view.result.diagnostic = {}; + record->view.result.value = nullptr; + try { + record->view.result.diagnostic = copy_view(*record, result->diagnostic); + if (result->status == MLN_STATUS_OK) { + copy_completion_value(*record, *result, *state); + } else { + record->view.result.value_count = 0; + } + } catch (...) { + state->listener(state->user_data, nullptr); + return; + } + auto* delivered = &record.release()->view; + state->listener(state->user_data, delivered); +} + +auto adapter_completion_release(void* user_data) noexcept -> void { + delete static_cast(user_data); +} auto matches_rule(std::uint32_t rule_kind, std::uint32_t request_kind) -> bool { return rule_kind == MLN_ADAPTER_RESOURCE_KIND_ANY || @@ -236,7 +566,7 @@ auto copy_prior_data(const mln_resource_request& request) auto copy_request( const mln_resource_request& request, mln_resource_request_handle handle -) -> AdapterQueuedResourceRequestView* { +) -> std::unique_ptr { auto copy = std::make_unique(); copy->requested_url = request.requested_url == nullptr ? std::string{} @@ -269,7 +599,7 @@ auto copy_request( .prior_data = copy->prior_data.empty() ? nullptr : copy->prior_data.data(), .prior_data_size = copy->prior_data.size(), }; - return ©.release()->view; + return copy; } void destroy_queued_request( @@ -284,19 +614,18 @@ void destroy_queued_request( auto copy_log_record( std::uint32_t severity, std::uint32_t event, std::int64_t code, - const char* message, bool retire_callback = false -) -> AdapterLogRecordView* { + const char* message +) -> std::unique_ptr { auto copy = std::make_unique(); copy->message = message == nullptr ? std::string{} : std::string{message}; copy->view = AdapterLogRecordView{ .owner = copy.get(), - .retire_callback = retire_callback, .severity = severity, .event = event, .code = code, .message = copy->message.c_str(), }; - return ©.release()->view; + return copy; } void destroy_log_record(AdapterLogRecordView* record) noexcept { @@ -306,16 +635,57 @@ void destroy_log_record(AdapterLogRecordView* record) noexcept { auto* owner = static_cast(record->owner); static_cast(std::unique_ptr{owner}); } +auto lease_resource_queue(mln_adapter_resource_request_queue queue) + -> std::shared_ptr { + return mln::core::handle_table() + .lease(queue); +} -void queue_log_retirement(mln_adapter_log_record_listener listener) noexcept { - if (listener == nullptr) { - return; +auto lease_log_queue(mln_adapter_log_queue queue) + -> std::shared_ptr { + return mln::core::handle_table().lease( + queue + ); +} + +auto enqueue_request( + const std::shared_ptr& queue, + std::unique_ptr request +) -> bool { + auto wake = std::shared_ptr{}; + auto should_wake = false; + { + const std::scoped_lock lock(queue->mutex); + if (queue->closed) { + return false; + } + should_wake = queue->records.empty(); + queue->records.push_back(std::move(request)); + queue->wake_pending = true; + wake = queue->wake; } - try { - listener(nullptr); - } catch (...) { - // Listener delivery is notification-only at this boundary. + if (should_wake) wake->notify(); + return true; +} + +auto enqueue_log( + const std::shared_ptr& queue, + std::unique_ptr record +) -> bool { + auto wake = std::shared_ptr{}; + auto should_wake = false; + { + const std::scoped_lock lock(queue->mutex); + if (queue->closed) { + return false; + } + should_wake = queue->records.empty(); + queue->records.push_back(std::move(record)); + queue->wake_pending = true; + wake = queue->wake; } + if (should_wake) wake->notify(); + return true; } void destroy_handle_leak_token(void* token) noexcept { @@ -326,6 +696,59 @@ void destroy_handle_leak_token(void* token) noexcept { } // namespace +extern "C" MLN_API auto mln_adapter_completion_create( + std::uint32_t copy_kind, std::size_t element_size, + mln_adapter_completion_listener listener, void* user_data, + mln_completion* out_completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + out_completion == nullptr || out_completion->size != 0 || + out_completion->callback != nullptr || + out_completion->user_data != nullptr || + out_completion->release_user_data != nullptr || listener == nullptr || + copy_kind > MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK + ) { + mln::core::set_thread_error("completion adapter arguments are invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto state = std::make_unique(); + state->copy_kind = copy_kind; + state->element_size = element_size; + state->listener = listener; + state->user_data = user_data; + *out_completion = mln_completion{ + .size = sizeof(mln_completion), + .callback = adapter_completion_callback, + .user_data = state.get(), + .release_user_data = adapter_completion_release, + }; + static_cast(state.release()); + return MLN_STATUS_OK; + }); +} + +extern "C" MLN_API void mln_adapter_completion_reject( + mln_completion* completion +) noexcept { + if ( + completion == nullptr || + completion->callback != adapter_completion_callback || + completion->release_user_data != adapter_completion_release + ) { + return; + } + adapter_completion_release(completion->user_data); + *completion = {}; +} + +extern "C" MLN_API void mln_adapter_completion_record_destroy( + mln_adapter_completion_record* record +) noexcept { + if (record == nullptr || record->owner == nullptr) return; + delete static_cast(record->owner); +} + extern "C" MLN_API auto mln_adapter_handle_leak_token_create( const char* type_name, std::uint64_t handle ) noexcept -> void* { @@ -362,6 +785,152 @@ extern "C" MLN_API void mln_adapter_handle_leak_report(void* token) noexcept { destroy_handle_leak_token(token); } +extern "C" MLN_API auto mln_adapter_resource_request_queue_create( + const mln_wake* wake, mln_adapter_resource_request_queue* out_queue +) noexcept -> mln_status { + try { + if (out_queue == nullptr || *out_queue != MLN_HANDLE_NULL) { + mln::core::set_thread_error("out_queue must point to the null handle"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto wake_status = mln::core::validate_wake(wake); + if (wake_status != MLN_STATUS_OK) return wake_status; + auto owned = + std::make_shared(); + owned->wake = std::make_shared(*wake); + const auto handle = + mln::core::handle_table() + .insert(owned); + owned->wake->accept(); + *out_queue = handle; + return MLN_STATUS_OK; + } catch (...) { + return MLN_STATUS_NATIVE_ERROR; + } +} + +extern "C" MLN_API auto mln_adapter_resource_request_queue_acquire( + mln_adapter_resource_request_queue queue, + mln_adapter_queued_resource_request** out_request +) noexcept -> mln_status { + try { + if (out_request == nullptr || *out_request != nullptr) { + mln::core::set_thread_error("out_request must point to null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto live = lease_resource_queue(queue); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto drain_lock = std::unique_lock{live->drain_mutex, std::try_to_lock}; + if (!drain_lock.owns_lock()) { + mln::core::set_thread_error( + "resource request queue already has an active drain" + ); + return MLN_STATUS_INVALID_STATE; + } + const auto queue_lock = std::scoped_lock{live->mutex}; + if (live->closed) { + mln::core::set_thread_error("resource request queue is closed"); + return MLN_STATUS_INVALID_STATE; + } + if (live->records.empty()) { + live->wake_pending = false; + return MLN_STATUS_OK; + } + auto record = std::move(live->records.front()); + live->records.pop_front(); + *out_request = &record.release()->view; + if (live->records.empty()) { + live->wake_pending = false; + } + return MLN_STATUS_OK; + } catch (...) { + return MLN_STATUS_NATIVE_ERROR; + } +} + +extern "C" MLN_API void mln_adapter_resource_request_queue_close( + mln_adapter_resource_request_queue queue +) noexcept { + const auto removed = + mln::core::handle_table() + .remove(queue); + if (removed != nullptr) { + removed->close(); + } +} + +extern "C" MLN_API auto mln_adapter_log_queue_create( + const mln_wake* wake, mln_adapter_log_queue* out_queue +) noexcept -> mln_status { + try { + if (out_queue == nullptr || *out_queue != MLN_HANDLE_NULL) { + mln::core::set_thread_error("out_queue must point to the null handle"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto wake_status = mln::core::validate_wake(wake); + if (wake_status != MLN_STATUS_OK) return wake_status; + auto owned = std::make_shared(); + owned->wake = std::make_shared(*wake); + const auto handle = + mln::core::handle_table().insert(owned); + owned->wake->accept(); + *out_queue = handle; + return MLN_STATUS_OK; + } catch (...) { + return MLN_STATUS_NATIVE_ERROR; + } +} + +extern "C" MLN_API auto mln_adapter_log_queue_acquire( + mln_adapter_log_queue queue, mln_adapter_log_record** out_record +) noexcept -> mln_status { + try { + if (out_record == nullptr || *out_record != nullptr) { + mln::core::set_thread_error("out_record must point to null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto live = lease_log_queue(queue); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto drain_lock = std::unique_lock{live->drain_mutex, std::try_to_lock}; + if (!drain_lock.owns_lock()) { + mln::core::set_thread_error("log queue already has an active drain"); + return MLN_STATUS_INVALID_STATE; + } + const auto queue_lock = std::scoped_lock{live->mutex}; + if (live->closed) { + mln::core::set_thread_error("log queue is closed"); + return MLN_STATUS_INVALID_STATE; + } + if (live->records.empty()) { + live->wake_pending = false; + return MLN_STATUS_OK; + } + auto record = std::move(live->records.front()); + live->records.pop_front(); + *out_record = &record.release()->view; + if (live->records.empty()) { + live->wake_pending = false; + } + return MLN_STATUS_OK; + } catch (...) { + return MLN_STATUS_NATIVE_ERROR; + } +} + +extern "C" MLN_API void mln_adapter_log_queue_close( + mln_adapter_log_queue queue +) noexcept { + const auto removed = + mln::core::handle_table().remove(queue); + if (removed != nullptr) { + removed->close(); + } +} + extern "C" MLN_API auto mln_adapter_log_callback( void* user_data, std::uint32_t severity, std::uint32_t event, std::int64_t code, const char* message @@ -369,80 +938,48 @@ extern "C" MLN_API auto mln_adapter_log_callback( if (user_data == nullptr) { return 0; } - mln_adapter_log_record_listener listener = nullptr; - std::uint32_t consume = 0; - { - const auto lock = std::scoped_lock{log_state_mutex}; - const auto iterator = log_callbacks.find(user_data); - if (iterator == log_callbacks.end() || iterator->second.retired) { - return 0; - } - listener = iterator->second.listener; - consume = iterator->second.consume; - ++iterator->second.in_flight; + const auto& state = *static_cast(user_data); + const auto queue = lease_log_queue(state.queue); + if (queue == nullptr) { + return 0; } - if (listener != nullptr) { - try { - listener(copy_log_record(severity, event, code, message)); - } catch (...) { - // Logging callbacks are notification-only at the host boundary. - } + try { + static_cast( + enqueue_log(queue, copy_log_record(severity, event, code, message)) + ); + } catch (...) { + // Logging cannot report allocation failure through its callback contract. } - mln_adapter_log_record_listener retirement_listener = nullptr; - { - const auto lock = std::scoped_lock{log_state_mutex}; - const auto iterator = log_callbacks.find(user_data); - if (iterator != log_callbacks.end()) { - --iterator->second.in_flight; - if (iterator->second.retired && iterator->second.in_flight == 0) { - retirement_listener = iterator->second.listener; - log_callbacks.erase(iterator); - } - } + return state.consume; +} + +namespace { + +auto release_adapter_log_callback_state(void* user_data) noexcept -> void { + auto* state = static_cast(user_data); + if (state != nullptr && state->release_user_data != nullptr) { + const auto release = state->release_user_data; + const auto context = state->release_context; + release(context); } - queue_log_retirement(retirement_listener); - return consume; } +} // namespace + extern "C" MLN_API auto mln_adapter_log_set_callback( mln_adapter_log_callback_state* state ) noexcept -> mln_status { const auto setter_lock = std::scoped_lock{log_setter_mutex}; - if (state != nullptr) { - const auto state_lock = std::scoped_lock{log_state_mutex}; - log_callbacks[state] = AdapterLogCallbackEntry{ - .listener = state->listener, - .consume = state->consume, - }; - } - const auto status = state == nullptr - ? mln_log_clear_callback() - : mln_log_set_callback(mln_adapter_log_callback, state); - if (status != MLN_STATUS_OK) { - if (state != nullptr) { - const auto state_lock = std::scoped_lock{log_state_mutex}; - log_callbacks.erase(state); - } - return status; - } - mln_adapter_log_record_listener retirement_listener = nullptr; - { - const auto state_lock = std::scoped_lock{log_state_mutex}; - auto* retired_state = active_log_callback; - active_log_callback = state; - if (retired_state != nullptr && retired_state != state) { - const auto iterator = log_callbacks.find(retired_state); - if (iterator != log_callbacks.end()) { - iterator->second.retired = true; - if (iterator->second.in_flight == 0) { - retirement_listener = iterator->second.listener; - log_callbacks.erase(iterator); - } - } - } + if (state != nullptr && lease_log_queue(state->queue) == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - queue_log_retirement(retirement_listener); - return MLN_STATUS_OK; + return state == nullptr ? mln_log_clear_callback() + : mln_log_set_callback( + mln_adapter_log_callback, state, + state->release_user_data == nullptr + ? nullptr + : release_adapter_log_callback_state + ); } extern "C" MLN_API void mln_adapter_log_record_destroy(void* record) noexcept { @@ -569,9 +1106,6 @@ extern "C" MLN_API auto mln_adapter_queued_resource_provider_callback( const auto& provider = *static_cast(user_data); - if (provider.listener == nullptr) { - return MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH; - } if (!request_matches_route( std::span{provider.routes, provider.route_count}, *request )) { @@ -579,8 +1113,13 @@ extern "C" MLN_API auto mln_adapter_queued_resource_provider_callback( } try { - auto* queued_request = copy_request(*request, handle); - provider.listener(queued_request); + const auto queue = lease_resource_queue(provider.queue); + if (queue == nullptr) { + throw std::runtime_error{"resource request queue is unavailable"}; + } + if (!enqueue_request(queue, copy_request(*request, handle))) { + throw std::runtime_error{"resource request queue is closed"}; + } return MLN_RESOURCE_PROVIDER_DECISION_HANDLE; } catch (...) { auto response = mln_resource_response{ @@ -613,14 +1152,6 @@ extern "C" MLN_API void mln_adapter_resource_provider_request_destroy( ); } -extern "C" MLN_API void mln_adapter_queued_resource_provider_retire( - mln_adapter_queued_resource_provider* provider -) noexcept { - if (provider != nullptr && provider->listener != nullptr) { - provider->listener(nullptr); - } -} - extern "C" MLN_API void mln_adapter_custom_geometry_callbacks_retire( mln_custom_geometry_source_tile_callback fetch_tile, mln_custom_geometry_source_tile_callback cancel_tile, void* user_data diff --git a/src/c_api/camera.cpp b/src/c_api/camera.cpp new file mode 100644 index 000000000..e1eb751fb --- /dev/null +++ b/src/c_api/camera.cpp @@ -0,0 +1,296 @@ +#define MLN_BUILDING_C + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytes/buffer.hpp" +#include "c_api/boundary.hpp" +#include "diagnostics/diagnostics.hpp" +#include "map/map.hpp" +#include "maplibre_native_c.h" +#include "runtime/runtime.hpp" + +auto mln_camera_options_default(void) noexcept -> mln_camera_options { + return mln::core::camera_options_default(); +} + +auto mln_animation_options_default(void) noexcept -> mln_animation_options { + return mln::core::animation_options_default(); +} +auto mln_camera_delta_default(void) noexcept -> mln_camera_delta { + return mln::core::camera_delta_default(); +} +auto mln_camera_update_default(void) noexcept -> mln_camera_update { + return mln::core::camera_update_default(); +} + +auto mln_camera_fit_options_default(void) noexcept -> mln_camera_fit_options { + return mln::core::camera_fit_options_default(); +} + +auto mln_bound_options_default(void) noexcept -> mln_bound_options { + return mln::core::bound_options_default(); +} + +auto mln_free_camera_options_default(void) noexcept -> mln_free_camera_options { + return mln::core::free_camera_options_default(); +} + +auto mln_projection_mode_default(void) noexcept -> mln_projection_mode { + return mln::core::projection_mode_default(); +} + +auto mln_map_viewport_options_default(void) noexcept + -> mln_map_viewport_options { + return mln::core::map_viewport_options_default(); +} + +auto mln_map_tile_options_default(void) noexcept -> mln_map_tile_options { + return mln::core::map_tile_options_default(); +} + +auto mln_map_camera_snapshot_get( + mln_map map, mln_camera_options* out_camera, uint64_t* out_generation +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_camera_snapshot_get(map, out_camera, out_generation); + }); +} + +auto mln_map_update_camera( + mln_map map, const mln_camera_update* update, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_update_camera(map, update, completion); + }); +} + +auto mln_map_apply_camera_delta( + mln_map map, const mln_camera_delta* delta, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_apply_camera_delta(map, delta, completion); + }); +} + +auto mln_map_camera_query( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_camera_query_start(map, completion); + }); +} + +auto mln_map_set_projection_mode( + mln_map map, const mln_projection_mode* mode, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_set_projection_mode(map, mode, completion); + }); +} + +auto mln_map_set_debug_options( + mln_map map, uint32_t options, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + const auto validation = mln::core::validate_debug_options_input(options); + if (validation != MLN_STATUS_OK) return validation; + return mln::core::submit_map_command( + map, + [map, options] { return mln::core::map_set_debug_options(map, options); }, + completion + ); + }); +} + +auto mln_map_set_rendering_stats_view_enabled( + mln_map map, bool enabled, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::submit_map_command( + map, + [map, enabled] { + return mln::core::map_set_rendering_stats_view_enabled(map, enabled); + }, + completion + ); + }); +} + +auto mln_map_dump_debug_logs( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::submit_map_command( + map, [map] { return mln::core::map_dump_debug_logs(map); }, completion + ); + }); +} + +auto mln_map_set_viewport_options( + mln_map map, const mln_map_viewport_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + const auto validation = mln::core::validate_viewport_options_input(options); + if (validation != MLN_STATUS_OK) return validation; + const auto copied = *options; + return mln::core::submit_map_command( + map, + [map, copied] { + return mln::core::map_set_viewport_options(map, &copied); + }, + completion + ); + }); +} + +auto mln_map_set_tile_options( + mln_map map, const mln_map_tile_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + const auto validation = mln::core::validate_tile_options_input(options); + if (validation != MLN_STATUS_OK) return validation; + const auto copied = *options; + return mln::core::submit_map_command( + map, + [map, copied] { return mln::core::map_set_tile_options(map, &copied); }, + completion + ); + }); +} + +auto mln_map_pixel_for_lat_lng( + mln_map map, mln_lat_lng coordinate, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_pixel_for_lat_lng_start(map, coordinate, completion); + }); +} + +auto mln_map_lat_lng_for_pixel( + mln_map map, mln_screen_point point, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_lat_lng_for_pixel_start(map, point, completion); + }); +} + +auto mln_map_pixels_for_lat_lngs( + mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_pixels_for_lat_lngs_start( + map, coordinates, coordinate_count, completion + ); + }); +} + +auto mln_map_lat_lngs_for_pixels( + mln_map map, const mln_screen_point* points, size_t point_count, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_lat_lngs_for_pixels_start( + map, points, point_count, completion + ); + }); +} + +auto mln_map_camera_for_lat_lng_bounds( + mln_map map, mln_lat_lng_bounds bounds, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_camera_for_lat_lng_bounds_start( + map, bounds, fit_options, completion + ); + }); +} + +auto mln_map_camera_for_lat_lngs( + mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_camera_for_lat_lngs_start( + map, coordinates, coordinate_count, fit_options, completion + ); + }); +} + +auto mln_map_camera_for_geometry( + mln_map map, mln_buffer_view geometry, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_camera_for_geometry_start( + map, geometry, fit_options, completion + ); + }); +} + +auto mln_map_lat_lng_bounds_for_camera( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_lat_lng_bounds_for_camera_start( + map, camera, completion + ); + }); +} + +auto mln_map_lat_lng_bounds_for_camera_unwrapped( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::map_lat_lng_bounds_for_camera_unwrapped_start( + map, camera, completion + ); + }); +} + +auto mln_map_set_bounds( + mln_map map, const mln_bound_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + const auto validation = mln::core::validate_bound_options_input(options); + if (validation != MLN_STATUS_OK) return validation; + const auto copied = *options; + return mln::core::submit_map_command( + map, [map, copied] { return mln::core::map_set_bounds(map, &copied); }, + completion + ); + }); +} + +auto mln_map_set_free_camera_options( + mln_map map, const mln_free_camera_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + const auto validation = + mln::core::validate_free_camera_options_input(options); + if (validation != MLN_STATUS_OK) return validation; + const auto copied = *options; + return mln::core::submit_map_command( + map, + [map, copied] { + return mln::core::map_set_free_camera_options(map, &copied); + }, + completion + ); + }); +} diff --git a/src/c_api/diagnostics.cpp b/src/c_api/diagnostics.cpp index c8d0f0280..abca4293d 100644 --- a/src/c_api/diagnostics.cpp +++ b/src/c_api/diagnostics.cpp @@ -1,8 +1,5 @@ #define MLN_BUILDING_C -#include -#include - #include "diagnostics/diagnostics.hpp" #include "maplibre_native_c.h" @@ -10,20 +7,3 @@ auto mln_thread_last_error_message(void) noexcept -> const char* { return mln::core::thread_last_error_message(); } - -namespace { - -// A counter rather than a platform thread id, which is not uniformly available -// as an integer. Zero is never handed out, so a host can use it as "not yet -// observed". -auto next_thread_token() noexcept -> uint64_t { - static std::atomic counter{0}; - return counter.fetch_add(1, std::memory_order_relaxed) + 1; -} - -} // namespace - -auto mln_thread_token(void) noexcept -> uint64_t { - static thread_local const uint64_t token = next_thread_token(); - return token; -} diff --git a/src/c_api/logging.cpp b/src/c_api/logging.cpp index 7ecc9ad38..3169f9375 100644 --- a/src/c_api/logging.cpp +++ b/src/c_api/logging.cpp @@ -7,10 +7,12 @@ #include "c_api/boundary.hpp" #include "maplibre_native_c.h" -auto mln_log_set_callback(mln_log_callback callback, void* user_data) noexcept - -> mln_status { +auto mln_log_set_callback( + mln_log_callback callback, void* user_data, + mln_log_callback_release release_user_data +) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::set_log_callback(callback, user_data); + return mln::core::set_log_callback(callback, user_data, release_user_data); }); } diff --git a/src/c_api/map.cpp b/src/c_api/map.cpp index 4c1f5042f..4bb7a3d2c 100644 --- a/src/c_api/map.cpp +++ b/src/c_api/map.cpp @@ -1,1364 +1,191 @@ #define MLN_BUILDING_C +#include #include #include +#include +#include +#include +#include +#include +#include #include "map/map.hpp" +#include "bytes/buffer.hpp" #include "c_api/boundary.hpp" -#include "geojson/geojson_source_data.hpp" +#include "diagnostics/diagnostics.hpp" +#include "geojson/geojson.hpp" +#include "map/feature_state.hpp" #include "maplibre_native_c.h" +#include "runtime/runtime.hpp" auto mln_map_options_default(void) noexcept -> mln_map_options { return mln::core::map_options_default(); } -auto mln_camera_options_default(void) noexcept -> mln_camera_options { - return mln::core::camera_options_default(); -} - -auto mln_animation_options_default(void) noexcept -> mln_animation_options { - return mln::core::animation_options_default(); -} - -auto mln_camera_fit_options_default(void) noexcept -> mln_camera_fit_options { - return mln::core::camera_fit_options_default(); -} - -auto mln_bound_options_default(void) noexcept -> mln_bound_options { - return mln::core::bound_options_default(); -} - -auto mln_free_camera_options_default(void) noexcept -> mln_free_camera_options { - return mln::core::free_camera_options_default(); -} - -auto mln_projection_mode_default(void) noexcept -> mln_projection_mode { - return mln::core::projection_mode_default(); -} - -auto mln_map_viewport_options_default(void) noexcept - -> mln_map_viewport_options { - return mln::core::map_viewport_options_default(); -} - -auto mln_map_tile_options_default(void) noexcept -> mln_map_tile_options { - return mln::core::map_tile_options_default(); -} - -auto mln_style_tile_source_options_default(void) noexcept - -> mln_style_tile_source_options { - return mln::core::style_tile_source_options_default(); -} - -auto mln_geojson_source_options_default(void) noexcept - -> mln_geojson_source_options { - return mln::core::geojson_source_options_default(); -} - -auto mln_custom_geometry_source_options_default(void) noexcept - -> mln_custom_geometry_source_options { - return mln::core::custom_geometry_source_options_default(); -} - -auto mln_custom_mvt_vector_source_options_default(void) noexcept - -> mln_custom_mvt_vector_source_options { - return mln::core::custom_mvt_vector_source_options_default(); -} - -auto mln_premultiplied_rgba8_image_default(void) noexcept - -> mln_premultiplied_rgba8_image { - return mln::core::premultiplied_rgba8_image_default(); -} - -auto mln_style_image_options_default(void) noexcept -> mln_style_image_options { - return mln::core::style_image_options_default(); -} - -auto mln_style_image_info_default(void) noexcept -> mln_style_image_info { - return mln::core::style_image_info_default(); -} - -auto mln_style_transition_options_default(void) noexcept - -> mln_style_transition_options { - return mln::core::style_transition_options_default(); -} - auto mln_map_create( - mln_runtime runtime, const mln_map_options* options, mln_map* out_map -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::create_map(runtime, options, out_map); - }); -} - -auto mln_map_destroy(mln_map map) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::destroy_map(map); - }); -} - -auto mln_map_request_repaint(mln_map map) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_request_repaint(map); - }); -} - -auto mln_map_set_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer_view state -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_feature_state(map, selector, state); - }); -} - -auto mln_map_get_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer* out_state -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_feature_state(map, selector, out_state); - }); -} - -auto mln_map_remove_feature_state( - mln_map map, const mln_feature_state_selector* selector -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_remove_feature_state(map, selector); - }); -} - -auto mln_map_request_still_image(mln_map map) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_request_still_image(map); - }); -} - -auto mln_map_set_style_url(mln_map map, const char* url) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_url(map, url); - }); -} - -auto mln_map_set_style_json(mln_map map, mln_buffer_view json) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_json(map, json); - }); -} - -auto mln_map_copy_loaded_style_json( - mln_map map, uint8_t* out_json, size_t json_capacity, size_t* out_json_size -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_loaded_style_json( - map, out_json, json_capacity, out_json_size - ); - }); -} - -auto mln_map_copy_style_url( - mln_map map, char* out_url, size_t url_capacity, size_t* out_url_size + mln_runtime runtime, const mln_map_options* options, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_style_url( - map, out_url, url_capacity, out_url_size - ); - }); -} - -auto mln_map_set_event_mask(mln_map map, uint64_t mask) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_event_mask(map, mask); + return mln::core::create_map_start(runtime, options, completion); }); } -auto mln_map_get_event_mask(mln_map map, uint64_t* out_mask) noexcept +auto mln_map_release(mln_map map, const mln_completion* completion) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_event_mask(map, out_mask); + return mln::core::release_map(map, completion); }); } -auto mln_style_id_list_count(mln_style_id_list list, size_t* out_count) noexcept +auto mln_map_snapshot_get(mln_map map, mln_map_snapshot* out_snapshot) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::style_id_list_count(list, out_count); - }); -} - -auto mln_style_id_list_get( - mln_style_id_list list, size_t index, mln_buffer_view* out_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::style_id_list_get(list, index, out_id); - }); -} - -auto mln_style_id_list_destroy(mln_style_id_list list) noexcept -> void { - mln::core::style_id_list_destroy(list); -} - -auto mln_style_string_list_count( - mln_style_string_list list, size_t* out_count -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::style_string_list_count(list, out_count); - }); -} - -auto mln_style_string_list_get( - mln_style_string_list list, size_t index, mln_buffer_view* out_value -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::style_string_list_get(list, index, out_value); - }); -} - -auto mln_style_string_list_destroy(mln_style_string_list list) noexcept - -> void { - mln::core::style_string_list_destroy(list); -} - -auto mln_map_add_style_source_json( - mln_map map, mln_buffer_view source_id, mln_buffer_view source_json -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_style_source_json(map, source_id, source_json); - }); -} - -auto mln_map_remove_style_source( - mln_map map, mln_buffer_view source_id, bool* out_removed -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_remove_style_source(map, source_id, out_removed); - }); -} - -auto mln_map_style_source_exists( - mln_map map, mln_buffer_view source_id, bool* out_exists -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_style_source_exists(map, source_id, out_exists); - }); -} - -auto mln_map_get_style_source_type( - mln_map map, mln_buffer_view source_id, uint32_t* out_source_type, - bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_source_type( - map, source_id, out_source_type, out_found - ); - }); -} - -auto mln_map_get_style_source_info( - mln_map map, mln_buffer_view source_id, mln_style_source_info* out_info, - bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_source_info( - map, source_id, out_info, out_found - ); - }); -} - -auto mln_map_copy_style_source_attribution( - mln_map map, mln_buffer_view source_id, char* out_attribution, - size_t attribution_capacity, size_t* out_attribution_size, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_style_source_attribution( - map, source_id, out_attribution, attribution_capacity, - out_attribution_size, out_found - ); - }); -} - -auto mln_map_copy_style_source_url( - mln_map map, mln_buffer_view source_id, char* out_url, size_t url_capacity, - size_t* out_url_size, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_style_source_url( - map, source_id, out_url, url_capacity, out_url_size, out_found - ); - }); -} - -auto mln_map_get_style_source_tile_urls( - mln_map map, mln_buffer_view source_id, mln_style_string_list* out_tile_urls, - bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_source_tile_urls( - map, source_id, out_tile_urls, out_found - ); - }); -} - -auto mln_map_list_style_source_ids( - mln_map map, mln_style_id_list* out_source_ids -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_list_style_source_ids(map, out_source_ids); - }); -} - -auto mln_map_add_geojson_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_geojson_source_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_geojson_source_url(map, source_id, url, options); - }); -} - -auto mln_geojson_source_data_create( - mln_buffer_view data, const mln_geojson_source_options* options, - mln_geojson_source_data* out_data -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::geojson_source_data_create(data, options, out_data); - }); -} - -void mln_geojson_source_data_destroy(mln_geojson_source_data data) noexcept { - mln::core::geojson_source_data_destroy(data); -} - -auto mln_map_add_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_geojson_source_data(map, source_id, data); - }); -} - -auto mln_map_set_geojson_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_geojson_source_url(map, source_id, url); - }); -} - -auto mln_map_set_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_geojson_source_data(map, source_id, data); - }); -} - -auto mln_map_set_geojson_source_synchronous_tiling( - mln_map map, mln_buffer_view source_id, bool enabled -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_geojson_source_synchronous_tiling( - map, source_id, enabled - ); - }); -} - -auto mln_map_add_vector_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_vector_source_url(map, source_id, url, options); - }); -} - -auto mln_map_add_vector_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_vector_source_tiles( - map, source_id, tiles, tile_count, options - ); - }); -} - -auto mln_map_add_raster_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_raster_source_url(map, source_id, url, options); + return mln::core::map_snapshot_get(map, out_snapshot); }); } -auto mln_map_add_raster_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options +auto mln_map_resize( + mln_map map, mln_logical_extent extent, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_raster_source_tiles( - map, source_id, tiles, tile_count, options - ); + return mln::core::map_resize(map, extent, completion); }); } -auto mln_map_add_raster_dem_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options +auto mln_map_request_repaint( + mln_map map, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_raster_dem_source_url( - map, source_id, url, options - ); + return mln::core::map_request_repaint(map, completion); }); } -auto mln_map_add_raster_dem_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options +auto mln_map_request_still_image( + mln_map map, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_raster_dem_source_tiles( - map, source_id, tiles, tile_count, options - ); + return mln::core::map_request_still_image_start(map, completion); }); } -auto mln_map_add_custom_geometry_source( - mln_map map, mln_buffer_view source_id, - const mln_custom_geometry_source_options* options +auto mln_map_set_event_mask( + mln_map map, uint64_t mask, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_custom_geometry_source(map, source_id, options); + return mln::core::map_set_event_mask(map, mask, completion); }); } -auto mln_map_set_custom_geometry_source_tile_data( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_custom_geometry_source_tile_data( - map, source_id, tile_id, data - ); - }); -} +namespace { -auto mln_map_invalidate_custom_geometry_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_invalidate_custom_geometry_source_tile( - map, source_id, tile_id - ); - }); -} +// Owns copies of the selector strings so a deferred command outlives the +// caller's buffers. +struct OwnedFeatureStateSelector { + uint32_t fields = 0; + std::string source_id; + std::string source_layer_id; + std::string feature_id; + std::string state_key; -auto mln_map_invalidate_custom_geometry_source_region( - mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_invalidate_custom_geometry_source_region( - map, source_id, bounds - ); - }); -} + explicit OwnedFeatureStateSelector(const mln_feature_state_selector& selector) + : fields(selector.fields), + source_id(copy_view(selector.source_id)), + source_layer_id(copy_view(selector.source_layer_id)), + feature_id(copy_view(selector.feature_id)), + state_key(copy_view(selector.state_key)) {} -auto mln_map_add_custom_mvt_vector_source( - mln_map map, mln_buffer_view source_id, - const mln_custom_mvt_vector_source_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_custom_mvt_vector_source(map, source_id, options); - }); -} + [[nodiscard]] auto view() const -> mln_feature_state_selector { + return mln_feature_state_selector{ + .size = sizeof(mln_feature_state_selector), + .fields = fields, + .source_id = {.data = source_id.data(), .size = source_id.size()}, + .source_layer_id = + {.data = source_layer_id.data(), .size = source_layer_id.size()}, + .feature_id = {.data = feature_id.data(), .size = feature_id.size()}, + .state_key = {.data = state_key.data(), .size = state_key.size()}, + }; + } -auto mln_map_set_custom_mvt_vector_source_tile_data( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_custom_mvt_vector_source_tile_data( - map, source_id, tile_id, data - ); - }); -} + private: + static auto copy_view(mln_buffer_view value) -> std::string { + return value.data == nullptr + ? std::string{} + : std::string{static_cast(value.data), value.size}; + } +}; -auto mln_map_set_custom_mvt_vector_source_tile_error( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view message -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_custom_mvt_vector_source_tile_error( - map, source_id, tile_id, message - ); - }); -} +} // namespace -auto mln_map_invalidate_custom_mvt_vector_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_invalidate_custom_mvt_vector_source_tile( - map, source_id, tile_id +auto mln_map_set_feature_state( + mln_map map, const mln_feature_state_selector* selector, + mln_buffer_view state, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + const auto selector_status = + mln::core::validate_feature_state_selector(selector, true); + if (selector_status != MLN_STATUS_OK) { + return selector_status; + } + if (state.data == nullptr || state.size == 0) { + mln::core::set_thread_error("state must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + // The header promises validation before return, so parse here; the + // command parses again on the worker, which stays cheap for the small + // objects feature state carries. + const auto parsed = mln::core::to_native_json_value(state); + if (!parsed || parsed->getObject() == nullptr) { + mln::core::set_thread_error("feature state value must be a JSON object"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto owned_selector = OwnedFeatureStateSelector{*selector}; + auto owned_state = + std::string{static_cast(state.data), state.size}; + return mln::core::submit_map_command( + map, + [map, owned_selector = std::move(owned_selector), + owned_state = std::move(owned_state)]() -> mln_status { + const auto selector = owned_selector.view(); + return mln::core::map_set_feature_state( + map, &selector, + {.data = owned_state.data(), .size = owned_state.size()} + ); + }, + completion ); }); } -auto mln_map_set_style_image( - mln_map map, mln_buffer_view image_id, - const mln_premultiplied_rgba8_image* image, - const mln_style_image_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_image(map, image_id, image, options); - }); -} - -auto mln_map_remove_style_image( - mln_map map, mln_buffer_view image_id, bool* out_removed -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_remove_style_image(map, image_id, out_removed); - }); -} - -auto mln_map_style_image_exists( - mln_map map, mln_buffer_view image_id, bool* out_exists +auto mln_map_get_feature_state( + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_style_image_exists(map, image_id, out_exists); + return mln::core::map_get_feature_state_start(map, selector, completion); }); } -auto mln_map_get_style_image_info( - mln_map map, mln_buffer_view image_id, mln_style_image_info* out_info, - bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_image_info( - map, image_id, out_info, out_found +auto mln_map_remove_feature_state( + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + const auto selector_status = + mln::core::validate_feature_state_selector(selector, false); + if (selector_status != MLN_STATUS_OK) { + return selector_status; + } + auto owned_selector = OwnedFeatureStateSelector{*selector}; + return mln::core::submit_map_command( + map, + [map, owned_selector = std::move(owned_selector)]() -> mln_status { + const auto selector = owned_selector.view(); + return mln::core::map_remove_feature_state(map, &selector); + }, + completion ); }); } - -auto mln_map_copy_style_image_stretches( - mln_map map, mln_buffer_view image_id, mln_image_stretch* out_stretch_x, - size_t stretch_x_capacity, size_t* out_stretch_x_count, - mln_image_stretch* out_stretch_y, size_t stretch_y_capacity, - size_t* out_stretch_y_count, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_style_image_stretches( - map, image_id, out_stretch_x, stretch_x_capacity, out_stretch_x_count, - out_stretch_y, stretch_y_capacity, out_stretch_y_count, out_found - ); - }); -} - -auto mln_map_copy_style_image_premultiplied_rgba8( - mln_map map, mln_buffer_view image_id, uint8_t* out_pixels, - size_t pixel_capacity, size_t* out_byte_length, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_style_image_premultiplied_rgba8( - map, image_id, out_pixels, pixel_capacity, out_byte_length, out_found - ); - }); -} - -auto mln_map_add_image_source_url( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, mln_buffer_view url -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_image_source_url( - map, source_id, coordinates, coordinate_count, url - ); - }); -} - -auto mln_map_add_image_source_image( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, const mln_premultiplied_rgba8_image* image -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_image_source_image( - map, source_id, coordinates, coordinate_count, image - ); - }); -} - -auto mln_map_set_image_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_image_source_url(map, source_id, url); - }); -} - -auto mln_map_set_image_source_image( - mln_map map, mln_buffer_view source_id, - const mln_premultiplied_rgba8_image* image -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_image_source_image(map, source_id, image); - }); -} - -auto mln_map_set_image_source_coordinates( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_image_source_coordinates( - map, source_id, coordinates, coordinate_count - ); - }); -} - -auto mln_map_get_image_source_coordinates( - mln_map map, mln_buffer_view source_id, mln_lat_lng* out_coordinates, - size_t coordinate_capacity, size_t* out_coordinate_count, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_image_source_coordinates( - map, source_id, out_coordinates, coordinate_capacity, - out_coordinate_count, out_found - ); - }); -} - -auto mln_map_add_hillshade_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_hillshade_layer( - map, layer_id, source_id, before_layer_id - ); - }); -} - -auto mln_map_add_color_relief_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_color_relief_layer( - map, layer_id, source_id, before_layer_id - ); - }); -} - -auto mln_map_add_location_indicator_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_location_indicator_layer( - map, layer_id, before_layer_id - ); - }); -} - -auto mln_map_set_location_indicator_location( - mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_location_indicator_location( - map, layer_id, coordinate, altitude - ); - }); -} - -auto mln_map_set_location_indicator_bearing( - mln_map map, mln_buffer_view layer_id, double bearing -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_location_indicator_bearing( - map, layer_id, bearing - ); - }); -} - -auto mln_map_set_location_indicator_accuracy_radius( - mln_map map, mln_buffer_view layer_id, double radius -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_location_indicator_accuracy_radius( - map, layer_id, radius - ); - }); -} - -auto mln_map_set_location_indicator_image_name( - mln_map map, mln_buffer_view layer_id, uint32_t image_kind, - mln_buffer_view image_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_location_indicator_image_name( - map, layer_id, image_kind, image_id - ); - }); -} - -auto mln_map_add_style_layer_json( - mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_add_style_layer_json( - map, layer_json, before_layer_id - ); - }); -} - -auto mln_map_remove_style_layer( - mln_map map, mln_buffer_view layer_id, bool* out_removed -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_remove_style_layer(map, layer_id, out_removed); - }); -} - -auto mln_map_style_layer_exists( - mln_map map, mln_buffer_view layer_id, bool* out_exists -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_style_layer_exists(map, layer_id, out_exists); - }); -} - -auto mln_map_get_style_layer_type( - mln_map map, mln_buffer_view layer_id, mln_buffer_view* out_layer_type, - bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_layer_type( - map, layer_id, out_layer_type, out_found - ); - }); -} - -auto mln_map_list_style_layer_ids( - mln_map map, mln_style_id_list* out_layer_ids -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_list_style_layer_ids(map, out_layer_ids); - }); -} - -auto mln_map_move_style_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_move_style_layer(map, layer_id, before_layer_id); - }); -} - -auto mln_map_get_style_layer_json( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_layer, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_layer_json( - map, layer_id, out_layer, out_found - ); - }); -} - -auto mln_map_set_style_light_json( - mln_map map, mln_buffer_view light_json -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_light_json(map, light_json); - }); -} - -auto mln_map_set_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer_view value -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_light_property(map, property_name, value); - }); -} - -auto mln_map_get_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer* out_value -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_light_property( - map, property_name, out_value - ); - }); -} - -auto mln_map_set_style_transition_options( - mln_map map, const mln_style_transition_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_style_transition_options(map, options); - }); -} - -auto mln_map_get_style_transition_options( - mln_map map, mln_style_transition_options* out_options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_style_transition_options(map, out_options); - }); -} - -auto mln_map_set_layer_property( - mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer_view value -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_property( - map, layer_id, property_name, value - ); - }); -} - -auto mln_map_get_layer_property( - mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer* out_value -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_layer_property( - map, layer_id, property_name, out_value - ); - }); -} - -auto mln_map_set_layer_filter( - mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_filter(map, layer_id, filter); - }); -} - -auto mln_map_get_layer_filter( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_filter -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_layer_filter(map, layer_id, out_filter); - }); -} - -auto mln_map_set_layer_source_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_source_layer(map, layer_id, source_layer); - }); -} - -auto mln_map_copy_layer_source_layer( - mln_map map, mln_buffer_view layer_id, char* out_source_layer, - size_t source_layer_capacity, size_t* out_source_layer_size -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_layer_source_layer( - map, layer_id, out_source_layer, source_layer_capacity, - out_source_layer_size - ); - }); -} - -auto mln_map_set_layer_source_id( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_source_id(map, layer_id, source_id); - }); -} - -auto mln_map_copy_layer_source_id( - mln_map map, mln_buffer_view layer_id, char* out_source_id, - size_t source_id_capacity, size_t* out_source_id_size -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_copy_layer_source_id( - map, layer_id, out_source_id, source_id_capacity, out_source_id_size - ); - }); -} - -auto mln_map_set_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double min_zoom -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_min_zoom(map, layer_id, min_zoom); - }); -} - -auto mln_map_get_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double* out_min_zoom -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_layer_min_zoom(map, layer_id, out_min_zoom); - }); -} - -auto mln_map_set_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double max_zoom -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_max_zoom(map, layer_id, max_zoom); - }); -} - -auto mln_map_get_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double* out_max_zoom -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_layer_max_zoom(map, layer_id, out_max_zoom); - }); -} - -auto mln_map_set_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t visibility -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_layer_visibility(map, layer_id, visibility); - }); -} - -auto mln_map_get_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t* out_visibility -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_layer_visibility(map, layer_id, out_visibility); - }); -} - -auto mln_map_get_camera(mln_map map, mln_camera_options* out_camera) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_camera(map, out_camera); - }); -} - -auto mln_map_jump_to(mln_map map, const mln_camera_options* camera) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_jump_to(map, camera); - }); -} - -auto mln_map_ease_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_ease_to(map, camera, animation); - }); -} - -auto mln_map_fly_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_fly_to(map, camera, animation); - }); -} - -auto mln_map_get_projection_mode( - mln_map map, mln_projection_mode* out_mode -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_projection_mode(map, out_mode); - }); -} - -auto mln_map_set_projection_mode( - mln_map map, const mln_projection_mode* mode -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_projection_mode(map, mode); - }); -} - -auto mln_map_set_debug_options(mln_map map, uint32_t options) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_debug_options(map, options); - }); -} - -auto mln_map_get_debug_options(mln_map map, uint32_t* out_options) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_debug_options(map, out_options); - }); -} - -auto mln_map_set_rendering_stats_view_enabled( - mln_map map, bool enabled -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_rendering_stats_view_enabled(map, enabled); - }); -} - -auto mln_map_get_rendering_stats_view_enabled( - mln_map map, bool* out_enabled -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_rendering_stats_view_enabled(map, out_enabled); - }); -} - -auto mln_map_is_fully_loaded(mln_map map, bool* out_loaded) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_is_fully_loaded(map, out_loaded); - }); -} - -auto mln_map_dump_debug_logs(mln_map map) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_dump_debug_logs(map); - }); -} - -auto mln_map_get_size( - mln_map map, uint32_t* out_width, uint32_t* out_height, - double* out_scale_factor -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_size( - map, out_width, out_height, out_scale_factor - ); - }); -} - -auto mln_map_get_viewport_options( - mln_map map, mln_map_viewport_options* out_options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_viewport_options(map, out_options); - }); -} - -auto mln_map_set_viewport_options( - mln_map map, const mln_map_viewport_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_viewport_options(map, options); - }); -} - -auto mln_map_get_tile_options( - mln_map map, mln_map_tile_options* out_options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_tile_options(map, out_options); - }); -} - -auto mln_map_set_tile_options( - mln_map map, const mln_map_tile_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_tile_options(map, options); - }); -} - -auto mln_map_pixel_for_lat_lng( - mln_map map, mln_lat_lng coordinate, mln_screen_point* out_point -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_pixel_for_lat_lng(map, coordinate, out_point); - }); -} - -auto mln_map_lat_lng_for_pixel( - mln_map map, mln_screen_point point, mln_lat_lng* out_coordinate -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_lat_lng_for_pixel(map, point, out_coordinate); - }); -} - -auto mln_map_pixels_for_lat_lngs( - mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, - mln_screen_point* out_points -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_pixels_for_lat_lngs( - map, coordinates, coordinate_count, out_points - ); - }); -} - -auto mln_map_lat_lngs_for_pixels( - mln_map map, const mln_screen_point* points, size_t point_count, - mln_lat_lng* out_coordinates -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_lat_lngs_for_pixels( - map, points, point_count, out_coordinates - ); - }); -} - -auto mln_map_projection_create( - mln_map map, mln_map_projection* out_projection -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_create(map, out_projection); - }); -} - -auto mln_map_projection_destroy(mln_map_projection projection) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_destroy(projection); - }); -} - -auto mln_map_projection_get_camera( - mln_map_projection projection, mln_camera_options* out_camera -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_get_camera(projection, out_camera); - }); -} - -auto mln_map_projection_set_camera( - mln_map_projection projection, const mln_camera_options* camera -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_set_camera(projection, camera); - }); -} - -auto mln_map_projection_set_visible_coordinates( - mln_map_projection projection, const mln_lat_lng* coordinates, - size_t coordinate_count, mln_edge_insets padding -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_set_visible_coordinates( - projection, coordinates, coordinate_count, padding - ); - }); -} - -auto mln_map_projection_set_visible_geometry( - mln_map_projection projection, mln_buffer_view geometry, - mln_edge_insets padding -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_set_visible_geometry( - projection, geometry, padding - ); - }); -} - -auto mln_map_projection_pixel_for_lat_lng( - mln_map_projection projection, mln_lat_lng coordinate, - mln_screen_point* out_point -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_pixel_for_lat_lng( - projection, coordinate, out_point - ); - }); -} - -auto mln_map_projection_lat_lng_for_pixel( - mln_map_projection projection, mln_screen_point point, - mln_lat_lng* out_coordinate -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_projection_lat_lng_for_pixel( - projection, point, out_coordinate - ); - }); -} - -auto mln_projected_meters_for_lat_lng( - mln_lat_lng coordinate, mln_projected_meters* out_meters -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::projected_meters_for_lat_lng(coordinate, out_meters); - }); -} - -auto mln_lat_lng_for_projected_meters( - mln_projected_meters meters, mln_lat_lng* out_coordinate -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::lat_lng_for_projected_meters(meters, out_coordinate); - }); -} - -auto mln_map_move_by(mln_map map, double delta_x, double delta_y) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_move_by(map, delta_x, delta_y); - }); -} - -auto mln_map_move_by_animated( - mln_map map, double delta_x, double delta_y, - const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_move_by_animated(map, delta_x, delta_y, animation); - }); -} - -auto mln_map_scale_by( - mln_map map, double scale, const mln_screen_point* anchor -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_scale_by(map, scale, anchor); - }); -} - -auto mln_map_scale_by_animated( - mln_map map, double scale, const mln_screen_point* anchor, - const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_scale_by_animated(map, scale, anchor, animation); - }); -} - -auto mln_map_rotate_by( - mln_map map, mln_screen_point first, mln_screen_point second -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_rotate_by(map, first, second); - }); -} - -auto mln_map_rotate_by_animated( - mln_map map, mln_screen_point first, mln_screen_point second, - const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_rotate_by_animated(map, first, second, animation); - }); -} - -auto mln_map_pitch_by(mln_map map, double pitch) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_pitch_by(map, pitch); - }); -} - -auto mln_map_pitch_by_animated( - mln_map map, double pitch, const mln_animation_options* animation -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_pitch_by_animated(map, pitch, animation); - }); -} - -auto mln_map_cancel_transitions(mln_map map) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_cancel_transitions(map); - }); -} - -auto mln_map_set_gesture_in_progress(mln_map map, bool in_progress) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_gesture_in_progress(map, in_progress); - }); -} - -auto mln_map_is_gesture_in_progress(mln_map map, bool* out_in_progress) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_is_gesture_in_progress(map, out_in_progress); - }); -} - -auto mln_map_camera_for_lat_lng_bounds( - mln_map map, mln_lat_lng_bounds bounds, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_camera_for_lat_lng_bounds( - map, bounds, fit_options, out_camera - ); - }); -} - -auto mln_map_camera_for_lat_lngs( - mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_camera_for_lat_lngs( - map, coordinates, coordinate_count, fit_options, out_camera - ); - }); -} - -auto mln_map_camera_for_geometry( - mln_map map, mln_buffer_view geometry, - const mln_camera_fit_options* fit_options, mln_camera_options* out_camera -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_camera_for_geometry( - map, geometry, fit_options, out_camera - ); - }); -} - -auto mln_map_lat_lng_bounds_for_camera( - mln_map map, const mln_camera_options* camera, mln_lat_lng_bounds* out_bounds -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_lat_lng_bounds_for_camera(map, camera, out_bounds); - }); -} - -auto mln_map_lat_lng_bounds_for_camera_unwrapped( - mln_map map, const mln_camera_options* camera, mln_lat_lng_bounds* out_bounds -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_lat_lng_bounds_for_camera_unwrapped( - map, camera, out_bounds - ); - }); -} - -auto mln_map_get_bounds(mln_map map, mln_bound_options* out_options) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_bounds(map, out_options); - }); -} - -auto mln_map_set_bounds(mln_map map, const mln_bound_options* options) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_bounds(map, options); - }); -} - -auto mln_map_get_free_camera_options( - mln_map map, mln_free_camera_options* out_options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_get_free_camera_options(map, out_options); - }); -} - -auto mln_map_set_free_camera_options( - mln_map map, const mln_free_camera_options* options -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::map_set_free_camera_options(map, options); - }); -} diff --git a/src/c_api/projection.cpp b/src/c_api/projection.cpp new file mode 100644 index 000000000..1a53ae352 --- /dev/null +++ b/src/c_api/projection.cpp @@ -0,0 +1,109 @@ +#define MLN_BUILDING_C + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytes/buffer.hpp" +#include "c_api/boundary.hpp" +#include "diagnostics/diagnostics.hpp" +#include "map/map.hpp" +#include "maplibre_native_c.h" +#include "runtime/runtime.hpp" + +auto mln_map_projection_create( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_create_start(map, completion); + }); +} + +auto mln_map_projection_close(mln_map_projection projection) noexcept + -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_close(projection); + }); +} + +auto mln_map_projection_get_camera( + mln_map_projection projection, mln_camera_options* out_camera +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_get_camera(projection, out_camera); + }); +} + +auto mln_map_projection_set_camera( + mln_map_projection projection, const mln_camera_options* camera +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_set_camera(projection, camera); + }); +} + +auto mln_map_projection_set_visible_coordinates( + mln_map_projection projection, const mln_lat_lng* coordinates, + size_t coordinate_count, mln_edge_insets padding +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_set_visible_coordinates( + projection, coordinates, coordinate_count, padding + ); + }); +} + +auto mln_map_projection_set_visible_geometry( + mln_map_projection projection, mln_buffer_view geometry, + mln_edge_insets padding +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_set_visible_geometry( + projection, geometry, padding + ); + }); +} + +auto mln_map_projection_pixel_for_lat_lng( + mln_map_projection projection, mln_lat_lng coordinate, + mln_screen_point* out_point +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_pixel_for_lat_lng( + projection, coordinate, out_point + ); + }); +} + +auto mln_map_projection_lat_lng_for_pixel( + mln_map_projection projection, mln_screen_point point, + mln_lat_lng* out_coordinate +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_projection_lat_lng_for_pixel( + projection, point, out_coordinate + ); + }); +} + +auto mln_projected_meters_for_lat_lng( + mln_lat_lng coordinate, mln_projected_meters* out_meters +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::projected_meters_for_lat_lng(coordinate, out_meters); + }); +} + +auto mln_lat_lng_for_projected_meters( + mln_projected_meters meters, mln_lat_lng* out_coordinate +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::lat_lng_for_projected_meters(meters, out_coordinate); + }); +} diff --git a/src/c_api/query.cpp b/src/c_api/query.cpp new file mode 100644 index 000000000..1a2821f59 --- /dev/null +++ b/src/c_api/query.cpp @@ -0,0 +1,533 @@ +#define MLN_BUILDING_C + +#include +#include +#include +#include +#include +#include +#include + +#include "bytes/buffer.hpp" +#include "c_api/boundary.hpp" +#include "maplibre_native_c.h" +#include "operation/operation.hpp" +#include "render/render_session_common.hpp" + +auto mln_rendered_feature_query_options_default(void) noexcept + -> mln_rendered_feature_query_options { + return mln_rendered_feature_query_options{ + .size = sizeof(mln_rendered_feature_query_options), + .fields = 0, + .layer_ids = nullptr, + .layer_id_count = 0, + .filter = nullptr + }; +} + +auto mln_source_feature_query_options_default(void) noexcept + -> mln_source_feature_query_options { + return mln_source_feature_query_options{ + .size = sizeof(mln_source_feature_query_options), + .fields = 0, + .source_layer_ids = nullptr, + .source_layer_id_count = 0, + .filter = nullptr + }; +} + +auto mln_rendered_query_geometry_point(mln_screen_point point) noexcept + -> mln_rendered_query_geometry { + return mln_rendered_query_geometry{ + .size = sizeof(mln_rendered_query_geometry), + .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT, + .data = {.point = point} + }; +} + +auto mln_rendered_query_geometry_box(mln_screen_box box) noexcept + -> mln_rendered_query_geometry { + return mln_rendered_query_geometry{ + .size = sizeof(mln_rendered_query_geometry), + .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX, + .data = {.box = box} + }; +} + +auto mln_rendered_query_geometry_line_string( + const mln_screen_point* points, size_t point_count +) noexcept -> mln_rendered_query_geometry { + return mln_rendered_query_geometry{ + .size = sizeof(mln_rendered_query_geometry), + .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING, + .data = {.line_string = {.points = points, .point_count = point_count}} + }; +} + +auto mln_render_session_query_rendered_features( + mln_render_session session, const mln_rendered_query_geometry* geometry, + const mln_rendered_feature_query_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::render_session_query_rendered_features_start( + session, geometry, options, completion + ); + }); +} + +auto mln_render_session_query_source_features( + mln_render_session session, mln_buffer_view source_id, + const mln_source_feature_query_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::render_session_query_source_features_start( + session, source_id, options, completion + ); + }); +} + +auto mln_render_session_query_feature_extensions( + mln_render_session session, mln_buffer_view source_id, + mln_buffer_view feature, mln_buffer_view extension, + mln_buffer_view extension_field, const mln_buffer_view* arguments, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::render_session_query_feature_extensions_start( + session, source_id, feature, extension, extension_field, arguments, + completion + ); + }); +} + +auto mln_queried_feature_default(void) noexcept -> mln_queried_feature { + return mln_queried_feature{ + .size = sizeof(mln_queried_feature), + .fields = 0, + .feature = {}, + .source_id = {}, + .source_layer_id = {}, + .state = {} + }; +} + +auto mln_queried_feature_list_count( + mln_queried_feature_list list, size_t* out_count +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::queried_feature_list_count(list, out_count); + }); +} + +auto mln_queried_feature_list_get( + mln_queried_feature_list list, size_t index, mln_queried_feature* out_feature +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::queried_feature_list_get(list, index, out_feature); + }); +} + +auto mln_queried_feature_list_destroy(mln_queried_feature_list list) noexcept + -> void { + mln::core::queried_feature_list_destroy(list); +} + +namespace mln::core { +namespace { +auto validate_screen_point(mln_screen_point point) -> mln_status { + if (!std::isfinite(point.x) || !std::isfinite(point.y)) { + set_thread_error("screen point coordinates must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto copy_view(mln_buffer_view view, std::string& out) -> mln_status { + if (view.size != 0 && view.data == nullptr) { + set_thread_error("buffer view data must not be null when size is nonzero"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto* bytes = static_cast(view.data); + out.assign(bytes == nullptr ? "" : bytes, view.size); + return MLN_STATUS_OK; +} + +auto take_buffer_bytes(mln_buffer buffer, std::any& out) -> mln_status { + mln_buffer_view view{}; + const auto status = buffer_get(buffer, &view); + if (status != MLN_STATUS_OK) { + buffer_destroy(buffer); + return status; + } + auto bytes = std::string{}; + const auto copy_status = copy_view(view, bytes); + buffer_destroy(buffer); + if (copy_status != MLN_STATUS_OK) { + return copy_status; + } + out = std::move(bytes); + return MLN_STATUS_OK; +} + +// Keeps the internal query result alive through its completion callback. +using OwnedQueriedFeatureList = std::shared_ptr; + +auto complete_feature_list( + const std::shared_ptr& completion, mln_status status, + std::string diagnostic, std::any result +) -> void { + auto* list = std::any_cast(&result); + if (status != MLN_STATUS_OK || list == nullptr || *list == nullptr) { + complete( + completion, status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK ? "render query produced an invalid result" + : std::move(diagnostic) + ); + return; + } + size_t count = 0; + if (queried_feature_list_count(**list, &count) != MLN_STATUS_OK) { + complete(completion, MLN_STATUS_NATIVE_ERROR, thread_last_error_message()); + return; + } + auto features = std::vector(count); + for (size_t index = 0; index < count; ++index) { + features[index] = mln_queried_feature_default(); + if ( + queried_feature_list_get(**list, index, &features[index]) != MLN_STATUS_OK + ) { + complete( + completion, MLN_STATUS_NATIVE_ERROR, thread_last_error_message() + ); + return; + } + } + completion->resolve([list = std::move(*list), features = std::move(features)]( + const mln_completion& descriptor + ) { + static_cast(list); + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + features.data(), features.size() + ); + }); +} + +auto complete_query_buffer( + const std::shared_ptr& completion, mln_status status, + std::string diagnostic, std::any result +) -> void { + auto* bytes = std::any_cast(&result); + if (status != MLN_STATUS_OK || bytes == nullptr) { + complete( + completion, status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK ? "render query produced an invalid result" + : std::move(diagnostic) + ); + return; + } + completion->resolve([bytes = + std::move(*bytes)](const mln_completion& descriptor) { + const auto view = + mln_buffer_view{.data = bytes.data(), .size = bytes.size()}; + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + &view, 1 + ); + }); +} + +auto store_feature_list(mln_queried_feature_list list, std::any& out) + -> mln_status { + try { + out = OwnedQueriedFeatureList{ + new mln_queried_feature_list{list}, [](mln_queried_feature_list* owned) { + queried_feature_list_destroy(*owned); + delete owned; + } + }; + } catch (...) { + queried_feature_list_destroy(list); + throw; + } + return MLN_STATUS_OK; +} + +struct CopiedRenderedOptions { + mln_rendered_feature_query_options value{}; + std::vector layer_ids; + std::optional filter; +}; + +auto copy_rendered_options( + const mln_rendered_feature_query_options* input, + std::optional& out +) -> mln_status { + if (input == nullptr) { + return MLN_STATUS_OK; + } + if (input->size < sizeof(*input)) { + set_thread_error("rendered feature query options size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS; + if ((input->fields & ~known_fields) != 0) { + set_thread_error("rendered feature query options have unknown fields"); + return MLN_STATUS_INVALID_ARGUMENT; + } + out.emplace(); + out->value = *input; + if ( + (input->fields & MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS) != 0 && + input->layer_id_count != 0 && input->layer_ids == nullptr + ) { + set_thread_error( + "layer_ids must not be null when layer_id_count is nonzero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ((input->fields & MLN_RENDERED_FEATURE_QUERY_OPTION_LAYER_IDS) != 0) { + out->layer_ids.resize(input->layer_id_count); + for (std::size_t i = 0; i < input->layer_id_count; ++i) { + const auto status = copy_view(input->layer_ids[i], out->layer_ids[i]); + if (status != MLN_STATUS_OK) return status; + } + } + if (input->filter != nullptr) { + out->filter.emplace(); + return copy_view(*input->filter, *out->filter); + } + return MLN_STATUS_OK; +} + +struct CopiedSourceOptions { + mln_source_feature_query_options value{}; + std::vector layer_ids; + std::optional filter; +}; + +auto copy_source_options( + const mln_source_feature_query_options* input, + std::optional& out +) -> mln_status { + if (input == nullptr) return MLN_STATUS_OK; + if (input->size < sizeof(*input)) { + set_thread_error("source feature query options size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = + MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS; + if ((input->fields & ~known_fields) != 0) { + set_thread_error("source feature query options have unknown fields"); + return MLN_STATUS_INVALID_ARGUMENT; + } + out.emplace(); + out->value = *input; + if ( + (input->fields & MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS) != 0 && + input->source_layer_id_count != 0 && input->source_layer_ids == nullptr + ) { + set_thread_error( + "source_layer_ids must not be null when source_layer_id_count is nonzero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ((input->fields & MLN_SOURCE_FEATURE_QUERY_OPTION_SOURCE_LAYER_IDS) != 0) { + out->layer_ids.resize(input->source_layer_id_count); + for (std::size_t i = 0; i < input->source_layer_id_count; ++i) { + const auto status = + copy_view(input->source_layer_ids[i], out->layer_ids[i]); + if (status != MLN_STATUS_OK) return status; + } + } + if (input->filter != nullptr) { + out->filter.emplace(); + return copy_view(*input->filter, *out->filter); + } + return MLN_STATUS_OK; +} + +template +auto make_views(const std::vector& strings, Options& options) + -> std::vector { + auto views = std::vector{}; + views.reserve(strings.size()); + for (const auto& string : strings) { + views.push_back(mln_buffer_view{string.data(), string.size()}); + } + (void)options; + return views; +} + +} // namespace + +auto render_session_query_rendered_features_start( + mln_render_session session, const mln_rendered_query_geometry* geometry, + const mln_rendered_feature_query_options* options, + const mln_completion* completion +) -> mln_status { + if (geometry == nullptr || geometry->size < sizeof(*geometry)) { + set_thread_error("rendered query geometry is null or too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto copied_geometry = *geometry; + auto points = std::vector{}; + switch (geometry->type) { + case MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT: + if (validate_screen_point(geometry->data.point) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + break; + case MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX: + if ( + validate_screen_point(geometry->data.box.min) != MLN_STATUS_OK || + validate_screen_point(geometry->data.box.max) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + break; + case MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING: { + const auto& line = geometry->data.line_string; + if (line.point_count == 0) { + set_thread_error("query line string must contain points"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (line.points == nullptr) { + set_thread_error("line string points must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + points.reserve(line.point_count); + for (std::size_t i = 0; i < line.point_count; ++i) { + if (validate_screen_point(line.points[i]) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + points.push_back(line.points[i]); + } + break; + } + default: + set_thread_error("rendered query geometry type is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto copied_options = std::optional{}; + const auto options_status = copy_rendered_options(options, copied_options); + if (options_status != MLN_STATUS_OK) return options_status; + + return enqueue_driver_result_operation( + session, + [copied_geometry, points = std::move(points), + copied_options = std::move(copied_options)]( + mln_render_session_object& target, std::any& result + ) mutable { + if ( + copied_geometry.type == MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING + ) { + copied_geometry.data.line_string = + mln_screen_line_string{points.data(), points.size()}; + } + const mln_rendered_feature_query_options* option_pointer = nullptr; + auto views = std::vector{}; + auto filter_view = mln_buffer_view{}; + if (copied_options) { + views = make_views(copied_options->layer_ids, copied_options->value); + copied_options->value.layer_ids = views.data(); + if (copied_options->filter) { + filter_view = mln_buffer_view{ + copied_options->filter->data(), copied_options->filter->size() + }; + copied_options->value.filter = &filter_view; + } + option_pointer = &copied_options->value; + } + auto list = mln_queried_feature_list{MLN_HANDLE_NULL}; + const auto status = render_session_query_rendered_features( + target.self, &copied_geometry, option_pointer, &list + ); + return status == MLN_STATUS_OK ? store_feature_list(list, result) + : status; + }, + completion, complete_feature_list + ); +} + +auto render_session_query_source_features_start( + mln_render_session session, mln_buffer_view source_id, + const mln_source_feature_query_options* options, + const mln_completion* completion +) -> mln_status { + auto source = std::string{}; + const auto source_status = copy_view(source_id, source); + if (source_status != MLN_STATUS_OK) return source_status; + auto copied_options = std::optional{}; + const auto options_status = copy_source_options(options, copied_options); + if (options_status != MLN_STATUS_OK) return options_status; + return enqueue_driver_result_operation( + session, + [source = std::move(source), copied_options = std::move(copied_options)]( + mln_render_session_object& target, std::any& result + ) mutable { + const mln_source_feature_query_options* option_pointer = nullptr; + auto views = std::vector{}; + auto filter_view = mln_buffer_view{}; + if (copied_options) { + views = make_views(copied_options->layer_ids, copied_options->value); + copied_options->value.source_layer_ids = views.data(); + if (copied_options->filter) { + filter_view = mln_buffer_view{ + copied_options->filter->data(), copied_options->filter->size() + }; + copied_options->value.filter = &filter_view; + } + option_pointer = &copied_options->value; + } + auto list = mln_queried_feature_list{MLN_HANDLE_NULL}; + const auto status = render_session_query_source_features( + target.self, mln_buffer_view{source.data(), source.size()}, + option_pointer, &list + ); + return status == MLN_STATUS_OK ? store_feature_list(list, result) + : status; + }, + completion, complete_feature_list + ); +} + +auto render_session_query_feature_extensions_start( + mln_render_session session, mln_buffer_view source_id, + mln_buffer_view feature, mln_buffer_view extension, + mln_buffer_view extension_field, const mln_buffer_view* arguments, + const mln_completion* completion +) -> mln_status { + auto copied = std::vector(5); + const mln_buffer_view inputs[] = { + source_id, feature, extension, extension_field, + arguments == nullptr ? mln_buffer_view{} : *arguments + }; + for (std::size_t i = 0; i < copied.size(); ++i) { + const auto status = copy_view(inputs[i], copied[i]); + if (status != MLN_STATUS_OK) return status; + } + const bool has_arguments = arguments != nullptr; + return enqueue_driver_result_operation( + session, + [copied = std::move(copied), + has_arguments](mln_render_session_object& target, std::any& result) { + const auto view = [&copied](std::size_t i) { + return mln_buffer_view{copied[i].data(), copied[i].size()}; + }; + const auto argument_view = view(4); + auto buffer = mln_buffer{MLN_HANDLE_NULL}; + const auto status = render_session_query_feature_extensions( + target.self, view(0), view(1), view(2), view(3), + has_arguments ? &argument_view : nullptr, &buffer + ); + return status == MLN_STATUS_OK ? take_buffer_bytes(buffer, result) + : status; + }, + completion, complete_query_buffer + ); +} + +} // namespace mln::core diff --git a/src/c_api/render_session.cpp b/src/c_api/render_session.cpp index b74907c28..a874e2e44 100644 --- a/src/c_api/render_session.cpp +++ b/src/c_api/render_session.cpp @@ -1,186 +1,193 @@ #define MLN_BUILDING_C -#include -#include - #include "c_api/boundary.hpp" -#include "geojson/geojson.hpp" #include "maplibre_native_c.h" #include "render/render_session_common.hpp" -auto mln_rendered_feature_query_options_default() noexcept - -> mln_rendered_feature_query_options { - return mln_rendered_feature_query_options{ - .size = sizeof(mln_rendered_feature_query_options), - .fields = 0, - .layer_ids = nullptr, - .layer_id_count = 0, - .filter = nullptr +auto mln_frame_demand_default() noexcept -> mln_frame_demand { + return mln_frame_demand{ + .size = sizeof(mln_frame_demand), + .flags = MLN_FRAME_DEMAND_IF_NEEDED, + .token = 0, + .coalescing_boundary = 0, + .timeout_ns = 0, }; } - -auto mln_source_feature_query_options_default() noexcept - -> mln_source_feature_query_options { - return mln_source_feature_query_options{ - .size = sizeof(mln_source_feature_query_options), - .fields = 0, - .source_layer_ids = nullptr, - .source_layer_id_count = 0, - .filter = nullptr +auto mln_render_session_attach_options_default() noexcept + -> mln_render_session_attach_options { + return mln_render_session_attach_options{ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + .requested_texture_ring_depth = 1, + .reserved = 0, + .frame_wake = mln_wake{sizeof(mln_wake), nullptr, nullptr, nullptr}, + .driver_work_wake = mln_wake{sizeof(mln_wake), nullptr, nullptr, nullptr}, }; } -auto mln_rendered_query_geometry_point(mln_screen_point point) noexcept - -> mln_rendered_query_geometry { - return mln_rendered_query_geometry{ - .size = sizeof(mln_rendered_query_geometry), - .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_POINT, - .data = {.point = point} +auto mln_gpu_sync_default() noexcept -> mln_gpu_sync { + return mln_gpu_sync{ + .size = sizeof(mln_gpu_sync), + .kind = MLN_GPU_SYNC_CPU_COMPLETE, + .object = nullptr, + .value = 0, }; } -auto mln_rendered_query_geometry_box(mln_screen_box box) noexcept - -> mln_rendered_query_geometry { - return mln_rendered_query_geometry{ - .size = sizeof(mln_rendered_query_geometry), - .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_BOX, - .data = {.box = box} - }; +auto mln_render_session_get_capabilities( + mln_render_session session, mln_render_session_capabilities* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_get_capabilities(session, out); + }); } -auto mln_rendered_query_geometry_line_string( - const mln_screen_point* points, size_t point_count -) noexcept -> mln_rendered_query_geometry { - return mln_rendered_query_geometry{ - .size = sizeof(mln_rendered_query_geometry), - .type = MLN_RENDERED_QUERY_GEOMETRY_TYPE_LINE_STRING, - .data = {.line_string = {.points = points, .point_count = point_count}} - }; +auto mln_render_session_get_snapshot( + mln_render_session session, mln_render_session_snapshot* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_get_snapshot(session, out); + }); } -auto mln_render_session_resize( - mln_render_session session, uint32_t width, uint32_t height, - double scale_factor +auto mln_render_session_request_frame( + mln_render_session session, const mln_frame_demand* demand ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_resize( - session, width, height, scale_factor - ); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_request_frame(session, demand); }); } -auto mln_render_session_render_update( - mln_render_session session, mln_render_result* out_result, - bool* out_needs_repaint +auto mln_render_session_drain_frame_results( + mln_render_session session, mln_render_frame_batch* out ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_render_update( - session, out_result, out_needs_repaint - ); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_drain_frame_results(session, out); }); } -auto mln_render_session_detach(mln_render_session session) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_detach(session); +auto mln_render_frame_batch_count( + mln_render_frame_batch batch, size_t* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_frame_batch_count(batch, out); }); } -auto mln_render_session_destroy(mln_render_session session) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_destroy(session); +auto mln_render_frame_batch_get( + mln_render_frame_batch batch, size_t index, mln_render_frame_result* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_frame_batch_get(batch, index, out); }); } -auto mln_render_session_reduce_memory_use(mln_render_session session) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_reduce_memory_use(session); +void mln_render_frame_batch_release(mln_render_frame_batch batch) noexcept { + mln::core::render_frame_batch_release(batch); +} + +auto mln_render_session_acquire_frame( + mln_render_session session, mln_acquired_frame* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_acquire_frame(session, out); }); } -auto mln_render_session_clear_data(mln_render_session session) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_clear_data(session); +auto mln_acquired_frame_get_result( + mln_acquired_frame frame, mln_render_frame_result* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::acquired_frame_get_result(frame, out); }); } -auto mln_render_session_dump_debug_logs(mln_render_session session) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_dump_debug_logs(session); +auto mln_acquired_frame_get_producer_sync( + mln_acquired_frame frame, mln_gpu_sync* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::acquired_frame_get_producer_sync(frame, out); }); } -auto mln_queried_feature_default() noexcept -> mln_queried_feature { - return mln_queried_feature{ - .size = sizeof(mln_queried_feature), - .fields = 0, - .feature = {}, - .source_id = {}, - .source_layer_id = {}, - .state = {} - }; +auto mln_acquired_frame_release( + mln_acquired_frame* frame, const mln_gpu_sync* sync +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::acquired_frame_release(frame, sync); + }); } -auto mln_queried_feature_list_count( - mln_queried_feature_list list, size_t* out_count +auto mln_render_session_resize( + mln_render_session session, const mln_render_target_extent* extent, + const mln_completion* out ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::queried_feature_list_count(list, out_count); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_resize_start(session, extent, out); }); } -auto mln_queried_feature_list_get( - mln_queried_feature_list list, size_t index, mln_queried_feature* out_feature +auto mln_render_session_barrier( + mln_render_session session, const mln_completion* out ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::queried_feature_list_get(list, index, out_feature); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_barrier_start(session, out); }); } -auto mln_queried_feature_list_destroy(mln_queried_feature_list list) noexcept - -> void { - mln::core::queried_feature_list_destroy(list); +auto mln_render_session_reduce_memory_use( + mln_render_session session, const mln_completion* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_maintenance_start(session, 0, out); + }); } -auto mln_render_session_query_rendered_features( - mln_render_session session, const mln_rendered_query_geometry* geometry, - const mln_rendered_feature_query_options* options, - mln_queried_feature_list* out_result +auto mln_render_session_clear_data( + mln_render_session session, const mln_completion* out ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_query_rendered_features( - session, geometry, options, out_result - ); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_maintenance_start(session, 1, out); }); } -auto mln_render_session_query_source_features( - mln_render_session session, mln_buffer_view source_id, - const mln_source_feature_query_options* options, - mln_queried_feature_list* out_result +auto mln_render_session_dump_debug_logs( + mln_render_session session, const mln_completion* out ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_query_source_features( - session, source_id, options, out_result - ); + return mln::c_api::status_boundary([&] { + return mln::core::render_session_maintenance_start(session, 2, out); }); } -auto mln_render_session_query_feature_extensions( - mln_render_session session, mln_buffer_view source_id, - mln_buffer_view feature, mln_buffer_view extension, - mln_buffer_view extension_field, const mln_buffer_view* arguments, - mln_buffer* out_result +auto mln_render_session_service_driver_work( + mln_render_session session, size_t maximum, size_t* serviced ) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::render_session_query_feature_extensions( - session, source_id, feature, extension, extension_field, arguments, - out_result + return mln::c_api::status_boundary([&] { + return mln::core::render_session_service_driver_work( + session, maximum, serviced ); }); } + +auto mln_render_session_detach( + mln_render_session session, const mln_completion* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_detach_start(session, out); + }); +} + +auto mln_render_session_abandon( + mln_render_session session, mln_render_abandon_result* out +) noexcept -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_abandon(session, out); + }); +} + +auto mln_render_session_destroy(mln_render_session session) noexcept + -> mln_status { + return mln::c_api::status_boundary([&] { + return mln::core::render_session_destroy(session); + }); +} diff --git a/src/c_api/runtime.cpp b/src/c_api/runtime.cpp index 738132f86..413c1af69 100644 --- a/src/c_api/runtime.cpp +++ b/src/c_api/runtime.cpp @@ -16,6 +16,12 @@ auto mln_runtime_options_default(void) noexcept -> mln_runtime_options { .asset_path = nullptr, .cache_path = nullptr, .event_mask = MLN_RUNTIME_EVENT_MASK_ALL, + .event_wake = mln_wake{ + .size = sizeof(mln_wake), + .callback = nullptr, + .user_data = nullptr, + .release_user_data = nullptr, + }, }; } @@ -28,17 +34,19 @@ auto mln_runtime_create( } auto mln_runtime_set_resource_provider( - mln_runtime runtime, const mln_resource_provider* provider + mln_runtime runtime, const mln_resource_provider* provider, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::set_resource_provider(runtime, provider); + return mln::core::set_resource_provider(runtime, provider, completion); }); } -auto mln_runtime_clear_resource_provider(mln_runtime runtime) noexcept - -> mln_status { +auto mln_runtime_clear_resource_provider( + mln_runtime runtime, const mln_completion* completion +) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::clear_resource_provider(runtime); + return mln::core::clear_resource_provider(runtime, completion); }); } @@ -72,10 +80,11 @@ auto mln_resource_request_release(mln_resource_request_handle handle) noexcept } auto mln_runtime_set_resource_transform( - mln_runtime runtime, const mln_resource_transform* transform + mln_runtime runtime, const mln_resource_transform* transform, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::set_resource_transform(runtime, transform); + return mln::core::set_resource_transform(runtime, transform, completion); }); } @@ -89,18 +98,20 @@ auto mln_resource_transform_response_set_url( }); } -auto mln_runtime_clear_resource_transform(mln_runtime runtime) noexcept - -> mln_status { +auto mln_runtime_clear_resource_transform( + mln_runtime runtime, const mln_completion* completion +) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::clear_resource_transform(runtime); + return mln::core::clear_resource_transform(runtime, completion); }); } auto mln_runtime_set_http_header_transform( - mln_runtime runtime, const mln_http_header_transform* transform + mln_runtime runtime, const mln_http_header_transform* transform, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::set_http_header_transform(runtime, transform); + return mln::core::set_http_header_transform(runtime, transform, completion); }); } @@ -115,217 +126,167 @@ auto mln_http_header_transform_response_set( }); } -auto mln_runtime_clear_http_header_transform(mln_runtime runtime) noexcept - -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::clear_http_header_transform(runtime); - }); -} - -auto mln_runtime_run_ambient_cache_operation_start( - mln_runtime runtime, uint32_t operation, - mln_offline_operation_id* out_operation_id +auto mln_runtime_clear_http_header_transform( + mln_runtime runtime, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::run_ambient_cache_operation_start( - runtime, operation, out_operation_id - ); + return mln::core::clear_http_header_transform(runtime, completion); }); } -auto mln_runtime_set_maximum_ambient_cache_size_start( - mln_runtime runtime, uint64_t size, mln_offline_operation_id* out_operation_id +auto mln_runtime_run_ambient_cache_operation( + mln_runtime runtime, uint32_t operation, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::set_maximum_ambient_cache_size_start( - runtime, size, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::run_ambient_cache_operation_start( + runtime, operation, completion + ); + }); }); } -auto mln_runtime_offline_operation_discard( - mln_runtime runtime, mln_offline_operation_id operation_id +auto mln_runtime_set_maximum_ambient_cache_size( + mln_runtime runtime, uint64_t size, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_operation_discard(runtime, operation_id); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::set_maximum_ambient_cache_size_start( + runtime, size, completion + ); + }); }); } -auto mln_runtime_offline_region_create_start( +auto mln_runtime_offline_region_create( mln_runtime runtime, const mln_offline_region_definition* definition, const uint8_t* metadata, size_t metadata_size, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_create_start( - runtime, definition, metadata, metadata_size, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_create_start( + runtime, definition, metadata, metadata_size, completion + ); + }); }); } -auto mln_runtime_offline_region_get_start( +auto mln_runtime_offline_region_get( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_get_start( - runtime, region_id, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_get_start( + runtime, region_id, completion + ); + }); }); } -auto mln_runtime_offline_regions_list_start( - mln_runtime runtime, mln_offline_operation_id* out_operation_id +auto mln_runtime_offline_regions_list( + mln_runtime runtime, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_regions_list_start(runtime, out_operation_id); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_regions_list_start(runtime, completion); + }); }); } -auto mln_runtime_offline_regions_merge_database_start( +auto mln_runtime_offline_regions_merge_database( mln_runtime runtime, const char* side_database_path, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_regions_merge_database_start( - runtime, side_database_path, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_regions_merge_database_start( + runtime, side_database_path, completion + ); + }); }); } -auto mln_runtime_offline_region_update_metadata_start( +auto mln_runtime_offline_region_update_metadata( mln_runtime runtime, mln_offline_region_id region_id, const uint8_t* metadata, - size_t metadata_size, mln_offline_operation_id* out_operation_id + size_t metadata_size, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_update_metadata_start( - runtime, region_id, metadata, metadata_size, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_update_metadata_start( + runtime, region_id, metadata, metadata_size, completion + ); + }); }); } -auto mln_runtime_offline_region_get_status_start( +auto mln_runtime_offline_region_get_status( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_get_status_start( - runtime, region_id, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_get_status_start( + runtime, region_id, completion + ); + }); }); } -auto mln_runtime_offline_region_set_observed_start( +auto mln_runtime_offline_region_set_observed( mln_runtime runtime, mln_offline_region_id region_id, bool observed, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_set_observed_start( - runtime, region_id, observed, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_set_observed_start( + runtime, region_id, observed, completion + ); + }); }); } -auto mln_runtime_offline_region_set_download_state_start( +auto mln_runtime_offline_region_set_download_state( mln_runtime runtime, mln_offline_region_id region_id, uint32_t state, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_set_download_state_start( - runtime, - mln::core::OfflineRegionDownloadStateRequest{ - .region_id = region_id, .state = state - }, - out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_set_download_state_start( + runtime, + mln::core::OfflineRegionDownloadStateRequest{ + .region_id = region_id, .state = state + }, + completion + ); + }); }); } -auto mln_runtime_offline_region_invalidate_start( +auto mln_runtime_offline_region_invalidate( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_invalidate_start( - runtime, region_id, out_operation_id - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_invalidate_start( + runtime, region_id, completion + ); + }); }); } -auto mln_runtime_offline_region_delete_start( +auto mln_runtime_offline_region_delete( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_delete_start( - runtime, region_id, out_operation_id - ); - }); -} - -auto mln_runtime_offline_region_create_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_create_take_result( - runtime, operation_id, out_region - ); - }); -} - -auto mln_runtime_offline_region_get_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region, bool* out_found -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_get_take_result( - runtime, operation_id, out_region, out_found - ); - }); -} - -auto mln_runtime_offline_regions_list_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_regions_list_take_result( - runtime, operation_id, out_regions - ); - }); -} - -auto mln_runtime_offline_regions_merge_database_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_regions_merge_database_take_result( - runtime, operation_id, out_regions - ); - }); -} - -auto mln_runtime_offline_region_update_metadata_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_update_metadata_take_result( - runtime, operation_id, out_region - ); - }); -} - -auto mln_runtime_offline_region_get_status_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_status* out_status + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::offline_region_get_status_take_result( - runtime, operation_id, out_status - ); + return mln::core::dispatch_runtime_sync(runtime, [&]() -> mln_status { + return mln::core::offline_region_delete_start( + runtime, region_id, completion + ); + }); }); } @@ -364,56 +325,40 @@ auto mln_offline_region_list_destroy(mln_offline_region_list list) noexcept mln::core::offline_region_list_destroy(list); } -auto mln_runtime_destroy(mln_runtime runtime) noexcept -> mln_status { +auto mln_runtime_barrier( + mln_runtime runtime, const mln_completion* completion +) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::destroy_runtime(runtime); + return mln::core::runtime_barrier_start(runtime, completion); }); } -auto mln_runtime_pump( - mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms +auto mln_runtime_release( + mln_runtime runtime, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::pump_runtime(runtime, timeout_ms, budget_ms); + return mln::core::release_runtime(runtime, completion); }); } -auto mln_runtime_wake_source_acquire( - mln_runtime runtime, mln_wake_source* out_source +auto mln_runtime_drain_events( + mln_runtime runtime, mln_event_batch* out_batch ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::acquire_wake_source(runtime, out_source); + return mln::core::drain_runtime_events(runtime, out_batch); }); } -auto mln_wake_source_signal(mln_wake_source source) noexcept -> mln_status { +auto mln_event_batch_get( + mln_event_batch batch, mln_runtime_event_batch_view* out_view +) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::signal_wake_source(source); + return mln::core::get_event_batch(batch, out_view); }); } -auto mln_wake_source_destroy(mln_wake_source source) noexcept -> void { - mln::core::destroy_wake_source(source); -} - -auto mln_runtime_event_batch_default(void) noexcept -> mln_runtime_event_batch { - return mln_runtime_event_batch{ - .size = sizeof(mln_runtime_event_batch), - .event_size = 0, - .events = nullptr, - .event_count = 0, - .messages = nullptr, - .messages_size = 0, - .remaining_count = 0, - }; -} - -auto mln_runtime_drain_events( - mln_runtime runtime, size_t max_events, mln_runtime_event_batch* out_batch -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::drain_runtime_events(runtime, max_events, out_batch); - }); +auto mln_event_batch_release(mln_event_batch batch) noexcept -> void { + mln::core::release_event_batch(batch); } auto mln_runtime_set_event_mask(mln_runtime runtime, uint64_t mask) noexcept diff --git a/src/c_api/style.cpp b/src/c_api/style.cpp new file mode 100644 index 000000000..5ba8af1d4 --- /dev/null +++ b/src/c_api/style.cpp @@ -0,0 +1,1964 @@ +#define MLN_BUILDING_C + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytes/buffer.hpp" +#include "c_api/boundary.hpp" +#include "diagnostics/diagnostics.hpp" +#include "geojson/geojson_source_data.hpp" +#include "map/map.hpp" +#include "maplibre_native_c.h" +#include "runtime/runtime.hpp" + +namespace { + +struct OwnedView { + std::string bytes; + + OwnedView() = default; + explicit OwnedView(mln_buffer_view view) + : bytes( + view.data == nullptr + ? std::string{} + : std::string{ + static_cast(view.data), + static_cast(view.data) + view.size + } + ) {} + + [[nodiscard]] auto view() const noexcept -> mln_buffer_view { + return {.data = bytes.data(), .size = bytes.size()}; + } +}; + +struct OwnedGeoJSONOptions { + mln_geojson_source_options value{}; + OwnedView cluster_properties; + + explicit OwnedGeoJSONOptions(const mln_geojson_source_options* options) + : value( + options == nullptr ? mln::core::geojson_source_options_default() + : *options + ), + cluster_properties(value.cluster_properties) { + value.cluster_properties = cluster_properties.view(); + } +}; + +struct OwnedTileOptions { + mln_style_tile_source_options value{}; + OwnedView attribution; + + explicit OwnedTileOptions(const mln_style_tile_source_options* options) + : value( + options == nullptr ? mln::core::style_tile_source_options_default() + : *options + ), + attribution(value.attribution) { + value.attribution = attribution.view(); + } +}; + +struct OwnedImage { + mln_premultiplied_rgba8_image value{}; + std::vector pixels; + + explicit OwnedImage(const mln_premultiplied_rgba8_image* image) + : value( + image == nullptr ? mln::core::premultiplied_rgba8_image_default() + : *image + ), + pixels( + image == nullptr || image->pixels == nullptr + ? std::vector{} + : std::vector{ + image->pixels, image->pixels + image->byte_length + } + ) { + value.pixels = pixels.data(); + } +}; + +struct OwnedImageOptions { + mln_style_image_options value{}; + std::vector stretch_x; + std::vector stretch_y; + + explicit OwnedImageOptions(const mln_style_image_options* options) + : value( + options == nullptr ? mln::core::style_image_options_default() + : *options + ), + stretch_x( + value.stretch_x == nullptr + ? std::vector{} + : std::vector< + mln_image_stretch>{value.stretch_x, value.stretch_x + value.stretch_x_count} + ), + stretch_y( + value.stretch_y == nullptr + ? std::vector{} + : std::vector{ + value.stretch_y, value.stretch_y + value.stretch_y_count + } + ) { + value.stretch_x = stretch_x.data(); + value.stretch_y = stretch_y.data(); + } +}; +template +struct OwnedCallbackSourceOptions { + enum class Ownership : std::uint8_t { + pending, + accepted, + adopted, + rejected, + }; + + Options value{}; + std::atomic ownership = Ownership::pending; + + explicit OwnedCallbackSourceOptions(const Options& options) + : value(options) {} + + ~OwnedCallbackSourceOptions() { + if ( + ownership.load(std::memory_order_acquire) == Ownership::accepted && + value.release_user_data != nullptr + ) { + try { + value.release_user_data(value.user_data); + } catch (...) { + } + } + } +}; + +using OwnedCustomGeometryOptions = + OwnedCallbackSourceOptions; +using OwnedCustomMvtVectorOptions = + OwnedCallbackSourceOptions; +auto valid_view(mln_buffer_view view, const char* name) -> bool { + if (view.data == nullptr && view.size != 0) { + mln::core::set_thread_error(name); + return false; + } + return true; +} + +auto take_buffer(mln_buffer buffer, std::string& out) -> mln_status { + mln_buffer_view view{}; + const auto status = mln::core::buffer_get(buffer, &view); + if (status == MLN_STATUS_OK) { + const auto* bytes = static_cast(view.data); + out.assign(bytes == nullptr ? "" : bytes, view.size); + } + mln::core::buffer_destroy(buffer); + return status; +} + +auto take_id_list(mln_style_id_list list, std::vector& out) + -> mln_status { + size_t count = 0; + auto status = mln::core::style_id_list_count(list, &count); + if (status == MLN_STATUS_OK) { + out.reserve(count); + for (size_t index = 0; index < count; ++index) { + mln_buffer_view view{}; + status = mln::core::style_id_list_get(list, index, &view); + if (status != MLN_STATUS_OK) break; + const auto* bytes = static_cast(view.data); + out.emplace_back(bytes == nullptr ? "" : bytes, view.size); + } + } + mln::core::style_id_list_destroy(list); + return status; +} + +auto take_string_list(mln_style_string_list list, std::vector& out) + -> mln_status { + size_t count = 0; + auto status = mln::core::style_string_list_count(list, &count); + if (status == MLN_STATUS_OK) { + out.reserve(count); + for (size_t index = 0; index < count; ++index) { + mln_buffer_view view{}; + status = mln::core::style_string_list_get(list, index, &view); + if (status != MLN_STATUS_OK) break; + const auto* bytes = static_cast(view.data); + out.emplace_back(bytes == nullptr ? "" : bytes, view.size); + } + } + mln::core::style_string_list_destroy(list); + return status; +} + +auto command( + mln_map map, std::function work, + const mln_completion* completion +) -> mln_status { + return mln::core::submit_map_command(map, std::move(work), completion); +} + +auto operation( + mln_map map, mln::core::StyleOperationKind kind, mln::core::StyleWork work, + const mln_completion* completion +) -> mln_status { + return mln::core::start_style_operation( + map, kind, std::move(work), completion + ); +} + +using TextCopy = + std::function; + +auto start_text_copy( + mln_map map, mln_buffer_view id, mln::core::StyleOperationKind kind, + TextCopy copy, const mln_completion* completion +) -> mln_status { + if (!valid_view(id, "style ID is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto owned = OwnedView{id}; + return operation( + map, kind, + [map, owned = std::move(owned), + copy = + std::move(copy)](mln::core::StyleOperationResult& result) -> mln_status { + size_t size = 0; + auto status = copy(map, owned.view(), nullptr, 0, &size); + if (status != MLN_STATUS_OK) { + return status; + } + auto bytes = std::string(size, '\0'); + status = copy(map, owned.view(), bytes.data(), bytes.size(), &size); + if (status == MLN_STATUS_OK) result.bytes = std::move(bytes); + return status; + }, + completion + ); +} + +} // namespace + +auto mln_style_tile_source_options_default(void) noexcept + -> mln_style_tile_source_options { + return mln::core::style_tile_source_options_default(); +} + +auto mln_geojson_source_options_default(void) noexcept + -> mln_geojson_source_options { + return mln::core::geojson_source_options_default(); +} + +auto mln_custom_geometry_source_options_default(void) noexcept + -> mln_custom_geometry_source_options { + return mln::core::custom_geometry_source_options_default(); +} + +auto mln_custom_mvt_vector_source_options_default(void) noexcept + -> mln_custom_mvt_vector_source_options { + return mln::core::custom_mvt_vector_source_options_default(); +} + +auto mln_premultiplied_rgba8_image_default(void) noexcept + -> mln_premultiplied_rgba8_image { + return mln::core::premultiplied_rgba8_image_default(); +} + +auto mln_style_image_options_default(void) noexcept -> mln_style_image_options { + return mln::core::style_image_options_default(); +} + +auto mln_style_image_info_default(void) noexcept -> mln_style_image_info { + return mln::core::style_image_info_default(); +} + +auto mln_style_transition_options_default(void) noexcept + -> mln_style_transition_options { + return mln::core::style_transition_options_default(); +} +auto mln_map_set_style_url( + mln_map map, const char* url, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (url == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto owned = std::string{url}; + return command( + map, + [map, owned = std::move(owned)]() -> mln_status { + return mln::core::map_set_style_url(map, owned.c_str()); + }, + completion + ); + }); +} + +auto mln_map_set_style_json( + mln_map map, mln_buffer_view json, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (json.size == 0 || !valid_view(json, "style JSON is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto owned = OwnedView{json}; + return command( + map, + [map, owned = std::move(owned)]() -> mln_status { + return mln::core::map_set_style_json(map, owned.view()); + }, + completion + ); + }); +} + +auto mln_map_loaded_style_json( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_loaded_style_json_start(map, completion); + }); +} + +auto mln_map_style_url(mln_map map, const mln_completion* completion) noexcept + -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::map_style_url_start(map, completion); + }); +} + +auto mln_style_id_list_count(mln_style_id_list list, size_t* out_count) noexcept + -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::style_id_list_count(list, out_count); + }); +} + +auto mln_style_id_list_get( + mln_style_id_list list, size_t index, mln_buffer_view* out_id +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::style_id_list_get(list, index, out_id); + }); +} + +auto mln_style_id_list_destroy(mln_style_id_list list) noexcept -> void { + mln::core::style_id_list_destroy(list); +} + +auto mln_style_string_list_count( + mln_style_string_list list, size_t* out_count +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::style_string_list_count(list, out_count); + }); +} + +auto mln_style_string_list_get( + mln_style_string_list list, size_t index, mln_buffer_view* out_value +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::style_string_list_get(list, index, out_value); + }); +} + +auto mln_style_string_list_destroy(mln_style_string_list list) noexcept + -> void { + mln::core::style_string_list_destroy(list); +} + +auto mln_map_add_style_source_json( + mln_map map, mln_buffer_view source_id, mln_buffer_view source_json, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || + !valid_view(source_json, "source_json is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto json = OwnedView{source_json}; + return command( + map, + [map, id = std::move(id), json = std::move(json)]() -> mln_status { + return mln::core::map_add_style_source_json( + map, id.view(), json.view() + ); + }, + completion + ); + }); +} + +auto mln_map_remove_style_source( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id)]() -> mln_status { + return mln::core::map_remove_style_source(map, id.view()); + }, + completion + ); + }); +} + +auto mln_map_get_style_source_info( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return operation( + map, mln::core::StyleOperationKind::SourceInfo, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + result.source_info = {}; + result.source_info.size = sizeof(mln_style_source_info); + auto status = mln::core::map_get_style_source_info( + map, id.view(), &result.source_info, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + auto copy_text = [&](auto copy, std::string& destination) { + size_t size = 0; + bool found = false; + auto copy_status = copy(map, id.view(), nullptr, 0, &size, &found); + if (copy_status != MLN_STATUS_OK || !found) return copy_status; + destination.resize(size); + return copy( + map, id.view(), destination.data(), destination.size(), &size, + &found + ); + }; + status = copy_text( + mln::core::map_copy_style_source_attribution, result.attribution + ); + if (status != MLN_STATUS_OK) return status; + status = copy_text(mln::core::map_copy_style_source_url, result.url); + if (status != MLN_STATUS_OK) return status; + auto list = mln_style_string_list{MLN_HANDLE_NULL}; + bool found = false; + status = mln::core::map_get_style_source_tile_urls( + map, id.view(), &list, &found + ); + return status == MLN_STATUS_OK && found + ? take_string_list(list, result.strings) + : status; + }, + completion + ); + }); +} +auto mln_map_copy_style_source_attribution( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return operation( + map, mln::core::StyleOperationKind::SourceAttribution, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + size_t size = 0; + auto status = mln::core::map_copy_style_source_attribution( + map, id.view(), nullptr, 0, &size, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) { + return status; + } + auto bytes = std::string(size, '\0'); + status = mln::core::map_copy_style_source_attribution( + map, id.view(), bytes.data(), bytes.size(), &size, &result.found + ); + if (status == MLN_STATUS_OK) result.bytes = std::move(bytes); + return status; + }, + completion + ); + }); +} + +auto mln_map_copy_style_source_url( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return operation( + map, mln::core::StyleOperationKind::SourceUrl, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + size_t size = 0; + auto status = mln::core::map_copy_style_source_url( + map, id.view(), nullptr, 0, &size, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) { + return status; + } + auto bytes = std::string(size, '\0'); + status = mln::core::map_copy_style_source_url( + map, id.view(), bytes.data(), bytes.size(), &size, &result.found + ); + if (status == MLN_STATUS_OK) result.bytes = std::move(bytes); + return status; + }, + completion + ); + }); +} + +auto mln_map_get_style_source_tile_urls( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return operation( + map, mln::core::StyleOperationKind::SourceTileUrls, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + auto list = mln_style_string_list{MLN_HANDLE_NULL}; + const auto status = mln::core::map_get_style_source_tile_urls( + map, id.view(), &list, &result.found + ); + return status == MLN_STATUS_OK && result.found + ? take_string_list(list, result.strings) + : status; + }, + completion + ); + }); +} + +auto mln_map_list_style_source_ids( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return operation( + map, mln::core::StyleOperationKind::SourceIds, + [map](mln::core::StyleOperationResult& result) -> mln_status { + auto list = mln_style_id_list{MLN_HANDLE_NULL}; + const auto status = mln::core::map_list_style_source_ids(map, &list); + return status == MLN_STATUS_OK ? take_id_list(list, result.strings) + : status; + }, + completion + ); + }); +} + +#define MLN_GEOJSON_COMMAND(NAME, CORE) \ + auto NAME( \ + mln_map map, mln_buffer_view source_id, mln_buffer_view input, \ + const mln_geojson_source_options* options, \ + const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + if ( \ + !valid_view(source_id, "source_id is invalid") || \ + !valid_view(input, "source input is invalid") || \ + mln::core::validate_geojson_command_options(options) != MLN_STATUS_OK \ + ) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + auto id = OwnedView{source_id}; \ + auto owned_input = OwnedView{input}; \ + auto owned_options = OwnedGeoJSONOptions{options}; \ + return command( \ + map, \ + [map, id = std::move(id), input = std::move(owned_input), \ + options = std::move(owned_options)]() mutable -> mln_status { \ + options.value.cluster_properties = \ + options.cluster_properties.view(); \ + return mln::core::CORE( \ + map, id.view(), input.view(), &options.value \ + ); \ + }, \ + completion \ + ); \ + }); \ + } + +#define MLN_TILE_URL_COMMAND(NAME, CORE, KIND) \ + auto NAME( \ + mln_map map, mln_buffer_view source_id, mln_buffer_view url, \ + const mln_style_tile_source_options* options, \ + const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + if ( \ + !valid_view(source_id, "source_id is invalid") || \ + !valid_view(url, "url is invalid") || \ + mln::core::validate_tile_command_options(options, KIND) != \ + MLN_STATUS_OK \ + ) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + auto id = OwnedView{source_id}; \ + auto owned_url = OwnedView{url}; \ + auto owned_options = OwnedTileOptions{options}; \ + return command( \ + map, \ + [map, id = std::move(id), url = std::move(owned_url), \ + options = std::move(owned_options)]() mutable -> mln_status { \ + options.value.attribution = options.attribution.view(); \ + return mln::core::CORE(map, id.view(), url.view(), &options.value); \ + }, \ + completion \ + ); \ + }); \ + } + +#define MLN_TILE_LIST_COMMAND(NAME, CORE, KIND) \ + auto NAME( \ + mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, \ + size_t tile_count, const mln_style_tile_source_options* options, \ + const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + if ( \ + !valid_view(source_id, "source_id is invalid") || \ + (tile_count != 0 && tiles == nullptr) || \ + mln::core::validate_tile_command_options(options, KIND) != \ + MLN_STATUS_OK \ + ) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + auto id = OwnedView{source_id}; \ + auto owned_tiles = std::vector{}; \ + owned_tiles.reserve(tile_count); \ + for (size_t index = 0; index < tile_count; ++index) { \ + if (!valid_view(tiles[index], "tile URL is invalid")) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + owned_tiles.emplace_back(tiles[index]); \ + } \ + auto owned_options = OwnedTileOptions{options}; \ + return command( \ + map, \ + [map, id = std::move(id), tiles = std::move(owned_tiles), \ + options = std::move(owned_options)]() mutable -> mln_status { \ + auto views = std::vector{}; \ + views.reserve(tiles.size()); \ + for (const auto& tile : tiles) { \ + views.push_back(tile.view()); \ + } \ + options.value.attribution = options.attribution.view(); \ + return mln::core::CORE( \ + map, id.view(), views.data(), views.size(), &options.value \ + ); \ + }, \ + completion \ + ); \ + }); \ + } + +MLN_GEOJSON_COMMAND(mln_map_add_geojson_source_url, map_add_geojson_source_url) + +auto mln_geojson_source_data_create( + mln_buffer_view data, const mln_geojson_source_options* options, + mln_geojson_source_data* out_data +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return mln::core::geojson_source_data_create(data, options, out_data); + }); +} + +auto mln_geojson_source_data_destroy(mln_geojson_source_data data) noexcept + -> void { + mln::core::geojson_source_data_destroy(data); +} + +auto mln_map_add_geojson_source_data( + mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + // The lease taken at submit keeps the prepared index alive until the + // command runs, so the host may destroy the handle right after this call. + auto prepared = mln::core::geojson_source_data_table().lease(data); + if (prepared == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), + prepared = + std::shared_ptr{ + std::move(prepared) + }]() -> mln_status { + return mln::core::map_add_geojson_source_data(map, id.view(), prepared); + }, + completion + ); + }); +} + +auto mln_map_set_geojson_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || + !valid_view(url, "url is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto value = OwnedView{url}; + return command( + map, + [map, id = std::move(id), value = std::move(value)]() -> mln_status { + return mln::core::map_set_geojson_source_url( + map, id.view(), value.view() + ); + }, + completion + ); + }); +} + +auto mln_map_set_geojson_source_data( + mln_map map, mln_buffer_view source_id, mln_geojson_source_data data, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + // The lease taken at submit keeps the prepared index alive until the + // command runs, so the host may destroy the handle right after this call. + auto prepared = mln::core::geojson_source_data_table().lease(data); + if (prepared == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), + prepared = + std::shared_ptr{ + std::move(prepared) + }]() -> mln_status { + return mln::core::map_set_geojson_source_data(map, id.view(), prepared); + }, + completion + ); + }); +} + +auto mln_map_set_geojson_source_synchronous_tiling( + mln_map map, mln_buffer_view source_id, bool enabled, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(source_id, "source_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), enabled]() -> mln_status { + return mln::core::map_set_geojson_source_synchronous_tiling( + map, id.view(), enabled + ); + }, + completion + ); + }); +} + +MLN_TILE_URL_COMMAND( + mln_map_add_vector_source_url, map_add_vector_source_url, 0 +) +MLN_TILE_LIST_COMMAND( + mln_map_add_vector_source_tiles, map_add_vector_source_tiles, 0 +) +MLN_TILE_URL_COMMAND( + mln_map_add_raster_source_url, map_add_raster_source_url, 1 +) +MLN_TILE_LIST_COMMAND( + mln_map_add_raster_source_tiles, map_add_raster_source_tiles, 1 +) +MLN_TILE_URL_COMMAND( + mln_map_add_raster_dem_source_url, map_add_raster_dem_source_url, 2 +) +MLN_TILE_LIST_COMMAND( + mln_map_add_raster_dem_source_tiles, map_add_raster_dem_source_tiles, 2 +) + +auto mln_map_add_custom_geometry_source( + mln_map map, mln_buffer_view source_id, + const mln_custom_geometry_source_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || + mln::core::validate_custom_geometry_command_options(options) != + MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto owned = std::make_shared(*options); + const auto status = command( + map, + [map, id = std::move(id), owned]() -> mln_status { + const auto add_status = mln::core::map_add_custom_geometry_source( + map, id.view(), &owned->value + ); + if (add_status == MLN_STATUS_OK) { + auto expected = OwnedCustomGeometryOptions::Ownership::pending; + if (!owned->ownership.compare_exchange_strong( + expected, OwnedCustomGeometryOptions::Ownership::adopted, + std::memory_order_acq_rel + )) { + owned->ownership.store( + OwnedCustomGeometryOptions::Ownership::adopted, + std::memory_order_release + ); + } + } + return add_status; + }, + completion + ); + if (status == MLN_STATUS_OK) { + auto expected = OwnedCustomGeometryOptions::Ownership::pending; + static_cast(owned->ownership.compare_exchange_strong( + expected, OwnedCustomGeometryOptions::Ownership::accepted, + std::memory_order_acq_rel + )); + } else { + owned->ownership.store( + OwnedCustomGeometryOptions::Ownership::rejected, + std::memory_order_release + ); + } + return status; + }); +} + +auto mln_map_set_custom_geometry_source_tile_data( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view data, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + auto owned = OwnedView{data}; + return command( + map, + [map, id = std::move(id), owned = std::move(owned), + tile_id]() -> mln_status { + return mln::core::map_set_custom_geometry_source_tile_data( + map, id.view(), tile_id, owned.view() + ); + }, + completion + ); + }); +} + +auto mln_map_invalidate_custom_geometry_source_tile( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), tile_id]() -> mln_status { + return mln::core::map_invalidate_custom_geometry_source_tile( + map, id.view(), tile_id + ); + }, + completion + ); + }); +} + +auto mln_map_invalidate_custom_geometry_source_region( + mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), bounds]() -> mln_status { + return mln::core::map_invalidate_custom_geometry_source_region( + map, id.view(), bounds + ); + }, + completion + ); + }); +} + +auto mln_map_add_custom_mvt_vector_source( + mln_map map, mln_buffer_view source_id, + const mln_custom_mvt_vector_source_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || + mln::core::validate_custom_mvt_command_options(options) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto owned = std::make_shared(*options); + const auto status = command( + map, + [map, id = std::move(id), owned]() -> mln_status { + const auto add_status = mln::core::map_add_custom_mvt_vector_source( + map, id.view(), &owned->value + ); + if (add_status == MLN_STATUS_OK) { + auto expected = OwnedCustomMvtVectorOptions::Ownership::pending; + if (!owned->ownership.compare_exchange_strong( + expected, OwnedCustomMvtVectorOptions::Ownership::adopted, + std::memory_order_acq_rel + )) { + owned->ownership.store( + OwnedCustomMvtVectorOptions::Ownership::adopted, + std::memory_order_release + ); + } + } + return add_status; + }, + completion + ); + if (status == MLN_STATUS_OK) { + auto expected = OwnedCustomMvtVectorOptions::Ownership::pending; + static_cast(owned->ownership.compare_exchange_strong( + expected, OwnedCustomMvtVectorOptions::Ownership::accepted, + std::memory_order_acq_rel + )); + } else { + owned->ownership.store( + OwnedCustomMvtVectorOptions::Ownership::rejected, + std::memory_order_release + ); + } + return status; + }); +} + +auto mln_map_set_custom_mvt_vector_source_tile_data( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view data, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + auto owned = OwnedView{data}; + return command( + map, + [map, id = std::move(id), owned = std::move(owned), + tile_id]() -> mln_status { + return mln::core::map_set_custom_mvt_vector_source_tile_data( + map, id.view(), tile_id, owned.view() + ); + }, + completion + ); + }); +} + +auto mln_map_set_custom_mvt_vector_source_tile_error( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view message, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + auto owned = OwnedView{message}; + return command( + map, + [map, id = std::move(id), owned = std::move(owned), + tile_id]() -> mln_status { + return mln::core::map_set_custom_mvt_vector_source_tile_error( + map, id.view(), tile_id, owned.view() + ); + }, + completion + ); + }); +} + +auto mln_map_invalidate_custom_mvt_vector_source_tile( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), tile_id]() -> mln_status { + return mln::core::map_invalidate_custom_mvt_vector_source_tile( + map, id.view(), tile_id + ); + }, + completion + ); + }); +} + +#undef MLN_TILE_LIST_COMMAND +#undef MLN_TILE_URL_COMMAND +#undef MLN_GEOJSON_COMMAND + +auto mln_map_set_style_image( + mln_map map, mln_buffer_view image_id, + const mln_premultiplied_rgba8_image* image, + const mln_style_image_options* options, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(image_id, "image_id is invalid") || image_id.size == 0 || + mln::core::validate_style_image_command_input(image, options) != + MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{image_id}; + auto owned_image = OwnedImage{image}; + auto owned_options = OwnedImageOptions{options}; + return command( + map, + [map, id = std::move(id), image = std::move(owned_image), + options = std::move(owned_options)]() mutable -> mln_status { + image.value.pixels = image.pixels.data(); + options.value.stretch_x = options.stretch_x.data(); + options.value.stretch_y = options.stretch_y.data(); + return mln::core::map_set_style_image( + map, id.view(), &image.value, &options.value + ); + }, + completion + ); + }); +} + +auto mln_map_remove_style_image( + mln_map map, mln_buffer_view image_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(image_id, "image_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{image_id}; + return command( + map, + [map, id = std::move(id)]() -> mln_status { + return mln::core::map_remove_style_image(map, id.view()); + }, + completion + ); + }); +} + +auto mln_map_get_style_image_info( + mln_map map, mln_buffer_view image_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{image_id}; + return operation( + map, mln::core::StyleOperationKind::ImageInfo, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + result.image_info = mln::core::style_image_info_default(); + auto status = mln::core::map_get_style_image_info( + map, id.view(), &result.image_info, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + size_t x = 0; + size_t y = 0; + bool found = false; + status = mln::core::map_copy_style_image_stretches( + map, id.view(), nullptr, 0, &x, nullptr, 0, &y, &found + ); + if (status != MLN_STATUS_OK) return status; + result.stretch_x.resize(x); + result.stretch_y.resize(y); + status = mln::core::map_copy_style_image_stretches( + map, id.view(), result.stretch_x.data(), result.stretch_x.size(), &x, + result.stretch_y.data(), result.stretch_y.size(), &y, &found + ); + if (status != MLN_STATUS_OK) return status; + size_t size = 0; + status = mln::core::map_copy_style_image_premultiplied_rgba8( + map, id.view(), nullptr, 0, &size, &found + ); + if (status != MLN_STATUS_OK) return status; + result.bytes.resize(size); + return mln::core::map_copy_style_image_premultiplied_rgba8( + map, id.view(), reinterpret_cast(result.bytes.data()), + result.bytes.size(), &size, &found + ); + }, + completion + ); + }); +} + +auto mln_map_copy_style_image_stretches( + mln_map map, mln_buffer_view image_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{image_id}; + return operation( + map, mln::core::StyleOperationKind::ImageStretches, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + size_t x = 0; + size_t y = 0; + auto status = mln::core::map_copy_style_image_stretches( + map, id.view(), nullptr, 0, &x, nullptr, 0, &y, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + result.stretch_x.resize(x); + result.stretch_y.resize(y); + return mln::core::map_copy_style_image_stretches( + map, id.view(), result.stretch_x.data(), result.stretch_x.size(), &x, + result.stretch_y.data(), result.stretch_y.size(), &y, &result.found + ); + }, + completion + ); + }); +} + +auto mln_map_copy_style_image_premultiplied_rgba8( + mln_map map, mln_buffer_view image_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{image_id}; + return operation( + map, mln::core::StyleOperationKind::ImagePixels, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + size_t size = 0; + auto status = mln::core::map_copy_style_image_premultiplied_rgba8( + map, id.view(), nullptr, 0, &size, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + auto bytes = std::string(size, '\0'); + status = mln::core::map_copy_style_image_premultiplied_rgba8( + map, id.view(), reinterpret_cast(bytes.data()), + bytes.size(), &size, &result.found + ); + if (status == MLN_STATUS_OK) result.bytes = std::move(bytes); + return status; + }, + completion + ); + }); +} + +auto mln_map_add_image_source_url( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count, mln_buffer_view url, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || source_id.size == 0 || + !valid_view(url, "url is invalid") || url.size == 0 || + mln::core::validate_image_source_command_coordinates( + coordinates, coordinate_count + ) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto value = OwnedView{url}; + auto points = + std::vector(coordinates, coordinates + coordinate_count); + return command( + map, + [map, id = std::move(id), value = std::move(value), + points = std::move(points)]() -> mln_status { + return mln::core::map_add_image_source_url( + map, id.view(), points.data(), points.size(), value.view() + ); + }, + completion + ); + }); +} + +auto mln_map_add_image_source_image( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count, const mln_premultiplied_rgba8_image* image, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || source_id.size == 0 || + mln::core::validate_image_source_command_coordinates( + coordinates, coordinate_count + ) != MLN_STATUS_OK || + mln::core::validate_style_image_command_input(image, nullptr) != + MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto owned = OwnedImage{image}; + auto points = + std::vector(coordinates, coordinates + coordinate_count); + return command( + map, + [map, id = std::move(id), owned = std::move(owned), + points = std::move(points)]() mutable -> mln_status { + owned.value.pixels = owned.pixels.data(); + return mln::core::map_add_image_source_image( + map, id.view(), points.data(), points.size(), &owned.value + ); + }, + completion + ); + }); +} + +auto mln_map_set_image_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || source_id.size == 0 || + !valid_view(url, "url is invalid") || url.size == 0 + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto value = OwnedView{url}; + return command( + map, + [map, id = std::move(id), value = std::move(value)]() -> mln_status { + return mln::core::map_set_image_source_url( + map, id.view(), value.view() + ); + }, + completion + ); + }); +} + +auto mln_map_set_image_source_image( + mln_map map, mln_buffer_view source_id, + const mln_premultiplied_rgba8_image* image, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || source_id.size == 0 || + mln::core::validate_style_image_command_input(image, nullptr) != + MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto owned = OwnedImage{image}; + return command( + map, + [map, id = std::move(id), + owned = std::move(owned)]() mutable -> mln_status { + owned.value.pixels = owned.pixels.data(); + return mln::core::map_set_image_source_image( + map, id.view(), &owned.value + ); + }, + completion + ); + }); +} + +auto mln_map_set_image_source_coordinates( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(source_id, "source_id is invalid") || source_id.size == 0 || + mln::core::validate_image_source_command_coordinates( + coordinates, coordinate_count + ) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{source_id}; + auto points = + std::vector(coordinates, coordinates + coordinate_count); + return command( + map, + [map, id = std::move(id), points = std::move(points)]() -> mln_status { + return mln::core::map_set_image_source_coordinates( + map, id.view(), points.data(), points.size() + ); + }, + completion + ); + }); +} + +auto mln_map_get_image_source_coordinates( + mln_map map, mln_buffer_view source_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{source_id}; + return operation( + map, mln::core::StyleOperationKind::ImageCoordinates, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + size_t count = 0; + auto status = mln::core::map_get_image_source_coordinates( + map, id.view(), nullptr, 0, &count, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + result.coordinates.resize(count); + return mln::core::map_get_image_source_coordinates( + map, id.view(), result.coordinates.data(), result.coordinates.size(), + &count, &result.found + ); + }, + completion + ); + }); +} + +#define MLN_LAYER_THREE_VIEW_COMMAND(NAME, CORE) \ + auto NAME( \ + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, \ + mln_buffer_view before_layer_id, const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + auto layer = OwnedView{layer_id}; \ + auto source = OwnedView{source_id}; \ + auto before = OwnedView{before_layer_id}; \ + return command( \ + map, \ + [map, layer = std::move(layer), source = std::move(source), \ + before = std::move(before)]() -> mln_status { \ + return mln::core::CORE( \ + map, layer.view(), source.view(), before.view() \ + ); \ + }, \ + completion \ + ); \ + }); \ + } +MLN_LAYER_THREE_VIEW_COMMAND( + mln_map_add_hillshade_layer, map_add_hillshade_layer +) +MLN_LAYER_THREE_VIEW_COMMAND( + mln_map_add_color_relief_layer, map_add_color_relief_layer +) +#undef MLN_LAYER_THREE_VIEW_COMMAND + +auto mln_map_add_location_indicator_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + auto before = OwnedView{before_layer_id}; + return command( + map, + [map, id = std::move(id), before = std::move(before)]() -> mln_status { + return mln::core::map_add_location_indicator_layer( + map, id.view(), before.view() + ); + }, + completion + ); + }); +} + +auto mln_map_set_location_indicator_location( + mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, + double altitude, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + return command( + map, + [map, id = std::move(id), coordinate, altitude]() -> mln_status { + return mln::core::map_set_location_indicator_location( + map, id.view(), coordinate, altitude + ); + }, + completion + ); + }); +} + +auto mln_map_set_location_indicator_bearing( + mln_map map, mln_buffer_view layer_id, double bearing, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + return command( + map, + [map, id = std::move(id), bearing]() -> mln_status { + return mln::core::map_set_location_indicator_bearing( + map, id.view(), bearing + ); + }, + completion + ); + }); +} + +auto mln_map_set_location_indicator_accuracy_radius( + mln_map map, mln_buffer_view layer_id, double radius, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + return command( + map, + [map, id = std::move(id), radius]() -> mln_status { + return mln::core::map_set_location_indicator_accuracy_radius( + map, id.view(), radius + ); + }, + completion + ); + }); +} + +auto mln_map_set_location_indicator_image_name( + mln_map map, mln_buffer_view layer_id, uint32_t image_kind, + mln_buffer_view image_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto layer = OwnedView{layer_id}; + auto image = OwnedView{image_id}; + return command( + map, + [map, layer = std::move(layer), image = std::move(image), + image_kind]() -> mln_status { + return mln::core::map_set_location_indicator_image_name( + map, layer.view(), image_kind, image.view() + ); + }, + completion + ); + }); +} +auto mln_map_add_style_layer_json( + mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(layer_json, "layer_json is invalid") || + !valid_view(before_layer_id, "before_layer_id is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto json = OwnedView{layer_json}; + auto before = OwnedView{before_layer_id}; + return command( + map, + [map, json = std::move(json), + before = std::move(before)]() -> mln_status { + return mln::core::map_add_style_layer_json( + map, json.view(), before.view() + ); + }, + completion + ); + }); +} + +auto mln_map_remove_style_layer( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(layer_id, "layer_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + return command( + map, + [map, id = std::move(id)]() -> mln_status { + return mln::core::map_remove_style_layer(map, id.view()); + }, + completion + ); + }); +} + +auto mln_map_get_style_layer_info( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(layer_id, "layer_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + return operation( + map, mln::core::StyleOperationKind::LayerInfo, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + result.layer_info = {}; + result.layer_info.size = sizeof(mln_style_layer_info); + auto status = mln::core::map_get_style_layer_info( + map, id.view(), &result.layer_info, &result.found + ); + if (status != MLN_STATUS_OK || !result.found) return status; + auto copy = [&](auto function, std::string& destination) { + size_t size = 0; + auto copy_status = function(map, id.view(), nullptr, 0, &size); + if (copy_status != MLN_STATUS_OK) return copy_status; + destination.resize(size); + return function( + map, id.view(), destination.data(), destination.size(), &size + ); + }; + status = copy(mln::core::map_copy_layer_source_id, result.source_id); + return status == MLN_STATUS_OK + ? copy( + mln::core::map_copy_layer_source_layer, result.source_layer + ) + : status; + }, + completion + ); + }); +} + +auto mln_map_list_style_layer_ids( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return operation( + map, mln::core::StyleOperationKind::LayerIds, + [map](mln::core::StyleOperationResult& result) -> mln_status { + auto list = mln_style_id_list{MLN_HANDLE_NULL}; + const auto status = mln::core::map_list_style_layer_ids(map, &list); + return status == MLN_STATUS_OK ? take_id_list(list, result.strings) + : status; + }, + completion + ); + }); +} + +auto mln_map_move_style_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(layer_id, "layer_id is invalid") || + !valid_view(before_layer_id, "before_layer_id is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + auto before = OwnedView{before_layer_id}; + return command( + map, + [map, id = std::move(id), before = std::move(before)]() -> mln_status { + return mln::core::map_move_style_layer(map, id.view(), before.view()); + }, + completion + ); + }); +} + +auto mln_map_get_style_layer_json( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(layer_id, "layer_id is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + return operation( + map, mln::core::StyleOperationKind::LayerJson, + [map, id = std::move(id)](mln::core::StyleOperationResult& result) + -> mln_status { + auto buffer = mln_buffer{MLN_HANDLE_NULL}; + const auto status = mln::core::map_get_style_layer_json( + map, id.view(), &buffer, &result.found + ); + return status == MLN_STATUS_OK && result.found + ? take_buffer(buffer, result.bytes) + : status; + }, + completion + ); + }); +} + +auto mln_map_set_style_light_json( + mln_map map, mln_buffer_view light_json, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(light_json, "light_json is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto json = OwnedView{light_json}; + return command( + map, + [map, json = std::move(json)]() -> mln_status { + return mln::core::map_set_style_light_json(map, json.view()); + }, + completion + ); + }); +} + +auto mln_map_set_style_light_property( + mln_map map, mln_buffer_view property_name, mln_buffer_view value, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(property_name, "property_name is invalid") || + !valid_view(value, "value is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto name = OwnedView{property_name}; + auto json = OwnedView{value}; + return command( + map, + [map, name = std::move(name), json = std::move(json)]() -> mln_status { + return mln::core::map_set_style_light_property( + map, name.view(), json.view() + ); + }, + completion + ); + }); +} + +auto mln_map_get_style_light_property( + mln_map map, mln_buffer_view property_name, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if (!valid_view(property_name, "property_name is invalid")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto name = OwnedView{property_name}; + return operation( + map, mln::core::StyleOperationKind::LightProperty, + [map, name = std::move(name)](mln::core::StyleOperationResult& result) + -> mln_status { + auto buffer = mln_buffer{MLN_HANDLE_NULL}; + const auto status = + mln::core::map_get_style_light_property(map, name.view(), &buffer); + if (status != MLN_STATUS_OK || buffer == MLN_HANDLE_NULL) return status; + result.found = true; + return take_buffer(buffer, result.bytes); + }, + completion + ); + }); +} + +auto mln_map_set_style_transition_options( + mln_map map, const mln_style_transition_options* options, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + options == nullptr || options->size < sizeof(mln_style_transition_options) + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto owned = *options; + return command( + map, + [map, owned]() -> mln_status { + return mln::core::map_set_style_transition_options(map, &owned); + }, + completion + ); + }); +} + +auto mln_map_get_style_transition_options( + mln_map map, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return operation( + map, mln::core::StyleOperationKind::TransitionOptions, + [map](mln::core::StyleOperationResult& result) -> mln_status { + result.transition_options = + mln::core::style_transition_options_default(); + return mln::core::map_get_style_transition_options( + map, &result.transition_options + ); + }, + completion + ); + }); +} + +#define MLN_STYLE_BUFFER_OPERATION(NAME, CORE, KIND) \ + auto NAME( \ + mln_map map, mln_buffer_view id, const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + if (!valid_view(id, "style ID is invalid")) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + auto owned = OwnedView{id}; \ + return operation( \ + map, mln::core::StyleOperationKind::KIND, \ + [map, owned = std::move(owned)]( \ + mln::core::StyleOperationResult& result \ + ) -> mln_status { \ + auto buffer = mln_buffer{MLN_HANDLE_NULL}; \ + const auto status = mln::core::CORE(map, owned.view(), &buffer); \ + if (status != MLN_STATUS_OK) return status; \ + if (buffer == MLN_HANDLE_NULL) return MLN_STATUS_OK; \ + result.found = true; \ + return take_buffer(buffer, result.bytes); \ + }, \ + completion \ + ); \ + }); \ + } + +#define MLN_STYLE_SCALAR_COMMAND(NAME, CORE, TYPE) \ + auto NAME( \ + mln_map map, mln_buffer_view id, TYPE value, \ + const mln_completion* completion \ + ) noexcept -> mln_status { \ + return mln::c_api::status_boundary([&]() -> mln_status { \ + if (!valid_view(id, "style ID is invalid")) { \ + return MLN_STATUS_INVALID_ARGUMENT; \ + } \ + auto owned = OwnedView{id}; \ + return command( \ + map, \ + [map, owned = std::move(owned), value]() -> mln_status { \ + return mln::core::CORE(map, owned.view(), value); \ + }, \ + completion \ + ); \ + }); \ + } + +auto mln_map_set_layer_property( + mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, + mln_buffer_view value, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(layer_id, "layer_id is invalid") || + !valid_view(property_name, "property_name is invalid") || + !valid_view(value, "value is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + auto name = OwnedView{property_name}; + auto json = OwnedView{value}; + return command( + map, + [map, id = std::move(id), name = std::move(name), + json = std::move(json)]() -> mln_status { + return mln::core::map_set_layer_property( + map, id.view(), name.view(), json.view() + ); + }, + completion + ); + }); +} + +auto mln_map_get_layer_property( + mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(layer_id, "layer_id is invalid") || + !valid_view(property_name, "property_name is invalid") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + auto name = OwnedView{property_name}; + return operation( + map, mln::core::StyleOperationKind::LayerProperty, + [map, id = std::move(id), name = std::move(name)]( + mln::core::StyleOperationResult& result + ) -> mln_status { + auto buffer = mln_buffer{MLN_HANDLE_NULL}; + const auto status = mln::core::map_get_layer_property( + map, id.view(), name.view(), &buffer + ); + if (status != MLN_STATUS_OK || buffer == MLN_HANDLE_NULL) return status; + result.found = true; + return take_buffer(buffer, result.bytes); + }, + completion + ); + }); +} + +auto mln_map_set_layer_filter( + mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + if ( + !valid_view(layer_id, "layer_id is invalid") || + (filter != nullptr && !valid_view(*filter, "filter is invalid")) + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto id = OwnedView{layer_id}; + auto owned_filter = filter == nullptr + ? std::optional{} + : std::optional{OwnedView{*filter}}; + return command( + map, + [map, id = std::move(id), + owned_filter = std::move(owned_filter)]() -> mln_status { + const auto view = + owned_filter ? std::optional{owned_filter->view()} + : std::nullopt; + return mln::core::map_set_layer_filter( + map, id.view(), view ? &*view : nullptr + ); + }, + completion + ); + }); +} + +MLN_STYLE_BUFFER_OPERATION( + mln_map_get_layer_filter, map_get_layer_filter, LayerFilter +) + +auto mln_map_set_layer_source_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + auto source = OwnedView{source_layer}; + return command( + map, + [map, id = std::move(id), source = std::move(source)]() -> mln_status { + return mln::core::map_set_layer_source_layer( + map, id.view(), source.view() + ); + }, + completion + ); + }); +} + +auto mln_map_set_layer_source_id( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, + const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + auto id = OwnedView{layer_id}; + auto source = OwnedView{source_id}; + return command( + map, + [map, id = std::move(id), source = std::move(source)]() -> mln_status { + return mln::core::map_set_layer_source_id( + map, id.view(), source.view() + ); + }, + completion + ); + }); +} + +MLN_STYLE_SCALAR_COMMAND( + mln_map_set_layer_min_zoom, map_set_layer_min_zoom, double +) +MLN_STYLE_SCALAR_COMMAND( + mln_map_set_layer_max_zoom, map_set_layer_max_zoom, double +) +MLN_STYLE_SCALAR_COMMAND( + mln_map_set_layer_visibility, map_set_layer_visibility, uint32_t +) + +#undef MLN_STYLE_SCALAR_COMMAND +#undef MLN_STYLE_BUFFER_OPERATION + +auto mln_map_copy_layer_source_layer( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return start_text_copy( + map, layer_id, mln::core::StyleOperationKind::LayerSourceLayer, + []( + mln_map value_map, mln_buffer_view id, char* text, size_t capacity, + size_t* size + ) -> mln_status { + return mln::core::map_copy_layer_source_layer( + value_map, id, text, capacity, size + ); + }, + completion + ); + }); +} + +auto mln_map_copy_layer_source_id( + mln_map map, mln_buffer_view layer_id, const mln_completion* completion +) noexcept -> mln_status { + return mln::c_api::status_boundary([&]() -> mln_status { + return start_text_copy( + map, layer_id, mln::core::StyleOperationKind::LayerSourceId, + []( + mln_map value_map, mln_buffer_view id, char* text, size_t capacity, + size_t* size + ) -> mln_status { + return mln::core::map_copy_layer_source_id( + value_map, id, text, capacity, size + ); + }, + completion + ); + }); +} diff --git a/src/c_api/surface.cpp b/src/c_api/surface.cpp index b811f7784..0cfd8d693 100644 --- a/src/c_api/surface.cpp +++ b/src/c_api/surface.cpp @@ -26,68 +26,92 @@ auto mln_webgpu_surface_descriptor_default(void) noexcept auto mln_metal_surface_attach( mln_map map, const mln_metal_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_surface_attach(map, descriptor, out_session); + return mln::core::metal_surface_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_vulkan_surface_attach( mln_map map, const mln_vulkan_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_surface_attach(map, descriptor, out_session); + return mln::core::vulkan_surface_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_opengl_surface_attach( mln_map map, const mln_opengl_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_surface_attach(map, descriptor, out_session); + return mln::core::opengl_surface_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_webgpu_surface_attach( mln_map map, const mln_webgpu_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_surface_attach(map, descriptor, out_session); + return mln::core::webgpu_surface_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_metal_surface_set_target( - mln_render_session session, const mln_metal_surface_descriptor* descriptor + mln_render_session session, const mln_metal_surface_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_surface_set_target(session, descriptor); + return mln::core::metal_surface_set_target_start( + session, descriptor, completion + ); }); } auto mln_vulkan_surface_set_target( - mln_render_session session, const mln_vulkan_surface_descriptor* descriptor + mln_render_session session, const mln_vulkan_surface_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_surface_set_target(session, descriptor); + return mln::core::vulkan_surface_set_target_start( + session, descriptor, completion + ); }); } auto mln_opengl_surface_set_target( - mln_render_session session, const mln_opengl_surface_descriptor* descriptor + mln_render_session session, const mln_opengl_surface_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_surface_set_target(session, descriptor); + return mln::core::opengl_surface_set_target_start( + session, descriptor, completion + ); }); } auto mln_webgpu_surface_set_target( - mln_render_session session, const mln_webgpu_surface_descriptor* descriptor + mln_render_session session, const mln_webgpu_surface_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_surface_set_target(session, descriptor); + return mln::core::webgpu_surface_set_target_start( + session, descriptor, completion + ); }); } diff --git a/src/c_api/tests/abi_tests.h b/src/c_api/tests/abi_tests.h index d91e0910d..0b8823d01 100644 --- a/src/c_api/tests/abi_tests.h +++ b/src/c_api/tests/abi_tests.h @@ -23,9 +23,11 @@ void run_render_target_lifecycle_abi_tests(void); void run_render_thread_abi_tests(void); void run_query_abi_tests(void); void run_mlt_decode_abi_tests(void); +void run_completion_abi_tests(void); +void run_projection_abi_tests(void); void run_resources_abi_tests(void); void run_runtime_events_abi_tests(void); -void run_runtime_wake_abi_tests(void); +void run_runtime_lifecycle_abi_tests(void); void run_style_values_abi_tests(void); #endif diff --git a/src/c_api/tests/browser_http_abi.c b/src/c_api/tests/browser_http_abi.c index f736b5b7e..3810a965b 100644 --- a/src/c_api/tests/browser_http_abi.c +++ b/src/c_api/tests/browser_http_abi.c @@ -28,12 +28,12 @@ static bool wait_for_style_loaded( ) { out_message[0] = '\0'; for (unsigned int attempt = 0; attempt < 600; attempt += 1) { - if (mln_runtime_pump(runtime, 10, -1) != MLN_STATUS_OK) { + if (mln_test_runtime_barrier(runtime) != MLN_STATUS_OK) { return false; } while (true) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { + mln_test_event_batch batch = mln_test_event_batch_default(); + if (mln_test_drain_events(runtime, &batch) != MLN_STATUS_OK) { return false; } if (batch.event_count == 0) { @@ -62,6 +62,7 @@ static bool wait_for_style_loaded( } } } + mln_test_sleep_millisecond(); } snprintf( out_message, capacity, @@ -85,7 +86,7 @@ static void style_loads_over_http_from_the_runner_origin(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_style_url(map, url)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_map_set_style_url(map, url)); char failure[512]; const bool loaded = @@ -97,36 +98,20 @@ static void style_loads_over_http_from_the_runner_origin(void) { return; } - // Checks which document loaded: a response from anywhere else carries a - // different layer. - mln_style_id_list layers = MLN_HANDLE_NULL; + // Checks which document loaded: a response from anywhere else lacks this + // layer. + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_style_layer_result)); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_list_style_layer_ids(map, &layers) + MLN_STATUS_OK, + mln_map_get_style_layer_info( + map, mln_test_buffer_view(fixture_layer_id, strlen(fixture_layer_id)), + &completion.descriptor + ) ); - size_t count = 0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_style_id_list_count(layers, &count)); - - bool found = false; - char seen[256]; - seen[0] = '\0'; - for (size_t index = 0; index < count; index += 1) { - mln_buffer_view id = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_id_list_get(layers, index, &id) - ); - if ( - id.size == strlen(fixture_layer_id) && - memcmp(id.data, fixture_layer_id, id.size) == 0 - ) { - found = true; - } - const size_t used = strlen(seen); - (void)snprintf( - seen + used, sizeof(seen) - used, "%s%.*s", used == 0 ? "" : ", ", - (int)id.size, id.data - ); - } - mln_style_id_list_destroy(layers); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + const bool found = mln_test_completion_value_count(&completion) == 1; + mln_test_completion_destroy(&completion); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); @@ -136,8 +121,8 @@ static void style_loads_over_http_from_the_runner_origin(void) { (void)snprintf( message, sizeof(message), "the loaded style has no %s layer, so it did not come from the runner's " - "document; layers: [%s]", - fixture_layer_id, seen + "document", + fixture_layer_id ); TEST_FAIL_MESSAGE(message); } diff --git a/src/c_api/tests/callback_adapter_abi.c b/src/c_api/tests/callback_adapter_abi.c index 1b5992c17..8e3322f41 100644 --- a/src/c_api/tests/callback_adapter_abi.c +++ b/src/c_api/tests/callback_adapter_abi.c @@ -18,23 +18,24 @@ static const char alias_url[] = "maplibre://maps/style"; static const char normalized_url[] = "https://demotiles.maplibre.org/style.json"; -// A handle the loader never issued: the listener below releases the copied -// record without completing or releasing the handle it carries. +// A handle the loader never issued. Releasing it after inspecting the copied +// record is a no-op. static const mln_resource_request_handle unissued_handle = 1; static size_t queued_requests; -// The record's strings die with the record, so the listener stores what it saw -// rather than pointers into it. static bool last_requested_url_null; static bool last_requested_url_empty; -static void count_queued_request(void* request) { - const mln_adapter_queued_resource_request* record = request; - queued_requests += 1; - last_requested_url_null = record->requested_url == NULL; - last_requested_url_empty = - !last_requested_url_null && record->requested_url[0] == '\0'; - mln_adapter_resource_provider_request_destroy(request); +static size_t completion_listener_calls; +static bool completion_listener_received_null; + +static void completion_listener( + void* user_data, mln_adapter_completion_record* record +) { + (void)user_data; + completion_listener_calls += 1; + completion_listener_received_null = record == NULL; + mln_adapter_completion_record_destroy(record); } static mln_resource_request style_request(void) { @@ -50,20 +51,38 @@ static mln_resource_request style_request(void) { }; } -// Reports the decision one route produces for one request, and increments -// queued_requests when the route claimed it. +// Reports the decision one route produces for one request, and drains the +// copied record when the route claimed it. static uint32_t route_decision( mln_adapter_queued_resource_provider_route route, const mln_resource_request* request ) { + mln_adapter_resource_request_queue queue = MLN_HANDLE_NULL; + const mln_wake wake = {.size = sizeof(mln_wake)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_adapter_resource_request_queue_create(&wake, &queue) + ); mln_adapter_queued_resource_provider provider = { .routes = &route, .route_count = 1, - .listener = count_queued_request, + .queue = queue, }; - return mln_adapter_queued_resource_provider_callback( + const uint32_t decision = mln_adapter_queued_resource_provider_callback( &provider, request, unissued_handle ); + mln_adapter_queued_resource_request* record = NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_adapter_resource_request_queue_acquire(queue, &record) + ); + if (record != NULL) { + queued_requests += 1; + last_requested_url_null = record->requested_url == NULL; + last_requested_url_empty = + !last_requested_url_null && record->requested_url[0] == '\0'; + mln_adapter_resource_provider_request_destroy(record); + } + mln_adapter_resource_request_queue_close(queue); + return decision; } static void assert_claims( @@ -301,6 +320,32 @@ static void http_header_validation_uses_the_native_policy(void) { ); } +static void completion_copy_failures_use_the_adapter_failure_channel(void) { + completion_listener_calls = 0; + completion_listener_received_null = false; + mln_completion completion = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_adapter_completion_create( + MLN_ADAPTER_COMPLETION_COPY_TEXTURE_READBACK, 0, + completion_listener, NULL, &completion + ) + ); + + // A texture readback completion must carry exactly one result. The invalid + // shape deterministically exercises the adapter's copy-failure path. + const mln_texture_readback_result value = {0}; + const mln_completion_result result = { + .size = sizeof(mln_completion_result), + .status = MLN_STATUS_OK, + .value = &value, + .value_count = 2, + }; + completion.callback(completion.user_data, &result); + TEST_ASSERT_EQUAL_size_t(1, completion_listener_calls); + TEST_ASSERT_TRUE(completion_listener_received_null); + completion.release_user_data(completion.user_data); +} + void run_callback_adapter_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(queued_provider_routes_reject_raw_invalid_route_descriptors); @@ -310,4 +355,5 @@ void run_callback_adapter_abi_tests(void) { RUN_TEST(glob_routes_backtrack_across_wildcard_runs); RUN_TEST(http_header_routes_match_exact_glob_kind_and_order); RUN_TEST(http_header_validation_uses_the_native_policy); + RUN_TEST(completion_copy_failures_use_the_adapter_failure_channel); } diff --git a/src/c_api/tests/completion_abi.c b/src/c_api/tests/completion_abi.c new file mode 100644 index 000000000..b7eb45d02 --- /dev/null +++ b/src/c_api/tests/completion_abi.c @@ -0,0 +1,115 @@ +// Raw C ABI coverage for one-shot completions and direct queue wakes. + +#include + +#include "abi_tests.h" +#include "test_support.h" +#include "unity.h" + +typedef struct callback_probe { + atomic_uint calls; + atomic_uint releases; + atomic_int status; + atomic_uint_fast64_t value; +} callback_probe; + +static void record_completion( + void* user_data, const mln_completion_result* result +) { + callback_probe* probe = user_data; + atomic_store(&probe->status, result->status); + if (result->value != NULL && result->value_count == 1) { + atomic_store(&probe->value, *(const mln_map*)result->value); + } + atomic_fetch_add(&probe->calls, 1); +} + +static void record_release(void* user_data) { + callback_probe* probe = user_data; + atomic_fetch_add(&probe->releases, 1); +} + +static mln_completion probed_completion(callback_probe* probe) { + return (mln_completion){ + .size = sizeof(mln_completion), + .callback = record_completion, + .user_data = probe, + .release_user_data = record_release, + }; +} + +static void an_accepted_completion_runs_and_releases_exactly_once(void) { + mln_runtime runtime = mln_test_create_runtime(); + callback_probe probe = {0}; + atomic_init(&probe.status, MLN_STATUS_INVALID_STATE); + mln_completion completion = probed_completion(&probe); + + const mln_map_options options = mln_map_options_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_create(runtime, &options, &completion) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_EQUAL_UINT32(1, atomic_load(&probe.calls)); + TEST_ASSERT_EQUAL_UINT32(1, atomic_load(&probe.releases)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, atomic_load(&probe.status)); + + const mln_map map = (mln_map)atomic_load(&probe.value); + TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, map); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +static void a_rejected_submission_leaves_callback_state_with_the_caller(void) { + callback_probe probe = {0}; + mln_completion completion = probed_completion(&probe); + const mln_map_options options = mln_map_options_default(); + + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_create(MLN_HANDLE_NULL, &options, &completion) + ); + TEST_ASSERT_EQUAL_UINT32(0, atomic_load(&probe.calls)); + TEST_ASSERT_EQUAL_UINT32(0, atomic_load(&probe.releases)); +} + +static void completion_state_handles_inline_abandonment_and_races(void) { + TEST_ASSERT_TRUE(mln_test_completion_contract()); +} + +static void record_wake(void* user_data) { + atomic_fetch_add((atomic_uint*)user_data, 1); +} + +static void runtime_events_wake_the_receiver_directly(void) { + atomic_uint wakes; + atomic_init(&wakes, 0); + const mln_wake event_wake = { + .size = sizeof(mln_wake), + .callback = record_wake, + .user_data = &wakes, + }; + mln_runtime_options options = mln_runtime_options_default(); + options.event_wake = event_wake; + mln_runtime runtime = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_create(&options, &runtime)); + mln_map map = mln_test_create_map(runtime); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_map_set_style_json( + map, MLN_BUFFER_LITERAL("{\"version\":8,\"sources\":{},\"layers\":[]}") + ) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_GREATER_THAN_UINT32(0, atomic_load(&wakes)); + TEST_ASSERT_GREATER_THAN_size_t(0, mln_test_drain_all(runtime)); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +void run_completion_abi_tests(void) { + UnitySetTestFile(__FILE__); + RUN_TEST(an_accepted_completion_runs_and_releases_exactly_once); + RUN_TEST(a_rejected_submission_leaves_callback_state_with_the_caller); + RUN_TEST(completion_state_handles_inline_abandonment_and_races); + RUN_TEST(runtime_events_wake_the_receiver_directly); +} diff --git a/src/c_api/tests/core_abi.c b/src/c_api/tests/core_abi.c index 54651e0d3..e353a5e87 100644 --- a/src/c_api/tests/core_abi.c +++ b/src/c_api/tests/core_abi.c @@ -31,56 +31,56 @@ static void runtime_rejects_invalid_arguments(void) { TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_runtime_create(&options, &runtime) ); + const mln_completion discard = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_destroy(MLN_HANDLE_NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_release(MLN_HANDLE_NULL, &discard) ); } static void runtime_rejects_stale_handles(void) { mln_runtime runtime = mln_test_create_runtime(); mln_test_destroy_runtime(runtime); + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_destroy(runtime) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_release(runtime, &completion) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_pump(runtime, 0, -1) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_barrier(runtime, &completion) ); } -static void runtime_pump_rejects_null_runtime(void) { +static void runtime_barrier_rejects_null_runtime(void) { + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_pump(MLN_HANDLE_NULL, 0, -1) + MLN_STATUS_INVALID_ARGUMENT, + mln_runtime_barrier(MLN_HANDLE_NULL, &completion) ); } static void map_create_rejects_invalid_arguments(void) { - mln_map map = MLN_HANDLE_NULL; const mln_map_options options = mln_map_options_default(); + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_create(MLN_HANDLE_NULL, &options, &map) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_create(MLN_HANDLE_NULL, &options, &completion) ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, map); mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_map_create(runtime, &options, NULL) ); - map = 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_create(runtime, &options, &map) - ); - - map = MLN_HANDLE_NULL; mln_map_options small_options = mln_map_options_default(); small_options.size = sizeof(mln_map_options) - 1; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_create(runtime, &small_options, &map) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_create(runtime, &small_options, &completion) ); mln_map_options invalid_options = mln_map_options_default(); invalid_options.map_mode = (mln_map_mode)999; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_create(runtime, &invalid_options, &map) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_create(runtime, &invalid_options, &completion) ); mln_test_destroy_runtime(runtime); } @@ -89,20 +89,21 @@ static void map_lifecycle_rejects_invalid_state_and_stale_handles(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_ARGUMENT, mln_map_destroy(map)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_ARGUMENT, mln_test_map_close(map)); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL("{}")) + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL("{}")) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(map) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(map) ); + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_still_image(map) + MLN_STATUS_INVALID_ARGUMENT, mln_map_request_still_image(map, &completion) ); mln_camera_options camera = mln_camera_options_default(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_camera(map, &camera) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_get_camera(map, &camera) ); mln_test_destroy_runtime(runtime); } @@ -112,10 +113,10 @@ static void style_functions_reject_null_inputs(void) { mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_json(map, (mln_buffer_view){0}) + mln_test_map_set_style_json(map, (mln_buffer_view){0}) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_style_url(map, NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_set_style_url(map, NULL) ); // The copy entry points treat a null buffer as a probe only at zero capacity, @@ -124,19 +125,19 @@ static void style_functions_reject_null_inputs(void) { char buffer[8] = {0}; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_loaded_style_json(map, NULL, sizeof(buffer), &size) + mln_test_map_copy_loaded_style_json(map, NULL, sizeof(buffer), &size) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_loaded_style_json(map, buffer, sizeof(buffer), NULL) + mln_test_map_copy_loaded_style_json(map, buffer, sizeof(buffer), NULL) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_url(map, NULL, sizeof(buffer), &size) + mln_test_map_copy_style_url(map, NULL, sizeof(buffer), &size) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_url(map, buffer, sizeof(buffer), NULL) + mln_test_map_copy_style_url(map, buffer, sizeof(buffer), NULL) ); mln_test_destroy_map(map); @@ -145,7 +146,7 @@ static void style_functions_reject_null_inputs(void) { static void failing_status_sets_and_successful_status_clears_diagnostics(void) { TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_destroy(MLN_HANDLE_NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_test_runtime_close(MLN_HANDLE_NULL) ); TEST_ASSERT_GREATER_THAN_size_t(0, strlen(mln_thread_last_error_message())); mln_runtime runtime = mln_test_create_runtime(); @@ -164,7 +165,7 @@ static DWORD WINAPI fail_on_thread(void* opaque_result) { static void* fail_on_thread(void* opaque_result) { #endif worker_diagnostic* result = opaque_result; - result->status = mln_runtime_destroy(MLN_HANDLE_NULL); + result->status = mln_test_runtime_close(MLN_HANDLE_NULL); result->message_length = strlen(mln_thread_last_error_message()); #if defined(_WIN32) return 0; @@ -175,7 +176,7 @@ static void* fail_on_thread(void* opaque_result) { static void diagnostics_are_thread_local(void) { TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_destroy(MLN_HANDLE_NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_test_runtime_close(MLN_HANDLE_NULL) ); char main_message[512] = {0}; strncpy( @@ -208,14 +209,55 @@ static void diagnostics_are_thread_local(void) { mln_test_destroy_runtime(runtime); } +static uint32_t ignore_log_record( + void* user_data, uint32_t severity, uint32_t event, int64_t code, + const char* message +) { + (void)user_data; + (void)severity; + (void)event; + (void)code; + (void)message; + return 0; +} + +static void count_log_callback_release(void* user_data) { ++*(int*)user_data; } + +static void log_callback_releases_owned_user_data(void) { + int first_releases = 0; + int second_releases = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_log_set_callback( + ignore_log_record, &first_releases, count_log_callback_release + ) + ); + TEST_ASSERT_EQUAL_INT(0, first_releases); + + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_log_set_callback( + ignore_log_record, &second_releases, count_log_callback_release + ) + ); + TEST_ASSERT_EQUAL_INT(1, first_releases); + TEST_ASSERT_EQUAL_INT(0, second_releases); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_log_clear_callback()); + TEST_ASSERT_EQUAL_INT(1, second_releases); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_log_clear_callback()); + TEST_ASSERT_EQUAL_INT(1, second_releases); +} + void run_core_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(runtime_rejects_invalid_arguments); RUN_TEST(runtime_rejects_stale_handles); - RUN_TEST(runtime_pump_rejects_null_runtime); + RUN_TEST(runtime_barrier_rejects_null_runtime); RUN_TEST(map_create_rejects_invalid_arguments); RUN_TEST(map_lifecycle_rejects_invalid_state_and_stale_handles); RUN_TEST(style_functions_reject_null_inputs); RUN_TEST(failing_status_sets_and_successful_status_clears_diagnostics); RUN_TEST(diagnostics_are_thread_local); + RUN_TEST(log_callback_releases_owned_user_data); } diff --git a/src/c_api/tests/custom_geometry_source_abi.c b/src/c_api/tests/custom_geometry_source_abi.c index 8ef766003..83848c20c 100644 --- a/src/c_api/tests/custom_geometry_source_abi.c +++ b/src/c_api/tests/custom_geometry_source_abi.c @@ -5,6 +5,7 @@ // the release runs independently of the event mask. A binding observes its own // release only through that same mechanism. +#include #include #include #include @@ -19,20 +20,22 @@ static const char background_style_json[] = static const char empty_style_json[] = "{\"version\":8,\"sources\":{},\"layers\":[]}"; static const char source_id[] = "custom-geometry"; -static const size_t style_pump_attempts = 200; +static const size_t style_wait_attempts = 200; +// The counters are written from native threads and read from the test thread, +// so they are atomic; the barrier polls below provide the ordering. typedef struct release_probe { - size_t release_count; - size_t fetch_count; + atomic_size_t release_count; + atomic_size_t fetch_count; } release_probe; static void probe_fetch_tile(void* user_data, mln_canonical_tile_id tile_id) { (void)tile_id; - ((release_probe*)user_data)->fetch_count += 1; + atomic_fetch_add(&((release_probe*)user_data)->fetch_count, 1); } static void probe_release(void* user_data) { - ((release_probe*)user_data)->release_count += 1; + atomic_fetch_add(&((release_probe*)user_data)->release_count, 1); } static mln_custom_geometry_source_options probe_options(release_probe* probe) { @@ -44,21 +47,23 @@ static mln_custom_geometry_source_options probe_options(release_probe* probe) { return options; } -// Loads an inline style and pumps, so the map reaches a loaded style without +// Loads an inline style and waits for the map to reach a loaded state without // reaching the network. -static void load_style_and_pump( +static void load_style_and_wait( mln_runtime runtime, mln_map map, mln_buffer_view style_json ) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_style_json(map, style_json)); - for (size_t attempt = 0; attempt < style_pump_attempts; attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 2, -1)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_map_set_style_json(map, style_json) + ); + for (size_t attempt = 0; attempt < style_wait_attempts; attempt += 1) { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); } } static mln_map create_map_without_style_events(mln_runtime runtime) { mln_map_options options = mln_map_options_default(); - options.width = 256; - options.height = 256; + options.initial_extent.width = 256; + options.initial_extent.height = 256; options.event_mask = MLN_RUNTIME_EVENT_MASK_ALL & ~(uint64_t)MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED; return mln_test_create_map_with_options(runtime, &options); @@ -70,25 +75,30 @@ static void a_style_replacement_releases_a_dropped_source_unsubscribed(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = create_map_without_style_events(runtime); release_probe probe = {0}; + mln_test_completion command = mln_test_completion_default(0); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); mln_custom_geometry_source_options options = probe_options(&probe); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &command.descriptor + ) ); - TEST_ASSERT_EQUAL_size_t(0, probe.release_count); + TEST_ASSERT_TRUE(mln_test_completion_wait(&command, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&command)); + mln_test_completion_destroy(&command); + TEST_ASSERT_EQUAL_size_t(0, atomic_load(&probe.release_count)); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); TEST_ASSERT_EQUAL_size_t( 0, mln_test_drain_counting(runtime, MLN_RUNTIME_EVENT_MAP_STYLE_LOADED) ); // The map no longer references the state, so retiring it releases nothing. mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -96,64 +106,98 @@ static void an_explicit_removal_releases_once(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); release_probe probe = {0}; + mln_test_completion add = mln_test_completion_default(0); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); mln_custom_geometry_source_options options = probe_options(&probe); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &add.descriptor + ) ); + TEST_ASSERT_TRUE(mln_test_completion_wait(&add, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&add)); + mln_test_completion_destroy(&add); - bool removed = false; + mln_test_completion removal = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_remove_style_source(map, MLN_BUFFER_LITERAL(source_id), &removed) + MLN_STATUS_OK, mln_map_remove_style_source( + map, MLN_BUFFER_LITERAL(source_id), &removal.descriptor + ) + ); + TEST_ASSERT_TRUE(mln_test_completion_wait(&removal, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&removal)); + TEST_ASSERT_EQUAL_UINT32( + MLN_COMMAND_DISPOSITION_COMMITTED, mln_test_completion_disposition(&removal) ); - TEST_ASSERT_TRUE(removed); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + mln_test_completion_destroy(&removal); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); // A style load after the removal has nothing left to reconcile. - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } -// A rejected add never referenced user_data, so releasing it would free state -// the host still owns. Retiring the map then releases the accepted source's -// state exactly once. -static void a_rejected_add_releases_nothing(void) { +// Synchronous rejection never references user_data. An asynchronously rejected +// accepted command does reference it and therefore releases it. Retiring the +// map then releases the successful source's state exactly once. +static void accepted_adds_release_their_callback_state(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); release_probe probe = {0}; + mln_test_completion rejected = mln_test_completion_default(0); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); mln_custom_geometry_source_options options = probe_options(&probe); options.fields = MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM; options.min_zoom = -1; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &rejected.descriptor + ) ); + rejected.descriptor.release_user_data(rejected.descriptor.user_data); + mln_test_completion_destroy(&rejected); options = probe_options(&probe); + mln_test_completion first = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &first.descriptor + ) ); - // The duplicate ID is rejected, and the accepted source keeps its own state. + TEST_ASSERT_TRUE(mln_test_completion_wait(&first, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&first)); + mln_test_completion_destroy(&first); + // The duplicate command is accepted, then fails application because the ID + // already exists. Its callback state is released independently. + mln_test_completion duplicate = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &duplicate.descriptor + ) ); - TEST_ASSERT_EQUAL_size_t(0, probe.release_count); + TEST_ASSERT_TRUE(mln_test_completion_wait(&duplicate, -1)); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, mln_test_completion_status(&duplicate)); + mln_test_completion_destroy(&duplicate); + // The duplicate's release is not ordered before a single barrier, so poll + // barriers until the count lands. + for (size_t attempt = 0; + attempt < style_wait_attempts && atomic_load(&probe.release_count) < 1; + attempt += 1) { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + } + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_EQUAL_size_t(2, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -161,5 +205,5 @@ void run_custom_geometry_source_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(a_style_replacement_releases_a_dropped_source_unsubscribed); RUN_TEST(an_explicit_removal_releases_once); - RUN_TEST(a_rejected_add_releases_nothing); + RUN_TEST(accepted_adds_release_their_callback_state); } diff --git a/src/c_api/tests/custom_mvt_vector_source_abi.c b/src/c_api/tests/custom_mvt_vector_source_abi.c index aba086a64..f0bec3f1d 100644 --- a/src/c_api/tests/custom_mvt_vector_source_abi.c +++ b/src/c_api/tests/custom_mvt_vector_source_abi.c @@ -5,6 +5,7 @@ // the release runs independently of the event mask. A binding observes its own // release only through that same mechanism. +#include #include #include #include @@ -20,20 +21,22 @@ static const char empty_style_json[] = "{\"version\":8,\"sources\":{},\"layers\":[]}"; static const char source_id[] = "custom-mvt-vector"; static const char geometry_source_id[] = "custom-geometry"; -static const size_t style_pump_attempts = 200; +static const size_t style_wait_attempts = 200; +// The counters are written from native threads and read from the test thread, +// so they are atomic; the barrier polls below provide the ordering. typedef struct release_probe { - size_t release_count; - size_t fetch_count; + atomic_size_t release_count; + atomic_size_t fetch_count; } release_probe; static void probe_fetch_tile(void* user_data, mln_canonical_tile_id tile_id) { (void)tile_id; - ((release_probe*)user_data)->fetch_count += 1; + atomic_fetch_add(&((release_probe*)user_data)->fetch_count, 1); } static void probe_release(void* user_data) { - ((release_probe*)user_data)->release_count += 1; + atomic_fetch_add(&((release_probe*)user_data)->release_count, 1); } static mln_custom_mvt_vector_source_options probe_options( @@ -58,21 +61,23 @@ static mln_custom_geometry_source_options geometry_probe_options( return options; } -// Loads an inline style and pumps, so the map reaches a loaded style without +// Loads an inline style and waits for the map to reach a loaded state without // reaching the network. -static void load_style_and_pump( +static void load_style_and_wait( mln_runtime runtime, mln_map map, mln_buffer_view style_json ) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_style_json(map, style_json)); - for (size_t attempt = 0; attempt < style_pump_attempts; attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 2, -1)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_map_set_style_json(map, style_json) + ); + for (size_t attempt = 0; attempt < style_wait_attempts; attempt += 1) { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); } } static mln_map create_map_without_style_events(mln_runtime runtime) { mln_map_options options = mln_map_options_default(); - options.width = 256; - options.height = 256; + options.initial_extent.width = 256; + options.initial_extent.height = 256; options.event_mask = MLN_RUNTIME_EVENT_MASK_ALL & ~(uint64_t)MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED; return mln_test_create_map_with_options(runtime, &options); @@ -80,11 +85,24 @@ static mln_map create_map_without_style_events(mln_runtime runtime) { static void add_mvt_source(mln_map map, release_probe* probe) { mln_custom_mvt_vector_source_options options = probe_options(probe); + mln_test_completion add = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_custom_mvt_vector_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_mvt_vector_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &add.descriptor + ) ); + TEST_ASSERT_TRUE(mln_test_completion_wait(&add, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&add)); + mln_test_completion_destroy(&add); +} + +// Waits out one submitted command and returns its terminal status. +static mln_status finish_command(mln_test_completion* completion) { + TEST_ASSERT_TRUE(mln_test_completion_wait(completion, -1)); + const mln_status status = mln_test_completion_status(completion); + mln_test_completion_destroy(completion); + return status; } // A host that never subscribes to style-loaded events still learns that a style @@ -94,18 +112,19 @@ static void a_style_replacement_releases_a_dropped_source_unsubscribed(void) { mln_map map = create_map_without_style_events(runtime); release_probe probe = {0}; - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); add_mvt_source(map, &probe); - TEST_ASSERT_EQUAL_size_t(0, probe.release_count); + TEST_ASSERT_EQUAL_size_t(0, atomic_load(&probe.release_count)); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); TEST_ASSERT_EQUAL_size_t( 0, mln_test_drain_counting(runtime, MLN_RUNTIME_EVENT_MAP_STYLE_LOADED) ); + // The map no longer references the state, so retiring it releases nothing. mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -114,64 +133,92 @@ static void an_explicit_removal_releases_once(void) { mln_map map = mln_test_create_map(runtime); release_probe probe = {0}; - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); add_mvt_source(map, &probe); - bool removed = false; + mln_test_completion removal = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_remove_style_source(map, MLN_BUFFER_LITERAL(source_id), &removed) + MLN_STATUS_OK, mln_map_remove_style_source( + map, MLN_BUFFER_LITERAL(source_id), &removal.descriptor + ) ); - TEST_ASSERT_TRUE(removed); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&removal)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); + // A style load after the removal has nothing left to reconcile. + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(empty_style_json)); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } -// A rejected add never referenced user_data, so releasing it would free state -// the host still owns. Retiring the map then releases the accepted source's -// state exactly once. -static void a_rejected_add_releases_nothing(void) { +// Synchronous rejection never references user_data. An asynchronously rejected +// accepted command does reference it and therefore releases it. Retiring the +// map then releases the successful source's state exactly once. +static void accepted_adds_release_their_callback_state(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); release_probe probe = {0}; - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); mln_custom_mvt_vector_source_options options = probe_options(&probe); options.fields = MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM; options.min_zoom = -1; + mln_test_completion rejected = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_add_custom_mvt_vector_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_add_custom_mvt_vector_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &rejected.descriptor + ) ); + rejected.descriptor.release_user_data(rejected.descriptor.user_data); + mln_test_completion_destroy(&rejected); options = probe_options(&probe); options.fetch_tile = NULL; + mln_test_completion missing_fetch = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_add_custom_mvt_vector_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_add_custom_mvt_vector_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &missing_fetch.descriptor + ) ); + missing_fetch.descriptor.release_user_data( + missing_fetch.descriptor.user_data + ); + mln_test_completion_destroy(&missing_fetch); options = probe_options(&probe); + mln_test_completion first = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_custom_mvt_vector_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_mvt_vector_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &first.descriptor + ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&first)); + // The duplicate command is accepted, then fails application because the ID + // already exists. Its callback state is released independently. + mln_test_completion duplicate = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_add_custom_mvt_vector_source( - map, MLN_BUFFER_LITERAL(source_id), &options - ) + MLN_STATUS_OK, + mln_map_add_custom_mvt_vector_source( + map, MLN_BUFFER_LITERAL(source_id), &options, &duplicate.descriptor + ) ); - TEST_ASSERT_EQUAL_size_t(0, probe.release_count); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, finish_command(&duplicate)); + // The duplicate's release is not ordered before a single barrier, so poll + // barriers until the count lands. + for (size_t attempt = 0; + attempt < style_wait_attempts && atomic_load(&probe.release_count) < 1; + attempt += 1) { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + } + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_EQUAL_size_t(2, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -180,42 +227,57 @@ static void tile_delivery_and_invalidate_accept_an_empty_tile(void) { mln_map map = mln_test_create_map(runtime); release_probe probe = {0}; - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); add_mvt_source(map, &probe); - uint32_t source_type = 0; - bool found = false; + mln_test_completion info = + mln_test_completion_default(sizeof(mln_style_source_result)); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_type( - map, MLN_BUFFER_LITERAL(source_id), &source_type, &found + MLN_STATUS_OK, mln_map_get_style_source_info( + map, MLN_BUFFER_LITERAL(source_id), &info.descriptor ) ); - TEST_ASSERT_TRUE(found); + TEST_ASSERT_TRUE(mln_test_completion_wait(&info, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&info)); + mln_style_source_result source_result = {0}; + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&info, &source_result, sizeof(source_result)) + ); + mln_test_completion_destroy(&info); TEST_ASSERT_EQUAL_UINT32( - MLN_STYLE_SOURCE_TYPE_CUSTOM_MVT_VECTOR, source_type + MLN_STYLE_SOURCE_TYPE_CUSTOM_MVT_VECTOR, source_result.info.type ); const mln_canonical_tile_id tile_id = {.z = 0, .x = 0, .y = 0}; const mln_buffer_view empty = {.data = NULL, .size = 0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_custom_mvt_vector_source_tile_data( - map, MLN_BUFFER_LITERAL(source_id), tile_id, empty - ) - ); + mln_test_completion data = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_custom_mvt_vector_source_tile_error( - map, MLN_BUFFER_LITERAL(source_id), tile_id, MLN_BUFFER_LITERAL("missing") + mln_map_set_custom_mvt_vector_source_tile_data( + map, MLN_BUFFER_LITERAL(source_id), tile_id, empty, &data.descriptor ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&data)); + mln_test_completion error = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_invalidate_custom_mvt_vector_source_tile( - map, MLN_BUFFER_LITERAL(source_id), tile_id + MLN_STATUS_OK, mln_map_set_custom_mvt_vector_source_tile_error( + map, MLN_BUFFER_LITERAL(source_id), tile_id, + MLN_BUFFER_LITERAL("missing"), &error.descriptor ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&error)); + mln_test_completion invalidate = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_invalidate_custom_mvt_vector_source_tile( + map, MLN_BUFFER_LITERAL(source_id), tile_id, &invalidate.descriptor + ) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&invalidate)); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, probe.release_count); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -225,54 +287,73 @@ static void tile_operations_reject_the_other_custom_source_kind(void) { release_probe mvt_probe = {0}; release_probe geometry_probe = {0}; - load_style_and_pump(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); + load_style_and_wait(runtime, map, MLN_BUFFER_LITERAL(background_style_json)); add_mvt_source(map, &mvt_probe); mln_custom_geometry_source_options geometry_options = geometry_probe_options(&geometry_probe); + mln_test_completion add = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_add_custom_geometry_source( - map, MLN_BUFFER_LITERAL(geometry_source_id), &geometry_options - ) + MLN_STATUS_OK, mln_map_add_custom_geometry_source( + map, MLN_BUFFER_LITERAL(geometry_source_id), + &geometry_options, &add.descriptor + ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, finish_command(&add)); + // A tile operation against the other custom source kind is accepted, then + // fails application on the worker. const mln_canonical_tile_id tile_id = {.z = 0, .x = 0, .y = 0}; const mln_buffer_view empty = {.data = NULL, .size = 0}; + mln_test_completion wrong_data = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_custom_mvt_vector_source_tile_data( - map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id, empty - ) + MLN_STATUS_OK, mln_map_set_custom_mvt_vector_source_tile_data( + map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id, + empty, &wrong_data.descriptor + ) ); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, finish_command(&wrong_data)); + mln_test_completion wrong_error = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_custom_mvt_vector_source_tile_error( - map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id, - MLN_BUFFER_LITERAL("missing") - ) + MLN_STATUS_OK, mln_map_set_custom_mvt_vector_source_tile_error( + map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id, + MLN_BUFFER_LITERAL("missing"), &wrong_error.descriptor + ) ); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, finish_command(&wrong_error)); + mln_test_completion wrong_invalidate = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_invalidate_custom_mvt_vector_source_tile( - map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id - ) + MLN_STATUS_OK, mln_map_invalidate_custom_mvt_vector_source_tile( + map, MLN_BUFFER_LITERAL(geometry_source_id), tile_id, + &wrong_invalidate.descriptor + ) ); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, finish_command(&wrong_invalidate)); + mln_test_completion wrong_geometry = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, + MLN_STATUS_OK, mln_map_set_custom_geometry_source_tile_data( map, MLN_BUFFER_LITERAL(source_id), tile_id, - MLN_BUFFER_LITERAL("{\"type\":\"FeatureCollection\",\"features\":[]}") + MLN_BUFFER_LITERAL("{\"type\":\"FeatureCollection\",\"features\":[]}"), + &wrong_geometry.descriptor ) ); + TEST_ASSERT_NOT_EQUAL(MLN_STATUS_OK, finish_command(&wrong_geometry)); + mln_test_completion wrong_geometry_invalidate = + mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_invalidate_custom_geometry_source_tile( - map, MLN_BUFFER_LITERAL(source_id), tile_id - ) + MLN_STATUS_OK, mln_map_invalidate_custom_geometry_source_tile( + map, MLN_BUFFER_LITERAL(source_id), tile_id, + &wrong_geometry_invalidate.descriptor + ) + ); + TEST_ASSERT_NOT_EQUAL( + MLN_STATUS_OK, finish_command(&wrong_geometry_invalidate) ); mln_test_destroy_map(map); - TEST_ASSERT_EQUAL_size_t(1, mvt_probe.release_count); - TEST_ASSERT_EQUAL_size_t(1, geometry_probe.release_count); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&mvt_probe.release_count)); + TEST_ASSERT_EQUAL_size_t(1, atomic_load(&geometry_probe.release_count)); mln_test_destroy_runtime(runtime); } @@ -280,7 +361,7 @@ void run_custom_mvt_vector_source_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(a_style_replacement_releases_a_dropped_source_unsubscribed); RUN_TEST(an_explicit_removal_releases_once); - RUN_TEST(a_rejected_add_releases_nothing); + RUN_TEST(accepted_adds_release_their_callback_state); RUN_TEST(tile_delivery_and_invalidate_accept_an_empty_tile); RUN_TEST(tile_operations_reject_the_other_custom_source_kind); } diff --git a/src/c_api/tests/geojson_tiling_abi.c b/src/c_api/tests/geojson_tiling_abi.c index d616d0ea4..e4acd3cdc 100644 --- a/src/c_api/tests/geojson_tiling_abi.c +++ b/src/c_api/tests/geojson_tiling_abi.c @@ -58,13 +58,18 @@ static char* build_feature_collection(void) { json, capacity, "{\"type\":\"FeatureCollection\",\"features\":[" ); for (unsigned int index = 0; index < TILING_POINT_COUNT; index += 1) { - const double longitude = -123.0 + (double)(index % 100) * 0.01; - const double latitude = 37.0 + (double)(index / 100) * 0.01; + // Integer hundredths of a degree: %f would honor a locale someone in the + // process set (a GL/Vulkan driver init is enough) and print a decimal + // comma, which breaks the JSON. + const int longitude_hundredths = -12300 + (int)(index % 100); + const int latitude_hundredths = 3700 + (int)(index / 100); const int written = snprintf( json + used, capacity - used, "%s{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\"," - "\"coordinates\":[%.3f,%.3f]},\"properties\":{}}", - index == 0 ? "" : ",", longitude, latitude + "\"coordinates\":[%d.%02d,%d.%02d]},\"properties\":{}}", + index == 0 ? "" : ",", longitude_hundredths / 100, + abs(longitude_hundredths) % 100, latitude_hundredths / 100, + latitude_hundredths % 100 ); if (written <= 0 || (size_t)written >= capacity - used) { free(json); @@ -99,20 +104,53 @@ static void render_until_stopped(void* argument) { // The target stays small so each render applies the replacement quickly, // dropping the previous dataset's references while its slices are in - // flight. + // flight. Each demand is serviced and drained before the next one so the + // queue never grows. probe.render_status = MLN_STATUS_OK; while (!atomic_load(&probe.stop)) { - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; - const mln_status status = mln_render_session_render_update( - fixture.session, &result, &needs_repaint - ); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + mln_status status = + mln_render_session_request_frame(fixture.session, &demand); if (status != MLN_STATUS_OK) { probe.render_status = status; break; } - if (result != MLN_RENDER_RESULT_RENDERED) { - mln_test_sleep_millisecond(); + bool settled = false; + while (!settled && status == MLN_STATUS_OK && !atomic_load(&probe.stop)) { + if (fixture.driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + size_t serviced = 0; + status = mln_render_session_service_driver_work( + fixture.session, SIZE_MAX, &serviced + ); + if (status != MLN_STATUS_OK) { + break; + } + } + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + status = mln_render_session_get_snapshot(fixture.session, &snapshot); + if (status != MLN_STATUS_OK) { + break; + } + if (snapshot.pending_demand_count != 0) { + mln_test_sleep_millisecond(); + continue; + } + mln_render_frame_batch batch = MLN_HANDLE_NULL; + const mln_status drain_status = + mln_render_session_drain_frame_results(fixture.session, &batch); + if (drain_status == MLN_STATUS_OK) { + mln_render_frame_batch_release(batch); + settled = true; + } else if (drain_status != MLN_STATUS_NOT_READY) { + status = drain_status; + } + } + if (status != MLN_STATUS_OK) { + probe.render_status = status; + break; } } @@ -130,35 +168,43 @@ static void replacing_data_during_async_tiling_survives(void) { mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(tiling_style_json)) + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL(tiling_style_json)) ); - mln_camera_options camera = mln_camera_options_default(); - camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; - camera.latitude = 37.4; - camera.longitude = -122.5; - camera.zoom = 7.0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_jump_to(map, &camera)); + mln_camera_update camera_update = mln_camera_update_default(); + camera_update.camera.fields = + MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; + camera_update.camera.latitude = 37.4; + camera_update.camera.longitude = -122.5; + camera_update.camera.zoom = 7.0; + const mln_completion discard = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_update_camera(map, &camera_update, &discard) + ); char* json = build_feature_collection(); TEST_ASSERT_NOT_NULL(json); const mln_buffer_view document = mln_test_buffer_view(json, strlen(json)); mln_geojson_source_data current = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_geojson_source_data_create(document, &options, ¤t) + const mln_status create_status = + mln_geojson_source_data_create(document, &options, ¤t); + TEST_ASSERT_EQUAL_INT_MESSAGE( + MLN_STATUS_OK, create_status, mln_thread_last_error_message() ); const mln_buffer_view source_id = MLN_BUFFER_LITERAL("points"); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_geojson_source_data(map, source_id, current) + MLN_STATUS_OK, + mln_map_add_geojson_source_data(map, source_id, current, &discard) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_add_style_layer_json( - map, MLN_BUFFER_LITERAL(tiling_layer_json), MLN_BUFFER_LITERAL("") - ) + MLN_STATUS_OK, mln_map_add_style_layer_json( + map, MLN_BUFFER_LITERAL(tiling_layer_json), + MLN_BUFFER_LITERAL(""), &discard + ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); // No assertion may run while the render thread is live: Unity failures // longjmp past the thread cleanup. From here on the test records statuses @@ -184,15 +230,15 @@ static void replacing_data_during_async_tiling_survives(void) { } } - // A non-blocking pump keeps the replacement period shorter than one + // Back-to-back replacements keep the replacement period shorter than one // slice, so each replacement lands while the slice is still in flight. for (unsigned int index = 0; loop_status == MLN_STATUS_OK && index < count && !atomic_load(&probe.finished); index += 1) { - loop_status = mln_runtime_pump(runtime, 0, -1); mln_test_drain_all(runtime); - loop_status = - mln_map_set_geojson_source_data(map, source_id, prepared[index]); + loop_status = mln_map_set_geojson_source_data( + map, source_id, prepared[index], &discard + ); if (loop_status == MLN_STATUS_OK) { mln_geojson_source_data_destroy(current); current = prepared[index]; @@ -206,12 +252,12 @@ static void replacing_data_during_async_tiling_survives(void) { free(json); atomic_store(&probe.stop, true); - // One pump_until budget is enough on a hardware backend, but a - // software-rendered CI runner can take far longer to wind the render - // thread down, so wait on a deadline instead. + // One wait budget is enough on a hardware backend, but a software-rendered + // CI runner can take far longer to wind the render thread down, so wait on + // a deadline instead. const uint64_t deadline = mln_test_monotonic_milliseconds() + 30000; bool finished = false; - while (!(finished = mln_test_pump_until(runtime, &probe.finished)) && + while (!(finished = mln_test_wait_until(runtime, &probe.finished)) && mln_test_monotonic_milliseconds() < deadline) { } // Join only a finished thread; a wedged one fails an assertion below diff --git a/src/c_api/tests/handles_abi.c b/src/c_api/tests/handles_abi.c index f3d3ed755..378f97aec 100644 --- a/src/c_api/tests/handles_abi.c +++ b/src/c_api/tests/handles_abi.c @@ -24,15 +24,15 @@ static void a_released_map_handle_never_names_a_later_map(void) { TEST_ASSERT_NOT_EQUAL_UINT64(first, second); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(first) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(first) ); TEST_ASSERT_TRUE_MESSAGE( last_error_mentions("stale"), "A released handle should report that it is stale." ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_ARGUMENT, mln_map_destroy(first)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_ARGUMENT, mln_test_map_close(first)); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_request_repaint(second)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_map_request_repaint(second)); mln_test_destroy_map(second); mln_test_destroy_runtime(runtime); @@ -40,30 +40,24 @@ static void a_released_map_handle_never_names_a_later_map(void) { static void a_handle_of_another_kind_is_rejected_by_kind(void) { mln_runtime runtime = mln_test_create_runtime(); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(source) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(runtime) ); TEST_ASSERT_TRUE_MESSAGE( - last_error_mentions("mln_wake_source"), + last_error_mentions("mln_runtime"), "A wrong-kind handle should name the kind it actually is." ); TEST_ASSERT_TRUE_MESSAGE( last_error_mentions("mln_map"), "A wrong-kind handle should name the kind that was expected." ); - - mln_wake_source_destroy(source); mln_test_destroy_runtime(runtime); } static void a_handle_this_process_never_issued_is_rejected(void) { TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(MLN_HANDLE_NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(MLN_HANDLE_NULL) ); TEST_ASSERT_TRUE_MESSAGE( last_error_mentions("null"), "The null handle should report as null." @@ -72,75 +66,14 @@ static void a_handle_this_process_never_issued_is_rejected(void) { // A well-formed map handle whose index is far past anything created. const mln_map unissued = (mln_map)0x0200000FFFFFFFFFULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(unissued) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(unissued) ); // A value whose kind byte names no handle type at all. const mln_map malformed = (mln_map)0xDEADBEEFDEADBEEFULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_request_repaint(malformed) - ); -} - -static void releasing_a_scoped_handle_twice_is_a_no_op(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - - mln_style_id_list layers = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_list_style_layer_ids(map, &layers) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, layers); - - mln_style_id_list_destroy(layers); - mln_style_id_list_destroy(layers); - mln_style_id_list_destroy(MLN_HANDLE_NULL); - - size_t count = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_style_id_list_count(layers, &count) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_request_repaint(malformed) ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void a_runtime_sourced_event_names_its_runtime_handle(void) { - mln_runtime runtime = mln_test_create_runtime(); - - mln_offline_operation_id operation = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_regions_list_start(runtime, &operation) - ); - - bool saw_completion = false; - for (int attempt = 0; attempt < 200 && !saw_completion; ++attempt) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 50, -1)); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); - for (size_t index = 0; index < batch.event_count; index += 1) { - const mln_runtime_event* event = - (const mln_runtime_event*)((const char*)batch.events + - (index * batch.event_size)); - if (event->source_type != MLN_RUNTIME_EVENT_SOURCE_RUNTIME) { - continue; - } - TEST_ASSERT_EQUAL_UINT64_MESSAGE( - runtime, event->source, - "A runtime-sourced event should carry the mln_runtime handle." - ); - if (event->type == MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED) { - saw_completion = true; - } - } - } - TEST_ASSERT_TRUE_MESSAGE( - saw_completion, "The offline list operation should report completion." - ); - - mln_test_destroy_runtime(runtime); } void run_handles_abi_tests(void) { @@ -148,6 +81,4 @@ void run_handles_abi_tests(void) { RUN_TEST(a_released_map_handle_never_names_a_later_map); RUN_TEST(a_handle_of_another_kind_is_rejected_by_kind); RUN_TEST(a_handle_this_process_never_issued_is_rejected); - RUN_TEST(releasing_a_scoped_handle_twice_is_a_no_op); - RUN_TEST(a_runtime_sourced_event_names_its_runtime_handle); } diff --git a/src/c_api/tests/main.c b/src/c_api/tests/main.c index cfee7071b..2a1da9a64 100644 --- a/src/c_api/tests/main.c +++ b/src/c_api/tests/main.c @@ -34,10 +34,12 @@ int main(void) { run_render_target_lifecycle_abi_tests(); run_render_thread_abi_tests(); run_query_abi_tests(); + run_completion_abi_tests(); + run_projection_abi_tests(); run_mlt_decode_abi_tests(); run_resources_abi_tests(); run_runtime_events_abi_tests(); - run_runtime_wake_abi_tests(); + run_runtime_lifecycle_abi_tests(); run_style_values_abi_tests(); const int failures = UNITY_END(); // main() runs on a pthread under -sPROXY_TO_PTHREAD, where a graphics device diff --git a/src/c_api/tests/map_options_abi.c b/src/c_api/tests/map_options_abi.c index c12cb901c..171983ea2 100644 --- a/src/c_api/tests/map_options_abi.c +++ b/src/c_api/tests/map_options_abi.c @@ -27,180 +27,238 @@ static void destroy_map_fixture(map_fixture fixture) { static mln_camera_options test_camera(void) { mln_camera_options camera = mln_camera_options_default(); camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM | - MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH; + MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH | + MLN_CAMERA_OPTION_PADDING | MLN_CAMERA_OPTION_ANCHOR; camera.latitude = 37.7749; camera.longitude = -122.4194; camera.zoom = 11.0; camera.bearing = 12.0; camera.pitch = 30.0; + camera.padding = (mln_edge_insets){1.0, 2.0, 3.0, 4.0}; + camera.anchor = (mln_screen_point){25.0, 30.0}; return camera; } -static void camera_rejects_invalid_arguments(void) { - map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_camera(fixture.map, NULL) - ); - mln_camera_options snapshot = mln_camera_options_default(); - snapshot.size = sizeof(mln_camera_options) - 1; +#define EXPECT_COMMAND_COMPLETES(expected_status, expression) \ + do { \ + mln_test_completion completion = mln_test_completion_default(0); \ + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, (expression)); \ + TEST_ASSERT_EQUAL_INT( \ + (expected_status), mln_test_completion_finish(&completion) \ + ); \ + mln_test_completion_destroy(&completion); \ + } while (false) + +static map_fixture create_static_map_fixture(void) { + map_fixture fixture = {.runtime = mln_test_create_runtime()}; + mln_map_options options = mln_map_options_default(); + options.initial_extent = + (mln_logical_extent){.width = 64, .height = 64, .scale_factor = 1.0}; + options.map_mode = MLN_MAP_MODE_STATIC; + fixture.map = mln_test_create_map_with_options(fixture.runtime, &options); + return fixture; +} + +static mln_test_completion start_pending_still_image(map_fixture fixture) { + mln_test_completion pending = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_camera(fixture.map, &snapshot) + MLN_STATUS_OK, mln_map_request_still_image(fixture.map, &pending.descriptor) ); + + mln_test_completion duplicate = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_jump_to(fixture.map, NULL) + MLN_STATUS_OK, + mln_map_request_still_image(fixture.map, &duplicate.descriptor) ); - mln_camera_options camera = test_camera(); - camera.size = sizeof(mln_camera_options) - 1; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_jump_to(fixture.map, &camera) + MLN_STATUS_INVALID_STATE, mln_test_completion_finish(&duplicate) ); - camera = test_camera(); - camera.fields |= UINT32_C(1) << 31; + mln_test_completion_destroy(&duplicate); + return pending; +} +static mln_map_snapshot read_settled_snapshot(map_fixture fixture) { TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_jump_to(fixture.map, &camera) + MLN_STATUS_OK, mln_test_runtime_barrier(fixture.runtime) ); + mln_map_snapshot snapshot = {.size = sizeof(mln_map_snapshot)}; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_ease_to(fixture.map, NULL, NULL) + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &snapshot) ); - camera = test_camera(); - mln_animation_options animation = mln_animation_options_default(); - animation.size = sizeof(mln_animation_options) - 1; + return snapshot; +} + +static mln_bound_options read_bounds(map_fixture fixture) { + return read_settled_snapshot(fixture).bounds; +} + +static void camera_rejects_invalid_arguments(void) { + map_fixture fixture = create_map_fixture(); + uint64_t generation = 0; + mln_camera_options camera = mln_camera_options_default(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_ease_to(fixture.map, &camera, &animation) + mln_map_camera_snapshot_get(fixture.map, NULL, &generation) ); - animation = mln_animation_options_default(); - animation.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_fly_to(fixture.map, &camera, &animation) + mln_map_camera_snapshot_get(fixture.map, &camera, NULL) ); + mln_completion rejected = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_is_gesture_in_progress(fixture.map, NULL) + mln_map_update_camera(fixture.map, NULL, &rejected) ); - bool in_progress = false; + mln_camera_update update = mln_camera_update_default(); + update.size -= 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_is_gesture_in_progress(MLN_HANDLE_NULL, &in_progress) + mln_map_update_camera(fixture.map, &update, &rejected) ); + update = mln_camera_update_default(); + update.mode = UINT32_MAX; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_gesture_in_progress(MLN_HANDLE_NULL, true) + mln_map_update_camera(fixture.map, &update, &rejected) ); destroy_map_fixture(fixture); } -typedef struct transition_event_tally { - uint32_t finished_count; - uint64_t last_transition_id; - bool did_change_followed_finish; - int32_t last_did_change_code; -} transition_event_tally; - -static transition_event_tally drain_transition_events(mln_runtime runtime) { - transition_event_tally tally = {0, 0, false, -1}; - for (;;) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); - if (batch.event_count == 0) { - return tally; - } - for (size_t index = 0; index < batch.event_count; index += 1) { - const mln_runtime_event* event = - (const mln_runtime_event*)((const char*)batch.events + - (index * batch.event_size)); - if (event->type == MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED) { - TEST_ASSERT_EQUAL_UINT32( - MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED, - event->payload_type - ); - tally.finished_count += 1; - tally.last_transition_id = - event->payload.camera_transition_finished.transition_id; - } else if (event->type == MLN_RUNTIME_EVENT_MAP_CAMERA_DID_CHANGE) { - tally.last_did_change_code = event->code; - if (tally.finished_count > 0) { - tally.did_change_followed_finish = true; - } - } - } - } -} - -static mln_animation_options transition_animation( - uint64_t transition_id, double duration_ms -) { - mln_animation_options animation = mln_animation_options_default(); - animation.fields = - MLN_ANIMATION_OPTION_TRANSITION_ID | MLN_ANIMATION_OPTION_DURATION; - animation.transition_id = transition_id; - animation.duration_ms = duration_ms; - return animation; -} - -static void camera_transition_id_reports_every_terminal_outcome(void) { +static void camera_snapshot_command_copy_and_disposition_are_ordered(void) { map_fixture fixture = create_map_fixture(); - mln_camera_options camera = test_camera(); + mln_test_drain_all(fixture.runtime); + + mln_map_snapshot before = {.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &before) + ); - // A zero-duration ease resolves inside the call, so the event is queued - // before mln_map_ease_to() returns and lands ahead of the did-change event. - mln_animation_options animation = transition_animation(7, 0.0); + mln_camera_update update = mln_camera_update_default(); + update.camera = test_camera(); + update.gesture_phase = MLN_GESTURE_PHASE_BEGIN; + update.animation.fields |= MLN_ANIMATION_OPTION_TRANSITION_ID; + update.animation.transition_id = UINT64_C(77); + mln_test_completion command = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(fixture.map, &camera, &animation) + MLN_STATUS_OK, + mln_map_update_camera(fixture.map, &update, &command.descriptor) ); - transition_event_tally tally = drain_transition_events(fixture.runtime); - TEST_ASSERT_EQUAL_UINT32(1, tally.finished_count); - TEST_ASSERT_EQUAL_UINT64(7, tally.last_transition_id); - TEST_ASSERT_TRUE(tally.did_change_followed_finish); - TEST_ASSERT_EQUAL_INT32( - MLN_CAMERA_CHANGE_MODE_IMMEDIATE, tally.last_did_change_code + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&command)); + TEST_ASSERT_EQUAL_UINT32( + MLN_COMMAND_DISPOSITION_COMMITTED, mln_test_completion_disposition(&command) ); + const uint64_t command_generation = mln_test_completion_generation(&command); + TEST_ASSERT_GREATER_THAN_UINT64(before.generation, command_generation); + mln_test_completion_destroy(&command); - // A superseded transition reports its end, and so does the superseding one - // when it is cancelled. - camera.zoom = 12.0; - animation = transition_animation(11, 5000.0); + update.camera.longitude = 12.0; + update.camera.zoom = 1.0; + update.camera.padding.left = 999.0; + + mln_test_completion query = + mln_test_completion_default(sizeof(mln_camera_query_result)); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(fixture.map, &camera, &animation) + MLN_STATUS_OK, mln_map_camera_query(fixture.map, &query.descriptor) + ); + mln_camera_query_result result = {.size = sizeof(mln_camera_query_result)}; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&query)); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&query, &result, sizeof(result)) ); - tally = drain_transition_events(fixture.runtime); - TEST_ASSERT_EQUAL_UINT32(0, tally.finished_count); + mln_test_completion_destroy(&query); + TEST_ASSERT_EQUAL_DOUBLE(-122.4194, result.camera.longitude); + TEST_ASSERT_EQUAL_DOUBLE(11.0, result.camera.zoom); + TEST_ASSERT_EQUAL_DOUBLE(2.0, result.camera.padding.left); + TEST_ASSERT_GREATER_THAN_UINT64(before.generation, result.generation); - camera.zoom = 13.0; - animation = transition_animation(12, 5000.0); + mln_map_snapshot after = {.size = sizeof(mln_map_snapshot)}; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(fixture.map, &camera, &animation) + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &after) ); - tally = drain_transition_events(fixture.runtime); - TEST_ASSERT_EQUAL_UINT32(1, tally.finished_count); - TEST_ASSERT_EQUAL_UINT64(11, tally.last_transition_id); - TEST_ASSERT_EQUAL_INT32( - MLN_CAMERA_CHANGE_MODE_ANIMATED, tally.last_did_change_code + TEST_ASSERT_GREATER_THAN_UINT64(before.generation, after.generation); + TEST_ASSERT_EQUAL_DOUBLE(-122.4194, after.camera.longitude); + destroy_map_fixture(fixture); +} + +static void relative_camera_commands_compose_in_runtime_order(void) { + map_fixture fixture = create_map_fixture(); + mln_camera_update update = mln_camera_update_default(); + update.camera = test_camera(); + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_update_camera(fixture.map, &update, &completion.descriptor) + ); + + const mln_screen_point anchor = {.x = 25.0, .y = 30.0}; + mln_camera_delta delta = mln_camera_delta_default(); + delta.offset = (mln_screen_point){.x = 5.0, .y = -3.0}; + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_apply_camera_delta(fixture.map, &delta, &completion.descriptor) + ); + delta.kind = MLN_CAMERA_DELTA_SCALE; + delta.amount = 2.0; + delta.has_anchor = true; + delta.anchor = anchor; + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_apply_camera_delta(fixture.map, &delta, &completion.descriptor) + ); + delta.kind = MLN_CAMERA_DELTA_BEARING; + delta.amount = 15.0; + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_apply_camera_delta(fixture.map, &delta, &completion.descriptor) + ); + delta.kind = MLN_CAMERA_DELTA_PITCH; + delta.amount = 5.0; + delta.has_anchor = false; + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_apply_camera_delta(fixture.map, &delta, &completion.descriptor) + ); + + mln_test_completion query = + mln_test_completion_default(sizeof(mln_camera_query_result)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_camera_query(fixture.map, &query.descriptor) + ); + mln_camera_query_result result = {.size = sizeof(mln_camera_query_result)}; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&query)); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&query, &result, sizeof(result)) ); + mln_test_completion_destroy(&query); + TEST_ASSERT_EQUAL_DOUBLE(12.0, result.camera.zoom); + TEST_ASSERT_EQUAL_DOUBLE(27.0, result.camera.bearing); + TEST_ASSERT_EQUAL_DOUBLE(35.0, result.camera.pitch); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_cancel_transitions(fixture.map)); - tally = drain_transition_events(fixture.runtime); - TEST_ASSERT_EQUAL_UINT32(1, tally.finished_count); - TEST_ASSERT_EQUAL_UINT64(12, tally.last_transition_id); + delta.kind = MLN_CAMERA_DELTA_SCALE; + delta.amount = 0.0; + mln_completion rejected = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_apply_camera_delta(fixture.map, &delta, &rejected) + ); - // Omitting the field leaves the transition silent. - animation = mln_animation_options_default(); - camera.zoom = 14.0; + delta = mln_camera_delta_default(); + delta.kind = UINT32_MAX; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(fixture.map, &camera, &animation) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_apply_camera_delta(fixture.map, &delta, &rejected) ); - tally = drain_transition_events(fixture.runtime); - TEST_ASSERT_EQUAL_UINT32(0, tally.finished_count); + delta = mln_camera_delta_default(); + delta.has_anchor = true; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_apply_camera_delta(fixture.map, &delta, &rejected) + ); destroy_map_fixture(fixture); } static void camera_fitting_rejects_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - mln_camera_options camera = mln_camera_options_default(); + mln_completion operation = mln_test_discard_completion(); mln_camera_fit_options fit = mln_camera_fit_options_default(); fit.size = sizeof(mln_camera_fit_options) - 1; const mln_lat_lng_bounds bounds = { @@ -209,12 +267,12 @@ static void camera_fitting_rejects_invalid_arguments(void) { }; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_camera_for_lat_lng_bounds(fixture.map, bounds, &fit, &camera) + mln_map_camera_for_lat_lng_bounds(fixture.map, bounds, &fit, &operation) ); const mln_lat_lng coordinate = {.latitude = 0.0, .longitude = 0.0}; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_camera_for_lat_lngs(fixture.map, NULL, 1, NULL, &camera) + mln_map_camera_for_lat_lngs(fixture.map, NULL, 1, NULL, &operation) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, @@ -223,41 +281,39 @@ static void camera_fitting_rejects_invalid_arguments(void) { TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_map_camera_for_geometry( - fixture.map, (mln_buffer_view){0}, NULL, &camera + fixture.map, (mln_buffer_view){0}, NULL, &operation ) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_lat_lng_bounds_for_camera(fixture.map, NULL, NULL) + mln_map_lat_lng_bounds_for_camera(fixture.map, NULL, &operation) ); destroy_map_fixture(fixture); } static void camera_bounds_constraints_reject_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_bounds(fixture.map, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_bounds(fixture.map, NULL) - ); mln_bound_options options = mln_bound_options_default(); options.size = sizeof(mln_bound_options) - 1; + mln_completion command = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_bounds(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, mln_map_set_bounds(fixture.map, NULL, &command) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_bounds(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_bounds(fixture.map, &options, &command) ); options = mln_bound_options_default(); options.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_bounds(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_bounds(fixture.map, &options, &command) ); options = mln_bound_options_default(); options.fields = MLN_BOUND_OPTION_BOUNDS | MLN_BOUND_OPTION_UNBOUNDED; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_bounds(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_bounds(fixture.map, &options, &command) ); destroy_map_fixture(fixture); } @@ -268,15 +324,26 @@ static bool near_longitude(double actual, double expected) { } static double jumped_longitude(mln_map map, double longitude) { - mln_camera_options camera = mln_camera_options_default(); - camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; - camera.latitude = 0.0; - camera.longitude = longitude; - camera.zoom = 2.0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_jump_to(map, &camera)); - mln_camera_options snapshot = mln_camera_options_default(); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_get_camera(map, &snapshot)); - return snapshot.longitude; + mln_camera_update update = mln_camera_update_default(); + update.camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; + update.camera.latitude = 0.0; + update.camera.longitude = longitude; + update.camera.zoom = 2.0; + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, mln_map_update_camera(map, &update, &completion.descriptor) + ); + mln_test_completion query = + mln_test_completion_default(sizeof(mln_camera_query_result)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_camera_query(map, &query.descriptor) + ); + mln_camera_query_result result = {.size = sizeof(mln_camera_query_result)}; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&query)); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&query, &result, sizeof(result)) + ); + mln_test_completion_destroy(&query); + return result.camera.longitude; } // The unbounded state is distinct from world bounds, which the @@ -284,13 +351,9 @@ static double jumped_longitude(mln_map map, double longitude) { static void camera_bounds_distinguish_unbounded_from_world(void) { map_fixture fixture = create_map_fixture(); - mln_bound_options snapshot = mln_bound_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_bounds(fixture.map, &snapshot) - ); + mln_bound_options snapshot = read_bounds(fixture); TEST_ASSERT_TRUE(snapshot.fields & MLN_BOUND_OPTION_UNBOUNDED); TEST_ASSERT_FALSE(snapshot.fields & MLN_BOUND_OPTION_BOUNDS); - // An unbounded map wraps across the antimeridian. TEST_ASSERT_TRUE( near_longitude(jumped_longitude(fixture.map, 200.0), -160.0) ); @@ -301,31 +364,27 @@ static void camera_bounds_distinguish_unbounded_from_world(void) { world.bounds.southwest.longitude = -180.0; world.bounds.northeast.latitude = 90.0; world.bounds.northeast.longitude = 180.0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_bounds(fixture.map, &world)); - - snapshot = mln_bound_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_bounds(fixture.map, &snapshot) + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_set_bounds(fixture.map, &world, &completion.descriptor) ); + + snapshot = read_bounds(fixture); TEST_ASSERT_TRUE(snapshot.fields & MLN_BOUND_OPTION_BOUNDS); TEST_ASSERT_FALSE(snapshot.fields & MLN_BOUND_OPTION_UNBOUNDED); TEST_ASSERT_TRUE(near_longitude(snapshot.bounds.northeast.longitude, 180.0)); - // World bounds clamp at the antimeridian instead of wrapping. TEST_ASSERT_TRUE(near_longitude(jumped_longitude(fixture.map, 200.0), 180.0)); mln_bound_options unbounded = mln_bound_options_default(); unbounded.fields = MLN_BOUND_OPTION_UNBOUNDED; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_bounds(fixture.map, &unbounded) + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_set_bounds(fixture.map, &unbounded, &completion.descriptor) ); - snapshot = mln_bound_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_bounds(fixture.map, &snapshot) - ); + snapshot = read_bounds(fixture); TEST_ASSERT_TRUE(snapshot.fields & MLN_BOUND_OPTION_UNBOUNDED); TEST_ASSERT_FALSE(snapshot.fields & MLN_BOUND_OPTION_BOUNDS); - // Releasing the constraint restores antimeridian wrapping. TEST_ASSERT_TRUE( near_longitude(jumped_longitude(fixture.map, 200.0), -160.0) ); @@ -335,90 +394,53 @@ static void camera_bounds_distinguish_unbounded_from_world(void) { static void free_camera_options_reject_raw_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_free_camera_options(fixture.map, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_free_camera_options(fixture.map, NULL) - ); mln_free_camera_options options = mln_free_camera_options_default(); options.size = sizeof(mln_free_camera_options) - 1; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_free_camera_options(fixture.map, &options) + mln_map_set_free_camera_options(fixture.map, NULL, &completion) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_free_camera_options(fixture.map, &options) + mln_map_set_free_camera_options(fixture.map, &options, &completion) ); options = mln_free_camera_options_default(); options.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_free_camera_options(fixture.map, &options) + mln_map_set_free_camera_options(fixture.map, &options, &completion) ); destroy_map_fixture(fixture); } static void map_projection_mode_rejects_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_projection_mode(fixture.map, NULL) - ); - mln_projection_mode snapshot = mln_projection_mode_default(); - snapshot.size = sizeof(mln_projection_mode) - 1; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_projection_mode(fixture.map, &snapshot) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_projection_mode(fixture.map, NULL) + mln_map_set_projection_mode(fixture.map, NULL, &completion) ); mln_projection_mode mode = mln_projection_mode_default(); mode.size = sizeof(mln_projection_mode) - 1; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_projection_mode(fixture.map, &mode) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_projection_mode(fixture.map, &mode, &completion) ); mode = mln_projection_mode_default(); mode.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_projection_mode(fixture.map, &mode) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_projection_mode(fixture.map, &mode, &completion) ); destroy_map_fixture(fixture); } static const mln_lat_lng center = {.latitude = 37.7749, .longitude = -122.4194}; -typedef struct projection_thread_probe { - mln_map_projection projection; - mln_status camera_status; - mln_status set_camera_status; - mln_status conversion_status; - mln_status destroy_status; -} projection_thread_probe; - -static void use_projection_from_another_thread(void* argument) { - projection_thread_probe* probe = argument; - mln_camera_options camera = mln_camera_options_default(); - probe->camera_status = - mln_map_projection_get_camera(probe->projection, &camera); - camera.fields = MLN_CAMERA_OPTION_ZOOM; - camera.zoom = 3.0; - probe->set_camera_status = - mln_map_projection_set_camera(probe->projection, &camera); - mln_screen_point point = {0}; - probe->conversion_status = - mln_map_projection_pixel_for_lat_lng(probe->projection, center, &point); - probe->destroy_status = mln_map_projection_destroy(probe->projection); - mln_test_release_thread_gpu_resources(); -} - static void map_coordinate_conversion_rejects_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - mln_screen_point point = {0}; - mln_lat_lng coordinate = {0}; + mln_completion operation = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_map_pixel_for_lat_lng(fixture.map, center, NULL) @@ -431,90 +453,15 @@ static void map_coordinate_conversion_rejects_invalid_arguments(void) { ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_pixels_for_lat_lngs(fixture.map, NULL, 1, &point) + mln_map_pixels_for_lat_lngs(fixture.map, NULL, 1, &operation) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_lat_lngs_for_pixels(fixture.map, NULL, 1, &coordinate) + mln_map_lat_lngs_for_pixels(fixture.map, NULL, 1, &operation) ); destroy_map_fixture(fixture); } -static void standalone_projection_rejects_invalid_arguments(void) { - map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_create(fixture.map, NULL) - ); - mln_map_projection projection = 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_projection_create(fixture.map, &projection) - ); - projection = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_projection_create(fixture.map, &projection) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, projection); - mln_camera_options camera = mln_camera_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_get_camera(projection, NULL) - ); - camera.size = sizeof(mln_camera_options) - 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_projection_get_camera(projection, &camera) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_set_camera(projection, NULL) - ); - const mln_edge_insets padding = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_projection_set_visible_coordinates(projection, NULL, 1, padding) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_set_visible_geometry( - projection, (mln_buffer_view){0}, padding - ) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_projection_pixel_for_lat_lng(projection, center, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_lat_lng_for_pixel( - projection, (mln_screen_point){0}, NULL - ) - ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_projection_destroy(projection)); - destroy_map_fixture(fixture); -} - -static void standalone_projection_is_usable_from_another_thread(void) { - map_fixture fixture = create_map_fixture(); - projection_thread_probe probe = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_projection_create(fixture.map, &probe.projection) - ); - mln_test_destroy_map(fixture.map); - mln_test_destroy_runtime(fixture.runtime); - - mln_test_thread* worker = - mln_test_thread_start(use_projection_from_another_thread, &probe); - TEST_ASSERT_NOT_NULL(worker); - mln_test_thread_join(worker); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.camera_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.set_camera_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.conversion_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.destroy_status); - mln_camera_options camera = mln_camera_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_projection_get_camera(probe.projection, &camera) - ); -} - static void projected_meters_reject_invalid_arguments(void) { TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_projected_meters_for_lat_lng(center, NULL) @@ -527,43 +474,23 @@ static void projected_meters_reject_invalid_arguments(void) { static void map_debug_options_reject_raw_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - uint32_t out_options = 0; - bool out_bool = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_debug_options(MLN_HANDLE_NULL, 0) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_debug_options(fixture.map, UINT32_C(1) << 31) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_debug_options(fixture.map, NULL) - ); + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_debug_options(MLN_HANDLE_NULL, &out_options) + mln_map_set_debug_options(MLN_HANDLE_NULL, 0, &completion) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_rendering_stats_view_enabled(MLN_HANDLE_NULL, true) + mln_map_set_debug_options(fixture.map, UINT32_C(1) << 31, &completion) ); + TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_rendering_stats_view_enabled(fixture.map, NULL) + mln_map_set_rendering_stats_view_enabled(MLN_HANDLE_NULL, true, &completion) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_rendering_stats_view_enabled(MLN_HANDLE_NULL, &out_bool) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_is_fully_loaded(fixture.map, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_is_fully_loaded(MLN_HANDLE_NULL, &out_bool) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_dump_debug_logs(MLN_HANDLE_NULL) + mln_map_dump_debug_logs(MLN_HANDLE_NULL, &completion) ); destroy_map_fixture(fixture); } @@ -583,148 +510,204 @@ static void map_options_default_leaves_fast_pfor_decoding_off(void) { mln_test_destroy_runtime(runtime); } -static void map_size_tracks_attach_and_resize(void) { +static void map_extent_snapshot_tracks_resize_and_scale_factor(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map_options options = mln_map_options_default(); - options.width = 512; - options.height = 256; - options.scale_factor = 1.1; + options.initial_extent = + (mln_logical_extent){.width = 512, .height = 256, .scale_factor = 1.1}; mln_map map = mln_test_create_map_with_options(runtime, &options); - uint32_t width = 0; - uint32_t height = 0; - double scale_factor = 0.0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) - ); - TEST_ASSERT_EQUAL_UINT32(512, width); - TEST_ASSERT_EQUAL_UINT32(256, height); - TEST_ASSERT_EQUAL_DOUBLE(1.1, scale_factor); - - // The fixture attaches a 64x64 target at scale factor 1.0, and the map keeps - // its own pixel ratio. A render session queues the size for the map's owner - // thread, so the map keeps its previous size until the host pumps. - mln_test_render_fixture render = {0}; - TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &render)); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) - ); - TEST_ASSERT_EQUAL_UINT32(512, width); - TEST_ASSERT_EQUAL_UINT32(256, height); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) - ); - TEST_ASSERT_EQUAL_UINT32(64, width); - TEST_ASSERT_EQUAL_UINT32(64, height); - TEST_ASSERT_EQUAL_DOUBLE(1.1, scale_factor); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_resize(render.session, 96, 48, 1.0) - ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) + mln_map_snapshot snapshot = {.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_snapshot_get(map, &snapshot)); + TEST_ASSERT_EQUAL_UINT32(512, snapshot.logical_extent.width); + TEST_ASSERT_EQUAL_UINT32(256, snapshot.logical_extent.height); + TEST_ASSERT_EQUAL_DOUBLE(1.1, snapshot.logical_extent.scale_factor); + const uint64_t initial_generation = snapshot.generation; + + EXPECT_COMMAND_COMPLETES( + MLN_STATUS_OK, + mln_map_resize( + map, + (mln_logical_extent){.width = 96, .height = 48, .scale_factor = 2.25}, + &completion.descriptor + ) ); - TEST_ASSERT_EQUAL_UINT32(96, width); - TEST_ASSERT_EQUAL_UINT32(48, height); - TEST_ASSERT_EQUAL_DOUBLE(1.1, scale_factor); - mln_test_render_fixture_destroy(&render); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + snapshot = (mln_map_snapshot){.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_snapshot_get(map, &snapshot)); + TEST_ASSERT_GREATER_THAN_UINT64(initial_generation, snapshot.generation); + TEST_ASSERT_EQUAL_UINT32(96, snapshot.logical_extent.width); + TEST_ASSERT_EQUAL_UINT32(48, snapshot.logical_extent.height); + TEST_ASSERT_EQUAL_DOUBLE(2.25, snapshot.logical_extent.scale_factor); + mln_completion rejected = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_size(MLN_HANDLE_NULL, &width, &height, &scale_factor) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_size(map, NULL, &height, &scale_factor) + mln_map_resize( + map, (mln_logical_extent){.width = 0, .height = 48, .scale_factor = 2.25}, + &rejected + ) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_size(map, &width, NULL, &scale_factor) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_size(map, &width, &height, NULL) + mln_map_snapshot_get(MLN_HANDLE_NULL, &snapshot) ); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -static void map_viewport_options_reject_invalid_arguments(void) { - map_fixture fixture = create_map_fixture(); +static void map_close_cancels_observed_pending_still_image(void) { + map_fixture fixture = create_static_map_fixture(); + mln_test_completion pending = start_pending_still_image(fixture); + + mln_test_destroy_map(fixture.map); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_viewport_options(fixture.map, NULL) + MLN_STATUS_CANCELLED, mln_test_completion_finish(&pending) ); + mln_test_completion_destroy(&pending); + mln_test_destroy_runtime(fixture.runtime); +} + +static void map_viewport_options_reject_invalid_arguments(void) { + map_fixture fixture = create_map_fixture(); mln_map_viewport_options options = mln_map_viewport_options_default(); options.size = sizeof(mln_map_viewport_options) - 1; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_viewport_options(fixture.map, &options) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_viewport_options(fixture.map, NULL) + mln_map_set_viewport_options(fixture.map, NULL, &completion) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_viewport_options(fixture.map, &options) + mln_map_set_viewport_options(fixture.map, &options, &completion) ); options = mln_map_viewport_options_default(); options.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_viewport_options(fixture.map, &options) + mln_map_set_viewport_options(fixture.map, &options, &completion) ); options = mln_map_viewport_options_default(); options.fields = MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION; options.north_orientation = 99; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_viewport_options(fixture.map, &options) + mln_map_set_viewport_options(fixture.map, &options, &completion) ); options = mln_map_viewport_options_default(); options.fields = MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE; options.constrain_mode = 99; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_viewport_options(fixture.map, &options) + mln_map_set_viewport_options(fixture.map, &options, &completion) ); options = mln_map_viewport_options_default(); options.fields = MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE; options.viewport_mode = 99; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_viewport_options(fixture.map, &options) + mln_map_set_viewport_options(fixture.map, &options, &completion) ); destroy_map_fixture(fixture); } static void map_tile_options_reject_invalid_arguments(void) { map_fixture fixture = create_map_fixture(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_tile_options(fixture.map, NULL) - ); mln_map_tile_options options = mln_map_tile_options_default(); options.size = sizeof(mln_map_tile_options) - 1; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_tile_options(fixture.map, &options) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_tile_options(fixture.map, NULL) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_tile_options(fixture.map, NULL, &completion) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_tile_options(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_tile_options(fixture.map, &options, &completion) ); options = mln_map_tile_options_default(); options.fields = UINT32_C(1) << 31; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_tile_options(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_tile_options(fixture.map, &options, &completion) ); options = mln_map_tile_options_default(); options.fields = MLN_MAP_TILE_OPTION_LOD_MODE; options.lod_mode = 99; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_tile_options(fixture.map, &options) + MLN_STATUS_INVALID_ARGUMENT, + mln_map_set_tile_options(fixture.map, &options, &completion) + ); + destroy_map_fixture(fixture); +} + +// A committed completion carries the snapshot generation it published. +static void committed_command_generation_matches_snapshot(void) { + map_fixture fixture = create_map_fixture(); + mln_test_drain_all(fixture.runtime); + + mln_test_completion completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_set_debug_options( + fixture.map, MLN_MAP_DEBUG_TILE_BORDERS, &completion.descriptor + ) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + const uint64_t committed = mln_test_completion_generation(&completion); + mln_test_completion_destroy(&completion); + TEST_ASSERT_NOT_EQUAL_UINT64(0, committed); + + mln_map_snapshot snapshot = {.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &snapshot) + ); + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(committed, snapshot.generation); + TEST_ASSERT_EQUAL_UINT32(MLN_MAP_DEBUG_TILE_BORDERS, snapshot.debug_options); + destroy_map_fixture(fixture); +} + +// Committed tile and viewport option commands are visible in the snapshot +// published at or past the commit's generation. +static void committed_option_commands_are_visible_in_snapshot(void) { + map_fixture fixture = create_map_fixture(); + mln_test_drain_all(fixture.runtime); + + mln_map_tile_options tile = mln_map_tile_options_default(); + tile.fields = MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA; + tile.prefetch_zoom_delta = 3; + mln_test_completion completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_set_tile_options(fixture.map, &tile, &completion.descriptor) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + uint64_t committed = mln_test_completion_generation(&completion); + mln_test_completion_destroy(&completion); + mln_map_snapshot snapshot = {.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &snapshot) + ); + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(committed, snapshot.generation); + TEST_ASSERT_EQUAL_UINT32(3, snapshot.tile.prefetch_zoom_delta); + + mln_map_viewport_options viewport = mln_map_viewport_options_default(); + viewport.fields = MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION; + viewport.north_orientation = MLN_NORTH_ORIENTATION_RIGHT; + completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_set_viewport_options(fixture.map, &viewport, &completion.descriptor) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + committed = mln_test_completion_generation(&completion); + mln_test_completion_destroy(&completion); + snapshot = (mln_map_snapshot){.size = sizeof(mln_map_snapshot)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_snapshot_get(fixture.map, &snapshot) + ); + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(committed, snapshot.generation); + TEST_ASSERT_EQUAL_UINT32( + MLN_NORTH_ORIENTATION_RIGHT, snapshot.viewport.north_orientation ); destroy_map_fixture(fixture); } @@ -732,19 +715,21 @@ static void map_tile_options_reject_invalid_arguments(void) { void run_map_options_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(camera_rejects_invalid_arguments); - RUN_TEST(camera_transition_id_reports_every_terminal_outcome); + RUN_TEST(camera_snapshot_command_copy_and_disposition_are_ordered); + RUN_TEST(relative_camera_commands_compose_in_runtime_order); RUN_TEST(camera_fitting_rejects_invalid_arguments); RUN_TEST(camera_bounds_constraints_reject_invalid_arguments); RUN_TEST(camera_bounds_distinguish_unbounded_from_world); RUN_TEST(free_camera_options_reject_raw_invalid_arguments); RUN_TEST(map_projection_mode_rejects_invalid_arguments); RUN_TEST(map_coordinate_conversion_rejects_invalid_arguments); - RUN_TEST(standalone_projection_rejects_invalid_arguments); - RUN_TEST(standalone_projection_is_usable_from_another_thread); RUN_TEST(projected_meters_reject_invalid_arguments); RUN_TEST(map_debug_options_reject_raw_invalid_arguments); RUN_TEST(map_options_default_leaves_fast_pfor_decoding_off); - RUN_TEST(map_size_tracks_attach_and_resize); + RUN_TEST(map_extent_snapshot_tracks_resize_and_scale_factor); + RUN_TEST(map_close_cancels_observed_pending_still_image); RUN_TEST(map_viewport_options_reject_invalid_arguments); RUN_TEST(map_tile_options_reject_invalid_arguments); + RUN_TEST(committed_command_generation_matches_snapshot); + RUN_TEST(committed_option_commands_are_visible_in_snapshot); } diff --git a/src/c_api/tests/metal_surface_test_support.mm b/src/c_api/tests/metal_surface_test_support.mm new file mode 100644 index 000000000..66c6a028f --- /dev/null +++ b/src/c_api/tests/metal_surface_test_support.mm @@ -0,0 +1,122 @@ +#include + +#import +#import +#import + +#include "test_support.h" + +@interface MLNTestDeallocationProbe : NSObject + +@property(nonatomic, assign) std::atomic_bool* deallocated; + +@end + +@implementation MLNTestDeallocationProbe + +- (void)dealloc { + self.deallocated->store(true); +} + +@end + +namespace { + +char deallocation_probe_key; + +auto finish(mln_test_completion& completion) -> bool { + const auto status = mln_test_completion_finish(&completion); + mln_test_completion_destroy(&completion); + return status == MLN_STATUS_OK; +} + +} // namespace + +extern "C" bool mln_test_metal_surface_retarget_retains_submission( + mln_map map +) { + @autoreleasepool { + id device = MTLCreateSystemDefaultDevice(); + CAMetalLayer* initial_layer = [[CAMetalLayer alloc] init]; + if (device == nil || initial_layer == nil) return false; + + auto descriptor = mln_metal_surface_descriptor_default(); + descriptor.context.device = (__bridge void*)device; + descriptor.layer = (__bridge void*)initial_layer; + auto options = mln_render_session_attach_options_default(); + options.driver = MLN_RENDER_DRIVER_CORE_WORKER; + mln_render_session session = MLN_HANDLE_NULL; + auto attach = mln_test_completion_default(0); + if ( + mln_metal_surface_attach( + map, &descriptor, &options, &session, &attach.descriptor + ) != MLN_STATUS_OK || + !finish(attach) + ) { + return false; + } + initial_layer = nil; + + atomic_bool entered = false; + atomic_bool release = false; + auto blocker = mln_test_completion_default(0); + if ( + mln_test_render_session_blocking_operation_create( + session, &entered, &release, &blocker.descriptor + ) != MLN_STATUS_OK + ) { + return false; + } + for (unsigned int attempt = 0; attempt < 10000 && !atomic_load(&entered); + ++attempt) { + mln_test_sleep_millisecond(); + } + if (!atomic_load(&entered)) return false; + + std::atomic_bool replacement_deallocated = false; + CAMetalLayer* replacement_layer = [[CAMetalLayer alloc] init]; + __weak CAMetalLayer* weak_replacement_layer = replacement_layer; + auto* probe = [[MLNTestDeallocationProbe alloc] init]; + probe.deallocated = &replacement_deallocated; + objc_setAssociatedObject( + replacement_layer, &deallocation_probe_key, probe, + OBJC_ASSOCIATION_RETAIN_NONATOMIC + ); + probe = nil; + descriptor.layer = (__bridge void*)replacement_layer; + auto replacement = mln_test_completion_default(0); + const auto replacement_status = mln_metal_surface_set_target( + session, &descriptor, &replacement.descriptor + ); + replacement_layer = nil; + const auto retained_before_driver_execution = + !replacement_deallocated.load(); + + atomic_store(&release, true); + const auto blocker_finished = finish(blocker); + const auto replacement_finished = + replacement_status == MLN_STATUS_OK && finish(replacement); + if (replacement_status != MLN_STATUS_OK) { + mln_test_completion_reject(&replacement); + mln_test_completion_destroy(&replacement); + } + + auto detach = mln_test_completion_default(0); + const auto detach_status = + mln_render_session_detach(session, &detach.descriptor); + const auto detached = detach_status == MLN_STATUS_OK && finish(detach); + if (detach_status != MLN_STATUS_OK) { + mln_test_completion_reject(&detach); + mln_test_completion_destroy(&detach); + } + const auto destroyed = mln_render_session_destroy(session) == MLN_STATUS_OK; + if (weak_replacement_layer != nil) { + objc_setAssociatedObject( + weak_replacement_layer, &deallocation_probe_key, nil, + OBJC_ASSOCIATION_RETAIN_NONATOMIC + ); + } + return retained_before_driver_execution && blocker_finished && + replacement_finished && detached && destroyed; + } +} diff --git a/src/c_api/tests/mlt_decode_abi.c b/src/c_api/tests/mlt_decode_abi.c index cfea159ca..5079520ca 100644 --- a/src/c_api/tests/mlt_decode_abi.c +++ b/src/c_api/tests/mlt_decode_abi.c @@ -53,12 +53,41 @@ static uint32_t serve_recorded_tile( atomic_fetch_add(&state->served, 1); return MLN_RESOURCE_PROVIDER_DECISION_HANDLE; } +static bool wait_for_frame_result( + const mln_test_render_fixture* fixture, mln_render_frame_batch* out_batch +) { + // This large recorded tile can take several seconds to render on Metal. + // Bound the fixture's terminal-result wait without imposing a demand + // deadline. + for (unsigned int attempt = 0; attempt < 60000; attempt += 1) { + if (fixture->driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + size_t serviced = 0; + if ( + mln_render_session_service_driver_work( + fixture->session, SIZE_MAX, &serviced + ) != MLN_STATUS_OK + ) { + return false; + } + } + const mln_status status = + mln_render_session_drain_frame_results(fixture->session, out_batch); + if (status == MLN_STATUS_OK) { + return true; + } + if (status != MLN_STATUS_NOT_READY) { + return false; + } + mln_test_sleep_millisecond(); + } + return false; +} -// Renders and pumps until the source reports features or the attempts run out. -// Tiles arrive through a worker thread, so the count stays zero until the map -// has parsed the tile and folded it into the render tree. +// Renders until the source reports features or the attempts run out. Tiles +// arrive through a worker thread, so the count stays zero until the runtime +// worker parses the tile and folds it into the render tree. static size_t query_admin_feature_count( - mln_runtime runtime, mln_render_session session + mln_runtime runtime, const mln_test_render_fixture* fixture ) { const mln_buffer_view source_layers[] = {MLN_STRING_LITERAL("admin")}; mln_source_feature_query_options options = @@ -69,24 +98,62 @@ static size_t query_admin_feature_count( size_t count = 0; for (unsigned int attempt = 0; attempt < 600 && count == 0; attempt += 1) { - mln_render_result render_result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_render_session_render_update(session, &render_result, &needs_repaint) + MLN_STATUS_OK, mln_render_session_request_frame(fixture->session, &demand) ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - mln_queried_feature_list result = MLN_HANDLE_NULL; - const mln_status status = mln_render_session_query_source_features( - session, MLN_STRING_LITERAL("mlt-source"), &options, &result + // Frame-result readiness is the documented completion signal for a demand. + // Waiting for it directly avoids placing query or barrier work behind a + // frame whose renderer has not been created yet. + mln_render_frame_batch frame_batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_frame_result(fixture, &frame_batch)); + size_t frame_result_count = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_frame_batch_count(frame_batch, &frame_result_count) ); - if (status == MLN_STATUS_OK) { + TEST_ASSERT_EQUAL_size_t(1, frame_result_count); + mln_render_frame_result frame_result = { + .size = sizeof(mln_render_frame_result) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(frame_batch, 0, &frame_result) + ); + mln_render_frame_batch_release(frame_batch); + + if (frame_result.disposition == MLN_RENDER_RESULT_RENDERED) { + mln_acquired_frame frame = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_acquire_frame(fixture->session, &frame) + ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_queried_feature_list_count(result, &count) + MLN_STATUS_OK, mln_acquired_frame_release(&frame, NULL) ); } - mln_queried_feature_list_destroy(result); + + mln_test_completion query = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_query_source_features( + fixture->session, MLN_STRING_LITERAL("mlt-source"), + &options, &query.descriptor + ) + ); + const mln_status query_status = + mln_test_render_fixture_finish_operation(fixture, &query); + if (query_status == MLN_STATUS_INVALID_STATE) { + // A terminal NO_UPDATE result can precede creation of the renderer. + // Retry after the next map update rather than ordering query work behind + // an in-flight demand. + mln_test_completion_destroy(&query); + } else { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, query_status); + count = mln_test_completion_value_count(&query); + mln_test_completion_destroy(&query); + } if (count == 0) { mln_test_sleep_millisecond(); } @@ -118,32 +185,45 @@ static size_t decode_recorded_tile( .callback = serve_recorded_tile, .user_data = &provider_state, }; + mln_test_completion provider_completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_runtime_set_resource_provider( + runtime, &provider, &provider_completion.descriptor + ) + ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, mln_test_completion_finish(&provider_completion) ); + mln_test_completion_destroy(&provider_completion); mln_map_options map_options = mln_map_options_default(); - map_options.width = 64; - map_options.height = 64; + map_options.initial_extent.width = 64; + map_options.initial_extent.height = 64; map_options.fast_pfor_enabled = fast_pfor_enabled; mln_map map = mln_test_create_map_with_options(runtime, &map_options); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(mlt_style_json)) + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL(mlt_style_json)) ); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - const size_t count = query_admin_feature_count(runtime, fixture.session); + const size_t count = query_admin_feature_count(runtime, &fixture); const int served = atomic_load(&provider_state.served); mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); + mln_test_completion clear_completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_runtime_clear_resource_provider(runtime, &clear_completion.descriptor) + ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_clear_resource_provider(runtime) + MLN_STATUS_OK, mln_test_completion_finish(&clear_completion) ); + mln_test_completion_destroy(&clear_completion); mln_test_destroy_runtime(runtime); free(tile_bytes); diff --git a/src/c_api/tests/owned_texture_abi.c b/src/c_api/tests/owned_texture_abi.c index f9358b4a1..ca1b36e51 100644 --- a/src/c_api/tests/owned_texture_abi.c +++ b/src/c_api/tests/owned_texture_abi.c @@ -1,46 +1,40 @@ -// Raw C ABI coverage: null and stale render-session handles are hidden by -// binding-owned handle state. +// Raw C ABI coverage for acquired-frame ownership and synchronization input. #include "abi_tests.h" #include "test_support.h" #include "unity.h" -static void render_session_maintenance_rejects_null_raw_handles(void) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_render_session_reduce_memory_use(MLN_HANDLE_NULL) - ); +static void gpu_sync_default_is_cpu_complete(void) { + const mln_gpu_sync sync = mln_gpu_sync_default(); + TEST_ASSERT_EQUAL_UINT32(sizeof(mln_gpu_sync), sync.size); + TEST_ASSERT_EQUAL_UINT32(MLN_GPU_SYNC_CPU_COMPLETE, sync.kind); + TEST_ASSERT_NULL(sync.object); + TEST_ASSERT_EQUAL_UINT64(0, sync.value); +} + +static void acquired_frame_release_validates_owned_handle_input(void) { + mln_acquired_frame frame = MLN_HANDLE_NULL; + const mln_gpu_sync sync = mln_gpu_sync_default(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_render_session_clear_data(MLN_HANDLE_NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_acquired_frame_release(&frame, &sync) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_render_session_dump_debug_logs(MLN_HANDLE_NULL) + mln_acquired_frame_get_result( + MLN_HANDLE_NULL, + &(mln_render_frame_result){.size = sizeof(mln_render_frame_result)} + ) ); -} - -static void render_session_rejects_stale_raw_handles(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - mln_test_render_fixture fixture = {0}; - TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - mln_render_session stale_session = fixture.session; - mln_test_render_fixture_destroy(&fixture); - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_render_session_render_update(stale_session, &result, &needs_repaint) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_render_session_destroy(stale_session) + mln_acquired_frame_get_producer_sync( + MLN_HANDLE_NULL, &(mln_gpu_sync){.size = sizeof(mln_gpu_sync)} + ) ); - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); } void run_owned_texture_abi_tests(void) { UnitySetTestFile(__FILE__); - RUN_TEST(render_session_maintenance_rejects_null_raw_handles); - RUN_TEST(render_session_rejects_stale_raw_handles); + RUN_TEST(gpu_sync_default_is_cpu_complete); + RUN_TEST(acquired_frame_release_validates_owned_handle_input); } diff --git a/src/c_api/tests/projection_abi.c b/src/c_api/tests/projection_abi.c new file mode 100644 index 000000000..6d8f3a78d --- /dev/null +++ b/src/c_api/tests/projection_abi.c @@ -0,0 +1,268 @@ +// Raw C ABI coverage for standalone projection lifecycle, ordered creation, +// synchronous reads and setters, and any-thread handles. + +#include +#include + +#include "abi_tests.h" +#include "test_support.h" +#include "unity.h" + +static mln_map_projection create_projection(mln_map map) { + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_map_projection)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_projection_create(map, &completion.descriptor) + ); + mln_map_projection projection = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&completion, &projection, sizeof(projection)) + ); + mln_test_completion_destroy(&completion); + return projection; +} + +static mln_camera_options read_camera(mln_map_projection projection) { + mln_camera_options camera = mln_camera_options_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_projection_get_camera(projection, &camera) + ); + return camera; +} + +static void projection_outlives_its_source_map_and_runtime(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_create(map, NULL) + ); + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_map_projection)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_projection_create(map, &completion.descriptor) + ); + mln_test_destroy_map(map); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + + mln_map_projection projection = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&completion, &projection, sizeof(projection)) + ); + mln_test_completion_destroy(&completion); + + mln_test_destroy_runtime(runtime); + const mln_camera_options source_camera = read_camera(projection); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 0.0, source_camera.latitude); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_projection_close(projection)); + + // Every call with the retired handle reports an invalid argument. + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_close(projection) + ); + mln_camera_options camera = mln_camera_options_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_projection_get_camera(projection, &camera) + ); + camera.fields = MLN_CAMERA_OPTION_ZOOM; + camera.zoom = 2.0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_projection_set_camera(projection, &camera) + ); + mln_screen_point point = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_projection_pixel_for_lat_lng( + projection, (mln_lat_lng){.latitude = 0.0, .longitude = 0.0}, &point + ) + ); +} + +static void creation_observes_earlier_map_camera_commands(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_JUMP; + update.camera = mln_camera_options_default(); + update.camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; + update.camera.latitude = 12.0; + update.camera.longitude = 34.0; + update.camera.zoom = 4.0; + mln_test_completion completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_update_camera(map, &update, &completion.descriptor) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + mln_test_completion_destroy(&completion); + + // Creation is ordered after the accepted camera command, so the projection + // copies the committed transform state. + mln_map_projection projection = create_projection(map); + const mln_camera_options camera = read_camera(projection); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 12.0, camera.latitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 34.0, camera.longitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 4.0, camera.zoom); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_projection_close(projection)); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +static void setters_apply_before_return_and_conversions_round_trip(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + mln_map_projection projection = create_projection(map); + + mln_camera_options too_small = {.size = sizeof(mln_camera_options) - 1}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_map_projection_get_camera(projection, &too_small) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, mln_map_projection_get_camera(projection, NULL) + ); + + mln_camera_options camera = mln_camera_options_default(); + camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; + camera.latitude = 20.0; + camera.longitude = 40.0; + camera.zoom = 6.0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_projection_set_camera(projection, &camera) + ); + const mln_camera_options committed = read_camera(projection); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 20.0, committed.latitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 40.0, committed.longitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 6.0, committed.zoom); + + // A committed camera changes what later conversions observe: the new center + // converts back to itself through a pixel round trip. + mln_screen_point center_pixel = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_projection_pixel_for_lat_lng( + projection, (mln_lat_lng){.latitude = 20.0, .longitude = 40.0}, + ¢er_pixel + ) + ); + mln_lat_lng round_trip = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_projection_lat_lng_for_pixel(projection, center_pixel, &round_trip) + ); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 20.0, round_trip.latitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 40.0, round_trip.longitude); + + const mln_lat_lng origin = {.latitude = 0.0, .longitude = 0.0}; + mln_screen_point origin_before_fit = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_projection_pixel_for_lat_lng(projection, origin, &origin_before_fit) + ); + + const mln_lat_lng coordinates[] = { + {.latitude = 0.0, .longitude = -10.0}, + {.latitude = 0.0, .longitude = 10.0}, + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_projection_set_visible_coordinates( + projection, coordinates, 2, (mln_edge_insets){0} + ) + ); + const mln_camera_options fitted = read_camera(projection); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 0.0, fitted.latitude); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 0.0, fitted.longitude); + mln_screen_point origin_after_fit = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_projection_pixel_for_lat_lng(projection, origin, &origin_after_fit) + ); + // The committed fit moved the camera, so the same coordinate lands on a + // different pixel than it did before the fit. + TEST_ASSERT_TRUE(origin_after_fit.x != origin_before_fit.x); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_projection_close(projection)); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +typedef struct projection_thread_probe { + mln_map_projection projection; + mln_status set_camera_status; + mln_status get_camera_status; + double observed_zoom; + mln_status pixel_status; + mln_status coordinate_status; + double round_trip_latitude; + mln_status close_status; +} projection_thread_probe; + +static void projection_foreign_thread(void* argument) { + projection_thread_probe* probe = argument; + mln_camera_options camera = mln_camera_options_default(); + camera.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM; + camera.latitude = 15.0; + camera.longitude = 25.0; + camera.zoom = 3.0; + probe->set_camera_status = + mln_map_projection_set_camera(probe->projection, &camera); + + camera = mln_camera_options_default(); + probe->get_camera_status = + mln_map_projection_get_camera(probe->projection, &camera); + probe->observed_zoom = camera.zoom; + + mln_screen_point pixel = {0}; + probe->pixel_status = mln_map_projection_pixel_for_lat_lng( + probe->projection, (mln_lat_lng){.latitude = 15.0, .longitude = 25.0}, + &pixel + ); + mln_lat_lng coordinate = {0}; + probe->coordinate_status = + mln_map_projection_lat_lng_for_pixel(probe->projection, pixel, &coordinate); + probe->round_trip_latitude = coordinate.latitude; + + probe->close_status = mln_map_projection_close(probe->projection); +} + +static void projection_handles_are_callable_from_foreign_threads(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + projection_thread_probe probe = { + .projection = create_projection(map), + .set_camera_status = MLN_STATUS_INVALID_STATE, + .get_camera_status = MLN_STATUS_INVALID_STATE, + .observed_zoom = 0.0, + .pixel_status = MLN_STATUS_INVALID_STATE, + .coordinate_status = MLN_STATUS_INVALID_STATE, + .round_trip_latitude = 0.0, + .close_status = MLN_STATUS_INVALID_STATE, + }; + mln_test_thread* thread = + mln_test_thread_start(projection_foreign_thread, &probe); + mln_test_thread_join(thread); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.set_camera_status); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.get_camera_status); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 3.0, probe.observed_zoom); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.pixel_status); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.coordinate_status); + TEST_ASSERT_DOUBLE_WITHIN(1e-7, 15.0, probe.round_trip_latitude); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.close_status); + + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +void run_projection_abi_tests(void) { + UnitySetTestFile(__FILE__); + RUN_TEST(projection_outlives_its_source_map_and_runtime); + RUN_TEST(creation_observes_earlier_map_camera_commands); + RUN_TEST(setters_apply_before_return_and_conversions_round_trip); + RUN_TEST(projection_handles_are_callable_from_foreign_threads); +} diff --git a/src/c_api/tests/query_abi.c b/src/c_api/tests/query_abi.c index a7fa9fe7e..3c7152748 100644 --- a/src/c_api/tests/query_abi.c +++ b/src/c_api/tests/query_abi.c @@ -13,17 +13,16 @@ static void feature_query_validation_rejects_raw_descriptor_shapes(void) { mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - mln_queried_feature_list result = MLN_HANDLE_NULL; + mln_completion completion = mln_test_discard_completion(); mln_rendered_query_geometry geometry = mln_rendered_query_geometry_point( (mln_screen_point){.x = 256.0, .y = 256.0} ); geometry.size = sizeof(mln_rendered_query_geometry) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_render_session_query_rendered_features( - fixture.session, &geometry, NULL, &result + fixture.session, &geometry, NULL, &completion ) ); - mln_queried_feature_list_destroy(result); mln_rendered_feature_query_options options = mln_rendered_feature_query_options_default(); @@ -32,93 +31,71 @@ static void feature_query_validation_rejects_raw_descriptor_shapes(void) { (mln_screen_point){.x = 256.0, .y = 256.0} ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_render_session_query_rendered_features( - fixture.session, &geometry, &options, &result - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_render_session_query_rendered_features( + fixture.session, &geometry, &options, &completion + ) ); - mln_queried_feature_list_destroy(result); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, mln_render_session_query_source_features( - fixture.session, (mln_buffer_view){.data = NULL, .size = 1}, NULL, &result + fixture.session, (mln_buffer_view){.data = NULL, .size = 1}, NULL, + &completion ) ); - mln_queried_feature_list_destroy(result); mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -static void feature_query_hits_are_owned_by_one_list_handle(void) { +static void empty_feature_query_completes_with_an_empty_borrowed_array(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_json( + mln_test_map_set_style_json( map, MLN_BUFFER_LITERAL("{\"version\":8,\"sources\":{},\"layers\":[]}") ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - mln_render_result render_result = MLN_RENDER_RESULT_NO_UPDATE; - for (unsigned int attempt = 0; - attempt < 500 && render_result != MLN_RENDER_RESULT_RENDERED; - attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - bool needs_repaint = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_render_update( - fixture.session, &render_result, &needs_repaint - ) - ); - if (render_result != MLN_RENDER_RESULT_RENDERED) { - mln_test_sleep_millisecond(); - } - } - TEST_ASSERT_EQUAL_INT(MLN_RENDER_RESULT_RENDERED, render_result); - - const mln_rendered_query_geometry geometry = - mln_rendered_query_geometry_point((mln_screen_point){0}); - mln_queried_feature_list result = MLN_HANDLE_NULL; + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_query_rendered_features( - fixture.session, &geometry, NULL, &result - ) + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, result); - - size_t count = 1; + mln_test_completion barrier = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_queried_feature_list_count(result, &count) + MLN_STATUS_OK, + mln_render_session_barrier(fixture.session, &barrier.descriptor) ); - TEST_ASSERT_EQUAL_size_t(0, count); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_count(result, NULL) + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &barrier) ); + mln_test_completion_destroy(&barrier); + mln_render_frame_batch frame_batch = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_count(map, &count) + MLN_STATUS_OK, + mln_render_session_drain_frame_results(fixture.session, &frame_batch) ); + mln_render_frame_batch_release(frame_batch); - mln_queried_feature hit = mln_queried_feature_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_get(result, 0, &hit) - ); - hit.size = sizeof(mln_queried_feature) - 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_get(result, 0, &hit) - ); + const mln_rendered_query_geometry geometry = + mln_rendered_query_geometry_point((mln_screen_point){0}); + mln_test_completion query = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_get(result, 0, NULL) + MLN_STATUS_OK, mln_render_session_query_rendered_features( + fixture.session, &geometry, NULL, &query.descriptor + ) ); - - mln_queried_feature_list_destroy(result); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_queried_feature_list_count(result, &count) + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &query) ); - mln_queried_feature_list_destroy(result); - mln_queried_feature_list_destroy(MLN_HANDLE_NULL); + TEST_ASSERT_EQUAL_size_t(0, mln_test_completion_value_count(&query)); + mln_test_completion_destroy(&query); mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); @@ -128,5 +105,5 @@ static void feature_query_hits_are_owned_by_one_list_handle(void) { void run_query_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(feature_query_validation_rejects_raw_descriptor_shapes); - RUN_TEST(feature_query_hits_are_owned_by_one_list_handle); + RUN_TEST(empty_feature_query_completes_with_an_empty_borrowed_array); } diff --git a/src/c_api/tests/render_backend_abi.c b/src/c_api/tests/render_backend_abi.c index 02a0a3430..0482703af 100644 --- a/src/c_api/tests/render_backend_abi.c +++ b/src/c_api/tests/render_backend_abi.c @@ -9,55 +9,103 @@ static void* const fake_handle = (void*)(uintptr_t)1; +static void discard_completion_result( + void* user_data, const mln_completion_result* result +) { + (void)user_data; + (void)result; +} + +#define MLN_TEST_DISCARD_COMPLETION \ + (&(mln_completion){ \ + .size = sizeof(mln_completion), .callback = discard_completion_result \ + }) + +static void finish_render_barrier(const mln_test_render_fixture* fixture) { + mln_test_completion completion = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_barrier(fixture->session, &completion.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_render_fixture_finish_operation(fixture, &completion) + ); + mln_test_completion_destroy(&completion); +} + #define EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( \ - descriptor_type, default_descriptor, attach, clear_required, shrink \ + descriptor_type, default_descriptor, attach_start, driver_kind, \ + clear_required, shrink \ ) \ do { \ mln_render_session session = MLN_HANDLE_NULL; \ + mln_completion completion = mln_test_discard_completion(); \ + mln_render_session_attach_options options = \ + mln_render_session_attach_options_default(); \ + options.driver = driver_kind; \ descriptor_type descriptor = default_descriptor(); \ TEST_ASSERT_EQUAL_INT( \ MLN_STATUS_INVALID_ARGUMENT, \ - attach(MLN_HANDLE_NULL, &descriptor, &session) \ + attach_start( \ + MLN_HANDLE_NULL, &descriptor, &options, &session, &completion \ + ) \ ); \ TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); \ mln_runtime runtime = mln_test_create_runtime(); \ mln_map map = mln_test_create_map(runtime); \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, NULL, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, NULL, &options, &session, &completion) \ ); \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &descriptor, NULL) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &descriptor, NULL, &session, &completion) \ + ); \ + TEST_ASSERT_EQUAL_INT( \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &descriptor, &options, NULL, &completion) \ + ); \ + TEST_ASSERT_EQUAL_INT( \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &descriptor, &options, &session, NULL) \ ); \ session = 1; \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &descriptor, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &descriptor, &options, &session, &completion) \ ); \ session = MLN_HANDLE_NULL; \ descriptor_type invalid = default_descriptor(); \ invalid.size = sizeof(descriptor_type) - 1; \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &invalid, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &invalid, &options, &session, &completion) \ ); \ invalid = default_descriptor(); \ invalid.extent.size = sizeof(mln_render_target_extent) - 1; \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &invalid, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &invalid, &options, &session, &completion) \ ); \ invalid = default_descriptor(); \ invalid.extent.width = UINT32_MAX; \ invalid.extent.scale_factor = 2.0; \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &invalid, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &invalid, &options, &session, &completion) \ ); \ invalid = default_descriptor(); \ shrink(&invalid); \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &invalid, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &invalid, &options, &session, &completion) \ ); \ invalid = default_descriptor(); \ clear_required(&invalid); \ TEST_ASSERT_EQUAL_INT( \ - MLN_STATUS_INVALID_ARGUMENT, attach(map, &invalid, &session) \ + MLN_STATUS_INVALID_ARGUMENT, \ + attach_start(map, &invalid, &options, &session, &completion) \ ); \ mln_test_destroy_map(map); \ mln_test_destroy_runtime(runtime); \ @@ -94,17 +142,29 @@ static void shrink_metal_owned(mln_metal_owned_texture_descriptor* descriptor) { static void metal_surface_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_metal_surface_descriptor, metal_surface_descriptor, - mln_metal_surface_attach, clear_metal_surface, shrink_metal_surface + mln_metal_surface_attach, MLN_RENDER_DRIVER_CORE_WORKER, + clear_metal_surface, shrink_metal_surface ); } static void metal_owned_texture_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_metal_owned_texture_descriptor, metal_owned_descriptor, - mln_metal_owned_texture_attach, clear_metal_owned, shrink_metal_owned + mln_metal_owned_texture_attach, MLN_RENDER_DRIVER_CORE_WORKER, + clear_metal_owned, shrink_metal_owned ); } +#if defined(MLN_FFI_TEST_BACKEND_METAL) +static void metal_surface_retarget_retains_submission_inputs(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + TEST_ASSERT_TRUE(mln_test_metal_surface_retarget_retains_submission(map)); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} +#endif + static void metal_borrowed_texture_rejects_unsafe_raw_descriptors(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); @@ -118,12 +178,26 @@ static void metal_borrowed_texture_rejects_unsafe_raw_descriptors(void) { invalid.texture = fake_handle; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_metal_borrowed_texture_attach(map, &invalid, &session) + mln_metal_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_metal_borrowed_texture_attach(map, &descriptor, &session) + mln_metal_borrowed_texture_attach( + map, &descriptor, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -150,7 +224,8 @@ static void shrink_webgpu_owned( static void webgpu_owned_texture_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_webgpu_owned_texture_descriptor, webgpu_owned_descriptor, - mln_webgpu_owned_texture_attach, clear_webgpu_owned, shrink_webgpu_owned + mln_webgpu_owned_texture_attach, MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + clear_webgpu_owned, shrink_webgpu_owned ); } @@ -173,7 +248,8 @@ static void shrink_webgpu_surface(mln_webgpu_surface_descriptor* descriptor) { static void webgpu_surface_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_webgpu_surface_descriptor, webgpu_surface_descriptor, - mln_webgpu_surface_attach, clear_webgpu_surface, shrink_webgpu_surface + mln_webgpu_surface_attach, MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + clear_webgpu_surface, shrink_webgpu_surface ); } @@ -187,7 +263,14 @@ static void webgpu_surface_attach_rejects_an_unspecified_format(void) { mln_render_session session = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_surface_attach(map, &descriptor, &session) + mln_webgpu_surface_attach( + map, &descriptor, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -209,35 +292,70 @@ static void webgpu_borrowed_texture_rejects_unsafe_raw_descriptors(void) { invalid.extent.size = sizeof(mln_render_target_extent) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_borrowed_texture_attach(map, &invalid, &session) + mln_webgpu_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.physical_width = 0; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_borrowed_texture_attach(map, &invalid, &session) + mln_webgpu_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.context.device = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_borrowed_texture_attach(map, &invalid, &session) + mln_webgpu_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.texture_view = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_borrowed_texture_attach(map, &invalid, &session) + mln_webgpu_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.format = 0; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_webgpu_borrowed_texture_attach(map, &invalid, &session) + mln_webgpu_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -328,14 +446,16 @@ static void shrink_opengl_owned( static void opengl_owned_texture_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_opengl_owned_texture_descriptor, opengl_owned_descriptor, - mln_opengl_owned_texture_attach, clear_opengl_owned, shrink_opengl_owned + mln_opengl_owned_texture_attach, MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + clear_opengl_owned, shrink_opengl_owned ); } static void opengl_surface_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_opengl_surface_descriptor, opengl_surface_descriptor, - mln_opengl_surface_attach, clear_opengl_surface, shrink_opengl_surface + mln_opengl_surface_attach, MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + clear_opengl_surface, shrink_opengl_surface ); } @@ -350,7 +470,14 @@ static void opengl_surface_attach_rejects_a_webgl_surface_handle(void) { mln_render_session session = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_surface_attach(map, &descriptor, &session) + mln_opengl_surface_attach( + map, &descriptor, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -373,7 +500,14 @@ static void opengl_dedicated_context_rejects_shared_session_fields(void) { mln_render_session session = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_surface_attach(map, &with_share, &session) + mln_opengl_surface_attach( + map, &with_share, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); @@ -381,22 +515,32 @@ static void opengl_dedicated_context_rejects_shared_session_fields(void) { mln_test_destroy_runtime(runtime); } -// A texture session hands its texture to a host that samples it from the host's -// own context, which is what the share group the descriptor names is for. -static void opengl_owned_texture_attach_rejects_a_dedicated_context(void) { +#if defined(MLN_FFI_TEST_OPENGL_WGL) +// WGL owned texture targets borrow a window device context and cannot transfer +// their graphics state to a core worker. +static void opengl_owned_texture_attach_rejects_dedicated_wgl(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); mln_opengl_owned_texture_descriptor descriptor = opengl_owned_descriptor(); descriptor.context.ownership = MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED; + descriptor.context.data.wgl.share_context = NULL; mln_render_session session = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_owned_texture_attach(map, &descriptor, &session) + MLN_STATUS_UNSUPPORTED, + mln_opengl_owned_texture_attach( + map, &descriptor, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CORE_WORKER + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } +#endif static const char dedicated_background_style_json[] = "{\"version\":8,\"sources\":{},\"layers\":" @@ -423,26 +567,28 @@ static void dedicated_egl_surface_renders_and_keeps_its_context_current(void) { TEST_ASSERT_EQUAL_INT(MLN_TEST_FIXTURE_OK, fixture_result); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_json( + MLN_STATUS_OK, mln_test_map_set_style_json( map, MLN_BUFFER_LITERAL(dedicated_background_style_json) ) ); - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - // The session owns this thread, which is also the map's, so the map only - // reaches the style and the extent when this loop pumps the runtime. - for (unsigned int attempt = 0; - attempt < 500 && result != MLN_RENDER_RESULT_RENDERED; attempt += 1) { - mln_runtime_pump(runtime, 0, -1); - bool needs_repaint = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_render_session_render_update(fixture.session, &result, &needs_repaint) - ); - if (result != MLN_RENDER_RESULT_RENDERED) { - mln_test_sleep_millisecond(); - } - } - TEST_ASSERT_EQUAL_INT(MLN_RENDER_RESULT_RENDERED, result); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = MLN_FRAME_DEMAND_PRESENT; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + finish_render_barrier(&fixture); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_drain_frame_results(fixture.session, &batch) + ); + mln_render_frame_result result = {.size = sizeof(mln_render_frame_result)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, 0, &result) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_RESULT_RENDERED, result.disposition); + mln_render_frame_batch_release(batch); TEST_ASSERT_TRUE(mln_test_egl_context_is_current()); mln_test_dedicated_egl_surface_destroy(&fixture); @@ -452,61 +598,235 @@ static void dedicated_egl_surface_renders_and_keeps_its_context_current(void) { mln_test_destroy_runtime(runtime); } -// The repaint flag a rendered frame carries: a settled static map reports -// false, and a running camera transition reports true. -static void render_update_reports_whether_the_map_needs_another_frame(void) { +// A private EGL owned texture needs no host graphics thread: its core worker +// creates the context, renders, reads pixels, and destroys the context. +static void dedicated_egl_texture_uses_a_readback_only_core_worker(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + mln_test_render_fixture fixture = {0}; + const mln_test_fixture_result fixture_result = + mln_test_dedicated_egl_texture_create(map, &fixture); + if (fixture_result == MLN_TEST_FIXTURE_UNAVAILABLE) { + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); + TEST_IGNORE_MESSAGE("this build has no EGL context provider"); + return; + } + TEST_ASSERT_EQUAL_INT(MLN_TEST_FIXTURE_OK, fixture_result); + + mln_render_session_capabilities capabilities = { + .size = sizeof(mln_render_session_capabilities) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_get_capabilities(fixture.session, &capabilities) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_DRIVER_CORE_WORKER, capabilities.driver); + TEST_ASSERT_EQUAL_UINT32(1, capabilities.texture_ring_depth); + TEST_ASSERT_EQUAL_UINT32( + MLN_RENDER_SESSION_CAPABILITY_READBACK, capabilities.flags + ); + size_t serviced = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, + mln_render_session_service_driver_work(fixture.session, 0, &serviced) + ); + mln_acquired_frame frame = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_UNSUPPORTED, + mln_render_session_acquire_frame(fixture.session, &frame) + ); + TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, frame); + + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_map_set_style_json( + map, MLN_BUFFER_LITERAL(dedicated_background_style_json) + ) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + demand.token = 41; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + finish_render_barrier(&fixture); + + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_drain_frame_results(fixture.session, &batch) + ); + mln_render_frame_result result = {.size = sizeof(mln_render_frame_result)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, 0, &result) + ); + TEST_ASSERT_EQUAL_UINT64(demand.token, result.token); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_RESULT_RENDERED, result.disposition); + mln_render_frame_batch_release(batch); + + mln_test_completion readback = mln_test_completion_readback(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_texture_read_premultiplied_rgba8(fixture.session, &readback.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &readback) + ); + mln_texture_readback_result readback_result = {0}; + TEST_ASSERT_TRUE(mln_test_completion_copy_value( + &readback, &readback_result, sizeof(readback_result) + )); + mln_buffer_view view = readback_result.data; + mln_texture_image_info info = readback_result.info; + TEST_ASSERT_EQUAL_UINT32(64, info.width); + TEST_ASSERT_EQUAL_UINT32(64, info.height); + TEST_ASSERT_EQUAL_UINT32(64 * 4, info.stride); + TEST_ASSERT_EQUAL_size_t(64 * 64 * 4, info.byte_length); + TEST_ASSERT_EQUAL_size_t(info.byte_length, view.size); + const uint8_t* rgba = view.data; + TEST_ASSERT_EQUAL_UINT8(255, rgba[0]); + TEST_ASSERT_EQUAL_UINT8(0, rgba[1]); + TEST_ASSERT_EQUAL_UINT8(0, rgba[2]); + TEST_ASSERT_EQUAL_UINT8(255, rgba[3]); + mln_test_completion_destroy(&readback); + + mln_test_dedicated_egl_texture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +// The repaint flag a rendered frame result carries: a settled static map +// reports false, and a running camera transition reports true. +static void frame_results_report_whether_the_map_needs_another_frame(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_json( + MLN_STATUS_OK, mln_test_map_set_style_json( map, MLN_BUFFER_LITERAL(dedicated_background_style_json) ) ); - // The session owns this thread, which is also the map's, so the map only - // reaches the style and the extent when this loop pumps the runtime. Render - // until the map settles: the last frame asks for no repaint. - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = true; - for (unsigned int attempt = 0; - attempt < 500 && (result != MLN_RENDER_RESULT_RENDERED || needs_repaint); - attempt += 1) { - mln_runtime_pump(runtime, 0, -1); + // Render until the map settles: the last frame asks for no repaint. + mln_render_frame_result result = {.size = sizeof(mln_render_frame_result)}; + bool settled = false; + const uint64_t settle_deadline = mln_test_monotonic_milliseconds() + 30000; + while (!settled && mln_test_monotonic_milliseconds() < settle_deadline) { + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + finish_render_barrier(&fixture); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_drain_frame_results(fixture.session, &batch) + ); + size_t count = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_count(batch, &count) + ); + for (size_t index = 0; index < count; index += 1) { + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, index, &result) + ); + if ( + result.disposition == MLN_RENDER_RESULT_RENDERED && + !result.needs_repaint + ) { + settled = true; + } + } + mln_render_frame_batch_release(batch); + if (!settled) { + mln_test_sleep_millisecond(); + } + } + TEST_ASSERT_TRUE(settled); + + // Once settled, a render-if-needed demand reports no update instead of + // rendering the same update again. A rendered result here only means a + // fresh update slipped in, so keep demanding until one demand finds none. + bool saw_no_update = false; + const uint64_t no_update_deadline = mln_test_monotonic_milliseconds() + 30000; + while (!saw_no_update && + mln_test_monotonic_milliseconds() < no_update_deadline) { + mln_frame_demand demand = mln_frame_demand_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + finish_render_barrier(&fixture); + mln_render_frame_batch batch = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_render_session_render_update(fixture.session, &result, &needs_repaint) + mln_render_session_drain_frame_results(fixture.session, &batch) ); - if (result != MLN_RENDER_RESULT_RENDERED || needs_repaint) { + size_t count = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_count(batch, &count) + ); + for (size_t index = 0; index < count; index += 1) { + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, index, &result) + ); + if (result.disposition == MLN_RENDER_RESULT_NO_UPDATE) { + saw_no_update = true; + } + } + mln_render_frame_batch_release(batch); + if (!saw_no_update) { mln_test_sleep_millisecond(); } } - TEST_ASSERT_EQUAL_INT(MLN_RENDER_RESULT_RENDERED, result); - TEST_ASSERT_FALSE(needs_repaint); + TEST_ASSERT_TRUE(saw_no_update); - mln_camera_options camera = mln_camera_options_default(); - camera.fields = MLN_CAMERA_OPTION_ZOOM; - camera.zoom = 4.0; - mln_animation_options animation = mln_animation_options_default(); - animation.fields = MLN_ANIMATION_OPTION_DURATION; - animation.duration_ms = 60000.0; + // A long camera transition asks for another frame from every rendered one. + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_EASE; + update.camera.fields = MLN_CAMERA_OPTION_ZOOM; + update.camera.zoom = 4.0; + update.animation.fields = MLN_ANIMATION_OPTION_DURATION; + update.animation.duration_ms = 60000.0; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(map, &camera, &animation) + MLN_STATUS_OK, + mln_map_update_camera(map, &update, MLN_TEST_DISCARD_COMPLETION) ); bool saw_repaint_request = false; - for (unsigned int attempt = 0; attempt < 500 && !saw_repaint_request; - attempt += 1) { - mln_runtime_pump(runtime, 0, -1); + const uint64_t repaint_deadline = mln_test_monotonic_milliseconds() + 30000; + while (!saw_repaint_request && + mln_test_monotonic_milliseconds() < repaint_deadline) { + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + finish_render_barrier(&fixture); + mln_render_frame_batch batch = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_render_session_render_update(fixture.session, &result, &needs_repaint) + mln_render_session_drain_frame_results(fixture.session, &batch) + ); + size_t count = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_count(batch, &count) ); - if (result == MLN_RENDER_RESULT_RENDERED && needs_repaint) { - saw_repaint_request = true; - } else { + for (size_t index = 0; index < count; index += 1) { + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, index, &result) + ); + if ( + result.disposition == MLN_RENDER_RESULT_RENDERED && result.needs_repaint + ) { + saw_repaint_request = true; + } + } + mln_render_frame_batch_release(batch); + if (!saw_repaint_request) { mln_test_sleep_millisecond(); } } @@ -530,21 +850,42 @@ static void opengl_borrowed_texture_rejects_unsafe_raw_descriptors(void) { invalid.extent.size = sizeof(mln_render_target_extent) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_borrowed_texture_attach(map, &invalid, &session) + mln_opengl_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.context.size = sizeof(mln_opengl_context_descriptor) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_borrowed_texture_attach(map, &invalid, &session) + mln_opengl_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.texture = 0; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_borrowed_texture_attach(map, &invalid, &session) + mln_opengl_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -592,14 +933,16 @@ static void shrink_vulkan_owned( static void vulkan_surface_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_vulkan_surface_descriptor, vulkan_surface_descriptor, - mln_vulkan_surface_attach, clear_vulkan_surface, shrink_vulkan_surface + mln_vulkan_surface_attach, MLN_RENDER_DRIVER_CORE_WORKER, + clear_vulkan_surface, shrink_vulkan_surface ); } static void vulkan_owned_texture_attach_rejects_unsafe_raw_inputs(void) { EXPECT_ATTACH_REJECTS_UNSAFE_INPUTS( mln_vulkan_owned_texture_descriptor, vulkan_owned_descriptor, - mln_vulkan_owned_texture_attach, clear_vulkan_owned, shrink_vulkan_owned + mln_vulkan_owned_texture_attach, MLN_RENDER_DRIVER_CORE_WORKER, + clear_vulkan_owned, shrink_vulkan_owned ); } @@ -619,21 +962,42 @@ static void vulkan_borrowed_texture_rejects_unsafe_raw_descriptors(void) { invalid.extent.size = sizeof(mln_render_target_extent) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_vulkan_borrowed_texture_attach(map, &invalid, &session) + mln_vulkan_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.context.size = sizeof(mln_vulkan_context_descriptor) - 1; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_vulkan_borrowed_texture_attach(map, &invalid, &session) + mln_vulkan_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); invalid = descriptor; invalid.image = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_vulkan_borrowed_texture_attach(map, &invalid, &session) + mln_vulkan_borrowed_texture_attach( + map, &invalid, + &(mln_render_session_attach_options){ + .size = sizeof(mln_render_session_attach_options), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + }, + &session, MLN_TEST_DISCARD_COMPLETION + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, session); mln_test_destroy_map(map); @@ -644,15 +1008,21 @@ void run_render_backend_abi_tests(void) { UnitySetTestFile(__FILE__); RUN_TEST(metal_surface_attach_rejects_unsafe_raw_inputs); RUN_TEST(metal_owned_texture_attach_rejects_unsafe_raw_inputs); +#if defined(MLN_FFI_TEST_BACKEND_METAL) + RUN_TEST(metal_surface_retarget_retains_submission_inputs); +#endif RUN_TEST(metal_borrowed_texture_rejects_unsafe_raw_descriptors); RUN_TEST(opengl_surface_attach_rejects_unsafe_raw_inputs); #if defined(MLN_FFI_TEST_OPENGL_WEBGL) RUN_TEST(opengl_surface_attach_rejects_a_webgl_surface_handle); #endif RUN_TEST(opengl_dedicated_context_rejects_shared_session_fields); - RUN_TEST(opengl_owned_texture_attach_rejects_a_dedicated_context); +#if defined(MLN_FFI_TEST_OPENGL_WGL) + RUN_TEST(opengl_owned_texture_attach_rejects_dedicated_wgl); +#endif RUN_TEST(dedicated_egl_surface_renders_and_keeps_its_context_current); - RUN_TEST(render_update_reports_whether_the_map_needs_another_frame); + RUN_TEST(dedicated_egl_texture_uses_a_readback_only_core_worker); + RUN_TEST(frame_results_report_whether_the_map_needs_another_frame); RUN_TEST(opengl_owned_texture_attach_rejects_unsafe_raw_inputs); RUN_TEST(opengl_borrowed_texture_rejects_unsafe_raw_descriptors); RUN_TEST(vulkan_surface_attach_rejects_unsafe_raw_inputs); diff --git a/src/c_api/tests/render_target_lifecycle_abi.c b/src/c_api/tests/render_target_lifecycle_abi.c index e7b1b3c4a..f97e7baf7 100644 --- a/src/c_api/tests/render_target_lifecycle_abi.c +++ b/src/c_api/tests/render_target_lifecycle_abi.c @@ -1,101 +1,36 @@ -// Raw C ABI coverage: what a render session keeps when its target changes. -// -// The session renderer is not reachable through the C API, so these tests probe -// for it with mln_render_session_dump_debug_logs(), which reports -// MLN_STATUS_INVALID_STATE while no renderer exists and MLN_STATUS_OK once one -// does. It is the one renderer-requiring entry point that leaves the renderer's -// state intact. - -#include +// Raw C ABI coverage for ordered detach and forced target abandonment. #include "abi_tests.h" #include "test_support.h" #include "unity.h" -// Renders until the session reports a frame, which is when it builds its -// renderer. Returns whether a frame arrived before the attempts ran out. -static bool render_until_frame( - mln_runtime runtime, mln_render_session session -) { - for (unsigned int attempt = 0; attempt < 200; attempt += 1) { - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; - if ( - mln_render_session_render_update(session, &result, &needs_repaint) != - MLN_STATUS_OK - ) { - return false; - } - if (result == MLN_RENDER_RESULT_RENDERED) { - return true; - } - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { - return false; - } - mln_test_sleep_millisecond(); - } - return false; -} - -// Whether the session currently holds a renderer. A backend failure reports -// NATIVE_ERROR, so it cannot read as a retired renderer. -static bool has_renderer(mln_render_session session) { - const mln_status status = mln_render_session_dump_debug_logs(session); - if (status == MLN_STATUS_INVALID_STATE) { - return false; - } - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, status); - return true; -} - -static void resize_keeps_the_session_renderer(void) { +static void normal_detach_runs_on_the_driver_and_retires_map_attachment(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - TEST_ASSERT_TRUE(render_until_frame(runtime, fixture.session)); - TEST_ASSERT_TRUE_MESSAGE( - has_renderer(fixture.session), "a rendered frame should leave a renderer" - ); - + mln_test_completion detach = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_resize(fixture.session, 96, 48, 1.0) + MLN_STATUS_OK, + mln_render_session_detach(fixture.session, &detach.descriptor) ); - TEST_ASSERT_TRUE_MESSAGE( - has_renderer(fixture.session), - "resizing at the same scale factor should keep the renderer" + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &detach) ); + mln_test_completion_destroy(&detach); - mln_test_render_fixture_destroy(&fixture); - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// The pixel ratio is baked into the renderer's shaders when it is built, so a -// scale factor change is the one resize that retires it. The replacement is -// built lazily on the next render. -static void scale_factor_change_rebuilds_the_session_renderer(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - mln_test_render_fixture fixture = {0}; - TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - - TEST_ASSERT_TRUE(render_until_frame(runtime, fixture.session)); - TEST_ASSERT_TRUE(has_renderer(fixture.session)); - + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_resize(fixture.session, 64, 64, 2.0) - ); - TEST_ASSERT_FALSE_MESSAGE( - has_renderer(fixture.session), - "changing the scale factor should retire the renderer" + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) ); - - TEST_ASSERT_TRUE(render_until_frame(runtime, fixture.session)); - TEST_ASSERT_TRUE_MESSAGE( - has_renderer(fixture.session), - "the next render should build a renderer for the new scale factor" + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_SESSION_STATE_DETACHED, snapshot.state); + mln_completion rejected = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, + mln_render_session_barrier(fixture.session, &rejected) ); mln_test_render_fixture_destroy(&fixture); @@ -103,93 +38,36 @@ static void scale_factor_change_rebuilds_the_session_renderer(void) { mln_test_destroy_runtime(runtime); } -// The descriptors here are defaults no backend would accept, so this test only -// reaches the session check because validation looks at the session first. -static void set_target_rejects_a_session_owned_texture(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - mln_test_render_fixture fixture = {0}; - TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - - const mln_metal_borrowed_texture_descriptor metal = - mln_metal_borrowed_texture_descriptor_default(); - const mln_vulkan_borrowed_texture_descriptor vulkan = - mln_vulkan_borrowed_texture_descriptor_default(); - const mln_opengl_borrowed_texture_descriptor opengl = - mln_opengl_borrowed_texture_descriptor_default(); - const mln_webgpu_borrowed_texture_descriptor webgpu = - mln_webgpu_borrowed_texture_descriptor_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_metal_borrowed_texture_set_target(fixture.session, &metal) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_vulkan_borrowed_texture_set_target(fixture.session, &vulkan) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_opengl_borrowed_texture_set_target(fixture.session, &opengl) - ); +static void stale_and_null_sessions_reject_the_new_control_surface(void) { + mln_completion operation = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_webgpu_borrowed_texture_set_target(fixture.session, &webgpu) - ); - - const mln_metal_surface_descriptor metal_surface = - mln_metal_surface_descriptor_default(); - const mln_vulkan_surface_descriptor vulkan_surface = - mln_vulkan_surface_descriptor_default(); - const mln_opengl_surface_descriptor opengl_surface = - mln_opengl_surface_descriptor_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_metal_surface_set_target(fixture.session, &metal_surface) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_vulkan_surface_set_target(fixture.session, &vulkan_surface) + MLN_STATUS_INVALID_ARGUMENT, + mln_render_session_reduce_memory_use(MLN_HANDLE_NULL, &operation) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_UNSUPPORTED, - mln_opengl_surface_set_target(fixture.session, &opengl_surface) + MLN_STATUS_INVALID_ARGUMENT, + mln_render_session_clear_data(MLN_HANDLE_NULL, &operation) ); - - // Every rejection left the session usable. TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_render_session_resize(fixture.session, 32, 32, 1.0) + MLN_STATUS_INVALID_ARGUMENT, + mln_render_session_dump_debug_logs(MLN_HANDLE_NULL, &operation) ); - mln_test_render_fixture_destroy(&fixture); - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// A stale session is reported as invalid whatever the descriptor says, so a -// host can tell a lost session from a build without that backend. -static void set_target_rejects_a_stale_session(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - const mln_render_session stale_session = fixture.session; + const mln_render_session stale = fixture.session; mln_test_render_fixture_destroy(&fixture); - - const mln_metal_surface_descriptor metal_surface = - mln_metal_surface_descriptor_default(); - const mln_opengl_borrowed_texture_descriptor opengl_texture = - mln_opengl_borrowed_texture_descriptor_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_metal_surface_set_target(stale_session, &metal_surface) - ); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_opengl_borrowed_texture_set_target(stale_session, &opengl_texture) + mln_render_session_get_snapshot(stale, &snapshot) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_metal_surface_set_target(MLN_HANDLE_NULL, &metal_surface) + MLN_STATUS_INVALID_ARGUMENT, mln_render_session_destroy(stale) ); mln_test_destroy_map(map); @@ -198,8 +76,6 @@ static void set_target_rejects_a_stale_session(void) { void run_render_target_lifecycle_abi_tests(void) { UnitySetTestFile(__FILE__); - RUN_TEST(resize_keeps_the_session_renderer); - RUN_TEST(scale_factor_change_rebuilds_the_session_renderer); - RUN_TEST(set_target_rejects_a_session_owned_texture); - RUN_TEST(set_target_rejects_a_stale_session); + RUN_TEST(normal_detach_runs_on_the_driver_and_retires_map_attachment); + RUN_TEST(stale_and_null_sessions_reject_the_new_control_surface); } diff --git a/src/c_api/tests/render_thread_abi.c b/src/c_api/tests/render_thread_abi.c index d3890d28e..9f453dd8a 100644 --- a/src/c_api/tests/render_thread_abi.c +++ b/src/c_api/tests/render_thread_abi.c @@ -1,465 +1,781 @@ -// Raw C ABI coverage: a render session's owner thread is the thread that -// attached it, which need not be the map's owner thread. +// Raw C ABI coverage for core-worker and caller-graphics-thread sessions. #include +#include #include +#include #include "abi_tests.h" #include "test_support.h" #include "unity.h" +static const char red_background_style[] = + "{\"version\":8,\"sources\":{},\"layers\":[{\"id\":\"bg\"," + "\"type\":\"background\",\"paint\":{\"background-color\":\"#ff0000\"}}]}"; -// A background layer needs no network, so the readback color is deterministic. -static const char background_style_json[] = - "{\"version\":8,\"sources\":{},\"layers\":" - "[{\"id\":\"bg\",\"type\":\"background\"," - "\"paint\":{\"background-color\":\"#ff0000\"}}]}"; +static void prepare_renderable_map(mln_runtime runtime, mln_map map) { + static const char empty_style[] = + "{\"version\":8,\"sources\":{},\"layers\":[]}"; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL(empty_style)) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); +} -// Renders until the map publishes an update for the session's extent, which -// takes at least one pump on the map owner thread. Returns the last status. -static mln_status render_until_frame( - mln_render_session session, bool* out_rendered +static bool service_fixture(const mln_test_render_fixture* fixture) { + if (fixture->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + return true; + } + size_t serviced = 0; + return mln_render_session_service_driver_work( + fixture->session, SIZE_MAX, &serviced + ) == MLN_STATUS_OK; +} + +static bool wait_for_results( + const mln_test_render_fixture* fixture, size_t minimum, + mln_render_frame_batch* out_batch ) { - mln_status status = MLN_STATUS_OK; - *out_rendered = false; - for (unsigned int attempt = 0; attempt < 500 && !*out_rendered; - attempt += 1) { - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; - status = mln_render_session_render_update(session, &result, &needs_repaint); - if (status != MLN_STATUS_OK) { - return status; + for (unsigned int attempt = 0; attempt < 10000; attempt += 1) { + if (!service_fixture(fixture)) { + return false; } - *out_rendered = result == MLN_RENDER_RESULT_RENDERED; - if (!*out_rendered) { + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + if ( + mln_render_session_get_snapshot(fixture->session, &snapshot) != + MLN_STATUS_OK || + snapshot.pending_demand_count != 0 + ) { mln_test_sleep_millisecond(); + continue; } + mln_render_frame_batch batch = MLN_HANDLE_NULL; + const mln_status status = + mln_render_session_drain_frame_results(fixture->session, &batch); + if (status == MLN_STATUS_OK) { + size_t count = 0; + if ( + mln_render_frame_batch_count(batch, &count) == MLN_STATUS_OK && + count >= minimum + ) { + *out_batch = batch; + return true; + } + mln_render_frame_batch_release(batch); + } else if (status != MLN_STATUS_NOT_READY) { + return false; + } + mln_test_sleep_millisecond(); } - return status; + return false; +} + +static mln_render_frame_result batch_result( + mln_render_frame_batch batch, size_t index +) { + mln_render_frame_result result = {.size = sizeof(mln_render_frame_result)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_frame_batch_get(batch, index, &result) + ); + return result; } -typedef struct render_probe { - mln_map map; - atomic_bool finished; - bool attached; - bool rendered; - mln_status render_status; - mln_status readback_status; - uint32_t width; - uint32_t height; - uint8_t pixel[4]; -} render_probe; - -static void attach_render_readback(void* argument) { - render_probe* probe = (render_probe*)argument; +static void attach_reports_the_selected_native_driver(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); mln_test_render_fixture fixture = {0}; - probe->attached = mln_test_render_fixture_create(probe->map, &fixture); - if (!probe->attached) { - atomic_store(&probe->finished, true); - return; - } + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - probe->render_status = render_until_frame(fixture.session, &probe->rendered); + mln_render_session_capabilities capabilities = { + .size = sizeof(mln_render_session_capabilities) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_get_capabilities(fixture.session, &capabilities) + ); + TEST_ASSERT_EQUAL_UINT32(fixture.driver, capabilities.driver); + TEST_ASSERT_EQUAL_UINT32(2, capabilities.texture_ring_depth); + TEST_ASSERT_BITS_HIGH( + MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION | + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC, + capabilities.flags + ); +#if defined(MLN_FFI_TEST_BACKEND_OPENGL) || defined(MLN_FFI_TEST_BACKEND_WEBGPU) + TEST_ASSERT_EQUAL_UINT32( + MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, capabilities.driver + ); + TEST_ASSERT_TRUE(fixture.observed_attaching); + TEST_ASSERT_TRUE(fixture.observed_driver_ready); +#else + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_DRIVER_CORE_WORKER, capabilities.driver); + size_t serviced = 99; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, + mln_render_session_service_driver_work(fixture.session, 1, &serviced) + ); +#endif - static uint8_t pixels[64 * 64 * 4]; - mln_texture_image_info info = {.size = sizeof(mln_texture_image_info)}; - probe->readback_status = mln_texture_read_premultiplied_rgba8( - fixture.session, pixels, sizeof(pixels), &info + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) ); - probe->width = info.width; - probe->height = info.height; - for (size_t channel = 0; channel < 4; channel += 1) { - probe->pixel[channel] = pixels[channel]; - } + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_SESSION_STATE_ATTACHED, snapshot.state); + TEST_ASSERT_EQUAL_UINT32(capabilities.driver, snapshot.driver); mln_test_render_fixture_destroy(&fixture); - atomic_store(&probe->finished, true); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); } -static void a_second_thread_attaches_and_renders(void) { +static void demand_coalescing_preserves_boundaries_and_generations(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + atomic_bool entered; + atomic_bool release; + atomic_init(&entered, false); + atomic_init(&release, false); + mln_test_completion blocker = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(background_style_json)) + MLN_STATUS_OK, mln_test_render_session_blocking_operation_create( + fixture.session, &entered, &release, &blocker.descriptor + ) ); + if (fixture.driver == MLN_RENDER_DRIVER_CORE_WORKER) { + for (unsigned int attempt = 0; attempt < 10000 && !atomic_load(&entered); + attempt += 1) { + mln_test_sleep_millisecond(); + } + TEST_ASSERT_TRUE(atomic_load(&entered)); + } - render_probe probe = {.map = map}; - atomic_init(&probe.finished, false); + mln_frame_demand first = mln_frame_demand_default(); + first.flags = 0; + first.token = 101; + first.coalescing_boundary = 7; + mln_frame_demand newest = first; + newest.token = 102; + mln_frame_demand separate = newest; + separate.token = 103; + separate.coalescing_boundary = 8; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &first) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &newest) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &separate) + ); + atomic_store(&release, true); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &blocker) + ); + mln_test_completion_destroy(&blocker); - mln_test_thread* thread = - mln_test_thread_start(attach_render_readback, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.finished)); - mln_test_thread_join(thread); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 3, &batch)); + mln_frame_demand later = separate; + later.token = 104; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &later) + ); + mln_render_frame_batch second_batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &second_batch)); + TEST_ASSERT_EQUAL_UINT64(104, batch_result(second_batch, 0).token); + mln_render_frame_batch_release(second_batch); + const mln_render_frame_result superseded = batch_result(batch, 0); + const mln_render_frame_result rendered = batch_result(batch, 1); + const mln_render_frame_result boundary = batch_result(batch, 2); + TEST_ASSERT_EQUAL_UINT64(101, superseded.token); + TEST_ASSERT_EQUAL_UINT32( + MLN_RENDER_RESULT_SUPERSEDED, superseded.disposition + ); + TEST_ASSERT_EQUAL_UINT64(102, rendered.token); + TEST_ASSERT_EQUAL_UINT64(103, boundary.token); + TEST_ASSERT_GREATER_THAN_UINT64(0, rendered.extent_generation); + TEST_ASSERT_GREATER_THAN_UINT64(0, rendered.map_update_generation); + TEST_ASSERT_GREATER_OR_EQUAL_UINT64( + rendered.map_update_generation, boundary.map_update_generation + ); + TEST_ASSERT_GREATER_THAN_UINT64(0, rendered.frame_generation); + mln_render_frame_batch_release(batch); - TEST_ASSERT_TRUE(probe.attached); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.render_status); - TEST_ASSERT_TRUE(probe.rendered); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.readback_status); - TEST_ASSERT_EQUAL_UINT32(64, probe.width); - TEST_ASSERT_EQUAL_UINT32(64, probe.height); - // The style paints an opaque red background. - TEST_ASSERT_EQUAL_UINT8(255, probe.pixel[0]); - TEST_ASSERT_EQUAL_UINT8(0, probe.pixel[1]); - TEST_ASSERT_EQUAL_UINT8(0, probe.pixel[2]); - TEST_ASSERT_EQUAL_UINT8(255, probe.pixel[3]); + mln_frame_demand timed_out = mln_frame_demand_default(); + timed_out.flags = 0; + timed_out.token = 104; + timed_out.timeout_ns = 1; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &timed_out) + ); + mln_test_sleep_millisecond(); + batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + const mln_render_frame_result missed = batch_result(batch, 0); + TEST_ASSERT_EQUAL_UINT64(104, missed.token); + TEST_ASSERT_EQUAL_UINT32( + MLN_RENDER_RESULT_DEADLINE_MISSED, missed.disposition + ); + mln_render_frame_batch_release(batch); + mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -typedef struct foreign_call_probe { - mln_render_session session; - atomic_bool finished; - mln_status render_status; - mln_status resize_status; - mln_status detach_status; - mln_status destroy_status; - mln_status reduce_memory_status; - mln_status clear_data_status; - mln_status readback_status; - mln_status surface_set_target_status[3]; - mln_status texture_set_target_status[4]; -} foreign_call_probe; - -static void call_session_from_a_foreign_thread(void* argument) { - foreign_call_probe* probe = (foreign_call_probe*)argument; - mln_render_result result = MLN_RENDER_RESULT_NO_UPDATE; - bool needs_repaint = false; - probe->render_status = - mln_render_session_render_update(probe->session, &result, &needs_repaint); - probe->resize_status = mln_render_session_resize(probe->session, 32, 32, 1.0); - probe->detach_status = mln_render_session_detach(probe->session); - probe->destroy_status = mln_render_session_destroy(probe->session); - probe->reduce_memory_status = - mln_render_session_reduce_memory_use(probe->session); - probe->clear_data_status = mln_render_session_clear_data(probe->session); - mln_texture_image_info info = {.size = sizeof(mln_texture_image_info)}; - probe->readback_status = - mln_texture_read_premultiplied_rgba8(probe->session, NULL, 0, &info); - - // Thread affinity is checked before the target kind, so these report the - // foreign thread rather than that this session owns its texture. - const mln_metal_surface_descriptor metal_surface = - mln_metal_surface_descriptor_default(); - const mln_vulkan_surface_descriptor vulkan_surface = - mln_vulkan_surface_descriptor_default(); - const mln_opengl_surface_descriptor opengl_surface = - mln_opengl_surface_descriptor_default(); - probe->surface_set_target_status[0] = - mln_metal_surface_set_target(probe->session, &metal_surface); - probe->surface_set_target_status[1] = - mln_vulkan_surface_set_target(probe->session, &vulkan_surface); - probe->surface_set_target_status[2] = - mln_opengl_surface_set_target(probe->session, &opengl_surface); - - const mln_metal_borrowed_texture_descriptor metal_texture = - mln_metal_borrowed_texture_descriptor_default(); - const mln_vulkan_borrowed_texture_descriptor vulkan_texture = - mln_vulkan_borrowed_texture_descriptor_default(); - const mln_opengl_borrowed_texture_descriptor opengl_texture = - mln_opengl_borrowed_texture_descriptor_default(); - const mln_webgpu_borrowed_texture_descriptor webgpu_texture = - mln_webgpu_borrowed_texture_descriptor_default(); - probe->texture_set_target_status[0] = - mln_metal_borrowed_texture_set_target(probe->session, &metal_texture); - probe->texture_set_target_status[1] = - mln_vulkan_borrowed_texture_set_target(probe->session, &vulkan_texture); - probe->texture_set_target_status[2] = - mln_opengl_borrowed_texture_set_target(probe->session, &opengl_texture); - probe->texture_set_target_status[3] = - mln_webgpu_borrowed_texture_set_target(probe->session, &webgpu_texture); - atomic_store(&probe->finished, true); +static mln_acquired_frame render_and_acquire( + const mln_test_render_fixture* fixture, uint64_t token +) { + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + demand.token = token; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture->session, &demand) + ); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(fixture, 1, &batch)); + const mln_render_frame_result result = batch_result(batch, 0); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_RESULT_RENDERED, result.disposition); + mln_render_frame_batch_release(batch); + mln_acquired_frame frame = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_acquire_frame(fixture->session, &frame) + ); + TEST_ASSERT_NOT_EQUAL(MLN_HANDLE_NULL, frame); + return frame; } - -// Attaching binds the session to the attaching thread, so every entry point, -// destroy included, rejects any other thread. -static void session_entry_points_reject_a_foreign_thread(void) { +static void frame_wake_runs_when_the_result_queue_becomes_nonempty(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); mln_test_render_fixture fixture = {0}; TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - - foreign_call_probe probe = {.session = fixture.session}; - atomic_init(&probe.finished, false); - - mln_test_thread* thread = - mln_test_thread_start(call_session_from_a_foreign_thread, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.finished)); - mln_test_thread_join(thread); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.render_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.resize_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.detach_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.destroy_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.reduce_memory_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.clear_data_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.readback_status); - const size_t surface_targets = - sizeof(probe.surface_set_target_status) / sizeof(mln_status); - for (size_t index = 0; index < surface_targets; index += 1) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_WRONG_THREAD, probe.surface_set_target_status[index] - ); - } - const size_t texture_targets = - sizeof(probe.texture_set_target_status) / sizeof(mln_status); - for (size_t index = 0; index < texture_targets; index += 1) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_WRONG_THREAD, probe.texture_set_target_status[index] - ); + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + const unsigned int before = atomic_load(&fixture.frame_wakes); + for (unsigned int attempt = 0; + attempt < 10000 && atomic_load(&fixture.frame_wakes) == before; + attempt += 1) { + TEST_ASSERT_TRUE(service_fixture(&fixture)); + if (atomic_load(&fixture.frame_wakes) == before) { + mln_test_sleep_millisecond(); + } } - + TEST_ASSERT_GREATER_THAN_UINT32(before, atomic_load(&fixture.frame_wakes)); + const unsigned int woke = atomic_load(&fixture.frame_wakes); + TEST_ASSERT_EQUAL_UINT32(woke, atomic_load(&fixture.frame_wakes)); + mln_render_frame_batch results = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_drain_frame_results(fixture.session, &results) + ); + mln_render_frame_batch_release(results); + TEST_ASSERT_EQUAL_UINT32(woke, atomic_load(&fixture.frame_wakes)); mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -typedef struct hold_session_probe { - mln_map map; - atomic_bool attached; - atomic_bool start_destroy; - atomic_bool destroyed; - bool attach_succeeded; - mln_status destroy_status; - mln_test_render_fixture fixture; -} hold_session_probe; - -static void attach_hold_destroy(void* argument) { - hold_session_probe* probe = (hold_session_probe*)argument; - probe->attach_succeeded = - mln_test_render_fixture_create(probe->map, &probe->fixture); - atomic_store(&probe->attached, true); - while (!atomic_load(&probe->start_destroy)) { - mln_test_sleep_millisecond(); - } - if (probe->attach_succeeded) { - probe->destroy_status = mln_render_session_destroy(probe->fixture.session); - probe->fixture.session = MLN_HANDLE_NULL; - mln_test_render_fixture_destroy(&probe->fixture); - } - atomic_store(&probe->destroyed, true); -} - -// The map cannot be destroyed while a session is attached, including one owned -// by a thread the destroying thread never observes. The map registry mutex is -// what makes that check race-free. -static void map_destroy_rejects_a_session_owned_by_another_thread(void) { +static void texture_ring_leases_apply_backpressure_until_cpu_release(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - hold_session_probe probe = {.map = map}; - atomic_init(&probe.attached, false); - atomic_init(&probe.start_destroy, false); - atomic_init(&probe.destroyed, false); + mln_acquired_frame first = render_and_acquire(&fixture, 201); + mln_acquired_frame second = render_and_acquire(&fixture, 202); + mln_gpu_sync producer = mln_gpu_sync_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_acquired_frame_get_producer_sync(first, &producer) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_GPU_SYNC_CPU_COMPLETE, producer.kind); + + mln_frame_demand blocked = mln_frame_demand_default(); + blocked.flags = 0; + blocked.token = 203; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &blocked) + ); + TEST_ASSERT_TRUE(service_fixture(&fixture)); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) + ); + TEST_ASSERT_EQUAL_UINT32(2, snapshot.acquired_frame_count); + mln_completion rejected_detach = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, + mln_render_session_detach(fixture.session, &rejected_detach) + ); + TEST_ASSERT_EQUAL_UINT32(1, snapshot.pending_demand_count); - mln_test_thread* thread = mln_test_thread_start(attach_hold_destroy, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.attached)); - TEST_ASSERT_TRUE(probe.attach_succeeded); + mln_gpu_sync cpu_complete = mln_gpu_sync_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_acquired_frame_release(&first, &cpu_complete) + ); + TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, first); + TEST_ASSERT_TRUE(service_fixture(&fixture)); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_STATE, mln_map_destroy(map)); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + TEST_ASSERT_EQUAL_UINT64(203, batch_result(batch, 0).token); + mln_render_frame_batch_release(batch); - atomic_store(&probe.start_destroy, true); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.destroyed)); - mln_test_thread_join(thread); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.destroy_status); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_acquired_frame_release(&second, &cpu_complete) + ); + mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -typedef struct resize_probe { - mln_map map; - atomic_bool ready_to_resize; - atomic_bool start_resize; - atomic_bool immediate_render_checked; - atomic_bool finished; - bool attached; - mln_render_result immediate_result; - bool rendered; - mln_status resize_status; -} resize_probe; - -static void attach_resize_render(void* argument) { - resize_probe* probe = (resize_probe*)argument; +static void acquired_frame_release_after_abandon_is_cpu_only(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); mln_test_render_fixture fixture = {0}; - probe->attached = mln_test_render_fixture_create(probe->map, &fixture); - if (!probe->attached) { - atomic_store(&probe->ready_to_resize, true); - atomic_store(&probe->immediate_render_checked, true); - atomic_store(&probe->finished, true); - return; - } + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + mln_acquired_frame frame = render_and_acquire(&fixture, 250); - atomic_store(&probe->ready_to_resize, true); - while (!atomic_load(&probe->start_resize)) { - mln_test_sleep_millisecond(); - } + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_abandon(fixture.session, &abandoned) + ); + TEST_ASSERT_EQUAL_UINT32( + MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED, abandoned.disposition + ); + TEST_ASSERT_GREATER_THAN_UINT32(0, abandoned.quarantined_resource_count); + mln_render_frame_result invalid = {.size = sizeof(mln_render_frame_result)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_TARGET_LOST, mln_acquired_frame_get_result(frame, &invalid) + ); + mln_gpu_sync sync = mln_gpu_sync_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_acquired_frame_release(&frame, &sync) + ); + TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, frame); - probe->resize_status = - mln_render_session_resize(fixture.session, 96, 48, 1.0); - - // Immediately after the resize the map still holds an update built for the - // previous extent, so rendering must report a pending size rather than - // project that update into the new target. - mln_render_result immediate = MLN_RENDER_RESULT_RENDERED; - bool needs_repaint = false; - if ( - mln_render_session_render_update( - fixture.session, &immediate, &needs_repaint - ) != MLN_STATUS_OK - ) { - immediate = MLN_RENDER_RESULT_RENDERED; - } - probe->immediate_result = immediate; - atomic_store(&probe->immediate_render_checked, true); + mln_test_render_fixture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} - bool rendered = immediate == MLN_RENDER_RESULT_RENDERED; - if (!rendered) { - (void)render_until_frame(fixture.session, &rendered); - } - probe->rendered = rendered; +static void texture_readback_is_an_ordered_owned_operation_result(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL(red_background_style)) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + mln_acquired_frame frame = render_and_acquire(&fixture, 260); + mln_test_completion readback = mln_test_completion_readback(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_texture_read_premultiplied_rgba8(fixture.session, &readback.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &readback) + ); + mln_texture_readback_result result = {0}; + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&readback, &result, sizeof(result)) + ); + mln_buffer_view bytes = result.data; + mln_texture_image_info info = result.info; + TEST_ASSERT_EQUAL_UINT32(64, info.width); + TEST_ASSERT_EQUAL_UINT32(64, info.height); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(4, (uint32_t)bytes.size); + TEST_ASSERT_EQUAL_UINT8(255, ((const uint8_t*)bytes.data)[0]); + TEST_ASSERT_EQUAL_UINT8(0, ((const uint8_t*)bytes.data)[1]); + TEST_ASSERT_EQUAL_UINT8(0, ((const uint8_t*)bytes.data)[2]); + TEST_ASSERT_EQUAL_UINT8(255, ((const uint8_t*)bytes.data)[3]); + mln_test_completion_destroy(&readback); + + mln_gpu_sync sync = mln_gpu_sync_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_acquired_frame_release(&frame, &sync) + ); mln_test_render_fixture_destroy(&fixture); - atomic_store(&probe->finished, true); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); } -// A resize from the render thread reaches the map through the map's owner -// thread. Rendering waits for the map to catch up, or the frame would take its -// projection from the old logical size and its viewport from the new physical -// one. -static void resize_from_the_render_thread_lands_on_the_map_thread(void) { +static void resize_and_barrier_order_frame_and_extent_generations(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + + mln_frame_demand before = mln_frame_demand_default(); + before.flags = 0; + before.token = 301; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &before) + ); + mln_test_completion barrier = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(background_style_json)) + mln_render_session_barrier(fixture.session, &barrier.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &barrier) + ); + mln_test_completion_destroy(&barrier); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + const mln_render_frame_result old_frame = batch_result(batch, 0); + mln_render_frame_batch_release(batch); + + const mln_render_target_extent extent = { + .size = sizeof(mln_render_target_extent), + .width = 96, + .height = 48, + .scale_factor = 2.0, + }; + mln_frame_demand during = mln_frame_demand_default(); + during.flags = 0; + during.token = 302; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &during) ); + mln_test_completion resize = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_resize(fixture.session, &extent, &resize.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_fixture_finish_operation(&fixture, &resize) + ); + mln_test_completion_destroy(&resize); + batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + const mln_render_frame_result resizing_frame = batch_result(batch, 0); + TEST_ASSERT_EQUAL_UINT64( + old_frame.extent_generation, resizing_frame.extent_generation + ); + mln_render_frame_batch_release(batch); - resize_probe probe = {.map = map}; - atomic_init(&probe.ready_to_resize, false); - atomic_init(&probe.start_resize, false); - atomic_init(&probe.immediate_render_checked, false); - atomic_init(&probe.finished, false); + mln_frame_demand after = mln_frame_demand_default(); + after.flags = 0; + after.token = 303; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &after) + ); + batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + const mln_render_frame_result new_frame = batch_result(batch, 0); + TEST_ASSERT_GREATER_THAN_UINT64( + old_frame.extent_generation, new_frame.extent_generation + ); + TEST_ASSERT_GREATER_THAN_UINT64( + old_frame.frame_generation, new_frame.frame_generation + ); + mln_render_frame_batch_release(batch); - mln_test_thread* thread = mln_test_thread_start(attach_resize_render, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.ready_to_resize)); - atomic_store(&probe.start_resize, true); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) + ); + TEST_ASSERT_EQUAL_UINT32(96, snapshot.extent.width); + TEST_ASSERT_EQUAL_UINT32(48, snapshot.extent.height); + TEST_ASSERT_DOUBLE_WITHIN(0.0, 2.0, snapshot.extent.scale_factor); + TEST_ASSERT_EQUAL_UINT64( + new_frame.extent_generation, snapshot.extent_generation + ); - // Keep the map owner thread from processing the queued resize until the - // render thread has checked the update for the previous extent. - for (unsigned int attempt = 0; - attempt < 500 && !atomic_load(&probe.immediate_render_checked); - attempt += 1) { - mln_test_sleep_millisecond(); - } - TEST_ASSERT_TRUE(atomic_load(&probe.immediate_render_checked)); + mln_test_render_fixture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.finished)); - mln_test_thread_join(thread); +typedef struct foreign_driver_probe { + mln_render_session session; + atomic_bool done; + mln_status status; +} foreign_driver_probe; + +static void service_from_foreign_thread(void* argument) { + foreign_driver_probe* probe = argument; + size_t serviced = 0; + probe->status = + mln_render_session_service_driver_work(probe->session, 1, &serviced); + atomic_store(&probe->done, true); +} - TEST_ASSERT_TRUE(probe.attached); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.resize_status); - TEST_ASSERT_EQUAL_INT(MLN_RENDER_RESULT_SIZE_PENDING, probe.immediate_result); - TEST_ASSERT_TRUE(probe.rendered); +static void driver_service_fixes_and_enforces_graphics_thread_identity(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); - uint32_t width = 0; - uint32_t height = 0; - double scale_factor = 0.0; + foreign_driver_probe probe = {.session = fixture.session}; + atomic_init(&probe.done, false); + mln_test_thread* thread = + mln_test_thread_start(service_from_foreign_thread, &probe); + TEST_ASSERT_TRUE(mln_test_wait_until(runtime, &probe.done)); + mln_test_thread_join(thread); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) + fixture.driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ? MLN_STATUS_WRONG_THREAD + : MLN_STATUS_INVALID_STATE, + probe.status ); - TEST_ASSERT_EQUAL_UINT32(96, width); - TEST_ASSERT_EQUAL_UINT32(48, height); + mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -typedef struct runtime_after_render_probe { - mln_map map; - atomic_bool finished; - mln_status create_status; -} runtime_after_render_probe; +typedef struct abandon_busy_probe { + mln_render_session session; + atomic_bool* entered; + atomic_bool* release; + mln_status status; +} abandon_busy_probe; + +static void abandon_when_driver_enters(void* argument) { + abandon_busy_probe* probe = argument; + while (!atomic_load(probe->entered)) { + mln_test_sleep_millisecond(); + } + mln_render_abandon_result result = { + .size = sizeof(mln_render_abandon_result) + }; + probe->status = mln_render_session_abandon(probe->session, &result); + atomic_store(probe->release, true); +} -static void render_then_create_runtime(void* argument) { - runtime_after_render_probe* probe = (runtime_after_render_probe*)argument; +static void abandon_is_busy_during_a_driver_call_and_changes_nothing(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); mln_test_render_fixture fixture = {0}; - if (mln_test_render_fixture_create(probe->map, &fixture)) { - bool rendered = false; - (void)render_until_frame(fixture.session, &rendered); - mln_test_render_fixture_destroy(&fixture); - } + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + atomic_bool entered; + atomic_bool release; + atomic_init(&entered, false); + atomic_init(&release, false); + mln_test_completion operation = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_render_session_blocking_operation_create( + fixture.session, &entered, &release, &operation.descriptor + ) + ); - // Rendering installs a scheduler as this thread's current one; a scheduler or - // a lazily created run loop left behind would keep the thread from hosting a - // runtime. - mln_runtime runtime = MLN_HANDLE_NULL; - const mln_runtime_options options = mln_runtime_options_default(); - probe->create_status = mln_runtime_create(&options, &runtime); - if (probe->create_status == MLN_STATUS_OK) { - (void)mln_runtime_destroy(runtime); + mln_status abandon_status = MLN_STATUS_NATIVE_ERROR; + if (fixture.driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + abandon_busy_probe probe = { + .session = fixture.session, .entered = &entered, .release = &release + }; + mln_test_thread* thread = + mln_test_thread_start(abandon_when_driver_enters, &probe); + size_t serviced = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_service_driver_work( + fixture.session, SIZE_MAX, &serviced + ) + ); + mln_test_thread_join(thread); + abandon_status = probe.status; + } else { + for (unsigned int attempt = 0; attempt < 10000 && !atomic_load(&entered); + attempt += 1) { + mln_test_sleep_millisecond(); + } + TEST_ASSERT_TRUE(atomic_load(&entered)); + mln_render_abandon_result result = { + .size = sizeof(mln_render_abandon_result) + }; + abandon_status = mln_render_session_abandon(fixture.session, &result); + atomic_store(&release, true); } - atomic_store(&probe->finished, true); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_BUSY, abandon_status); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_render_fixture_finish_operation(&fixture, &operation) + ); + mln_test_completion_destroy(&operation); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_SESSION_STATE_ATTACHED, snapshot.state); + + mln_test_render_fixture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); } -// Attaching and rendering leave the thread's MapLibre scheduler state as they -// found it. Runtime creation observes this because it rejects a thread that -// already has a scheduler. -static void render_thread_is_not_poisoned_for_runtime_creation(void) { +static void abandon_completes_pending_work_and_invalidates_accessors(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + + mln_test_completion pending = mln_test_completion_default(0); + bool submitted = false; + if (fixture.driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_render_session_reduce_memory_use(fixture.session, &pending.descriptor) + ); + submitted = true; + } + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(background_style_json)) + MLN_STATUS_OK, mln_render_session_abandon(fixture.session, &abandoned) ); + TEST_ASSERT_TRUE( + abandoned.disposition == MLN_RENDER_ABANDON_DISPOSITION_CLEAN || + abandoned.disposition == MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED + ); + if (submitted) { + TEST_ASSERT_TRUE(mln_test_completion_wait(&pending, 10000)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_TARGET_LOST, mln_test_completion_status(&pending) + ); + } else { + pending.descriptor.release_user_data(pending.descriptor.user_data); + } + mln_test_completion_destroy(&pending); - runtime_after_render_probe probe = {.map = map}; - atomic_init(&probe.finished, false); - mln_test_thread* thread = - mln_test_thread_start(render_then_create_runtime, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.finished)); - mln_test_thread_join(thread); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.create_status); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_SESSION_STATE_ABANDONED, snapshot.state); + mln_acquired_frame frame = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, + mln_render_session_acquire_frame(fixture.session, &frame) + ); + mln_test_render_fixture_destroy(&fixture); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -typedef struct token_probe { - atomic_bool finished; - uint64_t token; -} token_probe; +// A pending still image on a static map must complete while the host's +// keep-alive demands all resolve on the render-if-needed fast path. Worker +// results and the observer delivery that completes the still ride the session +// scheduler; only a rendering demand used to drain it, which stranded this +// exact await shape. +static void still_image_completes_under_if_needed_keepalive_demands(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map_options options = mln_map_options_default(); + options.map_mode = MLN_MAP_MODE_STATIC; + options.initial_extent.width = 64; + options.initial_extent.height = 64; + mln_map map = mln_test_create_map_with_options(runtime, &options); + prepare_renderable_map(runtime, map); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_render_fixture_create(map, &fixture)); + + mln_test_completion still = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_request_still_image(map, &still.descriptor) + ); + + bool completed = false; + for (unsigned int attempt = 0; attempt < 10000 && !completed; attempt += 1) { + mln_frame_demand demand = mln_frame_demand_default(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + mln_render_frame_batch_release(batch); + completed = mln_test_completion_wait(&still, 0); + if (!completed) { + mln_test_sleep_millisecond(); + } + } + TEST_ASSERT_TRUE(completed); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&still)); + mln_test_completion_destroy(&still); -static void read_thread_token(void* argument) { - token_probe* probe = (token_probe*)argument; - probe->token = mln_thread_token(); - atomic_store(&probe->finished, true); + mln_test_render_fixture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); } -// Hosts whose unit of execution is not pinned to a native thread compare this -// token to detect that they moved. -static void thread_tokens_are_stable_and_distinct(void) { - const uint64_t first = mln_thread_token(); - TEST_ASSERT_NOT_EQUAL_UINT64(0, first); - TEST_ASSERT_EQUAL_UINT64(first, mln_thread_token()); +#if defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WEBGL) +static void transferred_offscreen_canvas_runs_on_core_worker(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + prepare_renderable_map(runtime, map); + mln_test_render_fixture fixture = {0}; + TEST_ASSERT_TRUE(mln_test_transferred_webgl_surface_create(map, &fixture)); - token_probe probe = {0}; - atomic_init(&probe.finished, false); - mln_test_thread* thread = mln_test_thread_start(read_thread_token, &probe); - mln_test_thread_join(thread); + mln_render_session_snapshot snapshot = { + .size = sizeof(mln_render_session_snapshot) + }; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_get_snapshot(fixture.session, &snapshot) + ); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_DRIVER_CORE_WORKER, snapshot.driver); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_SESSION_STATE_ATTACHED, snapshot.state); + + mln_frame_demand demand = mln_frame_demand_default(); + demand.flags = 0; + demand.token = 901; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_render_session_request_frame(fixture.session, &demand) + ); + mln_render_frame_batch batch = MLN_HANDLE_NULL; + TEST_ASSERT_TRUE(wait_for_results(&fixture, 1, &batch)); + const mln_render_frame_result result = batch_result(batch, 0); + TEST_ASSERT_EQUAL_UINT32(MLN_RENDER_RESULT_RENDERED, result.disposition); + TEST_ASSERT_EQUAL_UINT64(901, result.token); + mln_render_frame_batch_release(batch); - TEST_ASSERT_NOT_EQUAL_UINT64(0, probe.token); - TEST_ASSERT_NOT_EQUAL_UINT64(first, probe.token); - TEST_ASSERT_EQUAL_UINT64(first, mln_thread_token()); + mln_test_render_fixture_destroy(&fixture); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); } +#endif void run_render_thread_abi_tests(void) { UnitySetTestFile(__FILE__); - RUN_TEST(thread_tokens_are_stable_and_distinct); - RUN_TEST(a_second_thread_attaches_and_renders); - RUN_TEST(session_entry_points_reject_a_foreign_thread); - RUN_TEST(map_destroy_rejects_a_session_owned_by_another_thread); - RUN_TEST(resize_from_the_render_thread_lands_on_the_map_thread); - RUN_TEST(render_thread_is_not_poisoned_for_runtime_creation); + RUN_TEST(attach_reports_the_selected_native_driver); + RUN_TEST(demand_coalescing_preserves_boundaries_and_generations); + RUN_TEST(driver_service_fixes_and_enforces_graphics_thread_identity); + RUN_TEST(abandon_is_busy_during_a_driver_call_and_changes_nothing); + RUN_TEST(frame_wake_runs_when_the_result_queue_becomes_nonempty); + RUN_TEST(texture_ring_leases_apply_backpressure_until_cpu_release); + RUN_TEST(texture_readback_is_an_ordered_owned_operation_result); + RUN_TEST(resize_and_barrier_order_frame_and_extent_generations); + RUN_TEST(abandon_completes_pending_work_and_invalidates_accessors); + RUN_TEST(acquired_frame_release_after_abandon_is_cpu_only); + RUN_TEST(still_image_completes_under_if_needed_keepalive_demands); +#if defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WEBGL) + RUN_TEST(transferred_offscreen_canvas_runs_on_core_worker); +#endif } diff --git a/src/c_api/tests/resources_abi.c b/src/c_api/tests/resources_abi.c index 31323e780..cafede80f 100644 --- a/src/c_api/tests/resources_abi.c +++ b/src/c_api/tests/resources_abi.c @@ -23,43 +23,86 @@ static const char credentialed_unsupported_scheme_style_url[] = static const uint8_t inline_style_json[] = "{\"version\":8,\"sources\":{},\"layers\":[]}"; +static mln_status set_resource_provider_committed( + mln_runtime runtime, const mln_resource_provider* provider +) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = mln_runtime_set_resource_provider( + runtime, provider, &completion.descriptor + ); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + +static mln_status clear_resource_provider_committed(mln_runtime runtime) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = + mln_runtime_clear_resource_provider(runtime, &completion.descriptor); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + +static mln_status set_resource_transform_committed( + mln_runtime runtime, const mln_resource_transform* transform +) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = mln_runtime_set_resource_transform( + runtime, transform, &completion.descriptor + ); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + +static mln_status clear_resource_transform_committed(mln_runtime runtime) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = + mln_runtime_clear_resource_transform(runtime, &completion.descriptor); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + +static mln_status set_http_header_transform_committed( + mln_runtime runtime, const mln_http_header_transform* transform +) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = mln_runtime_set_http_header_transform( + runtime, transform, &completion.descriptor + ); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + +static mln_status clear_http_header_transform_committed(mln_runtime runtime) { + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = + mln_runtime_clear_http_header_transform(runtime, &completion.descriptor); + if (status != MLN_STATUS_OK) mln_test_completion_reject(&completion); + const mln_status result = + status == MLN_STATUS_OK ? mln_test_completion_finish(&completion) : status; + mln_test_completion_destroy(&completion); + return result; +} + static bool wait_for_offline_completion( - mln_runtime runtime, mln_offline_operation_id operation_id + mln_runtime runtime, mln_test_completion* completion ) { - for (size_t attempt = 0; attempt < 5000; attempt += 1) { - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { - return false; - } - while (true) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { - return false; - } - if (batch.event_count == 0) { - break; - } - for (size_t index = 0; index < batch.event_count; index += 1) { - const mln_runtime_event* event = - (const mln_runtime_event*)((const char*)batch.events + - (index * batch.event_size)); - if ( - event->type != MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED || - event->payload_type != - MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED - ) { - continue; - } - if ( - event->payload.offline_operation_completed.operation_id == - operation_id - ) { - return true; - } - } - } - mln_test_sleep_millisecond(); - } - return false; + (void)runtime; + return mln_test_completion_wait(completion, 5000); } static bool wait_for_map_loading_failure( @@ -67,7 +110,7 @@ static bool wait_for_map_loading_failure( size_t out_message_capacity ) { for (size_t attempt = 0; attempt < 5000; attempt += 1) { - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { + if (mln_test_runtime_barrier(runtime) != MLN_STATUS_OK) { return false; } mln_runtime_event event = {0}; @@ -132,6 +175,47 @@ static mln_offline_region_definition offline_geometry_definition( }; } +static bool create_and_activate_offline_region( + mln_runtime runtime, const mln_offline_region_definition* definition, + mln_offline_region_id* out_region_id +) { + const uint8_t metadata[] = {1, 2, 3}; + mln_test_completion creation = + mln_test_completion_default(sizeof(mln_offline_region_info)); + const mln_status submission = mln_runtime_offline_region_create( + runtime, definition, metadata, sizeof(metadata), &creation.descriptor + ); + if (submission != MLN_STATUS_OK) { + mln_test_completion_reject(&creation); + mln_test_completion_destroy(&creation); + return false; + } + if ( + !wait_for_offline_completion(runtime, &creation) || + mln_test_completion_status(&creation) != MLN_STATUS_OK + ) { + mln_test_completion_destroy(&creation); + return false; + } + mln_offline_region_info info = {.size = sizeof(mln_offline_region_info)}; + if (!mln_test_completion_copy_value(&creation, &info, sizeof(info))) { + mln_test_completion_destroy(&creation); + return false; + } + mln_test_completion_destroy(&creation); + + mln_completion download = mln_test_discard_completion(); + if ( + mln_runtime_offline_region_set_download_state( + runtime, info.id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, &download + ) != MLN_STATUS_OK + ) { + return false; + } + if (out_region_id != NULL) *out_region_id = info.id; + return true; +} + static mln_resource_response style_response(void) { return (mln_resource_response){ .size = sizeof(mln_resource_response), @@ -150,6 +234,51 @@ static uint32_t resource_provider_stub( return MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH; } +static void count_runtime_callback_release(void* user_data) { + atomic_fetch_add((atomic_int*)user_data, 1); +} + +static void resource_provider_registration_releases_owned_state(void) { + atomic_int release_count; + atomic_init(&release_count, 0); + const mln_resource_provider provider = { + .size = sizeof(mln_resource_provider), + .callback = resource_provider_stub, + .user_data = &release_count, + .release_user_data = count_runtime_callback_release, + }; + + mln_completion rejected = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, + mln_runtime_set_resource_provider(MLN_HANDLE_NULL, &provider, &rejected) + ); + TEST_ASSERT_EQUAL_INT(0, atomic_load(&release_count)); + + mln_runtime runtime = mln_test_create_runtime(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) + ); + TEST_ASSERT_EQUAL_INT(0, atomic_load(&release_count)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) + ); + TEST_ASSERT_EQUAL_INT(1, atomic_load(&release_count)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, clear_resource_provider_committed(runtime) + ); + TEST_ASSERT_EQUAL_INT(2, atomic_load(&release_count)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) + ); + mln_test_destroy_runtime(runtime); + for (size_t attempt = 0; attempt < 10000 && atomic_load(&release_count) < 3; + attempt += 1) { + mln_test_sleep_millisecond(); + } + TEST_ASSERT_EQUAL_INT(3, atomic_load(&release_count)); +} + typedef struct inline_release_provider_state { atomic_bool callback_finished; atomic_int completion_status; @@ -222,14 +351,13 @@ static void network_status_get_rejects_raw_null_output(void) { static void ambient_cache_operations_validate_raw_operation_values(void) { mln_runtime runtime = mln_test_create_runtime(); - mln_offline_operation_id operation_id = 123; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_run_ambient_cache_operation_start( - runtime, (mln_ambient_cache_operation)999, &operation_id + mln_runtime_run_ambient_cache_operation( + runtime, (mln_ambient_cache_operation)999, &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); mln_test_destroy_runtime(runtime); } @@ -237,7 +365,7 @@ static void set_maximum_ambient_cache_size_rejects_raw_null_output(void) { mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_maximum_ambient_cache_size_start(runtime, 1024, NULL) + mln_runtime_set_maximum_ambient_cache_size(runtime, 1024, NULL) ); mln_test_destroy_runtime(runtime); } @@ -246,98 +374,60 @@ static void offline_regions_reject_raw_invalid_descriptors(void) { mln_runtime runtime = mln_test_create_runtime(); mln_offline_region_definition definition = offline_tile_definition(); const uint8_t metadata[] = {1, 2, 3}; - mln_offline_operation_id operation_id = 123; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_region_create_start( - runtime, NULL, metadata, sizeof(metadata), &operation_id + mln_runtime_offline_region_create( + runtime, NULL, metadata, sizeof(metadata), &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); definition.type = (mln_offline_region_definition_type)999; - operation_id = 123; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id + mln_runtime_offline_region_create( + runtime, &definition, metadata, sizeof(metadata), &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); definition = offline_tile_definition(); definition.data.tile_pyramid.style_url = NULL; - operation_id = 123; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id + mln_runtime_offline_region_create( + runtime, &definition, metadata, sizeof(metadata), &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); const mln_buffer_view geometry = MLN_BUFFER_LITERAL( "{\"type\":\"LineString\",\"coordinates\":[[2,1],[4,3]]}" ); definition = offline_geometry_definition(geometry); definition.data.geometry.style_url = NULL; - operation_id = 123; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id + mln_runtime_offline_region_create( + runtime, &definition, metadata, sizeof(metadata), &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); definition = offline_geometry_definition(geometry); definition.data.geometry.geometry = (mln_buffer_view){0}; - operation_id = 123; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id + mln_runtime_offline_region_create( + runtime, &definition, metadata, sizeof(metadata), &completion ) ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); mln_test_destroy_runtime(runtime); } static void offline_database_merge_rejects_raw_null_path(void) { mln_runtime runtime = mln_test_create_runtime(); - mln_offline_operation_id operation_id = 123; + mln_completion completion = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_offline_regions_merge_database_start( - runtime, NULL, &operation_id - ) - ); - TEST_ASSERT_EQUAL_UINT64(0, operation_id); - mln_test_destroy_runtime(runtime); -} - -static void offline_take_rejects_mismatched_result_kind(void) { - mln_runtime runtime = mln_test_create_runtime(); - const uint8_t metadata[] = {1, 2, 3}; - const mln_offline_region_definition definition = offline_tile_definition(); - mln_offline_operation_id operation_id = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id - ) - ); - TEST_ASSERT_NOT_EQUAL(0, operation_id); - TEST_ASSERT_TRUE(wait_for_offline_completion(runtime, operation_id)); - mln_offline_region_list wrong_list = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_STATE, mln_runtime_offline_regions_list_take_result( - runtime, operation_id, &wrong_list - ) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, wrong_list); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_operation_discard(runtime, operation_id) + mln_runtime_offline_regions_merge_database(runtime, NULL, &completion) ); mln_test_destroy_runtime(runtime); } @@ -346,24 +436,23 @@ static void resource_transform_rejects_raw_invalid_descriptors(void) { mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_clear_resource_transform(MLN_HANDLE_NULL) + clear_resource_transform_committed(MLN_HANDLE_NULL) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_transform(runtime, NULL) + MLN_STATUS_INVALID_ARGUMENT, set_resource_transform_committed(runtime, NULL) ); mln_resource_transform transform = { .size = 0, .callback = resource_transform_stub }; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_transform(runtime, &transform) + set_resource_transform_committed(runtime, &transform) ); transform.size = sizeof(mln_resource_transform); transform.callback = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_transform(runtime, &transform) + set_resource_transform_committed(runtime, &transform) ); mln_test_destroy_runtime(runtime); } @@ -380,14 +469,14 @@ static void http_header_transform_registration_follows_the_transport(void) { #if defined(__EMSCRIPTEN__) || defined(__OHOS__) TEST_ASSERT_EQUAL_INT( MLN_STATUS_UNSUPPORTED, - mln_runtime_set_http_header_transform(runtime, &transform) + set_http_header_transform_committed(runtime, &transform) ); #else TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_http_header_transform(runtime, &transform) + MLN_STATUS_OK, set_http_header_transform_committed(runtime, &transform) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_clear_http_header_transform(runtime) + MLN_STATUS_OK, clear_http_header_transform_committed(runtime) ); #endif mln_test_destroy_runtime(runtime); @@ -398,24 +487,24 @@ static void http_header_transform_rejects_raw_invalid_inputs(void) { mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_clear_http_header_transform(MLN_HANDLE_NULL) + clear_http_header_transform_committed(MLN_HANDLE_NULL) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_http_header_transform(runtime, NULL) + set_http_header_transform_committed(runtime, NULL) ); mln_http_header_transform transform = { .size = 0, .callback = http_header_transform_stub }; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_http_header_transform(runtime, &transform) + set_http_header_transform_committed(runtime, &transform) ); transform.size = sizeof(mln_http_header_transform); transform.callback = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_http_header_transform(runtime, &transform) + set_http_header_transform_committed(runtime, &transform) ); mln_http_header_transform_response response = { @@ -449,17 +538,23 @@ static void http_header_transform_rejects_raw_invalid_inputs(void) { mln_test_destroy_runtime(runtime); } -// The transform callback runs on a MapLibre file source thread and the second -// runtime on its own owner thread, so every field crosses a thread boundary. +// The transform callback and the second runtime executor run concurrently, so +// every field crosses a thread boundary. typedef struct teardown_probe { atomic_bool transform_entered; atomic_bool teardown_started; + atomic_bool transform_released; atomic_bool other_runtime_ready; atomic_bool other_runtime_call_done; atomic_bool other_runtime_call_observed; atomic_int other_runtime_status; } teardown_probe; +static void mark_transform_released(void* user_data) { + teardown_probe* probe = user_data; + atomic_store(&probe->transform_released, true); +} + // Wait budgets are generous on purpose: a slow machine delays the passing run // instead of turning it red. enum { @@ -469,8 +564,8 @@ enum { provider_callback_block_milliseconds = 200, }; -// The provider callback runs on a MapLibre file source thread while the owner -// thread clears the provider, so every field crosses a thread boundary. +// The provider callback runs on a MapLibre file source thread while a runtime +// command clears the provider, so every field crosses a thread boundary. typedef struct provider_quiescence_probe { atomic_bool entered; atomic_bool clear_started; @@ -488,8 +583,8 @@ static bool wait_for_flag(atomic_bool* flag) { return false; } -// Blocks so the per-runtime transform lock stays held while the owner thread -// tears the runtime down. Calls no C API function while blocked. +// Blocks so the per-runtime transform lock stays held while another thread +// starts runtime teardown. Calls no C API function while blocked. static mln_status blocking_resource_transform( void* user_data, uint32_t kind, const char* url, mln_resource_transform_response* out_response @@ -521,7 +616,7 @@ static void other_runtime_entry(void* argument) { teardown_probe* probe = argument; mln_runtime runtime = MLN_HANDLE_NULL; const mln_runtime_options options = mln_runtime_options_default(); - const mln_status create_status = mln_runtime_create(&options, &runtime); + const mln_status create_status = mln_test_runtime_create(&options, &runtime); if (create_status != MLN_STATUS_OK) { atomic_store(&probe->other_runtime_status, create_status); atomic_store(&probe->other_runtime_call_done, true); @@ -530,11 +625,11 @@ static void other_runtime_entry(void* argument) { atomic_store(&probe->other_runtime_ready, true); wait_for_flag(&probe->teardown_started); - // Give the owner thread time to reach the transform wait inside teardown. + // Give the teardown thread time to reach the transform wait. mln_test_sleep_milliseconds(teardown_call_delay_milliseconds); - atomic_store(&probe->other_runtime_status, mln_runtime_pump(runtime, 0, -1)); + atomic_store(&probe->other_runtime_status, mln_test_runtime_barrier(runtime)); atomic_store(&probe->other_runtime_call_done, true); - mln_runtime_destroy(runtime); + (void)mln_test_runtime_close(runtime); } // Creates a region for `style_url` and activates its download, which requests @@ -545,54 +640,17 @@ static bool activate_style_download( ) { const mln_offline_region_definition definition = offline_tile_definition_for_style(style_url); - const uint8_t metadata[] = {1, 2, 3}; - mln_offline_operation_id operation_id = 0; - if ( - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id - ) != MLN_STATUS_OK || - !wait_for_offline_completion(runtime, operation_id) - ) { - return false; - } - - mln_offline_region_snapshot snapshot = MLN_HANDLE_NULL; - if ( - mln_runtime_offline_region_create_take_result( - runtime, operation_id, &snapshot - ) != MLN_STATUS_OK - ) { - return false; - } - mln_offline_region_info info = {.size = sizeof(mln_offline_region_info)}; - const mln_status info_status = - mln_offline_region_snapshot_get(snapshot, &info); - const mln_offline_region_id region_id = info.id; - mln_offline_region_snapshot_destroy(snapshot); - if (info_status != MLN_STATUS_OK) { - return false; - } - - mln_offline_operation_id download_operation_id = 0; - if ( - mln_runtime_offline_region_set_download_state_start( - runtime, region_id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, - &download_operation_id - ) != MLN_STATUS_OK - ) { - return false; - } - return true; + return create_and_activate_offline_region(runtime, &definition, NULL); } -// Pumps the owner thread's run loop until `flag` is set by a MapLibre thread. -static bool pump_until_flag(mln_runtime runtime, atomic_bool* flag) { +// Waits until `flag` is set by a MapLibre thread. +static bool wait_until_flag(mln_runtime runtime, atomic_bool* flag) { for (size_t attempt = 0; attempt < teardown_probe_wait_attempts; attempt += 1) { if (atomic_load(flag)) { return true; } - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { + if (mln_test_runtime_barrier(runtime) != MLN_STATUS_OK) { return false; } mln_test_sleep_millisecond(); @@ -604,7 +662,7 @@ static bool start_offline_region_download( mln_runtime runtime, teardown_probe* probe ) { return activate_style_download(runtime, offline_style_url) && - pump_until_flag(runtime, &probe->transform_entered); + wait_until_flag(runtime, &probe->transform_entered); } // Runtime teardown waits for an in-flight resource transform callback without @@ -623,9 +681,10 @@ static void runtime_teardown_leaves_other_runtimes_responsive(void) { .size = sizeof(mln_resource_transform), .callback = blocking_resource_transform, .user_data = &probe, + .release_user_data = mark_transform_released, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_transform(runtime, &transform) + MLN_STATUS_OK, set_resource_transform_committed(runtime, &transform) ); TEST_ASSERT_TRUE(start_offline_region_download(runtime, &probe)); @@ -633,6 +692,7 @@ static void runtime_teardown_leaves_other_runtimes_responsive(void) { // Teardown blocks here until the transform callback returns. mln_test_destroy_runtime(runtime); mln_test_thread_join(other_thread); + TEST_ASSERT_TRUE(wait_for_flag(&probe.transform_released)); TEST_ASSERT_TRUE_MESSAGE( atomic_load(&probe.other_runtime_call_observed), @@ -688,8 +748,8 @@ static mln_status lookup_blocking_transform( } // Runs on the file source thread immediately before that thread looks up the -// resource transform: blocking here parks the thread until the owner thread has -// a transform writer waiting, and passing the request through then enters the +// resource transform. Blocking here parks the thread until the runtime worker +// has a transform writer waiting; passing the request through then enters the // lookup under test. static uint32_t lookup_probe_resource_provider( void* user_data, const mln_resource_request* request, @@ -712,7 +772,7 @@ static uint32_t lookup_probe_resource_provider( } mln_test_sleep_millisecond(); } - // Give the owner thread time to reach the exclusive transform lock, so the + // Give the runtime worker time to reach the exclusive transform lock, so the // lookup this thread is about to make queues behind a waiting writer. mln_test_sleep_milliseconds(teardown_call_delay_milliseconds); atomic_store(&probe->lookup_reached, true); @@ -725,7 +785,7 @@ static void lookup_other_runtime_entry(void* argument) { lookup_probe* probe = argument; mln_runtime runtime = MLN_HANDLE_NULL; const mln_runtime_options options = mln_runtime_options_default(); - const mln_status create_status = mln_runtime_create(&options, &runtime); + const mln_status create_status = mln_test_runtime_create(&options, &runtime); if (create_status != MLN_STATUS_OK) { atomic_store(&probe->other_runtime_status, create_status); atomic_store(&probe->other_runtime_call_done, true); @@ -736,9 +796,9 @@ static void lookup_other_runtime_entry(void* argument) { wait_for_flag(&probe->lookup_reached); // Give the file source thread time to reach the lookup itself. mln_test_sleep_milliseconds(teardown_call_delay_milliseconds); - atomic_store(&probe->other_runtime_status, mln_runtime_pump(runtime, 0, -1)); + atomic_store(&probe->other_runtime_status, mln_test_runtime_barrier(runtime)); atomic_store(&probe->other_runtime_call_done, true); - mln_runtime_destroy(runtime); + (void)mln_test_runtime_close(runtime); } // A file source resource transform lookup releases the process-global runtime @@ -759,7 +819,7 @@ static void resource_transform_lookup_leaves_other_runtimes_responsive(void) { .user_data = &probe, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) ); const mln_resource_transform transform = { .size = sizeof(mln_resource_transform), @@ -767,23 +827,23 @@ static void resource_transform_lookup_leaves_other_runtimes_responsive(void) { .user_data = &probe, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_transform(runtime, &transform) + MLN_STATUS_OK, set_resource_transform_committed(runtime, &transform) ); // The first download parks a transform callback inside the shared lock. TEST_ASSERT_TRUE(activate_style_download(runtime, lookup_blocking_style_url)); - TEST_ASSERT_TRUE(pump_until_flag(runtime, &probe.transform_entered)); + TEST_ASSERT_TRUE(wait_until_flag(runtime, &probe.transform_entered)); // The second download parks a file source thread in the provider callback, // one step ahead of the lookup under test. TEST_ASSERT_TRUE(activate_style_download(runtime, lookup_probe_style_url)); - TEST_ASSERT_TRUE(pump_until_flag(runtime, &probe.provider_entered)); + TEST_ASSERT_TRUE(wait_until_flag(runtime, &probe.provider_entered)); atomic_store(&probe.writer_pending, true); // Clearing waits for the in-flight transform callback to return, so it is // the pending writer the lookup queues behind. TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_clear_resource_transform(runtime) + MLN_STATUS_OK, clear_resource_transform_committed(runtime) ); mln_test_thread_join(other_thread); @@ -804,32 +864,31 @@ static void resource_transform_lookup_leaves_other_runtimes_responsive(void) { static void resource_provider_rejects_raw_invalid_descriptors(void) { mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_provider(runtime, NULL) + MLN_STATUS_INVALID_ARGUMENT, set_resource_provider_committed(runtime, NULL) ); mln_resource_provider provider = { .size = 0, .callback = resource_provider_stub }; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_provider(runtime, &provider) + set_resource_provider_committed(runtime, &provider) ); provider.size = sizeof(mln_resource_provider); provider.callback = NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_set_resource_provider(runtime, &provider) + set_resource_provider_committed(runtime, &provider) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_clear_resource_provider(MLN_HANDLE_NULL) + clear_resource_provider_committed(MLN_HANDLE_NULL) ); mln_test_destroy_runtime(runtime); } // Blocks inside the provider callback so the per-runtime provider lock stays -// held while the owner thread clears the provider. It calls no C API function -// while blocked. +// held while the runtime executor clears the provider. It calls no C API +// function while blocked. static uint32_t blocking_resource_provider_for_clear( void* user_data, const mln_resource_request* request, mln_resource_request_handle handle @@ -845,8 +904,8 @@ static uint32_t blocking_resource_provider_for_clear( } mln_test_sleep_millisecond(); } - // Keep running past the clear call so a clear that skips the wait returns - // while this callback is still using its user data. + // Keep running after clear submission so an implementation that emits the + // terminal event too early exposes callback-owned state. mln_test_sleep_milliseconds(provider_callback_block_milliseconds); atomic_store(&probe->callback_returned, true); return MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH; @@ -859,41 +918,7 @@ static bool wait_for_clear_provider_callback( mln_runtime runtime, provider_quiescence_probe* probe ) { const mln_offline_region_definition definition = offline_tile_definition(); - const uint8_t metadata[] = {1, 2, 3}; - mln_offline_operation_id operation_id = 0; - if ( - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id - ) != MLN_STATUS_OK || - !wait_for_offline_completion(runtime, operation_id) - ) { - return false; - } - - mln_offline_region_snapshot snapshot = MLN_HANDLE_NULL; - if ( - mln_runtime_offline_region_create_take_result( - runtime, operation_id, &snapshot - ) != MLN_STATUS_OK - ) { - return false; - } - mln_offline_region_info info = {.size = sizeof(mln_offline_region_info)}; - const mln_status info_status = - mln_offline_region_snapshot_get(snapshot, &info); - const mln_offline_region_id region_id = info.id; - mln_offline_region_snapshot_destroy(snapshot); - if (info_status != MLN_STATUS_OK) { - return false; - } - - mln_offline_operation_id download_operation_id = 0; - if ( - mln_runtime_offline_region_set_download_state_start( - runtime, region_id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, - &download_operation_id - ) != MLN_STATUS_OK - ) { + if (!create_and_activate_offline_region(runtime, &definition, NULL)) { return false; } @@ -902,16 +927,75 @@ static bool wait_for_clear_provider_callback( if (atomic_load(&probe->entered)) { return true; } - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { + if (mln_test_runtime_barrier(runtime) != MLN_STATUS_OK) { return false; } mln_test_sleep_millisecond(); } return false; } +typedef struct cross_thread_provider_submission { + mln_runtime runtime; + mln_resource_provider provider; + mln_status status; + mln_test_completion completion; +} cross_thread_provider_submission; + +static void submit_provider_from_thread(void* user_data) { + cross_thread_provider_submission* submission = user_data; + submission->status = mln_runtime_set_resource_provider( + submission->runtime, &submission->provider, + &submission->completion.descriptor + ); +} + +static uint32_t recording_resource_provider( + void* user_data, const mln_resource_request* request, + mln_resource_request_handle handle +) { + (void)request; + (void)handle; + provider_quiescence_probe* probe = user_data; + atomic_store(&probe->entered, true); + return MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH; +} + +static void resource_provider_command_copies_cross_thread_descriptor(void) { + provider_quiescence_probe probe = {0}; + mln_runtime runtime = mln_test_create_runtime(); + cross_thread_provider_submission submission = { + .runtime = runtime, + .provider = + { + .size = sizeof(mln_resource_provider), + .callback = recording_resource_provider, + .user_data = &probe, + }, + .status = MLN_STATUS_NATIVE_ERROR, + .completion = mln_test_completion_default(0), + }; + mln_test_thread* thread = + mln_test_thread_start(submit_provider_from_thread, &submission); + TEST_ASSERT_NOT_NULL(thread); + mln_test_thread_join(thread); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, submission.status); + + // The accepted command owns the descriptor shape, not this binding storage. + submission.provider.callback = NULL; + submission.provider.user_data = NULL; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_completion_finish(&submission.completion) + ); + mln_test_completion_destroy(&submission.completion); + TEST_ASSERT_TRUE(wait_for_clear_provider_callback(runtime, &probe)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, clear_resource_provider_committed(runtime) + ); + mln_test_destroy_runtime(runtime); +} -// Clearing the resource provider waits for a provider callback already running, -// so the callback and its user data are unreferenced once the clear returns. +// The clear command reaches its terminal event only after a provider callback +// that was already running returns. static void clearing_resource_provider_waits_for_in_flight_callback(void) { provider_quiescence_probe probe = {0}; mln_runtime runtime = mln_test_create_runtime(); @@ -921,19 +1005,18 @@ static void clearing_resource_provider_waits_for_in_flight_callback(void) { .user_data = &probe, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) ); TEST_ASSERT_TRUE(wait_for_clear_provider_callback(runtime, &probe)); atomic_store(&probe.clear_started, true); - // The clear blocks here until the provider callback returns. + // The terminal event waits until the provider callback returns. TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_clear_resource_provider(runtime) + MLN_STATUS_OK, clear_resource_provider_committed(runtime) ); TEST_ASSERT_TRUE_MESSAGE( atomic_load(&probe.callback_returned), - "clearing the resource provider returned while a provider callback was " - "still running" + "the clear completion ran while a provider callback was still running" ); mln_test_destroy_runtime(runtime); @@ -943,7 +1026,7 @@ static void unsupported_style_url_scheme_names_scheme_and_url(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_url(map, unsupported_scheme_style_url) + MLN_STATUS_OK, mln_test_map_set_style_url(map, unsupported_scheme_style_url) ); char message[512]; TEST_ASSERT_TRUE( @@ -961,7 +1044,7 @@ static void unsupported_style_url_diagnostic_redacts_credentials(void) { mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_url(map, credentialed_unsupported_scheme_style_url) + mln_test_map_set_style_url(map, credentialed_unsupported_scheme_style_url) ); char message[512]; TEST_ASSERT_TRUE( @@ -983,11 +1066,11 @@ static void unsupported_style_url_names_declining_provider(void) { .callback = resource_provider_stub, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) ); mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_url(map, unsupported_scheme_style_url) + MLN_STATUS_OK, mln_test_map_set_style_url(map, unsupported_scheme_style_url) ); char message[512]; TEST_ASSERT_TRUE( @@ -1013,15 +1096,15 @@ static void resource_provider_defers_inline_release_until_callback_returns( .user_data = &state, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) ); mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_url(map, "custom://inline-style.json") + MLN_STATUS_OK, mln_test_map_set_style_url(map, "custom://inline-style.json") ); for (size_t attempt = 0; attempt < 5000 && !atomic_load(&state.callback_finished); attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); mln_test_sleep_millisecond(); } TEST_ASSERT_TRUE(atomic_load(&state.callback_finished)); @@ -1034,8 +1117,14 @@ typedef struct provider_teardown_probe { atomic_bool entered; atomic_bool teardown_started; atomic_bool callback_returned; + atomic_bool released; } provider_teardown_probe; +static void mark_provider_released(void* user_data) { + provider_teardown_probe* probe = user_data; + atomic_store(&probe->released, true); +} + enum { provider_teardown_wait_attempts = 10000, provider_teardown_block_milliseconds = 200, @@ -1074,41 +1163,7 @@ static bool wait_for_provider_callback( mln_runtime runtime, provider_teardown_probe* probe ) { const mln_offline_region_definition definition = offline_tile_definition(); - const uint8_t metadata[] = {1, 2, 3}; - mln_offline_operation_id operation_id = 0; - if ( - mln_runtime_offline_region_create_start( - runtime, &definition, metadata, sizeof(metadata), &operation_id - ) != MLN_STATUS_OK || - !wait_for_offline_completion(runtime, operation_id) - ) { - return false; - } - - mln_offline_region_snapshot snapshot = MLN_HANDLE_NULL; - if ( - mln_runtime_offline_region_create_take_result( - runtime, operation_id, &snapshot - ) != MLN_STATUS_OK - ) { - return false; - } - mln_offline_region_info info = {.size = sizeof(mln_offline_region_info)}; - const mln_status info_status = - mln_offline_region_snapshot_get(snapshot, &info); - const mln_offline_region_id region_id = info.id; - mln_offline_region_snapshot_destroy(snapshot); - if (info_status != MLN_STATUS_OK) { - return false; - } - - mln_offline_operation_id download_operation_id = 0; - if ( - mln_runtime_offline_region_set_download_state_start( - runtime, region_id, MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE, - &download_operation_id - ) != MLN_STATUS_OK - ) { + if (!create_and_activate_offline_region(runtime, &definition, NULL)) { return false; } @@ -1117,7 +1172,7 @@ static bool wait_for_provider_callback( if (atomic_load(&probe->entered)) { return true; } - if (mln_runtime_pump(runtime, 0, -1) != MLN_STATUS_OK) { + if (mln_test_runtime_barrier(runtime) != MLN_STATUS_OK) { return false; } mln_test_sleep_millisecond(); @@ -1125,8 +1180,8 @@ static bool wait_for_provider_callback( return false; } -// The provider callback and borrowed user_data remain valid until runtime -// destruction returns, including callbacks already running on worker threads. +// Native retains provider user_data until every in-flight callback returns, +// even though public runtime release itself does not wait for teardown. static void runtime_teardown_waits_for_in_flight_provider_callback(void) { provider_teardown_probe probe = {0}; mln_runtime runtime = mln_test_create_runtime(); @@ -1134,14 +1189,16 @@ static void runtime_teardown_waits_for_in_flight_provider_callback(void) { .size = sizeof(mln_resource_provider), .callback = blocking_resource_provider, .user_data = &probe, + .release_user_data = mark_provider_released, }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) + MLN_STATUS_OK, set_resource_provider_committed(runtime, &provider) ); TEST_ASSERT_TRUE(wait_for_provider_callback(runtime, &probe)); atomic_store(&probe.teardown_started, true); mln_test_destroy_runtime(runtime); + TEST_ASSERT_TRUE(wait_for_flag(&probe.released)); TEST_ASSERT_TRUE_MESSAGE( atomic_load(&probe.callback_returned), "runtime teardown returned while a provider callback was still running" @@ -1156,13 +1213,14 @@ void run_resources_abi_tests(void) { RUN_TEST(set_maximum_ambient_cache_size_rejects_raw_null_output); RUN_TEST(offline_regions_reject_raw_invalid_descriptors); RUN_TEST(offline_database_merge_rejects_raw_null_path); - RUN_TEST(offline_take_rejects_mismatched_result_kind); RUN_TEST(resource_transform_rejects_raw_invalid_descriptors); RUN_TEST(http_header_transform_registration_follows_the_transport); RUN_TEST(http_header_transform_rejects_raw_invalid_inputs); RUN_TEST(runtime_teardown_leaves_other_runtimes_responsive); RUN_TEST(resource_transform_lookup_leaves_other_runtimes_responsive); RUN_TEST(resource_provider_rejects_raw_invalid_descriptors); + RUN_TEST(resource_provider_registration_releases_owned_state); + RUN_TEST(resource_provider_command_copies_cross_thread_descriptor); RUN_TEST(clearing_resource_provider_waits_for_in_flight_callback); RUN_TEST(unsupported_style_url_scheme_names_scheme_and_url); RUN_TEST(unsupported_style_url_diagnostic_redacts_credentials); diff --git a/src/c_api/tests/runtime_events_abi.c b/src/c_api/tests/runtime_events_abi.c index a2f35a87b..1124e0826 100644 --- a/src/c_api/tests/runtime_events_abi.c +++ b/src/c_api/tests/runtime_events_abi.c @@ -1,6 +1,6 @@ // Raw C ABI coverage for the batch event drain and the two subscription masks: -// batch layout and lifetime, bounded drains, mask validation, and the -// suppression a cleared mask bit causes at push time. +// batch layout and lifetime, mask validation, and the suppression a cleared +// mask bit causes at push time. #include #include @@ -17,11 +17,7 @@ static const char background_style_json[] = "{\"version\":8,\"sources\":{},\"layers\":[{\"id\":\"background\",\"type\":" "\"background\",\"paint\":{\"background-color\":\"#102030\"}}]}"; static const uint64_t unknown_mask_bit = UINT64_C(1) << 63U; -static const size_t style_pump_attempts = 200; -static const int64_t park_timeout_milliseconds = 200; -static const uint64_t park_floor_milliseconds = 100; -static const char missing_database_path[] = "does-not-exist.db"; -static const size_t take_result_attempts = 5000; +static const size_t style_barrier_attempts = 200; // The record layout every binding probes. A host reads a batch as two byte // ranges at these offsets, so a change here is an ABI break. @@ -61,10 +57,6 @@ static_assert( sizeof(mln_runtime_event_offline_region_tile_count_limit) == 16, "mln_runtime_event_offline_region_tile_count_limit is 16 bytes wide" ); -static_assert( - sizeof(mln_runtime_event_offline_operation_completed) == 24, - "mln_runtime_event_offline_operation_completed is 24 bytes wide" -); static_assert( sizeof(mln_runtime_event_payload) == 72, "mln_runtime_event_payload is 72 bytes wide" @@ -74,7 +66,7 @@ static_assert( "mln_runtime_event_payload is 8-byte aligned" ); static_assert( - sizeof(mln_runtime_event) == 104, "mln_runtime_event is 104 bytes wide" + sizeof(mln_runtime_event) == 112, "mln_runtime_event is 112 bytes wide" ); static_assert( _Alignof(mln_runtime_event) == 8, "mln_runtime_event is 8-byte aligned" @@ -104,12 +96,12 @@ static_assert( "mln_runtime_event.message_offset sits at offset 24" ); static_assert( - offsetof(mln_runtime_event, message_size) == 28, - "mln_runtime_event.message_size sits at offset 28" + offsetof(mln_runtime_event, message_size) == 32, + "mln_runtime_event.message_size sits at offset 32" ); static_assert( - offsetof(mln_runtime_event, payload) == 32, - "mln_runtime_event.payload sits at offset 32" + offsetof(mln_runtime_event, payload) == 40, + "mln_runtime_event.payload sits at offset 40" ); static_assert( sizeof(mln_map_options) == 40, "mln_map_options is 40 bytes wide" @@ -123,51 +115,54 @@ static_assert( "mln_runtime_options.flags sits at offset 4" ); static_assert( - MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS == UINT64_C(0x87FFFE), - "MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS is 0x87FFFE" + MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS == UINT64_C(0x47FFFE), + "MLN_RUNTIME_EVENT_MASK_ALL_MAP_EVENTS is 0x47FFFE" ); static_assert( - MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS == UINT64_C(0x780000), - "MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS is 0x780000" + MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS == UINT64_C(0x380000), + "MLN_RUNTIME_EVENT_MASK_ALL_RUNTIME_EVENTS is 0x380000" ); static_assert( - MLN_RUNTIME_EVENT_MASK_ALL == UINT64_C(0xFFFFFE), - "MLN_RUNTIME_EVENT_MASK_ALL is 0xFFFFFE" + MLN_RUNTIME_EVENT_MASK_ALL == UINT64_C(0x7FFFFE), + "MLN_RUNTIME_EVENT_MASK_ALL is 0x7FFFFE" ); -// These three carry pointers or size_t, so their extents follow the target's -// pointer width. Every field above is fixed width, so one probed offset table -// serves every ABI, wasm32 included. +// These structs carry pointers or size_t, so their extents follow the target's +// pointer width. #if UINTPTR_MAX == UINT64_MAX static_assert( - sizeof(mln_runtime_event_batch) == 48, - "mln_runtime_event_batch is 48 bytes wide" + sizeof(mln_runtime_event_batch_view) == 40, + "mln_runtime_event_batch_view is 40 bytes wide" ); static_assert( - sizeof(mln_runtime_options) == 32, "mln_runtime_options is 32 bytes wide" + sizeof(mln_runtime_options) == 64, "mln_runtime_options is 64 bytes wide" ); static_assert( offsetof(mln_runtime_options, event_mask) == 24, "mln_runtime_options.event_mask sits at offset 24" ); +static_assert( + offsetof(mln_runtime_options, event_wake) == 32, + "mln_runtime_options.event_wake sits at offset 32" +); #endif static const mln_runtime_event* batch_event( - const mln_runtime_event_batch* batch, size_t index + const mln_test_event_batch* batch, size_t index ) { return (const mln_runtime_event*)((const char*)batch->events + (index * batch->event_size)); } -// Loads an inline style and pumps until its events are queued, so a test that +// Loads an inline style and waits until its events are queued, so a test that // needs a populated queue does not depend on the network. -static void load_style_and_pump(mln_runtime runtime, mln_map map) { +static void load_style_and_wait(mln_runtime runtime, mln_map map) { TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(background_style_json)) + mln_test_map_set_style_json(map, MLN_BUFFER_LITERAL(background_style_json)) ); - for (size_t attempt = 0; attempt < style_pump_attempts; attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 2, -1)); + for (size_t attempt = 0; attempt < style_barrier_attempts; attempt += 1) { + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); } } @@ -177,24 +172,21 @@ static mln_map_options map_options_with_event_mask(uint64_t mask) { return options; } -// A batch header carries its own size, so a host built against an older header -// passes a shorter record than this one. Rejecting it keeps the drain from -// writing fields the caller does not own. -static void a_drain_rejects_an_undersized_batch_or_a_stale_runtime(void) { +// An owned output must start null, and a retired runtime accepts no new drain. +static void a_drain_rejects_a_nonnull_output_or_a_stale_runtime(void) { mln_runtime runtime = mln_test_create_runtime(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, 0, NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, NULL) ); - mln_runtime_event_batch small = mln_runtime_event_batch_default(); - small.size = sizeof(mln_runtime_event_batch) - 1; + mln_event_batch batch = UINT64_C(1); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, 0, &small) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, &batch) ); mln_test_destroy_runtime(runtime); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); + batch = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, 0, &batch) + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_drain_events(runtime, &batch) ); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, @@ -213,16 +205,17 @@ typedef struct foreign_thread_probe { static void call_event_api_from_a_foreign_thread(void* argument) { foreign_thread_probe* probe = argument; - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - probe->drain_status = mln_runtime_drain_events(probe->runtime, 0, &batch); + mln_event_batch batch = MLN_HANDLE_NULL; + probe->drain_status = mln_runtime_drain_events(probe->runtime, &batch); + mln_event_batch_release(batch); probe->runtime_mask_status = mln_runtime_set_event_mask(probe->runtime, MLN_RUNTIME_EVENT_MASK_ALL); probe->map_mask_status = - mln_map_set_event_mask(probe->map, MLN_RUNTIME_EVENT_MASK_ALL); + mln_test_map_set_event_mask(probe->map, MLN_RUNTIME_EVENT_MASK_ALL); atomic_store(&probe->finished, true); } -static void event_entry_points_reject_a_foreign_thread(void) { +static void event_drain_and_mask_changes_are_any_thread(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); @@ -230,12 +223,11 @@ static void event_entry_points_reject_a_foreign_thread(void) { atomic_init(&probe.finished, false); mln_test_thread* thread = mln_test_thread_start(call_event_api_from_a_foreign_thread, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.finished)); mln_test_thread_join(thread); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.drain_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.runtime_mask_status); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_WRONG_THREAD, probe.map_mask_status); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.drain_status); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.runtime_mask_status); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.map_mask_status); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); @@ -252,7 +244,8 @@ static void both_mask_setters_reject_unknown_bits_and_keep_foreign_ones(void) { mln_runtime_set_event_mask(runtime, unknown_mask_bit) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_event_mask(map, unknown_mask_bit) + MLN_STATUS_INVALID_ARGUMENT, + mln_test_map_set_event_mask(map, unknown_mask_bit) ); TEST_ASSERT_EQUAL_INT( @@ -260,7 +253,7 @@ static void both_mask_setters_reject_unknown_bits_and_keep_foreign_ones(void) { mln_runtime_set_event_mask(runtime, MLN_RUNTIME_EVENT_MASK_ALL) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL) + MLN_STATUS_OK, mln_test_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL) ); uint64_t runtime_mask = 0; @@ -269,7 +262,9 @@ static void both_mask_setters_reject_unknown_bits_and_keep_foreign_ones(void) { ); TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_ALL, runtime_mask); uint64_t map_mask = 0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_get_event_mask(map, &map_mask)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_map_get_event_mask(map, &map_mask) + ); TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_ALL, map_mask); // One in-group bit for the other source kind, which each setter accepts and @@ -289,7 +284,7 @@ static void both_mask_setters_reject_unknown_bits_and_keep_foreign_ones(void) { MLN_STATUS_INVALID_ARGUMENT, mln_runtime_get_event_mask(runtime, NULL) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_event_mask(map, NULL) + MLN_STATUS_INVALID_ARGUMENT, mln_test_map_get_event_mask(map, NULL) ); mln_test_destroy_map(map); @@ -306,10 +301,12 @@ static void a_fresh_map_and_runtime_select_every_event_type(void) { ); TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_ALL, runtime_mask); uint64_t map_mask = 0; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_get_event_mask(map, &map_mask)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_map_get_event_mask(map, &map_mask) + ); TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_ALL, map_mask); - load_style_and_pump(runtime, map); + load_style_and_wait(runtime, map); TEST_ASSERT_GREATER_THAN_size_t( 0, mln_test_drain_counting(runtime, MLN_RUNTIME_EVENT_MAP_STYLE_LOADED) ); @@ -324,7 +321,7 @@ static void runtime_options_reject_unknown_flags(void) { mln_runtime bad_runtime = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_create(&runtime_options, &bad_runtime) + mln_test_runtime_create(&runtime_options, &bad_runtime) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, bad_runtime); } @@ -339,7 +336,7 @@ static void options_reject_unknown_event_mask_bits(void) { mln_runtime bad_runtime = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_create(&runtime_options, &bad_runtime) + mln_test_runtime_create(&runtime_options, &bad_runtime) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, bad_runtime); @@ -348,7 +345,8 @@ static void options_reject_unknown_event_mask_bits(void) { map_options.event_mask = MLN_RUNTIME_EVENT_MASK_ALL | unknown; mln_map bad_map = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_create(runtime, &map_options, &bad_map) + MLN_STATUS_INVALID_ARGUMENT, + mln_test_map_create_status(runtime, &map_options, &bad_map) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, bad_map); @@ -356,7 +354,7 @@ static void options_reject_unknown_event_mask_bits(void) { mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL | unknown) + mln_test_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL | unknown) ); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); @@ -370,16 +368,16 @@ static void a_creation_mask_applies_during_construction(void) { mln_map map = mln_test_create_map_with_options(runtime, &options); uint64_t map_mask = MLN_RUNTIME_EVENT_MASK_ALL; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_get_event_mask(map, &map_mask)); - TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED, map_mask); - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) + MLN_STATUS_OK, mln_test_map_get_event_mask(map, &map_mask) ); + TEST_ASSERT_EQUAL_UINT64(MLN_RUNTIME_EVENT_MASK_MAP_STYLE_LOADED, map_mask); + + mln_test_event_batch batch = mln_test_event_batch_default(); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_drain_events(runtime, &batch)); TEST_ASSERT_EQUAL_size_t(0, batch.event_count); - load_style_and_pump(runtime, map); + load_style_and_wait(runtime, map); TEST_ASSERT_EQUAL_size_t( 0, mln_test_drain_counting( runtime, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE @@ -397,14 +395,14 @@ static void clearing_one_type_leaves_the_others_arriving(void) { mln_map map = mln_test_create_map(runtime); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, - mln_map_set_event_mask( + mln_test_map_set_event_mask( map, MLN_RUNTIME_EVENT_MASK_ALL & ~(uint64_t)MLN_RUNTIME_EVENT_MASK_MAP_RENDER_UPDATE_AVAILABLE ) ); mln_test_drain_all(runtime); - load_style_and_pump(runtime, map); + load_style_and_wait(runtime, map); TEST_ASSERT_EQUAL_size_t( 0, mln_test_drain_counting( runtime, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE @@ -412,9 +410,10 @@ static void clearing_one_type_leaves_the_others_arriving(void) { ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL) + MLN_STATUS_OK, mln_test_map_set_event_mask(map, MLN_RUNTIME_EVENT_MASK_ALL) ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_request_repaint(map)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_map_request_repaint(map)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); TEST_ASSERT_GREATER_THAN_size_t( 0, mln_test_drain_counting( runtime, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE @@ -425,9 +424,9 @@ static void clearing_one_type_leaves_the_others_arriving(void) { mln_test_destroy_runtime(runtime); } -// Suppression happens at push time, so a cleared type raises no wake flag and a -// parking pump waits out its timeout where it previously returned immediately. -static void a_suppressed_producer_leaves_a_pump_parked(void) { +// Suppression happens at push time, so a cleared type produces no queue record +// even though the runtime worker continues to process the command. +static void a_suppressed_producer_leaves_the_queue_empty(void) { mln_runtime runtime = mln_test_create_runtime(); const mln_map_options options = map_options_with_event_mask(MLN_RUNTIME_EVENT_MASK_NONE); @@ -436,109 +435,91 @@ static void a_suppressed_producer_leaves_a_pump_parked(void) { MLN_STATUS_OK, mln_runtime_set_event_mask(runtime, MLN_RUNTIME_EVENT_MASK_NONE) ); - // Two zero pumps and a drain leave the wake flag clear and the queue empty. - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - TEST_ASSERT_EQUAL_size_t(0, mln_test_drain_all(runtime)); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); TEST_ASSERT_EQUAL_size_t(0, mln_test_drain_all(runtime)); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_request_repaint(map)); - const uint64_t started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - const uint64_t elapsed = mln_test_monotonic_milliseconds() - started; - TEST_ASSERT_TRUE_MESSAGE( - elapsed >= park_floor_milliseconds, - "A repaint whose event type is cleared still released a parked pump." - ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_map_request_repaint(map)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); TEST_ASSERT_EQUAL_size_t(0, mln_test_drain_all(runtime)); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -static void a_batch_reports_this_headers_stride_and_ends_the_previous(void) { +static void an_owned_batch_remains_stable_across_later_drains(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); - load_style_and_pump(runtime, map); + load_style_and_wait(runtime, map); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); + mln_event_batch first = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) + MLN_STATUS_OK, mln_runtime_drain_events(runtime, &first) ); - TEST_ASSERT_EQUAL_UINT32(sizeof(mln_runtime_event), batch.event_size); - TEST_ASSERT_GREATER_THAN_size_t(1, batch.event_count); - TEST_ASSERT_EQUAL_size_t(0, batch.remaining_count); - TEST_ASSERT_NOT_NULL(batch.events); + mln_runtime_event_batch_view first_view = { + .size = sizeof(mln_runtime_event_batch_view) + }; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_event_batch_get(first, &first_view)); + TEST_ASSERT_EQUAL_UINT32(sizeof(mln_runtime_event), first_view.event_size); + TEST_ASSERT_GREATER_THAN_size_t(1, first_view.event_count); + const size_t first_count = first_view.event_count; + const mln_runtime_event first_event = first_view.events[0]; - // A drain that finds nothing reports an empty batch and ends the previous - // batch's readable window. + mln_event_batch second = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) + MLN_STATUS_OK, mln_runtime_drain_events(runtime, &second) ); - TEST_ASSERT_EQUAL_size_t(0, batch.event_count); - TEST_ASSERT_NULL(batch.events); - TEST_ASSERT_NULL(batch.messages); - TEST_ASSERT_EQUAL_size_t(0, batch.messages_size); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void a_bounded_drain_reports_what_stayed_queued(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - load_style_and_pump(runtime, map); - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); + mln_runtime_event_batch_view second_view = { + .size = sizeof(mln_runtime_event_batch_view) + }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 1, &batch) + MLN_STATUS_OK, mln_event_batch_get(second, &second_view) ); - TEST_ASSERT_EQUAL_size_t(1, batch.event_count); - TEST_ASSERT_GREATER_THAN_size_t(0, batch.remaining_count); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); - TEST_ASSERT_GREATER_THAN_size_t(0, batch.event_count); - TEST_ASSERT_EQUAL_size_t(0, batch.remaining_count); + TEST_ASSERT_EQUAL_size_t(0, second_view.event_count); + TEST_ASSERT_EQUAL_size_t(first_count, first_view.event_count); + TEST_ASSERT_EQUAL_UINT32(first_event.type, first_view.events[0].type); + mln_event_batch_release(second); + mln_event_batch_release(first); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -// A batch holds copies on the runtime, so destroying the map whose events it -// carries leaves it readable. -static void a_batch_outlives_the_map_that_produced_it(void) { +// Closing a source prevents future publication without rewriting history that +// is already queued. +static void queued_events_outlive_the_map_that_produced_them(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); - load_style_and_pump(runtime, map); + load_style_and_wait(runtime, map); + mln_test_destroy_map(map); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); + mln_event_batch batch = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) + MLN_STATUS_OK, mln_runtime_drain_events(runtime, &batch) ); - TEST_ASSERT_GREATER_THAN_size_t(0, batch.event_count); - const size_t event_count = batch.event_count; - const size_t messages_size = batch.messages_size; - - mln_test_destroy_map(map); + mln_runtime_event_batch_view view = { + .size = sizeof(mln_runtime_event_batch_view) + }; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_event_batch_get(batch, &view)); + TEST_ASSERT_GREATER_THAN_size_t(0, view.event_count); size_t map_sourced = 0; - for (size_t index = 0; index < event_count; index += 1) { - const mln_runtime_event* event = batch_event(&batch, index); + for (size_t index = 0; index < view.event_count; index += 1) { + const mln_runtime_event* event = + (const mln_runtime_event*)((const char*)view.events + + (index * view.event_size)); if (event->source_type == MLN_RUNTIME_EVENT_SOURCE_MAP) { TEST_ASSERT_EQUAL_UINT64(map, event->source); map_sourced += 1; } TEST_ASSERT_TRUE( - (size_t)event->message_offset + event->message_size <= messages_size + (size_t)event->message_offset + event->message_size <= view.messages_size ); } TEST_ASSERT_GREATER_THAN_size_t(0, map_sourced); mln_test_destroy_runtime(runtime); + TEST_ASSERT_GREATER_THAN_size_t(0, view.event_count); + mln_event_batch_release(batch); } // A transition reports its identity immediately before the camera change that @@ -554,14 +535,19 @@ static void one_batch_reports_events_in_queue_order(void) { mln_animation_options animation = mln_animation_options_default(); animation.fields = MLN_ANIMATION_OPTION_TRANSITION_ID; animation.transition_id = 77; + mln_camera_update update = mln_camera_update_default(); + update.mode = MLN_CAMERA_UPDATE_MODE_EASE; + update.camera = camera; + update.animation = animation; + mln_test_completion completion = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_ease_to(map, &camera, &animation) + MLN_STATUS_OK, mln_map_update_camera(map, &update, &completion.descriptor) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + mln_test_completion_destroy(&completion); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); + mln_test_event_batch batch = mln_test_event_batch_default(); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_drain_events(runtime, &batch)); bool saw_finish = false; bool did_change_followed_finish = false; for (size_t index = 0; index < batch.event_count; index += 1) { @@ -593,18 +579,17 @@ static void the_message_arena_carries_one_range_per_event(void) { mln_test_drain_all(runtime); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_NATIVE_ERROR, - mln_map_set_style_json(first, MLN_BUFFER_LITERAL("{\"version\":")) + MLN_STATUS_OK, + mln_test_map_set_style_json(first, MLN_BUFFER_LITERAL("{\"version\":")) ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_NATIVE_ERROR, - mln_map_set_style_json(second, MLN_BUFFER_LITERAL("not json at all")) + MLN_STATUS_OK, + mln_test_map_set_style_json(second, MLN_BUFFER_LITERAL("not json at all")) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); + mln_test_event_batch batch = mln_test_event_batch_default(); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_drain_events(runtime, &batch)); size_t failures = 0; uint32_t first_offset = 0; @@ -641,107 +626,19 @@ static void the_message_arena_carries_one_range_per_event(void) { mln_test_destroy_runtime(runtime); } -// A failed offline operation records its text before the mask is consulted, so -// the take-result diagnostic carries the same text as the completion event. -static void a_take_result_reports_the_operations_failure_text(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_offline_operation_id operation_id = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_regions_merge_database_start( - runtime, missing_database_path, &operation_id - ) - ); - TEST_ASSERT_NOT_EQUAL(0, operation_id); - - char event_message[512] = {0}; - mln_runtime_event event = {0}; - bool completed = false; - for (size_t attempt = 0; attempt < take_result_attempts && !completed; - attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 2, -1)); - completed = mln_test_drain_find( - runtime, MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED, MLN_HANDLE_NULL, - &event, event_message, sizeof(event_message) - ); - } - TEST_ASSERT_TRUE_MESSAGE( - completed, "Merging a missing database should report completion." - ); - TEST_ASSERT_EQUAL_UINT64( - operation_id, event.payload.offline_operation_completed.operation_id - ); - TEST_ASSERT_NOT_EQUAL_INT( - MLN_STATUS_OK, event.payload.offline_operation_completed.result_status - ); - TEST_ASSERT_GREATER_THAN_size_t(0, strlen(event_message)); - - mln_offline_region_list regions = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_STATE, - mln_runtime_offline_regions_merge_database_take_result( - runtime, operation_id, ®ions - ) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, regions); - TEST_ASSERT_EQUAL_STRING(event_message, mln_thread_last_error_message()); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_operation_discard(runtime, operation_id) - ); - - // The same failure with the completion event unselected reports the same - // text, so leaving that event type out loses nothing. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_runtime_set_event_mask(runtime, MLN_RUNTIME_EVENT_MASK_NONE) - ); - operation_id = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_regions_merge_database_start( - runtime, missing_database_path, &operation_id - ) - ); - bool reported = false; - for (size_t attempt = 0; attempt < take_result_attempts && !reported; - attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 2, -1)); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_STATE, - mln_runtime_offline_regions_merge_database_take_result( - runtime, operation_id, ®ions - ) - ); - reported = strcmp(mln_thread_last_error_message(), event_message) == 0; - } - TEST_ASSERT_TRUE_MESSAGE( - reported, - "An unselected completion event left the take-result diagnostic generic." - ); - TEST_ASSERT_EQUAL_size_t( - 0, mln_test_drain_counting( - runtime, MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED - ) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_offline_operation_discard(runtime, operation_id) - ); - mln_test_destroy_runtime(runtime); -} - void run_runtime_events_abi_tests(void) { UnitySetTestFile(__FILE__); - RUN_TEST(a_drain_rejects_an_undersized_batch_or_a_stale_runtime); - RUN_TEST(event_entry_points_reject_a_foreign_thread); + RUN_TEST(a_drain_rejects_a_nonnull_output_or_a_stale_runtime); + RUN_TEST(event_drain_and_mask_changes_are_any_thread); RUN_TEST(both_mask_setters_reject_unknown_bits_and_keep_foreign_ones); RUN_TEST(a_fresh_map_and_runtime_select_every_event_type); RUN_TEST(runtime_options_reject_unknown_flags); RUN_TEST(options_reject_unknown_event_mask_bits); RUN_TEST(a_creation_mask_applies_during_construction); RUN_TEST(clearing_one_type_leaves_the_others_arriving); - RUN_TEST(a_suppressed_producer_leaves_a_pump_parked); - RUN_TEST(a_batch_reports_this_headers_stride_and_ends_the_previous); - RUN_TEST(a_bounded_drain_reports_what_stayed_queued); - RUN_TEST(a_batch_outlives_the_map_that_produced_it); + RUN_TEST(a_suppressed_producer_leaves_the_queue_empty); + RUN_TEST(an_owned_batch_remains_stable_across_later_drains); + RUN_TEST(queued_events_outlive_the_map_that_produced_them); RUN_TEST(one_batch_reports_events_in_queue_order); RUN_TEST(the_message_arena_carries_one_range_per_event); - RUN_TEST(a_take_result_reports_the_operations_failure_text); } diff --git a/src/c_api/tests/runtime_lifecycle_abi.c b/src/c_api/tests/runtime_lifecycle_abi.c new file mode 100644 index 000000000..ae9a43e1f --- /dev/null +++ b/src/c_api/tests/runtime_lifecycle_abi.c @@ -0,0 +1,97 @@ +// Raw C ABI coverage for synchronous runtime lifecycle and ordered barriers. + +#include +#include +#include + +#include "abi_tests.h" +#include "test_support.h" +#include "unity.h" + +static void runtime_creation_returns_a_runtime(void) { + const mln_runtime_options options = mln_runtime_options_default(); + + mln_runtime runtime = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_create(&options, &runtime)); + TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, runtime); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_create(&options, &runtime) + ); + + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_close(runtime)); +} + +static void close_preflight_leaves_a_runtime_with_a_live_child_open(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + const mln_completion discard = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, mln_runtime_release(runtime, &discard) + ); + + uint64_t mask = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_runtime_get_event_mask(runtime, &mask) + ); + mln_test_destroy_map(map); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_reserve_child(runtime)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_STATE, mln_runtime_release(runtime, &discard) + ); + mln_test_runtime_abandon_child(runtime); + mln_test_destroy_runtime(runtime); +} + +static void a_barrier_completes_after_preceding_work(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_test_completion barrier = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_runtime_barrier(runtime, &barrier.descriptor) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&barrier)); + mln_test_completion_destroy(&barrier); + mln_test_destroy_runtime(runtime); +} + +typedef struct close_probe { + mln_runtime runtime; + atomic_int status; +} close_probe; + +static mln_runtime create_untracked_runtime(void) { + const mln_runtime_options options = mln_runtime_options_default(); + mln_runtime runtime = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_create(&options, &runtime)); + return runtime; +} + +static void close_from_foreign_thread(void* argument) { + close_probe* probe = argument; + const mln_completion discard = mln_test_discard_completion(); + atomic_store(&probe->status, mln_runtime_release(probe->runtime, &discard)); +} + +static void accepted_close_is_any_thread_and_retires_the_handle(void) { + mln_runtime runtime = create_untracked_runtime(); + close_probe probe = { + .runtime = runtime, + }; + atomic_init(&probe.status, MLN_STATUS_NATIVE_ERROR); + mln_test_thread* thread = + mln_test_thread_start(close_from_foreign_thread, &probe); + mln_test_thread_join(thread); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, atomic_load(&probe.status)); + + uint64_t mask = 0; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_INVALID_ARGUMENT, mln_runtime_get_event_mask(runtime, &mask) + ); +} + +void run_runtime_lifecycle_abi_tests(void) { + UnitySetTestFile(__FILE__); + RUN_TEST(runtime_creation_returns_a_runtime); + RUN_TEST(close_preflight_leaves_a_runtime_with_a_live_child_open); + RUN_TEST(a_barrier_completes_after_preceding_work); + RUN_TEST(accepted_close_is_any_thread_and_retires_the_handle); +} diff --git a/src/c_api/tests/runtime_wake_abi.c b/src/c_api/tests/runtime_wake_abi.c deleted file mode 100644 index ccc501ee3..000000000 --- a/src/c_api/tests/runtime_wake_abi.c +++ /dev/null @@ -1,521 +0,0 @@ -// Raw C ABI coverage for the mln_runtime_pump() wake flag, the signal sources -// that release a parked owner thread, wake source lifetime across runtime -// teardown, and render-update coalescing. - -#include -#include - -#include "abi_tests.h" -#include "test_support.h" -#include "unity.h" - -static const char wake_style_url[] = "http://example.com/wake-style.json"; -static const uint8_t wake_style_json[] = - "{\"version\":8,\"sources\":{},\"layers\":[]}"; - -// Every blocking pump here is bounded, and each test asserts on elapsed time so -// a missing wake fails rather than hangs. -static const int64_t park_timeout_milliseconds = 10000; -// Well below park_timeout_milliseconds, and far above the scheduling noise a -// loaded CI machine adds to a condition-variable wake. -static const uint64_t prompt_return_milliseconds = 5000; -static const int64_t idle_park_milliseconds = 200; -static const size_t style_load_attempts = 20; -static const unsigned int signal_delay_milliseconds = 20; -static const size_t coalesced_repaint_count = 5; -static const size_t idle_quiesce_attempts = 100; -// A map with a render session attached keeps producing frame and tile events -// for many more pumps than an idle map does, so that one site gets a larger -// budget instead of a weaker idle check. -static const size_t session_quiesce_attempts = 500; -// Each zero-budget pump runs at most one task, and three parses plus their -// follow-on tasks need well under this many turns. -static const size_t budget_pump_attempts = 5000; - -// Pumps until the runtime is idle: the wake flag is clear and no events are -// queued. The count is every event the runtime produced, so an idle runtime -// that keeps producing fails here rather than passing on the first attempt. -static void quiesce_within(mln_runtime runtime, size_t attempts) { - for (size_t attempt = 0; attempt < attempts; attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - if (mln_test_drain_all(runtime) == 0) { - // One more zero pump clears the flag the drained events set. - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - if (mln_test_drain_all(runtime) == 0) { - return; - } - } - } - TEST_FAIL_MESSAGE("The runtime kept producing events while idle."); -} - -static void quiesce(mln_runtime runtime) { - quiesce_within(runtime, idle_quiesce_attempts); -} - -typedef struct signal_probe { - mln_wake_source source; - atomic_int signal_status; - atomic_bool signal_done; -} signal_probe; - -// Signals from a thread that owns no runtime. -static void signal_wake_source_entry(void* argument) { - signal_probe* probe = argument; - mln_test_sleep_milliseconds(signal_delay_milliseconds); - atomic_store(&probe->signal_status, mln_wake_source_signal(probe->source)); - atomic_store(&probe->signal_done, true); -} - -typedef struct wrong_thread_probe { - mln_runtime runtime; - atomic_int pump_status; - atomic_int acquire_status; -} wrong_thread_probe; - -static void foreign_thread_entry(void* argument) { - wrong_thread_probe* probe = argument; - atomic_store(&probe->pump_status, mln_runtime_pump(probe->runtime, 0, -1)); - mln_wake_source source = MLN_HANDLE_NULL; - atomic_store( - &probe->acquire_status, - mln_runtime_wake_source_acquire(probe->runtime, &source) - ); - mln_wake_source_destroy(source); -} - -static uint32_t wake_style_provider( - void* user_data, const mln_resource_request* request, - mln_resource_request_handle handle -) { - (void)user_data; - (void)request; - const mln_resource_response response = { - .size = sizeof(mln_resource_response), - .status = MLN_RESOURCE_RESPONSE_STATUS_OK, - .error_reason = MLN_RESOURCE_ERROR_REASON_NONE, - .bytes = wake_style_json, - .byte_count = sizeof(wake_style_json) - 1, - }; - mln_resource_request_complete(handle, &response); - mln_resource_request_release(handle); - return MLN_RESOURCE_PROVIDER_DECISION_HANDLE; -} - -static void a_wake_source_releases_a_parked_owner_thread(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, source); - quiesce(runtime); - - signal_probe probe = {.source = source}; - mln_test_thread* thread = - mln_test_thread_start(signal_wake_source_entry, &probe); - - const uint64_t started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - const uint64_t elapsed = mln_test_monotonic_milliseconds() - started; - // Elapsed time is what separates a signalled wake from an expired timeout. - TEST_ASSERT_TRUE_MESSAGE( - elapsed < prompt_return_milliseconds, - "The parked owner thread timed out instead of taking the signal." - ); - - mln_test_thread_join(thread); - TEST_ASSERT_TRUE(atomic_load(&probe.signal_done)); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, atomic_load(&probe.signal_status)); - - mln_wake_source_destroy(source); - mln_test_destroy_runtime(runtime); -} - -static void a_signal_before_the_pump_sets_the_wake_flag(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); - quiesce(runtime); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_wake_source_signal(source)); - - uint64_t started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - TEST_ASSERT_TRUE_MESSAGE( - mln_test_monotonic_milliseconds() - started < prompt_return_milliseconds, - "A pump waited even though the wake flag was set." - ); - - // The pump above cleared the wake flag, so this one waits its full timeout. - started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, idle_park_milliseconds, -1) - ); - TEST_ASSERT_TRUE_MESSAGE( - mln_test_monotonic_milliseconds() - started >= - (uint64_t)idle_park_milliseconds / 2, - "The first pump left the wake flag set." - ); - - mln_wake_source_destroy(source); - mln_test_destroy_runtime(runtime); -} - -// The style response arrives on a MapLibre file source thread. -static void a_style_response_wakes_a_parked_owner_thread(void) { - mln_runtime runtime = mln_test_create_runtime(); - const mln_resource_provider provider = { - .size = sizeof(mln_resource_provider), - .callback = wake_style_provider, - .user_data = NULL, - }; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) - ); - mln_map map = mln_test_create_map(runtime); - quiesce(runtime); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_url(map, wake_style_url) - ); - - const uint64_t started = mln_test_monotonic_milliseconds(); - bool style_loaded = false; - for (size_t attempt = 0; attempt < style_load_attempts && !style_loaded; - attempt += 1) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); - for (size_t index = 0; index < batch.event_count; index += 1) { - const mln_runtime_event* event = - (const mln_runtime_event*)((const char*)batch.events + - (index * batch.event_size)); - TEST_ASSERT_NOT_EQUAL_INT( - MLN_RUNTIME_EVENT_MAP_LOADING_FAILED, (int)event->type - ); - if (event->type == MLN_RUNTIME_EVENT_MAP_STYLE_LOADED) { - style_loaded = true; - } - } - } - TEST_ASSERT_TRUE(style_loaded); - // Native work ends each park, so the load costs well under one timeout. - TEST_ASSERT_TRUE_MESSAGE( - mln_test_monotonic_milliseconds() - started < prompt_return_milliseconds, - "Parks sat out their timeouts while the style load was pending." - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -typedef struct session_resize_probe { - mln_map map; - atomic_bool attached; - atomic_bool start_resize; - atomic_int resize_status; - atomic_bool resize_done; - bool attach_succeeded; -} session_resize_probe; - -// Attaches on its own thread, so the resize below is queued from a thread that -// owns no runtime. -static void resize_session_entry(void* argument) { - session_resize_probe* probe = argument; - mln_test_render_fixture fixture = {0}; - probe->attach_succeeded = - mln_test_render_fixture_create(probe->map, &fixture); - atomic_store(&probe->attached, true); - if (!probe->attach_succeeded) { - atomic_store(&probe->resize_done, true); - return; - } - - while (!atomic_load(&probe->start_resize)) { - mln_test_sleep_millisecond(); - } - // Long enough for the owner thread to reach its park, so the wake under test - // is the queued resize rather than a flag already set when the pump began. - mln_test_sleep_milliseconds(signal_delay_milliseconds); - atomic_store( - &probe->resize_status, - mln_render_session_resize(fixture.session, 96, 48, 1.0) - ); - atomic_store(&probe->resize_done, true); - - mln_test_render_fixture_destroy(&fixture); -} - -// A render thread's resize queues the new size for the map's owner thread, -// which takes it from a park without a host wake source. -static void a_session_resize_releases_a_parked_owner_thread(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - - session_resize_probe probe = {.map = map}; - atomic_init(&probe.attached, false); - atomic_init(&probe.start_resize, false); - atomic_init(&probe.resize_status, MLN_STATUS_OK); - atomic_init(&probe.resize_done, false); - - mln_test_thread* thread = mln_test_thread_start(resize_session_entry, &probe); - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.attached)); - TEST_ASSERT_TRUE(probe.attach_succeeded); - quiesce_within(runtime, session_quiesce_attempts); - atomic_store(&probe.start_resize, true); - - const uint64_t started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - const uint64_t elapsed = mln_test_monotonic_milliseconds() - started; - TEST_ASSERT_TRUE_MESSAGE( - elapsed < prompt_return_milliseconds, - "The parked owner thread timed out instead of taking the queued resize." - ); - - TEST_ASSERT_TRUE(mln_test_pump_until(runtime, &probe.resize_done)); - mln_test_thread_join(thread); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, atomic_load(&probe.resize_status)); - - // The pump that took the wake is the one that applied the size. - uint32_t width = 0; - uint32_t height = 0; - double scale_factor = 0.0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_size(map, &width, &height, &scale_factor) - ); - TEST_ASSERT_EQUAL_UINT32(96, width); - TEST_ASSERT_EQUAL_UINT32(48, height); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void queued_events_return_from_the_pump_immediately(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - quiesce(runtime); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json( - map, mln_test_buffer_view(wake_style_json, sizeof(wake_style_json) - 1) - ) - ); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, -1)); - - // The queue holds unread events and the wake flag is clear, so the queued - // events are what return the pump. - const uint64_t started = mln_test_monotonic_milliseconds(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_pump(runtime, park_timeout_milliseconds, -1) - ); - TEST_ASSERT_TRUE_MESSAGE( - mln_test_monotonic_milliseconds() - started < prompt_return_milliseconds, - "A pump parked behind unread runtime events." - ); - - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_drain_events(runtime, 0, &batch) - ); - TEST_ASSERT_GREATER_THAN_size_t(0, batch.event_count); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// A render draws the latest update, so back-to-back invalidations collapse to -// one queued event. -static void render_updates_coalesce_at_the_queue_tail(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - quiesce(runtime); - - // Each repaint request invalidates the map synchronously on this thread, so - // the pushes land back to back. - for (size_t repaint = 0; repaint < coalesced_repaint_count; repaint += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_request_repaint(map)); - } - - const size_t render_updates = mln_test_drain_counting( - runtime, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE - ); - TEST_ASSERT_EQUAL_size_t(1, render_updates); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// A wake source stays signalable and releasable after its runtime closes, so -// hosts tear the two down in either order. -static void a_wake_source_outlives_its_runtime(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); - mln_test_destroy_runtime(runtime); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_wake_source_signal(source)); - mln_wake_source_destroy(source); - mln_wake_source_destroy(MLN_HANDLE_NULL); -} - -typedef struct stale_signal_probe { - mln_wake_source source; - atomic_int signal_status; - atomic_bool signal_done; -} stale_signal_probe; - -static void signal_stale_wake_source_entry(void* argument) { - stale_signal_probe* probe = argument; - atomic_store(&probe->signal_status, mln_wake_source_signal(probe->source)); - atomic_store(&probe->signal_done, true); -} - -// Signalling is any-thread, so a released wake source can reach -// mln_wake_source_signal() from a thread that never saw the release. -static void a_released_wake_source_rejects_a_foreign_thread_signal(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); - mln_wake_source_destroy(source); - - // Acquiring again reuses the slot the release freed, so the rejection below - // comes from the generation rather than from an empty slot. - mln_wake_source reused = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &reused) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(source, reused); - - stale_signal_probe probe = {.source = source}; - atomic_store(&probe.signal_status, MLN_STATUS_OK); - atomic_store(&probe.signal_done, false); - mln_test_thread* thread = - mln_test_thread_start(signal_stale_wake_source_entry, &probe); - TEST_ASSERT_NOT_NULL(thread); - mln_test_thread_join(thread); - TEST_ASSERT_TRUE(atomic_load(&probe.signal_done)); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, atomic_load(&probe.signal_status) - ); - - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_wake_source_signal(reused)); - mln_wake_source_destroy(reused); - mln_test_destroy_runtime(runtime); -} - -static void pump_and_wake_sources_reject_raw_invalid_arguments(void) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_pump(MLN_HANDLE_NULL, 0, -1) - ); - mln_wake_source source = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_wake_source_acquire(MLN_HANDLE_NULL, &source) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_wake_source_signal(MLN_HANDLE_NULL) - ); - - mln_runtime runtime = mln_test_create_runtime(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_runtime_wake_source_acquire(runtime, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_wake_source_acquire(runtime, &source) - ); - // A non-null output handle is rejected so the live handle survives. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_runtime_wake_source_acquire(runtime, &source) - ); - - wrong_thread_probe probe = {.runtime = runtime}; - mln_test_thread* thread = mln_test_thread_start(foreign_thread_entry, &probe); - mln_test_thread_join(thread); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_WRONG_THREAD, atomic_load(&probe.pump_status) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_WRONG_THREAD, atomic_load(&probe.acquire_status) - ); - - mln_wake_source_destroy(source); - mln_test_destroy_runtime(runtime); -} - -// Three style responses, one owner-thread task each, and each task parses its -// style and queues that map's loaded event. A zero budget grants one task per -// pump, so no single drain may deliver two style-loaded events, and the -// deferred tasks must re-arm the pump until every response ran. -static void a_budgeted_pump_defers_queued_tasks(void) { - mln_runtime runtime = mln_test_create_runtime(); - const mln_resource_provider provider = { - .size = sizeof(mln_resource_provider), - .callback = wake_style_provider, - .user_data = NULL, - }; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_runtime_set_resource_provider(runtime, &provider) - ); - mln_map maps[3]; - for (size_t index = 0; index < 3; index += 1) { - maps[index] = mln_test_create_map(runtime); - } - quiesce(runtime); - for (size_t index = 0; index < 3; index += 1) { - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_url(maps[index], wake_style_url) - ); - } - - size_t loaded = 0; - for (size_t attempt = 0; attempt < budget_pump_attempts; attempt += 1) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_pump(runtime, 0, 0)); - const size_t drained = - mln_test_drain_counting(runtime, MLN_RUNTIME_EVENT_MAP_STYLE_LOADED); - TEST_ASSERT_LESS_OR_EQUAL_size_t(1, drained); - loaded += drained; - if (loaded == 3) { - break; - } - mln_test_sleep_millisecond(); - } - TEST_ASSERT_EQUAL_size_t(3, loaded); - - for (size_t index = 0; index < 3; index += 1) { - mln_test_destroy_map(maps[index]); - } - mln_test_destroy_runtime(runtime); -} - -void run_runtime_wake_abi_tests(void) { - UnitySetTestFile(__FILE__); - RUN_TEST(a_wake_source_releases_a_parked_owner_thread); - RUN_TEST(a_signal_before_the_pump_sets_the_wake_flag); - RUN_TEST(a_style_response_wakes_a_parked_owner_thread); - RUN_TEST(a_session_resize_releases_a_parked_owner_thread); - RUN_TEST(queued_events_return_from_the_pump_immediately); - RUN_TEST(render_updates_coalesce_at_the_queue_tail); - RUN_TEST(a_wake_source_outlives_its_runtime); - RUN_TEST(a_released_wake_source_rejects_a_foreign_thread_signal); - RUN_TEST(pump_and_wake_sources_reject_raw_invalid_arguments); - RUN_TEST(a_budgeted_pump_defers_queued_tasks); -} diff --git a/src/c_api/tests/style_values_abi.c b/src/c_api/tests/style_values_abi.c index 2252e6386..7b94f3b4e 100644 --- a/src/c_api/tests/style_values_abi.c +++ b/src/c_api/tests/style_values_abi.c @@ -1,195 +1,479 @@ -// Raw C ABI coverage: malformed descriptor counts and unknown enum values are -// hidden by binding-owned style values. - #include #include #include "abi_tests.h" -#include "maplibre_native_c/callback_adapter.h" #include "test_support.h" #include "unity.h" -#define MLN_STRING_LITERAL(text) \ - ((mln_buffer_view){.data = (text), .size = sizeof(text) - 1}) +#define VIEW(text) ((mln_buffer_view){.data = (text), .size = sizeof(text) - 1}) -static void style_value_helpers_reject_unsafe_raw_descriptors(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - const mln_lat_lng coordinates[] = { - {.latitude = 38.0, .longitude = -123.0}, - {.latitude = 38.0, .longitude = -122.0}, - {.latitude = 37.0, .longitude = -122.0}, - {.latitude = 37.0, .longitude = -123.0}, - }; +static bool source_exists(mln_runtime runtime, mln_map map, const char* id) { + (void)runtime; + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_style_source_info)); + const mln_buffer_view view = {.data = id, .size = strlen(id)}; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_image_source_coordinates( - map, (mln_buffer_view){.data = "image-url-source", .size = 16}, - coordinates, 3 - ) + MLN_STATUS_OK, + mln_map_get_style_source_info(map, view, &completion.descriptor) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + const bool found = mln_test_completion_value_count(&completion) == 1; + mln_test_completion_destroy(&completion); + return found; +} - mln_style_tile_source_options options = - mln_style_tile_source_options_default(); - options.fields = MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING; - options.raster_encoding = 99; +static bool image_exists(mln_runtime runtime, mln_map map, const char* id) { + (void)runtime; + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_style_image_info)); + const mln_buffer_view view = {.data = id, .size = strlen(id)}; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_raster_dem_source_url( - map, (mln_buffer_view){.data = "bad-dem", .size = 7}, - (mln_buffer_view){.data = "https://example.com/bad.json", .size = 28}, - &options - ) + MLN_STATUS_OK, + mln_map_get_style_image_info(map, view, &completion.descriptor) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + const bool found = mln_test_completion_value_count(&completion) == 1; + mln_test_completion_destroy(&completion); + return found; +} + +#define EXPECT_STYLE_COMMAND(terminal_status, expression) \ + do { \ + mln_test_completion completion = mln_test_completion_default(0); \ + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, (expression)); \ + TEST_ASSERT_EQUAL_INT( \ + (terminal_status), mln_test_completion_finish(&completion) \ + ); \ + mln_test_completion_destroy(&completion); \ + } while (false) + +#define EXPECT_STYLE_COMMAND_FAILED(terminal_status, fragment, expression) \ + do { \ + mln_test_completion completion = mln_test_completion_default(0); \ + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, (expression)); \ + TEST_ASSERT_EQUAL_INT( \ + (terminal_status), mln_test_completion_finish(&completion) \ + ); \ + TEST_ASSERT_EQUAL_UINT32( \ + MLN_COMMAND_DISPOSITION_FAILED, \ + mln_test_completion_disposition(&completion) \ + ); \ + TEST_ASSERT_NOT_NULL( \ + strstr(mln_test_completion_diagnostic(&completion), (fragment)) \ + ); \ + mln_test_completion_destroy(&completion); \ + } while (false) + +static void style_command_deep_copies_and_ordered_read_observes_it(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + char id[] = "owned-source"; + char json[] = + "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\"," + "\"features\":[]}}"; + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_style_source_json( + map, (mln_buffer_view){.data = id, .size = strlen(id)}, + (mln_buffer_view){.data = json, .size = strlen(json)}, + &completion.descriptor + ) ); + memset(id, 'x', strlen(id)); + memset(json, ' ', strlen(json)); + TEST_ASSERT_TRUE(source_exists(runtime, map, "owned-source")); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} - static const char nul_style[] = - "{\"version\":8,\"sources\":{},\"layers\":[]}\0garbage"; +static void duplicate_id_is_an_async_failed_terminal_event(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + const mln_buffer_view id = VIEW("duplicate"); + const mln_buffer_view json = VIEW( + "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\"," + "\"features\":[]}}" + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_add_style_source_json(map, id, json, &completion.descriptor) + ); + mln_test_completion duplicate = mln_test_completion_default(0); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_add_style_source_json(map, id, json, &duplicate.descriptor) + ); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_style_json( - map, (mln_buffer_view){ - .data = nul_style, - .size = sizeof(nul_style) - 1, - } - ) + MLN_STATUS_INVALID_ARGUMENT, mln_test_completion_finish(&duplicate) ); + TEST_ASSERT_EQUAL_UINT32( + MLN_COMMAND_DISPOSITION_FAILED, mln_test_completion_disposition(&duplicate) + ); + mln_test_completion_destroy(&duplicate); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -// Bindings emit a full struct header and one complete cluster-properties -// object, so only raw C callers can present these unsafe values. -static void geojson_source_options_reject_unsafe_raw_values(void) { +static void image_source_coordinates_are_borrowed_by_the_completion(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); - const mln_buffer_view url = { - .data = "https://example.com/points.geojson", .size = 34 + const mln_lat_lng coordinates[4] = { + {.latitude = 1.0, .longitude = 2.0}, + {.latitude = 1.0, .longitude = 3.0}, + {.latitude = 0.0, .longitude = 3.0}, + {.latitude = 0.0, .longitude = 2.0}, }; - - mln_geojson_source_options short_size = mln_geojson_source_options_default(); - short_size.size = (uint32_t)(sizeof(mln_geojson_source_options) - 1); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "short-header", .size = 12}, url, - &short_size + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_add_image_source_url( + map, VIEW("probe-image"), coordinates, 4, + VIEW("https://example.invalid/image.png"), &completion.descriptor ) ); - mln_geojson_source_options unknown_field = - mln_geojson_source_options_default(); - unknown_field.fields = 1U << 31U; + mln_test_completion completion = + mln_test_completion_default(sizeof(coordinates)); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "unknown-field", .size = 13}, url, - &unknown_field - ) + MLN_STATUS_OK, mln_map_get_image_source_coordinates( + map, VIEW("probe-image"), &completion.descriptor + ) + ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + TEST_ASSERT_EQUAL_size_t(4, mln_test_completion_value_count(&completion)); + mln_lat_lng copied[4] = {0}; + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&completion, copied, sizeof(copied)) ); + TEST_ASSERT_EQUAL_MEMORY(coordinates, copied, sizeof(coordinates)); + mln_test_completion_destroy(&completion); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} - mln_geojson_source_options fractional_min_zoom = - mln_geojson_source_options_default(); - fractional_min_zoom.fields = MLN_GEOJSON_SOURCE_OPTION_MIN_ZOOM; - fractional_min_zoom.min_zoom = 1.5; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "fractional-min", .size = 14}, url, - &fractional_min_zoom +typedef struct stretch_probe { + atomic_bool done; + mln_status status; + mln_image_stretch x; + mln_image_stretch y; + size_t x_count; + size_t y_count; +} stretch_probe; + +static void copy_stretches( + void* user_data, const mln_completion_result* result +) { + stretch_probe* probe = user_data; + probe->status = result->status; + if (result->value_count == 1) { + const mln_style_image_stretches_result* stretches = result->value; + probe->x_count = stretches->stretch_x_count; + probe->y_count = stretches->stretch_y_count; + if (probe->x_count != 0) probe->x = stretches->stretch_x[0]; + if (probe->y_count != 0) probe->y = stretches->stretch_y[0]; + } + atomic_store(&probe->done, true); +} + +static void style_image_stretches_are_borrowed_by_the_completion(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + const uint8_t pixel[4] = {0, 0, 0, 0}; + mln_premultiplied_rgba8_image image = mln_premultiplied_rgba8_image_default(); + image.width = 1; + image.height = 1; + image.stride = 4; + image.pixels = pixel; + image.byte_length = sizeof(pixel); + const mln_image_stretch stretch_x = {.from = 0.0f, .to = 1.0f}; + const mln_image_stretch stretch_y = {.from = 0.0f, .to = 1.0f}; + mln_style_image_options options = mln_style_image_options_default(); + options.fields = + MLN_STYLE_IMAGE_OPTION_STRETCH_X | MLN_STYLE_IMAGE_OPTION_STRETCH_Y; + options.stretch_x = &stretch_x; + options.stretch_x_count = 1; + options.stretch_y = &stretch_y; + options.stretch_y_count = 1; + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_set_style_image( + map, VIEW("probe-stretches"), &image, &options, &completion.descriptor ) ); - mln_geojson_source_options fractional_max_zoom = - mln_geojson_source_options_default(); - fractional_max_zoom.fields = MLN_GEOJSON_SOURCE_OPTION_MAX_ZOOM; - fractional_max_zoom.max_zoom = 17.9; + stretch_probe probe = {.status = MLN_STATUS_INVALID_STATE}; + atomic_init(&probe.done, false); + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = copy_stretches, + .user_data = &probe, + }; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "fractional-max", .size = 14}, url, - &fractional_max_zoom - ) + MLN_STATUS_OK, mln_map_copy_style_image_stretches( + map, VIEW("probe-stretches"), &completion + ) ); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_barrier(runtime)); + TEST_ASSERT_TRUE(atomic_load(&probe.done)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, probe.status); + TEST_ASSERT_EQUAL_size_t(1, probe.x_count); + TEST_ASSERT_EQUAL_size_t(1, probe.y_count); + TEST_ASSERT_EQUAL_MEMORY(&stretch_x, &probe.x, sizeof(stretch_x)); + TEST_ASSERT_EQUAL_MEMORY(&stretch_y, &probe.y, sizeof(stretch_y)); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} - mln_geojson_source_options fractional_cluster_max_zoom = - mln_geojson_source_options_default(); - fractional_cluster_max_zoom.fields = - MLN_GEOJSON_SOURCE_OPTION_CLUSTER_MAX_ZOOM; - fractional_cluster_max_zoom.cluster_max_zoom = 12.25; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "fractional-cluster", .size = 18}, url, - &fractional_cluster_max_zoom - ) +static void remove_commands_commit_and_report_missing_ids(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + const mln_buffer_view json = VIEW( + "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\"," + "\"features\":[]}}" ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_style_source_json( + map, VIEW("doomed-source"), json, &completion.descriptor + ) + ); + + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_remove_style_source( + map, VIEW("doomed-source"), &completion.descriptor + ) + ); + TEST_ASSERT_FALSE(source_exists(runtime, map, "doomed-source")); - mln_geojson_source_options null_cluster_properties = - mln_geojson_source_options_default(); - null_cluster_properties.fields = MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES; + mln_test_completion missing = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "null-properties", .size = 15}, url, - &null_cluster_properties - ) + MLN_STATUS_OK, + mln_map_remove_style_source(map, VIEW("doomed-source"), &missing.descriptor) ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_NOT_FOUND, mln_test_completion_finish(&missing) + ); + TEST_ASSERT_NOT_NULL( + strstr(mln_test_completion_diagnostic(&missing), "doomed-source") + ); + mln_test_completion_destroy(&missing); - mln_geojson_source_options non_object_cluster_properties = - mln_geojson_source_options_default(); - non_object_cluster_properties.fields = - MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES; - non_object_cluster_properties.cluster_properties = MLN_BUFFER_LITERAL("null"); + missing = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "json-null-properties", .size = 20}, url, - &non_object_cluster_properties + MLN_STATUS_OK, + mln_map_remove_style_layer(map, VIEW("missing-layer"), &missing.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_NOT_FOUND, mln_test_completion_finish(&missing) + ); + TEST_ASSERT_NOT_NULL( + strstr(mln_test_completion_diagnostic(&missing), "missing-layer") + ); + mln_test_completion_destroy(&missing); + + const uint8_t pixel[4] = {0, 0, 0, 0}; + mln_premultiplied_rgba8_image image = mln_premultiplied_rgba8_image_default(); + image.width = 1; + image.height = 1; + image.stride = 4; + image.pixels = pixel; + image.byte_length = sizeof(pixel); + mln_style_image_options options = mln_style_image_options_default(); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_set_style_image( + map, VIEW("doomed-image"), &image, &options, &completion.descriptor ) ); - static const char injected_cluster_properties[] = - "{\"total\":[\"+\",1]},\"cluster\":true"; - mln_geojson_source_options injected_properties = - mln_geojson_source_options_default(); - injected_properties.fields = MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES; - injected_properties.cluster_properties = (mln_buffer_view){ - .data = injected_cluster_properties, - .size = sizeof(injected_cluster_properties) - 1, - }; + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_remove_style_image( + map, VIEW("doomed-image"), &completion.descriptor + ) + ); + TEST_ASSERT_FALSE(image_exists(runtime, map, "doomed-image")); + + missing = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, MLN_BUFFER_LITERAL("injected-properties"), url, &injected_properties + MLN_STATUS_OK, + mln_map_remove_style_image(map, VIEW("doomed-image"), &missing.descriptor) + ); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_NOT_FOUND, mln_test_completion_finish(&missing) + ); + TEST_ASSERT_NOT_NULL( + strstr(mln_test_completion_diagnostic(&missing), "doomed-image") + ); + mln_test_completion_destroy(&missing); + + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +// Copies one layer-info result from its completion. +static bool take_layer_info( + mln_runtime runtime, mln_map map, const char* id, mln_style_layer_info* info +) { + (void)runtime; + mln_test_completion completion = + mln_test_completion_default(sizeof(mln_style_layer_result)); + const mln_buffer_view view = {.data = id, .size = strlen(id)}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_map_get_style_layer_info(map, view, &completion.descriptor) + ); + memset(info, 0, sizeof(*info)); + info->size = sizeof(*info); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&completion)); + const bool found = mln_test_completion_value_count(&completion) == 1; + if (found) { + mln_style_layer_result result = {0}; + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&completion, &result, sizeof(result)) + ); + *info = result.info; + } + mln_test_completion_destroy(&completion); + return found; +} + +static void layer_info_reports_scalars_and_sizes_the_source_id_copy(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + mln_style_layer_info info = {.size = sizeof(mln_style_layer_info)}; + TEST_ASSERT_FALSE(take_layer_info(runtime, map, "missing-layer", &info)); + + const mln_buffer_view json = VIEW( + "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\"," + "\"features\":[]}}" + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_style_source_json( + map, VIEW("info-source"), json, &completion.descriptor + ) + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_style_layer_json( + map, + VIEW( + "{\"id\":\"info-layer\",\"type\":\"circle\",\"source\":" + "\"info-source\"}" + ), + VIEW(""), &completion.descriptor + ) + ); + TEST_ASSERT_TRUE(take_layer_info(runtime, map, "info-layer", &info)); + TEST_ASSERT_EQUAL_size_t(strlen("circle"), info.type.size); + TEST_ASSERT_EQUAL_MEMORY("circle", info.type.data, info.type.size); + TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_LAYER_VISIBILITY_VISIBLE, info.visibility); + TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_LAYER_INFO_SOURCE_ID, info.fields); + TEST_ASSERT_EQUAL_size_t(0, info.source_layer_size); + // Unbounded zooms report the documented infinities before bounds are set. + TEST_ASSERT_EQUAL_DOUBLE(-INFINITY, info.min_zoom); + TEST_ASSERT_EQUAL_DOUBLE(INFINITY, info.max_zoom); + + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_set_layer_min_zoom( + map, VIEW("info-layer"), 3.0, &completion.descriptor + ) + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_set_layer_max_zoom( + map, VIEW("info-layer"), 12.0, &completion.descriptor + ) + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_set_layer_visibility( + map, VIEW("info-layer"), MLN_STYLE_LAYER_VISIBILITY_NONE, + &completion.descriptor + ) + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_set_layer_source_layer( + map, VIEW("info-layer"), VIEW("roads"), &completion.descriptor ) ); + TEST_ASSERT_TRUE(take_layer_info(runtime, map, "info-layer", &info)); + TEST_ASSERT_EQUAL_DOUBLE(3.0, info.min_zoom); + TEST_ASSERT_EQUAL_DOUBLE(12.0, info.max_zoom); + TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_LAYER_VISIBILITY_NONE, info.visibility); + TEST_ASSERT_EQUAL_UINT32( + MLN_STYLE_LAYER_INFO_SOURCE_ID | MLN_STYLE_LAYER_INFO_SOURCE_LAYER, + info.fields + ); + TEST_ASSERT_EQUAL_size_t(strlen("info-source"), info.source_id_size); + TEST_ASSERT_EQUAL_size_t(strlen("roads"), info.source_layer_size); - static const char trailing_cluster_properties[] = - "{\"total\":[\"+\",1]}\0garbage"; - mln_geojson_source_options trailing_properties = - mln_geojson_source_options_default(); - trailing_properties.fields = MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES; - trailing_properties.cluster_properties = (mln_buffer_view){ - .data = trailing_cluster_properties, - .size = sizeof(trailing_cluster_properties) - 1, - }; + // The reported source ID length sizes the copy operation's result exactly. + mln_test_completion source_completion = mln_test_completion_buffer_view(); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_add_geojson_source_url( - map, MLN_BUFFER_LITERAL("trailing-properties"), url, &trailing_properties + MLN_STATUS_OK, mln_map_copy_layer_source_id( + map, VIEW("info-layer"), &source_completion.descriptor + ) + ); + mln_buffer_view copied = {0}; + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_test_completion_finish(&source_completion) + ); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&source_completion, &copied, sizeof(copied)) + ); + TEST_ASSERT_EQUAL_size_t(info.source_id_size, copied.size); + TEST_ASSERT_EQUAL_MEMORY("info-source", copied.data, copied.size); + mln_test_completion_destroy(&source_completion); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +static void an_in_use_source_removal_fails_and_leaves_the_source(void) { + mln_runtime runtime = mln_test_create_runtime(); + mln_map map = mln_test_create_map(runtime); + const mln_buffer_view json = VIEW( + "{\"type\":\"geojson\",\"data\":{\"type\":\"FeatureCollection\"," + "\"features\":[]}}" + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_style_source_json( + map, VIEW("in-use"), json, &completion.descriptor + ) + ); + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_add_style_layer_json( + map, VIEW("{\"id\":\"user\",\"type\":\"circle\",\"source\":\"in-use\"}"), + VIEW(""), &completion.descriptor ) ); + EXPECT_STYLE_COMMAND_FAILED( + MLN_STATUS_INVALID_STATE, "used by a layer", + mln_map_remove_style_source(map, VIEW("in-use"), &completion.descriptor) + ); + TEST_ASSERT_TRUE(source_exists(runtime, map, "in-use")); + + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_remove_style_layer(map, VIEW("user"), &completion.descriptor) + ); + + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, + mln_map_remove_style_source(map, VIEW("in-use"), &completion.descriptor) + ); + TEST_ASSERT_FALSE(source_exists(runtime, map, "in-use")); + mln_test_destroy_map(map); + mln_test_destroy_runtime(runtime); +} + +// Prepared GeoJSON data creation is synchronous and touches no runtime or +// map, so validation reports through status and the thread diagnostic. +static void geojson_source_data_create_rejects_unsafe_raw_values(void) { static const char trailing_geojson[] = "{\"type\":\"FeatureCollection\",\"features\":[]}garbage"; mln_geojson_source_data trailing_data = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_geojson_source_data_create( - (mln_buffer_view){ - .data = trailing_geojson, - .size = sizeof(trailing_geojson) - 1, - }, - NULL, &trailing_data - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_geojson_source_data_create( + MLN_BUFFER_LITERAL(trailing_geojson), NULL, &trailing_data + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, trailing_data); @@ -197,20 +481,18 @@ static void geojson_source_options_reject_unsafe_raw_values(void) { "{\"type\":\"FeatureCollection\",\"features\":[]}\0garbage"; mln_geojson_source_data nul_data = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_geojson_source_data_create( - (mln_buffer_view){ - .data = nul_geojson, - .size = sizeof(nul_geojson) - 1, - }, - NULL, &nul_data - ) + MLN_STATUS_INVALID_ARGUMENT, + mln_geojson_source_data_create( + (mln_buffer_view){.data = nul_geojson, .size = sizeof(nul_geojson) - 1}, + NULL, &nul_data + ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, nul_data); // A populated output handle is rejected rather than silently overwritten. - mln_geojson_source_data populated = MLN_HANDLE_NULL; static const char empty_collection[] = "{\"type\":\"FeatureCollection\",\"features\":[]}"; + mln_geojson_source_data populated = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, mln_geojson_source_data_create( MLN_BUFFER_LITERAL(empty_collection), NULL, &populated @@ -225,8 +507,9 @@ static void geojson_source_options_reject_unsafe_raw_values(void) { ); mln_geojson_source_data_destroy(populated); - // Unsafe raw options reject data preparation the same way they reject a - // URL add. + // Unsafe raw options reject data preparation up front. + mln_geojson_source_options short_size = mln_geojson_source_options_default(); + short_size.size = sizeof(uint32_t); mln_geojson_source_data short_size_data = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, @@ -235,20 +518,6 @@ static void geojson_source_options_reject_unsafe_raw_values(void) { ) ); TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, short_size_data); - - // A rejected descriptor leaves the source ID free for a later valid add. - mln_geojson_source_options clustered = mln_geojson_source_options_default(); - clustered.fields = MLN_GEOJSON_SOURCE_OPTION_CLUSTER; - clustered.cluster = true; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_geojson_source_url( - map, (mln_buffer_view){.data = "short-header", .size = 12}, - url, &clustered - ) - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); } // Supercluster reads every feature geometry as a point, so data preparation @@ -346,9 +615,9 @@ static void clustered_geojson_data_requires_a_feature_collection(void) { mln_geojson_source_data_destroy(prepared); } -// Prepared data installs on any number of sources; an update requires data -// whose baked-in options match the source's, and released handles reject -// installs while installed sources keep their own reference. +// Prepared data installs on any number of sources through commands; an update +// requires data whose baked-in options match the source's, and released +// handles reject installs while installed sources keep their own reference. static void prepared_geojson_data_installs_and_checks_options(void) { mln_runtime runtime = mln_test_create_runtime(); mln_map map = mln_test_create_map(runtime); @@ -375,31 +644,33 @@ static void prepared_geojson_data_installs_and_checks_options(void) { ) ); - const mln_buffer_view plain_id = {.data = "plain", .size = 5}; - const mln_buffer_view clustered_id = {.data = "clustered", .size = 9}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_geojson_source_data(map, plain_id, plain_data) + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_add_geojson_source_data( + map, VIEW("plain"), plain_data, &completion.descriptor + ) ); - TEST_ASSERT_EQUAL_INT( + EXPECT_STYLE_COMMAND( MLN_STATUS_OK, - mln_map_add_geojson_source_data(map, clustered_id, clustered_data) + mln_map_add_geojson_source_data( + map, VIEW("clustered"), clustered_data, &completion.descriptor + ) ); // Data prepared under different options tiles inconsistently with the - // source, so the mismatch is rejected and names the source. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_geojson_source_data(map, clustered_id, plain_data) + // source, so the mismatch fails the command and names the source. + EXPECT_STYLE_COMMAND_FAILED( + MLN_STATUS_INVALID_ARGUMENT, "do not match", + mln_map_set_geojson_source_data( + map, VIEW("clustered"), plain_data, &completion.descriptor + ) ); - const char* message = mln_thread_last_error_message(); - TEST_ASSERT_NOT_NULL(message); - TEST_ASSERT_NOT_NULL(strstr(message, "\"clustered\"")); - TEST_ASSERT_NOT_NULL(strstr(message, "do not match")); // One prepared handle installs on any number of matching sources. - TEST_ASSERT_EQUAL_INT( + EXPECT_STYLE_COMMAND( MLN_STATUS_OK, - mln_map_set_geojson_source_data(map, clustered_id, clustered_data) + mln_map_set_geojson_source_data( + map, VIEW("clustered"), clustered_data, &completion.descriptor + ) ); // Cluster aggregations are part of the options match: a different @@ -409,16 +680,17 @@ static void prepared_geojson_data_installs_and_checks_options(void) { aggregated.fields |= MLN_GEOJSON_SOURCE_OPTION_CLUSTER_PROPERTIES; aggregated.cluster_properties = MLN_BUFFER_LITERAL("{\"total\":[\"+\",[\"get\",\"rank\"]]}"); - const mln_buffer_view aggregated_id = {.data = "aggregated", .size = 10}; mln_geojson_source_data aggregated_data = MLN_HANDLE_NULL; TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, mln_geojson_source_data_create( MLN_BUFFER_LITERAL(points), &aggregated, &aggregated_data ) ); - TEST_ASSERT_EQUAL_INT( + EXPECT_STYLE_COMMAND( MLN_STATUS_OK, - mln_map_add_geojson_source_data(map, aggregated_id, aggregated_data) + mln_map_add_geojson_source_data( + map, VIEW("aggregated"), aggregated_data, &completion.descriptor + ) ); mln_geojson_source_options reaggregated = aggregated; reaggregated.cluster_properties = @@ -430,9 +702,11 @@ static void prepared_geojson_data_installs_and_checks_options(void) { MLN_BUFFER_LITERAL(points), &reaggregated, &reaggregated_data ) ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_geojson_source_data(map, aggregated_id, reaggregated_data) + EXPECT_STYLE_COMMAND_FAILED( + MLN_STATUS_INVALID_ARGUMENT, "do not match", + mln_map_set_geojson_source_data( + map, VIEW("aggregated"), reaggregated_data, &completion.descriptor + ) ); mln_geojson_source_data_destroy(reaggregated_data); mln_geojson_source_options reformatted = aggregated; @@ -444,1221 +718,72 @@ static void prepared_geojson_data_installs_and_checks_options(void) { MLN_BUFFER_LITERAL(points), &reformatted, &reformatted_data ) ); - TEST_ASSERT_EQUAL_INT( + EXPECT_STYLE_COMMAND( MLN_STATUS_OK, - mln_map_set_geojson_source_data(map, aggregated_id, reformatted_data) + mln_map_set_geojson_source_data( + map, VIEW("aggregated"), reformatted_data, &completion.descriptor + ) ); mln_geojson_source_data_destroy(reformatted_data); mln_geojson_source_data_destroy(aggregated_data); + + // The submit-time lease keeps the prepared index alive, so the handle may + // be destroyed as soon as the install command is submitted. + mln_test_completion install = mln_test_completion_default(0); TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_geojson_source_data(map, plain_id, plain_data) + MLN_STATUS_OK, mln_map_set_geojson_source_data( + map, VIEW("plain"), plain_data, &install.descriptor + ) ); + mln_geojson_source_data_destroy(plain_data); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_finish(&install)); + mln_test_completion_destroy(&install); // The runtime override takes a live GeoJSON source alone. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_geojson_source_synchronous_tiling(map, plain_id, true) + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_set_geojson_source_synchronous_tiling( + map, VIEW("plain"), true, &completion.descriptor + ) ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_geojson_source_synchronous_tiling(map, plain_id, false) + EXPECT_STYLE_COMMAND( + MLN_STATUS_OK, mln_map_set_geojson_source_synchronous_tiling( + map, VIEW("plain"), false, &completion.descriptor + ) ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, + EXPECT_STYLE_COMMAND_FAILED( + MLN_STATUS_INVALID_ARGUMENT, "source does not exist", mln_map_set_geojson_source_synchronous_tiling( - map, (mln_buffer_view){.data = "missing", .size = 7}, true + map, VIEW("missing"), true, &completion.descriptor ) ); - // Destroy releases the handle while installed sources keep their reference; - // the released id then rejects installs, and a second destroy is a no-op. - mln_geojson_source_data_destroy(plain_data); + // A released handle rejects new installs at submit, a second destroy is a + // no-op, and installed sources keep their own reference. mln_geojson_source_data_destroy(plain_data); mln_geojson_source_data_destroy(MLN_HANDLE_NULL); + mln_completion rejected = mln_test_discard_completion(); TEST_ASSERT_EQUAL_INT( MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_geojson_source_data(map, plain_id, plain_data) + mln_map_set_geojson_source_data(map, VIEW("plain"), plain_data, &rejected) ); + TEST_ASSERT_TRUE(source_exists(runtime, map, "plain")); mln_geojson_source_data_destroy(clustered_data); mln_test_destroy_map(map); mln_test_destroy_runtime(runtime); } -// A vector source plus one layer that takes a source and one that does not, so -// the typed layer accessors can be exercised against both. -static const char layer_accessor_style_json[] = - "{\"version\":8,\"sources\":{\"vec\":{\"type\":\"vector\"," - "\"tiles\":[\"https://example.com/{z}/{x}/{y}.mvt\"]}}," - "\"layers\":[{\"id\":\"lines\",\"type\":\"line\",\"source\":\"vec\"," - "\"source-layer\":\"roads\"},{\"id\":\"bg\",\"type\":\"background\"}]}"; - -static void optional_json_style_values_return_null_handles(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(layer_accessor_style_json)) - ); - - mln_buffer value = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_light_property( - map, MLN_STRING_LITERAL("intensity"), &value - ) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - - const mln_buffer_view layer_id = MLN_STRING_LITERAL("bg"); - const mln_buffer_view property_name = - MLN_STRING_LITERAL("background-opacity"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_layer_property(map, layer_id, property_name, &value) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_filter(map, layer_id, &value) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_property( - map, layer_id, property_name, MLN_BUFFER_LITERAL("0.5") - ) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_layer_property(map, layer_id, property_name, &value) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - mln_buffer_destroy(value); - value = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_property( - map, layer_id, property_name, MLN_BUFFER_LITERAL("null") - ) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_layer_property(map, layer_id, property_name, &value) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - - const mln_buffer_view filter = - MLN_BUFFER_LITERAL("[\"==\",[\"get\",\"kind\"],\"park\"]"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_filter(map, layer_id, &filter) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_filter(map, layer_id, &value) - ); - TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - mln_buffer_destroy(value); - value = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_filter(map, layer_id, NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_filter(map, layer_id, &value) - ); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, value); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// MapLibre's own setProperty path accepts a sourceless layer as a silent no-op; -// the typed accessors reject it. -static void layer_source_accessors_reject_sourceless_layer_types(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(layer_accessor_style_json)) - ); - - const mln_buffer_view background = MLN_STRING_LITERAL("bg"); - const mln_buffer_view source_layer = MLN_STRING_LITERAL("roads"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_layer_source_layer(map, background, source_layer) - ); - const char* message = mln_thread_last_error_message(); - TEST_ASSERT_NOT_NULL(message); - TEST_ASSERT_NOT_NULL(strstr(message, "source-layer")); - - const mln_buffer_view source_id = MLN_STRING_LITERAL("vec"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_layer_source_id(map, background, source_id) - ); - - // The style-spec property path still reaches the same layer and reports OK - // without changing it, which is why the typed setters exist. - const mln_buffer_view property_name = MLN_STRING_LITERAL("source-layer"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_layer_property( - map, background, property_name, MLN_BUFFER_LITERAL("\"roads\"") - ) - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void layer_text_accessors_report_required_capacity(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(layer_accessor_style_json)) - ); - - // A null buffer with zero capacity is a size probe, so it reports the length - // and succeeds rather than sharing a status with a missing layer. - const mln_buffer_view lines = MLN_STRING_LITERAL("lines"); - size_t required = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_layer_source_layer(map, lines, NULL, 0, &required) - ); - TEST_ASSERT_EQUAL_size_t(5, required); - - char too_small[4] = {0}; - required = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_layer_source_layer( - map, lines, too_small, sizeof(too_small), &required - ) - ); - TEST_ASSERT_EQUAL_size_t(5, required); - - char buffer[8] = {0}; - required = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_layer_source_layer( - map, lines, buffer, sizeof(buffer), &required - ) - ); - TEST_ASSERT_EQUAL_size_t(5, required); - TEST_ASSERT_EQUAL_INT(0, memcmp(buffer, "roads", 5)); - - // A sourceless layer reads back as empty rather than failing. - const mln_buffer_view background = MLN_STRING_LITERAL("bg"); - required = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_layer_source_id( - map, background, buffer, sizeof(buffer), &required - ) - ); - TEST_ASSERT_EQUAL_size_t(0, required); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_layer_source_layer(map, lines, buffer, sizeof(buffer), NULL) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_layer_source_layer(map, lines, NULL, sizeof(buffer), &required) - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// An unbounded zoom range crosses the ABI as infinities. -static void layer_zoom_and_visibility_accessors_carry_raw_domains(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(layer_accessor_style_json)) - ); - - const mln_buffer_view lines = MLN_STRING_LITERAL("lines"); - double min_zoom = 0.0; - double max_zoom = 0.0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_min_zoom(map, lines, &min_zoom) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_max_zoom(map, lines, &max_zoom) - ); - TEST_ASSERT_TRUE(isinf(min_zoom) && min_zoom < 0.0); - TEST_ASSERT_TRUE(isinf(max_zoom) && max_zoom > 0.0); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_min_zoom(map, lines, 4.0) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_layer_max_zoom(map, lines, 12.5) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_min_zoom(map, lines, &min_zoom) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_max_zoom(map, lines, &max_zoom) - ); - TEST_ASSERT_EQUAL_DOUBLE(4.0, min_zoom); - TEST_ASSERT_EQUAL_DOUBLE(12.5, max_zoom); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_layer_min_zoom(map, lines, NAN) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_layer_max_zoom(map, lines, NAN) - ); - - uint32_t visibility = 999; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_visibility(map, lines, &visibility) - ); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_LAYER_VISIBILITY_VISIBLE, visibility); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_layer_visibility(map, lines, MLN_STYLE_LAYER_VISIBILITY_NONE) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_layer_visibility(map, lines, &visibility) - ); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_LAYER_VISIBILITY_NONE, visibility); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_layer_visibility(map, lines, 999) - ); - - // A missing layer is rejected the same way the rest of the layer family does. - const mln_buffer_view missing = MLN_STRING_LITERAL("nope"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_layer_min_zoom(map, missing, &min_zoom) - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void style_image_stretch_descriptors_reject_unsafe_raw_values(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL(layer_accessor_style_json)) - ); - - const uint8_t pixels[4 * 4] = {0}; - mln_premultiplied_rgba8_image image = mln_premultiplied_rgba8_image_default(); - image.width = 2; - image.height = 2; - image.stride = 8; - image.pixels = pixels; - image.byte_length = sizeof(pixels); - - const mln_buffer_view image_id = MLN_STRING_LITERAL("patch"); - - // A backwards interval, a non-finite bound, and a null array with a non-zero - // count are all rejected. - const mln_image_stretch backwards[] = {{.from = 2.0F, .to = 1.0F}}; - mln_style_image_options options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_STRETCH_X; - options.stretch_x = backwards; - options.stretch_x_count = 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - // A zero-width interval would leave MapLibre dividing by a zero stretch sum. - const mln_image_stretch zero_width[] = {{.from = 1.0F, .to = 1.0F}}; - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_STRETCH_X; - options.stretch_x = zero_width; - options.stretch_x_count = 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - // Intervals that run out of order or overlap their predecessor are rejected. - const mln_image_stretch unordered[] = { - {.from = 2.0F, .to = 3.0F}, {.from = 0.0F, .to = 1.0F} - }; - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_STRETCH_X; - options.stretch_x = unordered; - options.stretch_x_count = 2; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - const mln_image_stretch overlapping[] = { - {.from = 0.0F, .to = 2.0F}, {.from = 1.0F, .to = 3.0F} - }; - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_STRETCH_Y; - options.stretch_y = overlapping; - options.stretch_y_count = 2; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_STRETCH_Y; - options.stretch_y = NULL; - options.stretch_y_count = 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_CONTENT; - options.content.left = 2.0F; - options.content.right = 1.0F; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - options = mln_style_image_options_default(); - options.fields = MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH; - options.text_fit_width = 999; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_image(map, image_id, &image, &options) - ); - - // An accepted nine-patch reports its stretches, content, and text fit back. - const mln_image_stretch stretch_x[] = {{.from = 0.0F, .to = 1.0F}}; - const mln_image_stretch stretch_y[] = { - {.from = 0.0F, .to = 1.0F}, {.from = 1.0F, .to = 2.0F} - }; - options = mln_style_image_options_default(); - options.fields = - MLN_STYLE_IMAGE_OPTION_STRETCH_X | MLN_STYLE_IMAGE_OPTION_STRETCH_Y | - MLN_STYLE_IMAGE_OPTION_CONTENT | MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT; - options.stretch_x = stretch_x; - options.stretch_x_count = 1; - options.stretch_y = stretch_y; - options.stretch_y_count = 2; - options.content.left = 0.5F; - options.content.top = 0.5F; - options.content.right = 1.5F; - options.content.bottom = 1.5F; - options.text_fit_height = MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_image(map, image_id, &image, &options) - ); - - mln_style_image_info info = mln_style_image_info_default(); - bool found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_image_info(map, image_id, &info, &found) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_size_t(1, info.stretch_x_count); - TEST_ASSERT_EQUAL_size_t(2, info.stretch_y_count); - TEST_ASSERT_TRUE(info.has_content); - TEST_ASSERT_EQUAL_FLOAT(0.5F, info.content.left); - TEST_ASSERT_EQUAL_FLOAT(1.5F, info.content.bottom); - TEST_ASSERT_FALSE(info.has_text_fit_width); - TEST_ASSERT_TRUE(info.has_text_fit_height); - TEST_ASSERT_EQUAL_UINT32( - MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL, info.text_fit_height - ); - - // Null arrays with zero capacity probe the counts and succeed. - size_t x_count = 0; - size_t y_count = 0; - found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_image_stretches( - map, image_id, NULL, 0, &x_count, NULL, 0, &y_count, &found - ) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_size_t(1, x_count); - TEST_ASSERT_EQUAL_size_t(2, y_count); - - // An undersized array reports the counts and fails. - mln_image_stretch too_small[1] = {{.from = 0.0F, .to = 0.0F}}; - x_count = 0; - y_count = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_image_stretches( - map, image_id, NULL, 0, &x_count, too_small, 1, &y_count, &found - ) - ); - TEST_ASSERT_EQUAL_size_t(2, y_count); - - mln_image_stretch copied_x[1] = {{.from = 0.0F, .to = 0.0F}}; - mln_image_stretch copied_y[2] = { - {.from = 0.0F, .to = 0.0F}, {.from = 0.0F, .to = 0.0F} - }; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_style_image_stretches( - map, image_id, copied_x, 1, &x_count, copied_y, 2, &y_count, &found - ) - ); - TEST_ASSERT_EQUAL_FLOAT(1.0F, copied_x[0].to); - TEST_ASSERT_EQUAL_FLOAT(1.0F, copied_y[1].from); - TEST_ASSERT_EQUAL_FLOAT(2.0F, copied_y[1].to); - - // A missing image reports zero counts without failing. - const mln_buffer_view missing = MLN_STRING_LITERAL("nope"); - x_count = 123; - y_count = 123; - found = true; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_image_stretches( - map, missing, NULL, 0, &x_count, NULL, 0, &y_count, &found - ) - ); - TEST_ASSERT_FALSE(found); - TEST_ASSERT_EQUAL_size_t(0, x_count); - TEST_ASSERT_EQUAL_size_t(0, y_count); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void copy_entry_points_answer_a_null_buffer_as_a_size_probe(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json( - map, - MLN_BUFFER_LITERAL( - "{\"version\":8,\"sources\":{\"vec\":{\"type\":\"vector\"," - "\"attribution\":\"probe\"," - "\"tiles\":[\"https://example.com/{z}/{x}/{y}.mvt\"]}},\"layers\":[]}" - ) - ) - ); - - // Source attribution reports its length and succeeds. - const mln_buffer_view source_id = MLN_STRING_LITERAL("vec"); - size_t attribution_size = 0; - bool found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_source_attribution( - map, source_id, NULL, 0, &attribution_size, &found - ) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_size_t(5, attribution_size); - - // A non-null buffer that is too small still reports the length and fails. - char too_small[2] = {0}; - attribution_size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_source_attribution( - map, source_id, too_small, sizeof(too_small), &attribution_size, &found - ) - ); - TEST_ASSERT_EQUAL_size_t(5, attribution_size); - - // Image pixels report their byte length and succeed. - const uint8_t pixels[4] = {0}; - mln_premultiplied_rgba8_image image = mln_premultiplied_rgba8_image_default(); - image.width = 1; - image.height = 1; - image.stride = 4; - image.pixels = pixels; - image.byte_length = sizeof(pixels); - const mln_buffer_view image_id = MLN_STRING_LITERAL("dot"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_image(map, image_id, &image, NULL) - ); - - size_t byte_length = 0; - found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_image_premultiplied_rgba8( - map, image_id, NULL, 0, &byte_length, &found - ) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_size_t(4, byte_length); - - uint8_t small_pixels[2] = {0}; - byte_length = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_image_premultiplied_rgba8( - map, image_id, small_pixels, sizeof(small_pixels), &byte_length, &found - ) - ); - TEST_ASSERT_EQUAL_size_t(4, byte_length); - - // The map-level copy entry points answer a probe the same way, with no id to - // resolve first. - size_t document_size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_loaded_style_json(map, NULL, 0, &document_size) - ); - TEST_ASSERT_TRUE(document_size > 0); - - char small_document[2] = {0}; - size_t reported = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_loaded_style_json( - map, small_document, sizeof(small_document), &reported - ) - ); - TEST_ASSERT_EQUAL_size_t(document_size, reported); - - size_t url_size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, NULL, 0, &url_size) - ); - TEST_ASSERT_EQUAL_size_t(0, url_size); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// Source metadata is split between fixed fields, copy-out strings, and an owned -// list because no caller-owned struct can retain variable-length TileJSON data -// across the ABI. -static void style_source_info_rebuilds_an_inline_tile_source(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json( - map, MLN_BUFFER_LITERAL( - "{\"version\":8,\"sources\":{\"vec\":{\"type\":\"vector\"," - "\"tiles\":[\"https://a.example/{z}/{x}/{y}.mlt\"," - "\"https://b.example/{z}/{x}/{y}.mlt\"],\"minzoom\":2," - "\"maxzoom\":7,\"scheme\":\"tms\",\"bounds\":[-10,-5,20,15]," - "\"encoding\":\"mlt\",\"attribution\":\"example\"}}," - "\"layers\":[]}" - ) - ) - ); - - const mln_buffer_view source_id = MLN_STRING_LITERAL("vec"); - mln_style_source_info info = {.size = sizeof(mln_style_source_info)}; - bool found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_info(map, source_id, &info, &found) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_SOURCE_TYPE_VECTOR, info.type); - TEST_ASSERT_BITS_HIGH( - MLN_STYLE_SOURCE_INFO_TILEJSON | MLN_STYLE_SOURCE_INFO_BOUNDS | - MLN_STYLE_SOURCE_INFO_TILE_SIZE | MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING, - info.fields - ); - TEST_ASSERT_BITS_LOW(MLN_STYLE_SOURCE_INFO_URL, info.fields); - TEST_ASSERT_EQUAL_size_t(2, info.tile_count); - TEST_ASSERT_EQUAL_DOUBLE(2.0, info.min_zoom); - TEST_ASSERT_EQUAL_DOUBLE(7.0, info.max_zoom); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_TILE_SCHEME_TMS, info.scheme); - TEST_ASSERT_EQUAL_DOUBLE(-5.0, info.bounds.southwest.latitude); - TEST_ASSERT_EQUAL_DOUBLE(-10.0, info.bounds.southwest.longitude); - TEST_ASSERT_EQUAL_DOUBLE(15.0, info.bounds.northeast.latitude); - TEST_ASSERT_EQUAL_DOUBLE(20.0, info.bounds.northeast.longitude); - TEST_ASSERT_EQUAL_UINT32(512, info.tile_size); - TEST_ASSERT_EQUAL_UINT32( - MLN_STYLE_VECTOR_TILE_ENCODING_MLT, info.vector_encoding - ); - - mln_style_string_list tile_urls = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_style_source_tile_urls(map, source_id, &tile_urls, &found) - ); - TEST_ASSERT_TRUE(found); - size_t tile_count = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_string_list_count(tile_urls, &tile_count) - ); - TEST_ASSERT_EQUAL_size_t(2, tile_count); - - mln_buffer_view tiles[2] = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_string_list_get(tile_urls, 0, &tiles[0]) - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_string_list_get(tile_urls, 1, &tiles[1]) - ); - TEST_ASSERT_EQUAL_STRING_LEN( - "https://a.example/{z}/{x}/{y}.mlt", tiles[0].data, tiles[0].size - ); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_style_string_list_get(tile_urls, tile_count, &tiles[0]) - ); - - char attribution[7] = {0}; - size_t attribution_size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_source_attribution( - map, source_id, attribution, sizeof(attribution), - &attribution_size, &found - ) - ); - TEST_ASSERT_EQUAL_size_t(7, attribution_size); - TEST_ASSERT_EQUAL_STRING_LEN("example", attribution, attribution_size); - - mln_style_tile_source_options options = - mln_style_tile_source_options_default(); - options.fields = MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM | - MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM | - MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION | - MLN_STYLE_TILE_SOURCE_OPTION_SCHEME | - MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS | - MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING; - options.min_zoom = info.min_zoom; - options.max_zoom = info.max_zoom; - options.attribution = - (mln_buffer_view){.data = attribution, .size = attribution_size}; - options.scheme = info.scheme; - options.bounds = info.bounds; - options.vector_encoding = info.vector_encoding; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_add_vector_source_tiles( - map, MLN_STRING_LITERAL("rebuilt"), tiles, tile_count, &options - ) - ); - - mln_style_string_list_destroy(tile_urls); - mln_style_string_list_destroy(tile_urls); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_style_string_list_count(tile_urls, &tile_count) - ); - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// A URL-backed tile source stays represented by its URL. The resolved remote -// TileJSON is runtime state rather than part of its reconstructible descriptor. -static void style_source_info_reports_url_backed_tile_source(void) { - static const char source_url[] = "https://example.com/source.json"; - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - const mln_buffer_view source_id = MLN_STRING_LITERAL("remote"); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_vector_source_url( - map, source_id, MLN_STRING_LITERAL(source_url), NULL - ) - ); - - mln_style_source_info info = {.size = sizeof(mln_style_source_info)}; - bool found = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_info(map, source_id, &info, &found) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_BITS_HIGH(MLN_STYLE_SOURCE_INFO_URL, info.fields); - TEST_ASSERT_BITS_LOW(MLN_STYLE_SOURCE_INFO_TILEJSON, info.fields); - TEST_ASSERT_EQUAL_size_t(sizeof(source_url) - 1, info.url_size); - - size_t copied_size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_style_source_url(map, source_id, NULL, 0, &copied_size, &found) - ); - TEST_ASSERT_EQUAL_size_t(sizeof(source_url) - 1, copied_size); - char too_small[2] = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_source_url( - map, source_id, too_small, sizeof(too_small), &copied_size, &found - ) - ); - char copied_url[sizeof(source_url)] = {0}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_style_source_url( - map, source_id, copied_url, sizeof(copied_url), &copied_size, &found - ) - ); - TEST_ASSERT_EQUAL_STRING_LEN(source_url, copied_url, copied_size); - - mln_style_string_list urls = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_style_source_tile_urls(map, source_id, &urls, &found) - ); - size_t count = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_string_list_count(urls, &count) - ); - TEST_ASSERT_EQUAL_size_t(0, count); - mln_style_string_list_destroy(urls); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -static void style_source_info_reports_other_source_shapes(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_set_style_json( - map, - MLN_BUFFER_LITERAL( - "{\"version\":8,\"sources\":{" - "\"dem\":{\"type\":\"raster-dem\",\"tiles\":[\"https://example.com/dem/" - "{z}/{x}/{y}.png\"],\"tileSize\":256,\"encoding\":\"terrarium\"}," - "\"geo-url\":{\"type\":\"geojson\",\"data\":\"https://example.com/" - "data.geojson\"}," - "\"geo-data\":{\"type\":\"geojson\",\"data\":{\"type\":" - "\"FeatureCollection\",\"features\":[]}}," - "\"image\":{\"type\":\"image\",\"url\":\"https://example.com/" - "image.png\",\"coordinates\":[[-1,1],[1,1],[1,-1],[-1,-1]]}}," - "\"layers\":[]}" - ) - ) - ); - - bool found = false; - mln_style_source_info info = {.size = sizeof(mln_style_source_info)}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_style_source_info(map, MLN_STRING_LITERAL("dem"), &info, &found) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_BITS_HIGH( - MLN_STYLE_SOURCE_INFO_TILEJSON | MLN_STYLE_SOURCE_INFO_TILE_SIZE | - MLN_STYLE_SOURCE_INFO_RASTER_ENCODING, - info.fields - ); - TEST_ASSERT_EQUAL_UINT32(256, info.tile_size); - TEST_ASSERT_EQUAL_DOUBLE(0.0, info.min_zoom); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_TILE_SCHEME_XYZ, info.scheme); - TEST_ASSERT_EQUAL_UINT32( - MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM, info.raster_encoding - ); - - const mln_buffer_view url_ids[] = { - MLN_STRING_LITERAL("geo-url"), MLN_STRING_LITERAL("image") - }; - for (size_t index = 0; index < 2; index += 1) { - info = (mln_style_source_info){.size = sizeof(mln_style_source_info)}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_get_style_source_info(map, url_ids[index], &info, &found) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_BITS_HIGH(MLN_STYLE_SOURCE_INFO_URL, info.fields); - TEST_ASSERT_TRUE(info.url_size > 0); - } - - info = (mln_style_source_info){.size = sizeof(mln_style_source_info)}; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_info( - map, MLN_STRING_LITERAL("geo-data"), &info, &found - ) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_UINT32(0, info.fields); - - size_t url_size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_style_source_url( - map, MLN_STRING_LITERAL("geo-data"), NULL, 0, &url_size, &found - ) - ); - TEST_ASSERT_TRUE(found); - TEST_ASSERT_EQUAL_size_t(0, url_size); - - mln_style_string_list empty = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_tile_urls( - map, MLN_STRING_LITERAL("geo-data"), &empty, &found - ) - ); - size_t count = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_style_string_list_count(empty, &count) - ); - TEST_ASSERT_EQUAL_size_t(0, count); - mln_style_string_list_destroy(empty); - - mln_style_string_list missing = MLN_HANDLE_NULL; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_tile_urls( - map, MLN_STRING_LITERAL("missing"), &missing, &found - ) - ); - TEST_ASSERT_FALSE(found); - TEST_ASSERT_EQUAL_UINT64(MLN_HANDLE_NULL, missing); - url_size = 123; - found = true; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_style_source_url( - map, MLN_STRING_LITERAL("missing"), NULL, 0, &url_size, &found - ) - ); - TEST_ASSERT_FALSE(found); - TEST_ASSERT_EQUAL_size_t(0, url_size); - - info = (mln_style_source_info){.size = sizeof(mln_style_source_info)}; - found = true; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_source_info( - map, MLN_STRING_LITERAL("missing"), &info, &found - ) - ); - TEST_ASSERT_FALSE(found); - TEST_ASSERT_EQUAL_UINT32(MLN_STYLE_SOURCE_TYPE_UNKNOWN, info.type); - TEST_ASSERT_EQUAL_UINT32(0, info.fields); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_style_source_tile_urls( - map, MLN_STRING_LITERAL("dem"), &empty, &found - ) - ); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// The loaded document is what the style loader last parsed, not a serialization -// of the live style. -static void loaded_style_document_reports_the_parsed_bytes(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - - // A map that has parsed no style reports no document and no URL. - size_t size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_loaded_style_json(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - - static const char style_json[] = - "{\"version\":8,\"name\":\"probe\",\"sources\":{},\"layers\":[]}"; - const size_t style_json_length = sizeof(style_json) - 1; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_json(map, MLN_BUFFER_LITERAL(style_json)) - ); - - // The copy is byte-for-byte the string that was loaded, so a host can hand it - // straight back to mln_map_set_style_json(). - size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_loaded_style_json(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(style_json_length, size); - - uint8_t document[sizeof(style_json)] = {0}; - size = 0; - // An exact-length buffer is enough because the copy carries no terminator. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_loaded_style_json(map, document, style_json_length, &size) - ); - TEST_ASSERT_EQUAL_size_t(style_json_length, size); - TEST_ASSERT_EQUAL_INT(0, memcmp(document, style_json, style_json_length)); - - // Loading inline JSON clears the style URL. - size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - - // Mutating the live style does not rewrite the parsed document. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_add_style_layer_json( - map, - MLN_BUFFER_LITERAL( - "{\"id\":\"stale-background\",\"type\":\"background\"}" - ), - (mln_buffer_view){NULL, 0} - ) - ); - - uint8_t after[sizeof(style_json)] = {0}; - size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_loaded_style_json(map, after, sizeof(after), &size) - ); - TEST_ASSERT_EQUAL_size_t(style_json_length, size); - TEST_ASSERT_EQUAL_INT(0, memcmp(after, style_json, style_json_length)); - - // A failed parse leaves the previously parsed document in place. - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_NATIVE_ERROR, - mln_map_set_style_json(map, MLN_BUFFER_LITERAL("{\"version\":")) - ); - uint8_t retained[sizeof(style_json)] = {0}; - size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, - mln_map_copy_loaded_style_json(map, retained, sizeof(retained), &size) - ); - TEST_ASSERT_EQUAL_size_t(style_json_length, size); - TEST_ASSERT_EQUAL_INT(0, memcmp(retained, style_json, style_json_length)); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// The style URL is recorded when the request is made, so it reports live state -// while the document reports what last parsed. -static void style_url_reports_the_requested_url(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - - // An unreachable URL is still recorded: the URL is request state, not a - // result. The load fails later through the event stream. - static const char style_url[] = "custom://style.json"; - const size_t style_url_length = sizeof(style_url) - 1; - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_style_url(map, style_url)); - - size_t size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(style_url_length, size); - - char url[sizeof(style_url)] = {0}; - size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, url, style_url_length, &size) - ); - TEST_ASSERT_EQUAL_size_t(style_url_length, size); - TEST_ASSERT_EQUAL_INT(0, memcmp(url, style_url, style_url_length)); - - char too_small[4] = {0}; - size = 0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_copy_style_url(map, too_small, sizeof(too_small), &size) - ); - TEST_ASSERT_EQUAL_size_t(style_url_length, size); - - // Nothing parsed yet, so the document stays empty while the URL is set. - size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_loaded_style_json(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - - // An empty URL is accepted and reads back as zero bytes, which this entry - // point cannot tell apart from no URL requested. - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_set_style_url(map, "")); - size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, NULL, 0, &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - size = 123; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_copy_style_url(map, url, sizeof(url), &size) - ); - TEST_ASSERT_EQUAL_size_t(0, size); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - -// Bindings always emit a full struct header and reject non-finite durations -// before the call, so only raw C callers reach these rejections. -static void style_transition_options_reject_unsafe_raw_headers(void) { - mln_runtime runtime = mln_test_create_runtime(); - mln_map map = mln_test_create_map(runtime); - - // Every rejection below leaves this applied configuration untouched, so a - // setter that committed one field before validating another fails here. - mln_style_transition_options applied = mln_style_transition_options_default(); - applied.fields = MLN_STYLE_TRANSITION_OPTION_DURATION | - MLN_STYLE_TRANSITION_OPTION_DELAY | - MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; - applied.duration_ms = 42.0; - applied.delay_ms = 7.0; - applied.enable_placement_transitions = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &applied) - ); - - // Omitting the placement bit leaves the cross-fade on rather than carrying - // the struct's zero value through. The getter always reports the bit. - mln_style_transition_options omitted = mln_style_transition_options_default(); - omitted.fields = MLN_STYLE_TRANSITION_OPTION_DURATION; - omitted.duration_ms = 5.0; - omitted.enable_placement_transitions = false; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &omitted) - ); - mln_style_transition_options kept = mln_style_transition_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_transition_options(map, &kept) - ); - TEST_ASSERT_TRUE( - (kept.fields & MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS) != - 0U - ); - TEST_ASSERT_TRUE(kept.enable_placement_transitions); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &applied) - ); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_set_style_transition_options(map, NULL) - ); - - mln_style_transition_options short_size = - mln_style_transition_options_default(); - short_size.size = (uint32_t)(sizeof(mln_style_transition_options) - 1); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_transition_options(map, &short_size) - ); - - mln_style_transition_options unknown_field = - mln_style_transition_options_default(); - unknown_field.fields = 1U << 20U; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_transition_options(map, &unknown_field) - ); - - // Each rejected duration carries a valid delay and vice versa, so a setter - // that commits before validating leaves the baseline half-overwritten in - // whichever order it validates. - mln_style_transition_options infinite_duration = - mln_style_transition_options_default(); - infinite_duration.fields = - MLN_STYLE_TRANSITION_OPTION_DURATION | MLN_STYLE_TRANSITION_OPTION_DELAY; - infinite_duration.duration_ms = INFINITY; - infinite_duration.delay_ms = 99.0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_transition_options(map, &infinite_duration) - ); - - mln_style_transition_options negative_delay = - mln_style_transition_options_default(); - negative_delay.fields = - MLN_STYLE_TRANSITION_OPTION_DURATION | MLN_STYLE_TRANSITION_OPTION_DELAY; - negative_delay.duration_ms = 99.0; - negative_delay.delay_ms = -1.0; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_set_style_transition_options(map, &negative_delay) - ); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, mln_map_get_style_transition_options(map, NULL) - ); - - mln_style_transition_options short_out = - mln_style_transition_options_default(); - short_out.size = (uint32_t)(sizeof(mln_style_transition_options) - 1); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_INVALID_ARGUMENT, - mln_map_get_style_transition_options(map, &short_out) - ); - - // The largest accepted duration must survive conversion into native ticks, - // where a rounded-up maximum wraps to the most negative duration. The ceiling - // is bisected rather than named because it follows from the native duration - // type rather than from this API. - mln_style_transition_options probe = mln_style_transition_options_default(); - probe.fields = MLN_STYLE_TRANSITION_OPTION_DURATION; - // Zero is the one duration this API always accepts. - double accepted = 0.0; - double rejected = 1.0; - probe.duration_ms = accepted; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &probe) - ); - // Doubling until one is rejected brackets the bisection below. A non-finite - // bound is rejected too, so this terminates. - for (int step = 0; step < 4096; step++) { - probe.duration_ms = rejected; - if (mln_map_set_style_transition_options(map, &probe) != MLN_STATUS_OK) { - break; - } - accepted = rejected; - rejected *= 2.0; - } - for (int step = 0; step < 4096; step++) { - const double midpoint = accepted + ((rejected - accepted) / 2.0); - if (midpoint <= accepted || midpoint >= rejected) { - break; - } - probe.duration_ms = midpoint; - if (mln_map_set_style_transition_options(map, &probe) == MLN_STATUS_OK) { - accepted = midpoint; - } else { - rejected = midpoint; - } - } - probe.duration_ms = accepted; - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &probe) - ); - mln_style_transition_options ceiling = mln_style_transition_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_transition_options(map, &ceiling) - ); - TEST_ASSERT_TRUE(ceiling.duration_ms >= 0.0); - - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_set_style_transition_options(map, &applied) - ); - - mln_style_transition_options unchanged = - mln_style_transition_options_default(); - TEST_ASSERT_EQUAL_INT( - MLN_STATUS_OK, mln_map_get_style_transition_options(map, &unchanged) - ); - TEST_ASSERT_EQUAL_UINT32(applied.fields, unchanged.fields); - TEST_ASSERT_EQUAL_DOUBLE(applied.duration_ms, unchanged.duration_ms); - TEST_ASSERT_EQUAL_DOUBLE(applied.delay_ms, unchanged.delay_ms); - TEST_ASSERT_FALSE(unchanged.enable_placement_transitions); - - mln_test_destroy_map(map); - mln_test_destroy_runtime(runtime); -} - void run_style_values_abi_tests(void) { UnitySetTestFile(__FILE__); - RUN_TEST(style_value_helpers_reject_unsafe_raw_descriptors); - RUN_TEST(geojson_source_options_reject_unsafe_raw_values); + RUN_TEST(style_command_deep_copies_and_ordered_read_observes_it); + RUN_TEST(duplicate_id_is_an_async_failed_terminal_event); + RUN_TEST(remove_commands_commit_and_report_missing_ids); + RUN_TEST(an_in_use_source_removal_fails_and_leaves_the_source); + RUN_TEST(geojson_source_data_create_rejects_unsafe_raw_values); RUN_TEST(clustered_geojson_data_reports_non_point_geometry); RUN_TEST(clustered_geojson_data_requires_a_feature_collection); RUN_TEST(prepared_geojson_data_installs_and_checks_options); - RUN_TEST(optional_json_style_values_return_null_handles); - RUN_TEST(layer_source_accessors_reject_sourceless_layer_types); - RUN_TEST(layer_text_accessors_report_required_capacity); - RUN_TEST(layer_zoom_and_visibility_accessors_carry_raw_domains); - RUN_TEST(style_image_stretch_descriptors_reject_unsafe_raw_values); - RUN_TEST(copy_entry_points_answer_a_null_buffer_as_a_size_probe); - RUN_TEST(style_source_info_rebuilds_an_inline_tile_source); - RUN_TEST(style_source_info_reports_url_backed_tile_source); - RUN_TEST(style_source_info_reports_other_source_shapes); - RUN_TEST(loaded_style_document_reports_the_parsed_bytes); - RUN_TEST(style_url_reports_the_requested_url); - RUN_TEST(style_transition_options_reject_unsafe_raw_headers); + RUN_TEST(layer_info_reports_scalars_and_sizes_the_source_id_copy); + RUN_TEST(image_source_coordinates_are_borrowed_by_the_completion); + RUN_TEST(style_image_stretches_are_borrowed_by_the_completion); } diff --git a/src/c_api/tests/test_support.c b/src/c_api/tests/test_support.c index 263996913..4118f16ac 100644 --- a/src/c_api/tests/test_support.c +++ b/src/c_api/tests/test_support.c @@ -43,6 +43,29 @@ EM_JS( }; } ); +EM_JS( + void*, mln_test_register_transferred_offscreen_canvas, + (const char* name, int width, int height), { + const id = UTF8ToString(name); + const selector = "#" + id; + const canvas = new OffscreenCanvas(width, height); + const canvasSharedPtr = _malloc(12); + HEAP32[canvasSharedPtr >> 2] = width; + HEAP32[canvasSharedPtr + 4 >> 2] = height; + HEAPU32[canvasSharedPtr + 8 >> 2] = 0; + Module["GL"].offscreenCanvases[selector] = { + canvas : canvas, + offscreenCanvas : canvas, + canvasSharedPtr : canvasSharedPtr, + id : id, + }; + return canvasSharedPtr; + } +); +EM_JS( + void, mln_test_unregister_transferred_offscreen_canvas, (const char* name), + { delete Module["GL"].offscreenCanvases["#" + UTF8ToString(name)]; } +); EM_JS(void, mln_test_unregister_offscreen_canvas, (const char* name), { delete Module["GL"].offscreenCanvases[UTF8ToString(name)]; }); @@ -86,6 +109,7 @@ typedef struct tracked_session { } tracked_session; static MLN_FFI_TEST_THREAD_LOCAL mln_runtime tracked_runtime; +static MLN_FFI_TEST_THREAD_LOCAL mln_event_batch compatibility_batch_handle; static MLN_FFI_TEST_THREAD_LOCAL mln_map tracked_maps[MLN_FFI_TEST_TRACKED_CAPACITY]; static MLN_FFI_TEST_THREAD_LOCAL size_t tracked_map_count; @@ -132,7 +156,8 @@ static void reserve_session_slot(void) { static void track_session(const mln_test_render_fixture* fixture) { tracked_sessions[tracked_session_count] = (tracked_session){ - .session = fixture->session, .backend_state = fixture->backend_state + .session = fixture->session, + .backend_state = fixture->backend_state, }; tracked_session_count += 1; } @@ -147,29 +172,208 @@ static void untrack_session(mln_render_session session) { } } +mln_status mln_test_runtime_create( + const mln_runtime_options* options, mln_runtime* out_runtime +) { + return mln_runtime_create(options, out_runtime); +} + +mln_status mln_test_runtime_close(mln_runtime runtime) { + mln_test_completion teardown = mln_test_completion_default(0); + const mln_status status = mln_runtime_release(runtime, &teardown.descriptor); + if (status != MLN_STATUS_OK) { + // A rejected submission leaves user_data caller-owned, so release it here + // before destroy waits for the release marker. + mln_test_completion_reject(&teardown); + mln_test_completion_destroy(&teardown); + return status; + } + // Waiting keeps process exit ordered after native teardown, which otherwise + // races MapLibre's process-wide singletons in short-lived hosts. + const bool completed = mln_test_completion_wait(&teardown, -1); + const mln_status teardown_status = mln_test_completion_status(&teardown); + mln_test_completion_destroy(&teardown); + if (!completed) { + return MLN_STATUS_NATIVE_ERROR; + } + return teardown_status; +} + mln_runtime mln_test_create_runtime(void) { mln_runtime runtime = MLN_HANDLE_NULL; - const mln_runtime_options options = mln_runtime_options_default(); - const mln_status status = mln_runtime_create(&options, &runtime); - if (status == MLN_STATUS_INVALID_STATE) { - TEST_FAIL_MESSAGE( - "This thread already owns a live runtime, so an earlier test leaked one. " - "Look for the first failing test above and destroy the runtime it " - "created." - ); - } - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, status); + mln_runtime_options options = mln_runtime_options_default(); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_create(&options, &runtime)); TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, runtime); tracked_runtime = runtime; return runtime; } +mln_status mln_test_map_create_status( + mln_runtime runtime, const mln_map_options* options, mln_map* out_map +) { + mln_test_completion completion = + mln_test_completion_default(sizeof(*out_map)); + mln_status status = mln_map_create(runtime, options, &completion.descriptor); + if (status != MLN_STATUS_OK) { + completion.descriptor.release_user_data(completion.descriptor.user_data); + mln_test_completion_destroy(&completion); + return status; + } + if (!mln_test_completion_wait(&completion, -1)) { + status = MLN_STATUS_NATIVE_ERROR; + } else { + status = mln_test_completion_status(&completion); + if ( + status == MLN_STATUS_OK && + !mln_test_completion_copy_value(&completion, out_map, sizeof(*out_map)) + ) { + status = MLN_STATUS_NATIVE_ERROR; + } + } + mln_test_completion_destroy(&completion); + return status; +} + +mln_status mln_test_map_close(mln_map map) { + mln_test_completion teardown = mln_test_completion_default(0); + const mln_status status = mln_map_release(map, &teardown.descriptor); + if (status != MLN_STATUS_OK) { + mln_test_completion_reject(&teardown); + mln_test_completion_destroy(&teardown); + return status; + } + const bool completed = mln_test_completion_wait(&teardown, -1); + const mln_status teardown_status = mln_test_completion_status(&teardown); + mln_test_completion_destroy(&teardown); + return completed ? teardown_status : MLN_STATUS_NATIVE_ERROR; +} + +mln_status mln_test_map_get_event_mask(mln_map map, uint64_t* out_mask) { + if (out_mask == NULL) { + return MLN_STATUS_INVALID_ARGUMENT; + } + mln_map_snapshot snapshot = {.size = sizeof(mln_map_snapshot)}; + const mln_status status = mln_map_snapshot_get(map, &snapshot); + if (status == MLN_STATUS_OK) { + *out_mask = snapshot.event_mask; + } + return status; +} + +mln_status mln_test_map_get_camera( + mln_map map, mln_camera_options* out_camera +) { + if (out_camera == NULL) { + return MLN_STATUS_INVALID_ARGUMENT; + } + uint64_t generation = 0; + return mln_map_camera_snapshot_get(map, out_camera, &generation); +} + +static void discard_completion( + void* user_data, const mln_completion_result* result +) { + (void)user_data; + (void)result; +} + +mln_completion mln_test_discard_completion(void) { + const mln_completion completion = { + .size = sizeof(mln_completion), + .callback = discard_completion, + }; + return completion; +} + +mln_status mln_test_map_request_repaint(mln_map map) { + const mln_completion completion = mln_test_discard_completion(); + return mln_map_request_repaint(map, &completion); +} + +mln_status mln_test_map_set_event_mask(mln_map map, uint64_t mask) { + const mln_completion completion = mln_test_discard_completion(); + return mln_map_set_event_mask(map, mask, &completion); +} + +mln_status mln_test_map_set_style_json(mln_map map, mln_buffer_view json) { + const mln_completion completion = mln_test_discard_completion(); + return mln_map_set_style_json(map, json, &completion); +} + +mln_status mln_test_map_set_style_url(mln_map map, const char* url) { + const mln_completion completion = mln_test_discard_completion(); + return mln_map_set_style_url(map, url, &completion); +} + +static mln_status copy_map_buffer_completion( + mln_status (*submit)(mln_map, const mln_completion*), mln_map map, char* out, + size_t capacity, size_t* out_size +) { + if (out_size == NULL || (out == NULL && capacity != 0)) { + return MLN_STATUS_INVALID_ARGUMENT; + } + mln_test_completion completion = mln_test_completion_buffer_view(); + mln_status status = submit(map, &completion.descriptor); + if (status != MLN_STATUS_OK) { + completion.descriptor.release_user_data(completion.descriptor.user_data); + mln_test_completion_destroy(&completion); + return status; + } + mln_buffer_view view = {0}; + if (!mln_test_completion_wait(&completion, -1)) { + status = MLN_STATUS_NATIVE_ERROR; + } else { + status = mln_test_completion_status(&completion); + if ( + status == MLN_STATUS_OK && + !mln_test_completion_copy_value(&completion, &view, sizeof(view)) + ) { + status = MLN_STATUS_NATIVE_ERROR; + } + } + if (status == MLN_STATUS_OK) { + *out_size = view.size; + if (capacity < view.size) { + status = MLN_STATUS_INVALID_ARGUMENT; + } else if (view.size != 0) { + memcpy(out, view.data, view.size); + } + } + mln_test_completion_destroy(&completion); + return status; +} + +mln_status mln_test_map_copy_loaded_style_json( + mln_map map, char* out, size_t capacity, size_t* out_size +) { + return copy_map_buffer_completion( + mln_map_loaded_style_json, map, out, capacity, out_size + ); +} + +mln_status mln_test_map_copy_style_url( + mln_map map, char* out, size_t capacity, size_t* out_size +) { + return copy_map_buffer_completion( + mln_map_style_url, map, out, capacity, out_size + ); +} + mln_map mln_test_create_map_with_options( mln_runtime runtime, const mln_map_options* options ) { mln_map map = MLN_HANDLE_NULL; + mln_test_completion completion = mln_test_completion_default(sizeof(map)); reserve_map_slot(); - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_create(runtime, options, &map)); + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, mln_map_create(runtime, options, &completion.descriptor) + ); + TEST_ASSERT_TRUE(mln_test_completion_wait(&completion, -1)); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_completion_status(&completion)); + TEST_ASSERT_TRUE( + mln_test_completion_copy_value(&completion, &map, sizeof(map)) + ); + mln_test_completion_destroy(&completion); TEST_ASSERT_NOT_EQUAL_UINT64(MLN_HANDLE_NULL, map); track_map(map); return map; @@ -177,22 +381,42 @@ mln_map mln_test_create_map_with_options( mln_map mln_test_create_map(mln_runtime runtime) { mln_map_options options = mln_map_options_default(); - options.width = 512; - options.height = 512; + options.initial_extent.width = 64; + options.initial_extent.height = 64; return mln_test_create_map_with_options(runtime, &options); } -// Untracking happens only after the destroy succeeds, so a handle whose destroy -// is rejected for now stays tracked for teardown to reclaim. void mln_test_destroy_runtime(mln_runtime runtime) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_runtime_destroy(runtime)); + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_test_runtime_close(runtime)); if (tracked_runtime == runtime) { tracked_runtime = MLN_HANDLE_NULL; } } +mln_status mln_test_runtime_barrier(mln_runtime runtime) { + mln_test_completion completion = mln_test_completion_default(0); + mln_status status = mln_runtime_barrier(runtime, &completion.descriptor); + if (status != MLN_STATUS_OK) { + completion.descriptor.release_user_data(completion.descriptor.user_data); + mln_test_completion_destroy(&completion); + return status; + } + if (!mln_test_completion_wait(&completion, -1)) { + status = MLN_STATUS_NATIVE_ERROR; + } else { + status = mln_test_completion_status(&completion); + } + mln_test_completion_destroy(&completion); + return status; +} + void mln_test_destroy_map(mln_map map) { - TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_destroy(map)); + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; + const mln_completion release = mln_test_discard_completion(); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_OK, mln_map_release(map, &release)); untrack_map(map); } @@ -331,6 +555,40 @@ void mln_test_thread_join(mln_test_thread* thread) { free(thread); } +static void count_wake(void* user_data) { + atomic_fetch_add((atomic_uint*)user_data, 1U); +} + +mln_status mln_test_render_fixture_finish_operation( + const mln_test_render_fixture* fixture, mln_test_completion* completion +) { + if ( + fixture == NULL || fixture->session == MLN_HANDLE_NULL || completion == NULL + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + bool completed = false; + for (unsigned int attempt = 0; attempt < 10000 && !completed; attempt += 1) { + if (fixture->driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + size_t serviced = 0; + const mln_status service_status = mln_render_session_service_driver_work( + fixture->session, SIZE_MAX, &serviced + ); + if (service_status != MLN_STATUS_OK) { + return service_status; + } + } + completed = mln_test_completion_poll(completion); + if (!completed) { + mln_test_sleep_millisecond(); + } + } + if (!completed) { + return MLN_STATUS_NOT_READY; + } + return mln_test_completion_status(completion); +} + #if defined(MLN_FFI_TEST_BACKEND_METAL) extern void* MTLCreateSystemDefaultDevice(void); @@ -487,12 +745,10 @@ static bool create_backend_state(void** out_state, void* out_context) { // EGL_DEFAULT_DISPLAY, so the display is shared with everything else in the // process, and terminating it retires EGL objects the C API still holds. A // display stays initialized for the process either way, so leaving it is free. -mln_test_fixture_result mln_test_dedicated_egl_surface_create( - mln_map map, mln_test_render_fixture* fixture -) { +static egl_state* create_dedicated_egl_state(void) { egl_state* state = calloc(1, sizeof(egl_state)); if (state == NULL) { - return MLN_TEST_FIXTURE_UNAVAILABLE; + return NULL; } state->display = get_egl_display(); if ( @@ -500,7 +756,7 @@ mln_test_fixture_result mln_test_dedicated_egl_surface_create( eglInitialize(state->display, NULL, NULL) == EGL_FALSE ) { free(state); - return MLN_TEST_FIXTURE_UNAVAILABLE; + return NULL; } const EGLint config_attributes[] = { EGL_SURFACE_TYPE, @@ -529,6 +785,16 @@ mln_test_fixture_result mln_test_dedicated_egl_surface_create( config_count == 0 || state->config == NULL ) { free(state); + return NULL; + } + return state; +} + +mln_test_fixture_result mln_test_dedicated_egl_surface_create( + mln_map map, mln_test_render_fixture* fixture +) { + egl_state* state = create_dedicated_egl_state(); + if (state == NULL) { return MLN_TEST_FIXTURE_UNAVAILABLE; } const EGLint surface_attributes[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE}; @@ -566,14 +832,36 @@ mln_test_fixture_result mln_test_dedicated_egl_surface_create( }; descriptor.surface = state->surface; - mln_render_session session = 0; - if (mln_opengl_surface_attach(map, &descriptor, &session) != MLN_STATUS_OK) { + fixture->driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; + fixture->backend_state = state; + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = fixture->driver; + mln_test_completion attach = mln_test_completion_default(0); + const mln_status attach_status = mln_opengl_surface_attach( + map, &descriptor, &options, &fixture->session, &attach.descriptor + ); + if ( + attach_status != MLN_STATUS_OK || + mln_test_render_fixture_finish_operation(fixture, &attach) != MLN_STATUS_OK + ) { + if (attach_status != MLN_STATUS_OK) { + attach.descriptor.release_user_data(attach.descriptor.user_data); + } + mln_test_completion_destroy(&attach); + if (fixture->session != MLN_HANDLE_NULL) { + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(fixture->session, &abandoned); + (void)mln_render_session_destroy(fixture->session); + } eglDestroySurface(state->display, state->surface); free(state); + *fixture = (mln_test_render_fixture){0}; return MLN_TEST_FIXTURE_ATTACH_FAILED; } - fixture->session = session; - fixture->backend_state = state; + mln_test_completion_destroy(&attach); return MLN_TEST_FIXTURE_OK; } @@ -582,7 +870,17 @@ void mln_test_dedicated_egl_surface_destroy(mln_test_render_fixture* fixture) { return; } if (fixture->session != 0) { - mln_render_session_destroy(fixture->session); + mln_test_completion detach = mln_test_completion_default(0); + if ( + mln_render_session_detach(fixture->session, &detach.descriptor) == + MLN_STATUS_OK + ) { + (void)mln_test_render_fixture_finish_operation(fixture, &detach); + } else { + detach.descriptor.release_user_data(detach.descriptor.user_data); + } + mln_test_completion_destroy(&detach); + (void)mln_render_session_destroy(fixture->session); fixture->session = 0; } egl_state* state = fixture->backend_state; @@ -597,6 +895,92 @@ bool mln_test_egl_context_is_current(void) { return eglGetCurrentContext() != EGL_NO_CONTEXT; } +mln_test_fixture_result mln_test_dedicated_egl_texture_create( + mln_map map, mln_test_render_fixture* fixture +) { + egl_state* state = create_dedicated_egl_state(); + if (state == NULL) { + return MLN_TEST_FIXTURE_UNAVAILABLE; + } + mln_opengl_owned_texture_descriptor descriptor = + mln_opengl_owned_texture_descriptor_default(); + descriptor.extent.width = 64; + descriptor.extent.height = 64; + descriptor.context = (mln_opengl_context_descriptor){ + .size = sizeof(mln_opengl_context_descriptor), + .platform = MLN_OPENGL_CONTEXT_PLATFORM_EGL, + .ownership = MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED, + .data = { + .egl = { + .size = sizeof(mln_egl_context_descriptor), + .display = state->display, + .config = state->config, + .share_context = NULL, + .client_api = MLN_OPENGL_CLIENT_API_GLES, + .get_proc_address = NULL, + } + }, + }; + fixture->driver = MLN_RENDER_DRIVER_CORE_WORKER; + fixture->backend_state = state; + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = fixture->driver; + options.requested_texture_ring_depth = 3; + mln_test_completion attach = mln_test_completion_default(0); + const mln_status attach_status = mln_opengl_owned_texture_attach( + map, &descriptor, &options, &fixture->session, &attach.descriptor + ); + const mln_status finish_status = + attach_status == MLN_STATUS_OK + ? mln_test_render_fixture_finish_operation(fixture, &attach) + : attach_status; + if (attach_status != MLN_STATUS_OK) { + attach.descriptor.release_user_data(attach.descriptor.user_data); + } + mln_test_completion_destroy(&attach); + if (finish_status != MLN_STATUS_OK) { + if (fixture->session != MLN_HANDLE_NULL) { + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(fixture->session, &abandoned); + (void)mln_render_session_destroy(fixture->session); + } + free(state); + *fixture = (mln_test_render_fixture){0}; + return MLN_TEST_FIXTURE_ATTACH_FAILED; + } + return MLN_TEST_FIXTURE_OK; +} + +void mln_test_dedicated_egl_texture_destroy(mln_test_render_fixture* fixture) { + if (fixture == NULL) { + return; + } + if (fixture->session != MLN_HANDLE_NULL) { + mln_test_completion detach = mln_test_completion_default(0); + mln_status detach_status = + mln_render_session_detach(fixture->session, &detach.descriptor); + if (detach_status == MLN_STATUS_OK) { + detach_status = + mln_test_render_fixture_finish_operation(fixture, &detach); + } else { + detach.descriptor.release_user_data(detach.descriptor.user_data); + } + mln_test_completion_destroy(&detach); + if (detach_status != MLN_STATUS_OK) { + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(fixture->session, &abandoned); + } + (void)mln_render_session_destroy(fixture->session); + } + free(fixture->backend_state); + *fixture = (mln_test_render_fixture){0}; +} + static void destroy_backend_state(void* opaque_state) { egl_state* state = opaque_state; if (state == NULL) { @@ -758,16 +1142,36 @@ static void destroy_backend_state(void* opaque_state) { (void)opaque_state; } typedef struct webgl_state { EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context; char id[32]; + void* canvas_shared_ptr; } webgl_state; static atomic_uint webgl_canvas_counter; +static _Thread_local webgl_state thread_webgl_state; // The fixture creates a real WebGL2 context and hands it over, the way a // browser host would. static bool create_backend_state(void** out_state, void* out_context) { - webgl_state* state = calloc(1, sizeof(*state)); - if (state == NULL) { - return false; + webgl_state* state = &thread_webgl_state; + if (state->context > 0) { + if ( + emscripten_webgl_make_context_current(state->context) != + EMSCRIPTEN_RESULT_SUCCESS + ) { + return false; + } + *(mln_opengl_context_descriptor*)out_context = + (mln_opengl_context_descriptor){ + .size = sizeof(mln_opengl_context_descriptor), + .platform = MLN_OPENGL_CONTEXT_PLATFORM_WEBGL, + .data = { + .webgl = { + .size = sizeof(mln_webgl_context_descriptor), + .context = state->context, + } + }, + }; + *out_state = state; + return true; } EmscriptenWebGLContextAttributes attributes; @@ -798,7 +1202,7 @@ static bool create_backend_state(void** out_state, void* out_context) { (long)state->context ); mln_test_unregister_offscreen_canvas(state->id); - free(state); + *state = (webgl_state){0}; return false; } const EMSCRIPTEN_RESULT current_result = @@ -810,7 +1214,7 @@ static bool create_backend_state(void** out_state, void* out_context) { ); emscripten_webgl_destroy_context(state->context); mln_test_unregister_offscreen_canvas(state->id); - free(state); + *state = (webgl_state){0}; return false; } @@ -830,13 +1234,16 @@ static bool create_backend_state(void** out_state, void* out_context) { } static void destroy_backend_state(void* opaque_state) { + // WebGL contexts are expensive and browsers cap their live count. Sequential + // fixtures on one test thread share one host-owned context; the thread-level + // cleanup releases it after the suite or foreign thread exits. A transferred + // canvas has no host-owned context and is released with its fixture. webgl_state* state = opaque_state; - if (state == NULL) { - return; + if (state != NULL && state != &thread_webgl_state) { + mln_test_unregister_transferred_offscreen_canvas(state->id); + free(state->canvas_shared_ptr); + free(state); } - emscripten_webgl_destroy_context(state->context); - mln_test_unregister_offscreen_canvas(state->id); - free(state); } #elif defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WGL) @@ -1138,6 +1545,18 @@ void mln_test_dedicated_egl_surface_destroy(mln_test_render_fixture* fixture) { } bool mln_test_egl_context_is_current(void) { return false; } + +mln_test_fixture_result mln_test_dedicated_egl_texture_create( + mln_map map, mln_test_render_fixture* fixture +) { + (void)map; + (void)fixture; + return MLN_TEST_FIXTURE_UNAVAILABLE; +} + +void mln_test_dedicated_egl_texture_destroy(mln_test_render_fixture* fixture) { + (void)fixture; +} #endif #if defined(MLN_FFI_TEST_BACKEND_WEBGPU) @@ -1181,10 +1600,19 @@ void mln_test_release_thread_gpu_resources(void) { #endif } +#elif defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WEBGL) + +void mln_test_release_thread_gpu_resources(void) { + if (thread_webgl_state.context > 0) { + emscripten_webgl_destroy_context(thread_webgl_state.context); + mln_test_unregister_offscreen_canvas(thread_webgl_state.id); + } + thread_webgl_state = (webgl_state){0}; +} + #else void mln_test_release_thread_gpu_resources(void) {} - #endif bool mln_test_render_fixture_create( @@ -1195,7 +1623,11 @@ bool mln_test_render_fixture_create( } reserve_session_slot(); *fixture = (mln_test_render_fixture){0}; + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.requested_texture_ring_depth = 2; #if defined(MLN_FFI_TEST_BACKEND_METAL) + fixture->driver = MLN_RENDER_DRIVER_CORE_WORKER; mln_metal_context_descriptor context = {0}; if (!create_backend_state(&fixture->backend_state, &context)) { return false; @@ -1203,6 +1635,7 @@ bool mln_test_render_fixture_create( mln_metal_owned_texture_descriptor descriptor = mln_metal_owned_texture_descriptor_default(); #elif defined(MLN_FFI_TEST_BACKEND_OPENGL) + fixture->driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; mln_opengl_context_descriptor context = {0}; if (!create_backend_state(&fixture->backend_state, &context)) { return false; @@ -1210,6 +1643,7 @@ bool mln_test_render_fixture_create( mln_opengl_owned_texture_descriptor descriptor = mln_opengl_owned_texture_descriptor_default(); #elif defined(MLN_FFI_TEST_BACKEND_VULKAN) + fixture->driver = MLN_RENDER_DRIVER_CORE_WORKER; mln_vulkan_context_descriptor context = {0}; if (!create_backend_state(&fixture->backend_state, &context)) { return false; @@ -1217,6 +1651,7 @@ bool mln_test_render_fixture_create( mln_vulkan_owned_texture_descriptor descriptor = mln_vulkan_owned_texture_descriptor_default(); #elif defined(MLN_FFI_TEST_BACKEND_WEBGPU) + fixture->driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD; mln_webgpu_context_descriptor context = {0}; if (!create_backend_state(&fixture->backend_state, &context)) { return false; @@ -1224,31 +1659,202 @@ bool mln_test_render_fixture_create( mln_webgpu_owned_texture_descriptor descriptor = mln_webgpu_owned_texture_descriptor_default(); #endif + options.driver = fixture->driver; + options.frame_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = count_wake, + .user_data = &fixture->frame_wakes + }; + options.driver_work_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = count_wake, + .user_data = &fixture->driver_wakes + }; descriptor.extent.width = 64; descriptor.extent.height = 64; descriptor.context = context; + mln_test_completion completion = mln_test_completion_default(0); #if defined(MLN_FFI_TEST_BACKEND_METAL) - const mln_status status = - mln_metal_owned_texture_attach(map, &descriptor, &fixture->session); + const mln_status status = mln_metal_owned_texture_attach( + map, &descriptor, &options, &fixture->session, &completion.descriptor + ); #elif defined(MLN_FFI_TEST_BACKEND_OPENGL) - const mln_status status = - mln_opengl_owned_texture_attach(map, &descriptor, &fixture->session); + const mln_status status = mln_opengl_owned_texture_attach( + map, &descriptor, &options, &fixture->session, &completion.descriptor + ); #elif defined(MLN_FFI_TEST_BACKEND_VULKAN) - const mln_status status = - mln_vulkan_owned_texture_attach(map, &descriptor, &fixture->session); + const mln_status status = mln_vulkan_owned_texture_attach( + map, &descriptor, &options, &fixture->session, &completion.descriptor + ); #elif defined(MLN_FFI_TEST_BACKEND_WEBGPU) - const mln_status status = - mln_webgpu_owned_texture_attach(map, &descriptor, &fixture->session); + const mln_status status = mln_webgpu_owned_texture_attach( + map, &descriptor, &options, &fixture->session, &completion.descriptor + ); #endif - if (status != MLN_STATUS_OK || fixture->session == MLN_HANDLE_NULL) { + if (status == MLN_STATUS_OK && fixture->session != MLN_HANDLE_NULL) { + mln_render_session_snapshot attaching = { + .size = sizeof(mln_render_session_snapshot) + }; + fixture->observed_attaching = + mln_render_session_get_snapshot(fixture->session, &attaching) == + MLN_STATUS_OK && + attaching.state == MLN_RENDER_SESSION_STATE_ATTACHING; + if (fixture->driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + fixture->observed_driver_ready = atomic_load(&fixture->driver_wakes) != 0; + } + } + const mln_status finish_status = + status == MLN_STATUS_OK && fixture->session != MLN_HANDLE_NULL + ? mln_test_render_fixture_finish_operation(fixture, &completion) + : MLN_STATUS_INVALID_STATE; + if ( + status != MLN_STATUS_OK || fixture->session == MLN_HANDLE_NULL || + finish_status != MLN_STATUS_OK + ) { + if (status != MLN_STATUS_OK) { + completion.descriptor.release_user_data(completion.descriptor.user_data); + } + mln_test_completion_destroy(&completion); + if (fixture->session != MLN_HANDLE_NULL) { + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(fixture->session, &abandoned); + (void)mln_render_session_destroy(fixture->session); + } + destroy_backend_state(fixture->backend_state); + *fixture = (mln_test_render_fixture){0}; + return false; + } + mln_test_completion_destroy(&completion); + track_session(fixture); + return true; +} +#if defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WEBGL) + +bool mln_test_transferred_webgl_surface_create( + mln_map map, mln_test_render_fixture* fixture +) { + if (map == MLN_HANDLE_NULL || fixture == NULL) { + return false; + } + reserve_session_slot(); + *fixture = (mln_test_render_fixture){0}; + fixture->driver = MLN_RENDER_DRIVER_CORE_WORKER; + webgl_state* state = calloc(1, sizeof(*state)); + if (state == NULL) { + return false; + } + const unsigned int serial = atomic_fetch_add(&webgl_canvas_counter, 1U) + 1U; + (void)snprintf(state->id, sizeof(state->id), "mln-transfer-%u", serial); + state->canvas_shared_ptr = + mln_test_register_transferred_offscreen_canvas(state->id, 64, 64); + fixture->backend_state = state; + + char target[sizeof(state->id) + 1]; + (void)snprintf(target, sizeof(target), "#%s", state->id); + mln_opengl_surface_descriptor descriptor = + mln_opengl_surface_descriptor_default(); + descriptor.extent.width = 64; + descriptor.extent.height = 64; + descriptor.context.platform = MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; + descriptor.context.ownership = MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED; + descriptor.context.data.webgl = (mln_webgl_context_descriptor){ + .size = sizeof(mln_webgl_context_descriptor), + .kind = MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS, + .canvas_selector = mln_test_buffer_view(target, strlen(target)), + }; + mln_render_session_attach_options options = + mln_render_session_attach_options_default(); + options.driver = fixture->driver; + options.frame_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = count_wake, + .user_data = &fixture->frame_wakes + }; + options.driver_work_wake = (mln_wake){ + .size = sizeof(mln_wake), + .callback = count_wake, + .user_data = &fixture->driver_wakes + }; + mln_test_completion completion = mln_test_completion_default(0); + const mln_status status = mln_opengl_surface_attach( + map, &descriptor, &options, &fixture->session, &completion.descriptor + ); + const mln_status finish_status = + status == MLN_STATUS_OK && fixture->session != MLN_HANDLE_NULL + ? mln_test_render_fixture_finish_operation(fixture, &completion) + : MLN_STATUS_INVALID_STATE; + if ( + status != MLN_STATUS_OK || finish_status != MLN_STATUS_OK || + fixture->session == MLN_HANDLE_NULL + ) { + if (status != MLN_STATUS_OK) { + completion.descriptor.release_user_data(completion.descriptor.user_data); + } + fprintf( + stderr, + "transferred WebGL attach failed: submit=%d finish=%d diagnostic=%s\n", + status, finish_status, mln_test_completion_diagnostic(&completion) + ); + mln_test_completion_destroy(&completion); + if (fixture->session != MLN_HANDLE_NULL) { + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(fixture->session, &abandoned); + (void)mln_render_session_destroy(fixture->session); + } destroy_backend_state(fixture->backend_state); *fixture = (mln_test_render_fixture){0}; return false; } + mln_test_completion_destroy(&completion); track_session(fixture); return true; } +#endif + +mln_test_event_batch mln_test_event_batch_default(void) { + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; + return (mln_test_event_batch){.size = sizeof(mln_test_event_batch)}; +} + +mln_status mln_test_drain_events( + mln_runtime runtime, mln_test_event_batch* out_batch +) { + if (out_batch == NULL || out_batch->size < sizeof(mln_test_event_batch)) { + return MLN_STATUS_INVALID_ARGUMENT; + } + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; + mln_event_batch batch = MLN_HANDLE_NULL; + mln_status status = mln_runtime_drain_events(runtime, &batch); + if (status != MLN_STATUS_OK) { + return status; + } + mln_runtime_event_batch_view view = { + .size = sizeof(mln_runtime_event_batch_view) + }; + status = mln_event_batch_get(batch, &view); + if (status != MLN_STATUS_OK) { + mln_event_batch_release(batch); + return status; + } + compatibility_batch_handle = batch; + *out_batch = (mln_test_event_batch){ + .size = sizeof(mln_test_event_batch), + .event_size = view.event_size, + .events = view.events, + .event_count = view.event_count, + .messages = view.messages, + .messages_size = view.messages_size, + }; + return MLN_STATUS_OK; +} + size_t mln_test_drain_all(mln_runtime runtime) { return mln_test_drain_counting(runtime, 0); } @@ -1256,8 +1862,8 @@ size_t mln_test_drain_all(mln_runtime runtime) { size_t mln_test_drain_counting(mln_runtime runtime, uint32_t type) { size_t total = 0; for (;;) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { + mln_test_event_batch batch = mln_test_event_batch_default(); + if (mln_test_drain_events(runtime, &batch) != MLN_STATUS_OK) { return total; } if (batch.event_count == 0) { @@ -1281,8 +1887,8 @@ bool mln_test_drain_find( ) { bool found = false; for (;;) { - mln_runtime_event_batch batch = mln_runtime_event_batch_default(); - if (mln_runtime_drain_events(runtime, 0, &batch) != MLN_STATUS_OK) { + mln_test_event_batch batch = mln_test_event_batch_default(); + if (mln_test_drain_events(runtime, &batch) != MLN_STATUS_OK) { return found; } if (batch.event_count == 0) { @@ -1316,16 +1922,11 @@ bool mln_test_drain_find( } } -bool mln_test_pump_until(mln_runtime runtime, atomic_bool* flag) { +bool mln_test_wait_until(mln_runtime runtime, atomic_bool* flag) { for (unsigned int attempt = 0; attempt < 500; attempt += 1) { if (atomic_load(flag)) { return true; } - // A short park rather than zero: this waits on another thread's flag, so - // spinning would burn the whole loop budget before that thread ran. - if (mln_runtime_pump(runtime, 2, -1) != MLN_STATUS_OK) { - return false; - } // Drain so the queue does not grow without bound while we wait. mln_test_drain_all(runtime); if (atomic_load(flag)) { @@ -1337,10 +1938,25 @@ bool mln_test_pump_until(mln_runtime runtime, atomic_bool* flag) { } void mln_test_render_fixture_destroy(mln_test_render_fixture* fixture) { + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; if (fixture == NULL) { return; } if (fixture->session != MLN_HANDLE_NULL) { + mln_test_completion detach = mln_test_completion_default(0); + const mln_status detach_status = + mln_render_session_detach(fixture->session, &detach.descriptor); + if (detach_status == MLN_STATUS_OK) { + TEST_ASSERT_EQUAL_INT( + MLN_STATUS_OK, + mln_test_render_fixture_finish_operation(fixture, &detach) + ); + } else { + detach.descriptor.release_user_data(detach.descriptor.user_data); + TEST_ASSERT_EQUAL_INT(MLN_STATUS_INVALID_STATE, detach_status); + } + mln_test_completion_destroy(&detach); TEST_ASSERT_EQUAL_INT( MLN_STATUS_OK, mln_render_session_destroy(fixture->session) ); @@ -1351,6 +1967,8 @@ void mln_test_render_fixture_destroy(mln_test_render_fixture* fixture) { } bool mln_test_reclaim_thread_resources(void) { + mln_event_batch_release(compatibility_batch_handle); + compatibility_batch_handle = MLN_HANDLE_NULL; bool reclaimed = false; // Render session before map before runtime: the C API keeps a map with a live // session and a runtime with live maps alive on purpose. @@ -1358,18 +1976,22 @@ bool mln_test_reclaim_thread_resources(void) { tracked_session_count -= 1; const tracked_session entry = tracked_sessions[tracked_session_count]; if (entry.session != MLN_HANDLE_NULL) { - mln_render_session_destroy(entry.session); + mln_render_abandon_result abandoned = { + .size = sizeof(mln_render_abandon_result) + }; + (void)mln_render_session_abandon(entry.session, &abandoned); + (void)mln_render_session_destroy(entry.session); } destroy_backend_state(entry.backend_state); reclaimed = true; } while (tracked_map_count > 0) { tracked_map_count -= 1; - mln_map_destroy(tracked_maps[tracked_map_count]); + (void)mln_test_map_close(tracked_maps[tracked_map_count]); reclaimed = true; } if (tracked_runtime != MLN_HANDLE_NULL) { - mln_runtime_destroy(tracked_runtime); + (void)mln_test_runtime_close(tracked_runtime); tracked_runtime = MLN_HANDLE_NULL; reclaimed = true; } diff --git a/src/c_api/tests/test_support.cpp b/src/c_api/tests/test_support.cpp new file mode 100644 index 000000000..19d884157 --- /dev/null +++ b/src/c_api/tests/test_support.cpp @@ -0,0 +1,341 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_support.h" + +#include "completion/completion.hpp" +#include "render/render_session_test_support.hpp" +#include "runtime/runtime.hpp" + +namespace { + +struct CompletionState { + std::mutex mutex; + std::condition_variable condition; + bool completed = false; + bool released = false; + mln_status status = MLN_STATUS_INVALID_STATE; + std::uint32_t disposition = MLN_COMMAND_DISPOSITION_COMMITTED; + std::uint64_t generation = 0; + std::string diagnostic; + std::vector value; + std::size_t value_count = 0; + std::size_t value_size = 0; + bool copy_readback = false; + bool copy_buffer_view = false; + std::vector nested; +}; + +auto state(mln_test_completion* completion) -> CompletionState* { + return completion == nullptr + ? nullptr + : static_cast(completion->state); +} + +void receive_completion( + void* user_data, const mln_completion_result* result +) noexcept { + auto* probe = static_cast(user_data); + if (probe == nullptr || result == nullptr) return; + { + const auto lock = std::scoped_lock{probe->mutex}; + probe->status = result->status; + probe->disposition = result->disposition; + probe->generation = result->generation; + probe->diagnostic.assign( + static_cast(result->diagnostic.data), result->diagnostic.size + ); + probe->value_count = result->value_count; + if (result->value != nullptr && probe->value_size != 0) { + probe->value.resize(probe->value_size); + std::memcpy(probe->value.data(), result->value, probe->value_size); + if (probe->copy_readback) { + auto* copied = + reinterpret_cast(probe->value.data()); + const auto* source = static_cast(copied->data.data); + probe->nested.assign(source, source + copied->data.size); + copied->data.data = probe->nested.data(); + } else if (probe->copy_buffer_view) { + auto* copied = reinterpret_cast(probe->value.data()); + const auto* source = static_cast(copied->data); + if (copied->size != 0) { + probe->nested.assign(source, source + copied->size); + copied->data = probe->nested.data(); + } + } + } + probe->completed = true; + } + probe->condition.notify_all(); +} + +void release_completion(void* user_data) noexcept { + auto* probe = static_cast(user_data); + if (probe == nullptr) return; + { + const auto lock = std::scoped_lock{probe->mutex}; + probe->released = true; + } + probe->condition.notify_all(); +} + +} // namespace + +extern "C" mln_test_completion mln_test_completion_default( + const size_t value_size +) { + auto* probe = new CompletionState{}; + probe->value_size = value_size; + return mln_test_completion{ + .descriptor = + mln_completion{ + .size = sizeof(mln_completion), + .callback = receive_completion, + .user_data = probe, + .release_user_data = release_completion, + }, + .state = probe, + }; +} + +extern "C" mln_test_completion mln_test_completion_readback(void) { + auto completion = + mln_test_completion_default(sizeof(mln_texture_readback_result)); + state(&completion)->copy_readback = true; + return completion; +} + +extern "C" mln_test_completion mln_test_completion_buffer_view(void) { + auto completion = mln_test_completion_default(sizeof(mln_buffer_view)); + state(&completion)->copy_buffer_view = true; + return completion; +} + +extern "C" void mln_test_completion_destroy(mln_test_completion* completion) { + auto* probe = state(completion); + if (probe == nullptr) return; + { + auto lock = std::unique_lock{probe->mutex}; + probe->condition.wait(lock, [probe]() { return probe->released; }); + } + delete probe; + *completion = {}; +} + +extern "C" void mln_test_completion_reject(mln_test_completion* completion) { + if ( + completion == nullptr || completion->descriptor.release_user_data == nullptr + ) { + return; + } + completion->descriptor.release_user_data(completion->descriptor.user_data); +} + +extern "C" bool mln_test_completion_wait( + mln_test_completion* completion, const int64_t timeout_ms +) { + auto* probe = state(completion); + if (probe == nullptr) return false; + auto lock = std::unique_lock{probe->mutex}; + if (timeout_ms < 0) { + probe->condition.wait(lock, [probe]() { return probe->completed; }); + return true; + } + return probe->condition.wait_for( + lock, std::chrono::milliseconds{timeout_ms}, + [probe]() { return probe->completed; } + ); +} + +extern "C" mln_status mln_test_completion_finish( + mln_test_completion* completion +) { + if (!mln_test_completion_wait(completion, -1)) { + return MLN_STATUS_INVALID_STATE; + } + return mln_test_completion_status(completion); +} + +extern "C" bool mln_test_completion_poll(mln_test_completion* completion) { + auto* probe = state(completion); + if (probe == nullptr) return false; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->completed; +} + +extern "C" mln_status mln_test_completion_status( + mln_test_completion* completion +) { + auto* probe = state(completion); + if (probe == nullptr) return MLN_STATUS_INVALID_ARGUMENT; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->completed ? probe->status : MLN_STATUS_INVALID_STATE; +} + +extern "C" uint32_t mln_test_completion_disposition( + mln_test_completion* completion +) { + auto* probe = state(completion); + if (probe == nullptr) return MLN_COMMAND_DISPOSITION_CANCELLED; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->disposition; +} + +extern "C" uint64_t mln_test_completion_generation( + mln_test_completion* completion +) { + auto* probe = state(completion); + if (probe == nullptr) return 0; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->generation; +} + +extern "C" const char* mln_test_completion_diagnostic( + mln_test_completion* completion +) { + auto* probe = state(completion); + if (probe == nullptr) return "invalid completion probe"; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->diagnostic.c_str(); +} + +extern "C" size_t mln_test_completion_value_count( + mln_test_completion* completion +) { + auto* probe = state(completion); + if (probe == nullptr) return 0; + const auto lock = std::scoped_lock{probe->mutex}; + return probe->value_count; +} + +extern "C" bool mln_test_completion_copy_value( + mln_test_completion* completion, void* out_value, const size_t value_size +) { + auto* probe = state(completion); + if (probe == nullptr || out_value == nullptr) return false; + const auto lock = std::scoped_lock{probe->mutex}; + if (!probe->completed || probe->value.size() != value_size) return false; + std::memcpy(out_value, probe->value.data(), value_size); + return true; +} + +extern "C" bool mln_test_completion_contract(void) { + struct Probe { + std::atomic_uint calls = 0; + std::atomic_uint releases = 0; + std::atomic_int phase = 0; + std::atomic_int status = MLN_STATUS_INVALID_STATE; + }; + const auto callback = + +[](void* user_data, const mln_completion_result* result) -> void { + auto& probe = *static_cast(user_data); + auto expected = 0; + if (!probe.phase.compare_exchange_strong(expected, 1)) { + probe.phase.store(-1); + } + probe.status.store(result->status); + probe.calls.fetch_add(1); + }; + const auto release = +[](void* user_data) -> void { + auto& probe = *static_cast(user_data); + auto expected = 1; + if (!probe.phase.compare_exchange_strong(expected, 2)) { + probe.phase.store(-1); + } + probe.releases.fetch_add(1); + }; + const auto descriptor = [&](Probe& probe) -> mln_completion { + return { + .size = sizeof(mln_completion), + .callback = callback, + .user_data = &probe, + .release_user_data = release, + }; + }; + + auto inline_probe = Probe{}; + auto inline_completion = + std::make_shared(descriptor(inline_probe)); + mln::core::complete_value( + inline_completion, MLN_STATUS_OK, std::string{}, std::uint32_t{7} + ); + if (inline_probe.calls.load() != 0) return false; + inline_completion->accept(); + inline_completion->resolve([](const mln_completion&) {}); + if ( + inline_probe.calls.load() != 1 || inline_probe.releases.load() != 1 || + inline_probe.phase.load() != 2 || + inline_probe.status.load() != MLN_STATUS_OK + ) + return false; + + auto rejected_probe = Probe{}; + { + auto rejected = + std::make_shared(descriptor(rejected_probe)); + rejected->reject(); + } + if (rejected_probe.calls.load() != 0 || rejected_probe.releases.load() != 0) + return false; + + auto abandoned_probe = Probe{}; + { + auto abandoned = + std::make_shared(descriptor(abandoned_probe)); + abandoned->accept(); + } + if ( + abandoned_probe.calls.load() != 1 || abandoned_probe.releases.load() != 1 || + abandoned_probe.status.load() != MLN_STATUS_CANCELLED + ) + return false; + + for (auto iteration = 0; iteration < 100; ++iteration) { + auto race_probe = Probe{}; + auto completion = + std::make_shared(descriptor(race_probe)); + auto accept = std::thread{[completion]() { completion->accept(); }}; + auto resolve = std::thread{[completion]() { + mln::core::complete(completion, MLN_STATUS_OK); + }}; + accept.join(); + resolve.join(); + if ( + race_probe.calls.load() != 1 || race_probe.releases.load() != 1 || + race_probe.phase.load() != 2 + ) + return false; + } + return true; +} + +extern "C" mln_status mln_test_runtime_reserve_child(mln_runtime runtime) { + auto live = mln::core::lease_runtime(runtime); + if (live == nullptr) return MLN_STATUS_INVALID_ARGUMENT; + return live->control.reserve_child() ? MLN_STATUS_OK + : MLN_STATUS_INVALID_STATE; +} + +extern "C" void mln_test_runtime_abandon_child(mln_runtime runtime) { + auto live = mln::core::lease_runtime(runtime); + if (live != nullptr) live->control.abandon_child_reservation(); +} + +extern "C" mln_status mln_test_render_session_blocking_operation_create( + mln_render_session session, atomic_bool* entered, const atomic_bool* release, + const mln_completion* completion +) { + if (entered == nullptr || release == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + return mln::core::enqueue_blocking_test_render_operation( + session, entered, release, completion + ); +} diff --git a/src/c_api/tests/test_support.h b/src/c_api/tests/test_support.h index acc8f3a3b..416e4a96e 100644 --- a/src/c_api/tests/test_support.h +++ b/src/c_api/tests/test_support.h @@ -8,6 +8,9 @@ #include #include "maplibre_native_c.h" +#ifdef __cplusplus +extern "C" { +#endif #define MLN_BUFFER_LITERAL(literal) \ ((mln_buffer_view){.data = (literal), .size = sizeof(literal) - 1}) @@ -21,25 +24,86 @@ static inline mln_buffer_view mln_test_buffer_view( typedef struct mln_test_render_fixture { mln_render_session session; void* backend_state; + uint32_t driver; + bool observed_attaching; + bool observed_driver_ready; + atomic_uint frame_wakes; + atomic_uint driver_wakes; } mln_test_render_fixture; -// Opaque host thread used by tests that need a second owner thread. +// Opaque host thread used by tests that need a second native thread. typedef struct mln_test_thread mln_test_thread; mln_test_thread* mln_test_thread_start(void (*entry)(void*), void* argument); void mln_test_thread_join(mln_test_thread* thread); void mln_test_sleep_milliseconds(unsigned int milliseconds); -// Monotonic milliseconds, for tests that assert a pump returned promptly rather -// than sat out its timeout. +// Monotonic milliseconds for bounded concurrency assertions. uint64_t mln_test_monotonic_milliseconds(void); +typedef struct mln_test_completion { + mln_completion descriptor; + void* state; +} mln_test_completion; + +mln_test_completion mln_test_completion_default(size_t value_size); +mln_test_completion mln_test_completion_buffer_view(void); +mln_test_completion mln_test_completion_readback(void); +mln_completion mln_test_discard_completion(void); +void mln_test_completion_destroy(mln_test_completion* completion); +void mln_test_completion_reject(mln_test_completion* completion); +bool mln_test_completion_wait( + mln_test_completion* completion, int64_t timeout_ms +); +mln_status mln_test_completion_finish(mln_test_completion* completion); +bool mln_test_completion_poll(mln_test_completion* completion); +mln_status mln_test_completion_status(mln_test_completion* completion); +uint32_t mln_test_completion_disposition(mln_test_completion* completion); +uint64_t mln_test_completion_generation(mln_test_completion* completion); +const char* mln_test_completion_diagnostic(mln_test_completion* completion); +size_t mln_test_completion_value_count(mln_test_completion* completion); +bool mln_test_completion_copy_value( + mln_test_completion* completion, void* out_value, size_t value_size +); +bool mln_test_completion_contract(void); + +#if defined(MLN_FFI_TEST_BACKEND_METAL) +bool mln_test_metal_surface_retarget_retains_submission(mln_map map); +#endif + +mln_status mln_test_runtime_reserve_child(mln_runtime runtime); +void mln_test_runtime_abandon_child(mln_runtime runtime); +mln_status mln_test_render_session_blocking_operation_create( + mln_render_session session, atomic_bool* entered, const atomic_bool* release, + const mln_completion* completion +); // These helpers track what they create per calling thread so the suite can // reclaim handles a test left behind. The matching destroy helpers untrack. mln_runtime mln_test_create_runtime(void); +mln_status mln_test_runtime_barrier(mln_runtime runtime); +mln_status mln_test_runtime_create( + const mln_runtime_options* options, mln_runtime* out_runtime +); +mln_status mln_test_runtime_close(mln_runtime runtime); mln_map mln_test_create_map(mln_runtime runtime); mln_map mln_test_create_map_with_options( mln_runtime runtime, const mln_map_options* options ); +mln_status mln_test_map_create_status( + mln_runtime runtime, const mln_map_options* options, mln_map* out_map +); +mln_status mln_test_map_close(mln_map map); +mln_status mln_test_map_get_event_mask(mln_map map, uint64_t* out_mask); +mln_status mln_test_map_get_camera(mln_map map, mln_camera_options* out_camera); +mln_status mln_test_map_request_repaint(mln_map map); +mln_status mln_test_map_set_event_mask(mln_map map, uint64_t mask); +mln_status mln_test_map_set_style_json(mln_map map, mln_buffer_view json); +mln_status mln_test_map_set_style_url(mln_map map, const char* url); +mln_status mln_test_map_copy_loaded_style_json( + mln_map map, char* out, size_t capacity, size_t* out_size +); +mln_status mln_test_map_copy_style_url( + mln_map map, char* out, size_t capacity, size_t* out_size +); void mln_test_destroy_runtime(mln_runtime runtime); void mln_test_destroy_map(mln_map map); void mln_test_sleep_millisecond(void); @@ -52,7 +116,16 @@ uint8_t* mln_test_read_fixture(const char* relative_path, size_t* out_size); bool mln_test_render_fixture_create( mln_map map, mln_test_render_fixture* fixture ); +#if defined(MLN_FFI_TEST_BACKEND_OPENGL) && defined(MLN_FFI_TEST_OPENGL_WEBGL) +// Transfers an OffscreenCanvas into a core-worker WebGL surface session. +bool mln_test_transferred_webgl_surface_create( + mln_map map, mln_test_render_fixture* fixture +); +#endif void mln_test_render_fixture_destroy(mln_test_render_fixture* fixture); +mln_status mln_test_render_fixture_finish_operation( + const mln_test_render_fixture* fixture, mln_test_completion* completion +); // Outcome of building the dedicated EGL surface fixture. Unavailable is a skip; // a failed attach is a failure, because it is the behavior under test. @@ -68,17 +141,31 @@ mln_test_fixture_result mln_test_dedicated_egl_surface_create( mln_map map, mln_test_render_fixture* fixture ); void mln_test_dedicated_egl_surface_destroy(mln_test_render_fixture* fixture); -// Whether an EGL context is current on the calling thread. A dedicated session -// leaves its own current between renders; a shared one leaves none. bool mln_test_egl_context_is_current(void); - -// Pumps the runtime until `flag` is set or the deadline passes. Returns whether -// the flag was observed. -bool mln_test_pump_until(mln_runtime runtime, atomic_bool* flag); - -// Drains until a batch reports no events, and returns how many it discarded. A -// bounded batch still empties the queue, because each drain reports what stayed -// behind. +// Attaches a private OpenGL owned texture target whose context and driver both +// belong to the native core worker. +mln_test_fixture_result mln_test_dedicated_egl_texture_create( + mln_map map, mln_test_render_fixture* fixture +); +void mln_test_dedicated_egl_texture_destroy(mln_test_render_fixture* fixture); +// Waits until `flag` is set while draining runtime events. Returns whether the +// flag was observed before the bounded deadline. +bool mln_test_wait_until(mln_runtime runtime, atomic_bool* flag); + +typedef struct mln_test_event_batch { + uint32_t size; + uint32_t event_size; + const mln_runtime_event* events; + size_t event_count; + const char* messages; + size_t messages_size; +} mln_test_event_batch; + +mln_test_event_batch mln_test_event_batch_default(void); +mln_status mln_test_drain_events( + mln_runtime runtime, mln_test_event_batch* out_batch +); +// Drains until a batch reports no events, and returns how many it discarded. size_t mln_test_drain_all(mln_runtime runtime); // The same, counting the events whose type matches. @@ -104,4 +191,8 @@ bool mln_test_reclaim_thread_resources(void); // where the backend caches nothing per thread. void mln_test_release_thread_gpu_resources(void); +#ifdef __cplusplus +} +#endif + #endif diff --git a/src/c_api/texture.cpp b/src/c_api/texture.cpp index 1ad9079f1..08cdc04e3 100644 --- a/src/c_api/texture.cpp +++ b/src/c_api/texture.cpp @@ -73,191 +73,186 @@ auto mln_render_target_extent_physical_size( auto mln_metal_owned_texture_attach( mln_map map, const mln_metal_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_owned_texture_attach(map, descriptor, out_session); + return mln::core::metal_owned_texture_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_metal_borrowed_texture_attach( mln_map map, const mln_metal_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_borrowed_texture_attach( - map, descriptor, out_session + return mln::core::metal_borrowed_texture_attach_start( + map, descriptor, options, out_session, completion ); }); } auto mln_vulkan_owned_texture_attach( mln_map map, const mln_vulkan_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_owned_texture_attach(map, descriptor, out_session); + return mln::core::vulkan_owned_texture_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_vulkan_borrowed_texture_attach( mln_map map, const mln_vulkan_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_borrowed_texture_attach( - map, descriptor, out_session + return mln::core::vulkan_borrowed_texture_attach_start( + map, descriptor, options, out_session, completion ); }); } auto mln_opengl_owned_texture_attach( mln_map map, const mln_opengl_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_owned_texture_attach(map, descriptor, out_session); + return mln::core::opengl_owned_texture_attach_start( + map, descriptor, options, out_session, completion + ); }); } auto mln_opengl_borrowed_texture_attach( mln_map map, const mln_opengl_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_borrowed_texture_attach( - map, descriptor, out_session + return mln::core::opengl_borrowed_texture_attach_start( + map, descriptor, options, out_session, completion ); }); } -auto mln_metal_borrowed_texture_set_target( - mln_render_session session, - const mln_metal_borrowed_texture_descriptor* descriptor +auto mln_webgpu_owned_texture_attach( + mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_borrowed_texture_set_target(session, descriptor); + return mln::core::webgpu_owned_texture_attach_start( + map, descriptor, options, out_session, completion + ); }); } -auto mln_vulkan_borrowed_texture_set_target( - mln_render_session session, - const mln_vulkan_borrowed_texture_descriptor* descriptor +auto mln_webgpu_borrowed_texture_attach( + mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_borrowed_texture_set_target(session, descriptor); + return mln::core::webgpu_borrowed_texture_attach_start( + map, descriptor, options, out_session, completion + ); }); } -auto mln_opengl_borrowed_texture_set_target( +auto mln_metal_borrowed_texture_set_target( mln_render_session session, - const mln_opengl_borrowed_texture_descriptor* descriptor -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_borrowed_texture_set_target(session, descriptor); - }); -} - -auto mln_texture_read_premultiplied_rgba8( - mln_render_session session, uint8_t* out_data, size_t out_data_capacity, - mln_texture_image_info* out_info + const mln_metal_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::texture_read_premultiplied_rgba8( - session, out_data, out_data_capacity, out_info + return mln::core::metal_borrowed_texture_set_target_start( + session, descriptor, completion ); }); } -auto mln_metal_owned_texture_acquire_frame( - mln_render_session session, mln_metal_owned_texture_frame* out_frame -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_owned_texture_acquire_frame(session, out_frame); - }); -} - -auto mln_metal_owned_texture_release_frame( - mln_render_session session, const mln_metal_owned_texture_frame* frame -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::metal_owned_texture_release_frame(session, frame); - }); -} - -auto mln_vulkan_owned_texture_acquire_frame( - mln_render_session session, mln_vulkan_owned_texture_frame* out_frame -) noexcept -> mln_status { - return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_owned_texture_acquire_frame(session, out_frame); - }); -} - -auto mln_vulkan_owned_texture_release_frame( - mln_render_session session, const mln_vulkan_owned_texture_frame* frame +auto mln_vulkan_borrowed_texture_set_target( + mln_render_session session, + const mln_vulkan_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::vulkan_owned_texture_release_frame(session, frame); + return mln::core::vulkan_borrowed_texture_set_target_start( + session, descriptor, completion + ); }); } -auto mln_opengl_owned_texture_acquire_frame( - mln_render_session session, mln_opengl_owned_texture_frame* out_frame +auto mln_opengl_borrowed_texture_set_target( + mln_render_session session, + const mln_opengl_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_owned_texture_acquire_frame(session, out_frame); + return mln::core::opengl_borrowed_texture_set_target_start( + session, descriptor, completion + ); }); } -auto mln_opengl_owned_texture_release_frame( - mln_render_session session, const mln_opengl_owned_texture_frame* frame +auto mln_webgpu_borrowed_texture_set_target( + mln_render_session session, + const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::opengl_owned_texture_release_frame(session, frame); + return mln::core::webgpu_borrowed_texture_set_target_start( + session, descriptor, completion + ); }); } -auto mln_webgpu_owned_texture_attach( - mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, - mln_render_session* out_session +auto mln_texture_read_premultiplied_rgba8( + mln_render_session session, const mln_completion* completion ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_owned_texture_attach(map, descriptor, out_session); + return mln::core::texture_read_premultiplied_rgba8_start( + session, completion + ); }); } -auto mln_webgpu_borrowed_texture_attach( - mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session +auto mln_acquired_frame_get_metal_texture( + mln_acquired_frame frame, mln_metal_owned_texture_frame* out_frame ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_borrowed_texture_attach( - map, descriptor, out_session - ); + return mln::core::acquired_frame_get_metal_texture(frame, out_frame); }); } -auto mln_webgpu_borrowed_texture_set_target( - mln_render_session session, - const mln_webgpu_borrowed_texture_descriptor* descriptor +auto mln_acquired_frame_get_vulkan_texture( + mln_acquired_frame frame, mln_vulkan_owned_texture_frame* out_frame ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_borrowed_texture_set_target(session, descriptor); + return mln::core::acquired_frame_get_vulkan_texture(frame, out_frame); }); } -auto mln_webgpu_owned_texture_acquire_frame( - mln_render_session session, mln_webgpu_owned_texture_frame* out_frame +auto mln_acquired_frame_get_opengl_texture( + mln_acquired_frame frame, mln_opengl_owned_texture_frame* out_frame ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_owned_texture_acquire_frame(session, out_frame); + return mln::core::acquired_frame_get_opengl_texture(frame, out_frame); }); } -auto mln_webgpu_owned_texture_release_frame( - mln_render_session session, const mln_webgpu_owned_texture_frame* frame +auto mln_acquired_frame_get_webgpu_texture( + mln_acquired_frame frame, mln_webgpu_owned_texture_frame* out_frame ) noexcept -> mln_status { return mln::c_api::status_boundary([&]() -> mln_status { - return mln::core::webgpu_owned_texture_release_frame(session, frame); + return mln::core::acquired_frame_get_webgpu_texture(frame, out_frame); }); } diff --git a/src/completion/completion.cpp b/src/completion/completion.cpp new file mode 100644 index 000000000..92c4b0f69 --- /dev/null +++ b/src/completion/completion.cpp @@ -0,0 +1,170 @@ +#include + +#include "completion/completion.hpp" + +#include "diagnostics/diagnostics.hpp" + +namespace mln::core { + +auto invoke_completion( + const mln_completion& descriptor, mln_status status, + std::uint32_t disposition, std::uint64_t generation, + const std::string& diagnostic, const void* value, std::size_t value_count +) noexcept -> void { + const auto result = mln_completion_result{ + .size = sizeof(mln_completion_result), + .status = status, + .disposition = disposition, + .reserved = 0, + .generation = generation, + .diagnostic = + mln_buffer_view{.data = diagnostic.data(), .size = diagnostic.size()}, + .value = value, + .value_count = value_count, + }; + try { + descriptor.callback(descriptor.user_data, &result); + } catch (...) { + // Host callbacks must not unwind through the C boundary. + } +} + +Completion::Completion(const mln_completion& descriptor) + : descriptor_(descriptor) {} + +Completion::~Completion() { + auto abandoned = false; + { + const std::scoped_lock lock(mutex_); + abandoned = state_ == State::Accepted; + } + if (abandoned) { + resolve([diagnostic = std::string{"asynchronous work was abandoned"}]( + const mln_completion& descriptor + ) { + invoke_completion( + descriptor, MLN_STATUS_CANCELLED, MLN_COMMAND_DISPOSITION_CANCELLED, 0, + diagnostic, nullptr, 0 + ); + }); + } +} + +auto Completion::accept() noexcept -> void { + auto pending = Delivery{}; + { + const std::scoped_lock lock(mutex_); + if (state_ != State::Pending) { + return; + } + if (pending_) { + state_ = State::Resolved; + pending = std::move(pending_); + } else { + state_ = State::Accepted; + } + } + if (pending) { + deliver(std::move(pending)); + } +} + +auto Completion::reject() noexcept -> void { + const std::scoped_lock lock(mutex_); + if (state_ == State::Pending) { + state_ = State::Rejected; + pending_ = {}; + descriptor_ = {}; + } +} + +auto Completion::resolve(Delivery delivery) noexcept -> void { + auto invoke_now = false; + { + const std::scoped_lock lock(mutex_); + switch (state_) { + case State::Pending: + if (!pending_) { + pending_ = std::move(delivery); + } + return; + case State::Accepted: + state_ = State::Resolved; + invoke_now = true; + break; + case State::Rejected: + case State::Resolved: + return; + } + } + if (invoke_now) { + deliver(std::move(delivery)); + } +} + +auto Completion::deliver(Delivery delivery) noexcept -> void { + try { + delivery(descriptor_); + } catch (...) { + // Completion delivery cannot reopen terminal native work. + } + release(); +} + +auto Completion::release() noexcept -> void { + const auto release_user_data = descriptor_.release_user_data; + const auto user_data = descriptor_.user_data; + descriptor_ = {}; + if (release_user_data != nullptr) { + try { + release_user_data(user_data); + } catch (...) { + // Release callbacks must not unwind through the C boundary. + } + } +} + +auto validate_completion(const mln_completion* completion) -> mln_status { + if (completion == nullptr) { + set_thread_error("completion must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (completion->size < sizeof(mln_completion)) { + set_thread_error("mln_completion.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (completion->callback == nullptr) { + set_thread_error("completion callback must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto complete( + const std::shared_ptr& completion, mln_status status, + std::string diagnostic, const void* value, std::size_t value_count +) noexcept -> void { + completion->resolve([status, diagnostic = std::move(diagnostic), value, + value_count](const mln_completion& descriptor) { + invoke_completion( + descriptor, status, MLN_COMMAND_DISPOSITION_COMMITTED, 0, diagnostic, + value, value_count + ); + }); +} + +auto complete_command( + const std::shared_ptr& completion, std::uint32_t disposition, + mln_status status, std::uint64_t generation, std::string diagnostic +) noexcept -> void { + completion->resolve( + [disposition, status, generation, + diagnostic = std::move(diagnostic)](const mln_completion& descriptor) { + invoke_completion( + descriptor, status, disposition, generation, diagnostic, nullptr, 0 + ); + } + ); +} + +} // namespace mln::core diff --git a/src/completion/completion.hpp b/src/completion/completion.hpp new file mode 100644 index 000000000..a072fadf3 --- /dev/null +++ b/src/completion/completion.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "maplibre_native_c/completion.h" + +namespace mln::core { + +class Completion final { + public: + using Delivery = std::function; + + explicit Completion(const mln_completion& descriptor); + Completion(const Completion&) = delete; + Completion(Completion&&) = delete; + auto operator=(const Completion&) -> Completion& = delete; + auto operator=(Completion&&) -> Completion& = delete; + ~Completion(); + + auto accept() noexcept -> void; + auto reject() noexcept -> void; + auto resolve(Delivery delivery) noexcept -> void; + + private: + enum class State : std::uint8_t { Pending, Accepted, Rejected, Resolved }; + + auto deliver(Delivery delivery) noexcept -> void; + auto release() noexcept -> void; + + std::mutex mutex_; + mln_completion descriptor_{}; + State state_ = State::Pending; + Delivery pending_; +}; + +auto validate_completion(const mln_completion* completion) -> mln_status; + +auto invoke_completion( + const mln_completion& descriptor, mln_status status, + std::uint32_t disposition, std::uint64_t generation, + const std::string& diagnostic, const void* value, std::size_t value_count +) noexcept -> void; + +auto complete( + const std::shared_ptr& completion, mln_status status, + std::string diagnostic = {}, const void* value = nullptr, + std::size_t value_count = 0 +) noexcept -> void; + +auto complete_command( + const std::shared_ptr& completion, std::uint32_t disposition, + mln_status status, std::uint64_t generation = 0, std::string diagnostic = {} +) noexcept -> void; + +template +auto complete_value( + const std::shared_ptr& completion, mln_status status, + std::string diagnostic, Value value +) noexcept -> void { + completion->resolve([status, diagnostic = std::move(diagnostic), + value = + std::move(value)](const mln_completion& descriptor) { + invoke_completion( + descriptor, status, MLN_COMMAND_DISPOSITION_COMMITTED, 0, diagnostic, + status == MLN_STATUS_OK ? &value : nullptr, + status == MLN_STATUS_OK ? 1 : 0 + ); + }); +} + +} // namespace mln::core diff --git a/src/execution/control_state.hpp b/src/execution/control_state.hpp new file mode 100644 index 000000000..c77355822 --- /dev/null +++ b/src/execution/control_state.hpp @@ -0,0 +1,176 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "diagnostics/diagnostics.hpp" +#include "maplibre_native_c.h" + +namespace mln::core { + +// Coordinates handle lookup, committed submissions, child reservations, and +// close. Registry locks are never held while waiting for a submission or child. +class ControlState { + public: + [[nodiscard]] auto acquire() -> bool { + const std::scoped_lock lock(mutex_); + if (closing_) { + set_thread_error("handle is closing"); + return false; + } + ++submissions_; + return true; + } + + auto release() noexcept -> void { + auto drained = std::function{}; + { + const std::scoped_lock lock(mutex_); + if (submissions_ > 0) { + --submissions_; + } + if (closing_ && submissions_ == 0) { + drained = std::move(drained_); + } + } + condition_.notify_all(); + if (drained) { + try { + drained(); + } catch (...) { + } + } + } + + [[nodiscard]] auto reserve_child() -> bool { + const std::scoped_lock lock(mutex_); + if (closing_) { + set_thread_error("handle is closing"); + return false; + } + ++child_reservations_; + return true; + } + + auto commit_child() noexcept -> void { + const std::scoped_lock lock(mutex_); + if (child_reservations_ > 0) { + --child_reservations_; + } + ++children_; + } + + auto abandon_child_reservation() noexcept -> void { + { + const std::scoped_lock lock(mutex_); + if (child_reservations_ > 0) { + --child_reservations_; + } + } + condition_.notify_all(); + } + + auto release_child() noexcept -> void { + { + const std::scoped_lock lock(mutex_); + if (children_ > 0) { + --children_; + } + } + condition_.notify_all(); + } + + [[nodiscard]] auto begin_close() -> mln_status { + const std::scoped_lock lock(mutex_); + if (closing_) { + set_thread_error("handle is already closing"); + return MLN_STATUS_INVALID_STATE; + } + if (children_ != 0 || child_reservations_ != 0) { + set_thread_error("handle still owns live or pending children"); + return MLN_STATUS_INVALID_STATE; + } + closing_ = true; + return MLN_STATUS_OK; + } + // Rolls back a close that failed before any close work became reachable. + auto abort_close() noexcept -> void { + const std::scoped_lock lock(mutex_); + closing_ = false; + drained_ = {}; + } + + // Runs once, outside the control lock, after close has begun and every + // submission lease has retired. + auto notify_when_drained(std::function callback) noexcept -> void { + auto invoke_now = false; + { + const std::scoped_lock lock(mutex_); + if (closing_ && submissions_ == 0) { + invoke_now = true; + } else { + drained_ = std::move(callback); + } + } + if (invoke_now && callback) { + try { + callback(); + } catch (...) { + } + } + } + + auto wait_for_submissions() noexcept -> void { + auto lock = std::unique_lock{mutex_}; + condition_.wait(lock, [this]() noexcept -> bool { + return submissions_ == 0; + }); + } + + [[nodiscard]] auto is_closing() const noexcept -> bool { + const std::scoped_lock lock(mutex_); + return closing_; + } + + private: + mutable std::mutex mutex_; + std::condition_variable condition_; + std::size_t submissions_ = 0; + std::size_t children_ = 0; + std::size_t child_reservations_ = 0; + bool closing_ = false; + std::function drained_; +}; + +class ControlLease { + public: + ControlLease() = default; + explicit ControlLease(ControlState* state) : state_(state) {} + ControlLease(const ControlLease&) = delete; + ControlLease(ControlLease&& other) noexcept + : state_(std::exchange(other.state_, nullptr)) {} + auto operator=(const ControlLease&) -> ControlLease& = delete; + auto operator=(ControlLease&& other) noexcept -> ControlLease& { + if (this != &other) { + reset(); + state_ = std::exchange(other.state_, nullptr); + } + return *this; + } + ~ControlLease() { reset(); } + + auto reset() noexcept -> void { + if (state_ != nullptr) { + state_->release(); + state_ = nullptr; + } + } + + private: + ControlState* state_ = nullptr; +}; + +} // namespace mln::core diff --git a/src/execution/runtime_executor.cpp b/src/execution/runtime_executor.cpp new file mode 100644 index 000000000..98bb6ec8d --- /dev/null +++ b/src/execution/runtime_executor.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include "execution/runtime_executor.hpp" + +namespace mln::core { + +RuntimeExecutor::~RuntimeExecutor() { stop(); } + +auto RuntimeExecutor::start(std::function initialize) -> void { + { + const std::scoped_lock lock(mutex_); + if (starting_ || run_loop_ != nullptr || worker_.joinable()) { + throw std::runtime_error{"runtime executor is already running"}; + } + starting_ = true; + startup_error_ = nullptr; + } + + worker_ = std::thread( + [this, initialize = std::move(initialize)]() mutable noexcept -> void { + try { + auto loop = mln::util::RunLoop{mln::util::RunLoop::Type::New}; + { + const std::scoped_lock lock(mutex_); + worker_id_ = std::this_thread::get_id(); + run_loop_ = &loop; + } + if (initialize) { + auto pooled = [&initialize]() -> mln_status { + std::invoke(std::move(initialize)); + return MLN_STATUS_OK; + }; + static_cast(mln::c_api::with_autorelease_pool(pooled)); + } + { + const std::scoped_lock lock(mutex_); + starting_ = false; + } + started_.notify_all(); + loop.run(); + { + const std::scoped_lock lock(mutex_); + run_loop_ = nullptr; + worker_id_ = {}; + } + } catch (...) { + { + const std::scoped_lock lock(mutex_); + startup_error_ = std::current_exception(); + run_loop_ = nullptr; + worker_id_ = {}; + starting_ = false; + } + started_.notify_all(); + } + } + ); + + auto lock = std::unique_lock{mutex_}; + started_.wait(lock, [this]() noexcept -> bool { return !starting_; }); + if (startup_error_ != nullptr) { + auto error = std::exchange(startup_error_, nullptr); + lock.unlock(); + if (worker_.joinable()) { + worker_.join(); + } + std::rethrow_exception(error); + } +} + +auto RuntimeExecutor::stop() noexcept -> void { + auto* loop = run_loop(); + if (loop != nullptr) { + loop->invoke([loop]() noexcept -> void { loop->stop(); }); + } + if (worker_.joinable()) { + if (worker_.get_id() == std::this_thread::get_id()) { + worker_.detach(); + } else { + worker_.join(); + } + } +} + +} // namespace mln::core diff --git a/src/execution/runtime_executor.hpp b/src/execution/runtime_executor.hpp new file mode 100644 index 000000000..9407aaa71 --- /dev/null +++ b/src/execution/runtime_executor.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "c_api/autorelease_pool.hpp" + +namespace mln::core { + +// Owns one continuously running MapLibre run loop and its native thread. +class RuntimeExecutor { + public: + RuntimeExecutor() = default; + RuntimeExecutor(const RuntimeExecutor&) = delete; + RuntimeExecutor(RuntimeExecutor&&) = delete; + auto operator=(const RuntimeExecutor&) -> RuntimeExecutor& = delete; + auto operator=(RuntimeExecutor&&) -> RuntimeExecutor& = delete; + ~RuntimeExecutor(); + + auto start(std::function initialize = std::function{}) + -> void; + auto stop() noexcept -> void; + + [[nodiscard]] auto is_worker_thread() const noexcept -> bool { + const std::scoped_lock lock(mutex_); + return std::this_thread::get_id() == worker_id_; + } + + [[nodiscard]] auto run_loop() const noexcept -> mln::util::RunLoop* { + const std::scoped_lock lock(mutex_); + return run_loop_; + } + + template + auto invoke(Function&& function) -> void { + auto* loop = run_loop(); + if (loop == nullptr) { + throw std::runtime_error{"runtime executor is not running"}; + } + loop->invoke([work = std::forward(function)]() mutable -> void { + auto pooled = [&work]() -> mln_status { + std::invoke(std::move(work)); + return MLN_STATUS_OK; + }; + static_cast(mln::c_api::with_autorelease_pool(pooled)); + }); + } + + template + auto invoke_sync(Function&& function) -> std::invoke_result_t { + using Result = std::invoke_result_t; + if (is_worker_thread()) { + if constexpr (std::is_void_v) { + std::invoke(std::forward(function)); + return; + } else { + return std::invoke(std::forward(function)); + } + } + + auto promise = std::make_shared>(); + auto result = promise->get_future(); + invoke( + [promise, function = std::forward(function)]() mutable -> void { + try { + if constexpr (std::is_void_v) { + std::invoke(std::move(function)); + promise->set_value(); + } else { + promise->set_value(std::invoke(std::move(function))); + } + } catch (...) { + promise->set_exception(std::current_exception()); + } + } + ); + return result.get(); + } + + private: + mutable std::mutex mutex_; + std::condition_variable started_; + std::thread worker_; + std::thread::id worker_id_; + mln::util::RunLoop* run_loop_ = nullptr; + std::exception_ptr startup_error_; + bool starting_ = false; +}; + +} // namespace mln::core diff --git a/src/geojson/geojson_source_data.hpp b/src/geojson/geojson_source_data.hpp index 775b6867d..be784f28e 100644 --- a/src/geojson/geojson_source_data.hpp +++ b/src/geojson/geojson_source_data.hpp @@ -16,7 +16,7 @@ namespace mln::core { // Prepared GeoJSON source data: a parsed document tiled or clustered into the // index mln::style::GeoJSONSource consumes, together with the options the // index was built with. Immutable after creation, so any thread may create, -// read, or destroy one; installing on a map stays an owner-thread call. +// read, destroy, or install one on a map. struct GeoJsonSourceDataObject { std::shared_ptr data; mln::Immutable options; diff --git a/src/handles/handle_table.cpp b/src/handles/handle_table.cpp index 2c5275751..fd440d882 100644 --- a/src/handles/handle_table.cpp +++ b/src/handles/handle_table.cpp @@ -48,12 +48,20 @@ auto handle_kind_name(std::uint8_t kind) noexcept -> const char* { return "mln_buffer"; case HandleKind::StyleIdList: return "mln_style_id_list"; - case HandleKind::WakeSource: - return "mln_wake_source"; case HandleKind::ResourceRequest: return "mln_resource_request_handle"; case HandleKind::StyleStringList: return "mln_style_string_list"; + case HandleKind::EventBatch: + return "mln_event_batch"; + case HandleKind::AdapterResourceRequestQueue: + return "mln_adapter_resource_request_queue"; + case HandleKind::AdapterLogQueue: + return "mln_adapter_log_queue"; + case HandleKind::AcquiredFrame: + return "mln_acquired_frame"; + case HandleKind::RenderFrameBatch: + return "mln_render_frame_batch"; case HandleKind::GeoJsonSourceData: return "mln_geojson_source_data"; case HandleKind::QueriedFeatureList: diff --git a/src/handles/handle_table.hpp b/src/handles/handle_table.hpp index 987f5617c..508571732 100644 --- a/src/handles/handle_table.hpp +++ b/src/handles/handle_table.hpp @@ -27,11 +27,15 @@ enum class HandleKind : std::uint8_t { OfflineRegionList = 6, Buffer = 7, StyleIdList = 8, - WakeSource = 11, ResourceRequest = 12, StyleStringList = 13, - GeoJsonSourceData = 14, - QueriedFeatureList = 15, + EventBatch = 16, + AdapterResourceRequestQueue = 18, + AdapterLogQueue = 19, + AcquiredFrame = 20, + RenderFrameBatch = 21, + GeoJsonSourceData = 22, + QueriedFeatureList = 23, }; inline constexpr auto handle_generation_bits = std::uint32_t{36}; @@ -191,6 +195,17 @@ class HandleTable { } return slot->object; } + [[nodiscard]] auto lease_locked(std::uint64_t handle) const + -> std::shared_ptr + requires(Traits::leasable) + { + const auto* slot = find_slot(handle); + if (slot == nullptr) { + set_handle_fault_error(Traits::kind, handle, fault_for(handle)); + return nullptr; + } + return slot->object; + } [[nodiscard]] auto try_lease(std::uint64_t handle) const noexcept -> std::shared_ptr diff --git a/src/logging/logging.cpp b/src/logging/logging.cpp index 5e9211dde..91b73c484 100644 --- a/src/logging/logging.cpp +++ b/src/logging/logging.cpp @@ -14,8 +14,19 @@ namespace { class CallbackLogObserver final : public mln::Log::Observer { public: - CallbackLogObserver(mln_log_callback callback, void* user_data) - : callback_(callback), user_data_(user_data) {} + CallbackLogObserver( + mln_log_callback callback, void* user_data, + mln_log_callback_release release_user_data + ) + : callback_(callback), + user_data_(user_data), + release_user_data_(release_user_data) {} + + ~CallbackLogObserver() override { + if (release_user_data_ != nullptr) { + release_user_data_(user_data_); + } + } auto onRecord( mln::EventSeverity severity, mln::Event event, std::int64_t code, @@ -34,6 +45,7 @@ class CallbackLogObserver final : public mln::Log::Observer { private: mln_log_callback callback_ = nullptr; void* user_data_ = nullptr; + mln_log_callback_release release_user_data_ = nullptr; }; auto set_severity_async( @@ -46,15 +58,22 @@ auto set_severity_async( namespace mln::core { -auto set_log_callback(mln_log_callback callback, void* user_data) - -> mln_status { +auto set_log_callback( + mln_log_callback callback, void* user_data, + mln_log_callback_release release_user_data +) -> mln_status { if (callback == nullptr) { mln::Log::removeObserver(); + if (release_user_data != nullptr) { + release_user_data(user_data); + } return MLN_STATUS_OK; } mln::Log::setObserver( - std::make_unique(callback, user_data) + std::make_unique( + callback, user_data, release_user_data + ) ); return MLN_STATUS_OK; } diff --git a/src/logging/logging.hpp b/src/logging/logging.hpp index 6c34bb777..115af1879 100644 --- a/src/logging/logging.hpp +++ b/src/logging/logging.hpp @@ -6,7 +6,10 @@ namespace mln::core { -auto set_log_callback(mln_log_callback callback, void* user_data) -> mln_status; +auto set_log_callback( + mln_log_callback callback, void* user_data, + mln_log_callback_release release_user_data +) -> mln_status; auto clear_log_callback() -> mln_status; auto set_log_async_severity_mask(std::uint32_t mask) -> mln_status; diff --git a/src/map/map.cpp b/src/map/map.cpp index 5568ee2ed..59248d9c0 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -1,14 +1,16 @@ #include +#include #include #include #include #include #include +#include #include #include #include +#include #include -#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include #include @@ -40,6 +43,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep @@ -81,78 +85,24 @@ #include "map/map.hpp" #include "bytes/buffer.hpp" +#include "completion/completion.hpp" #include "diagnostics/diagnostics.hpp" #include "geojson/geojson.hpp" -#include "geojson/geojson_source_data.hpp" #include "handles/handle_table.hpp" -#include "map/feature_state.hpp" +#include "map/map_internal.hpp" #include "maplibre_native_c.h" +#include "operation/operation.hpp" #include "runtime/runtime.hpp" #include "style/style_value.hpp" -namespace mln::core { - -struct StyleIdListObject { - std::vector ids; -}; - -template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::StyleIdList; - static constexpr auto leasable = false; -}; - -struct StyleStringListObject { - std::vector values; -}; - -template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::StyleStringList; - static constexpr auto leasable = false; -}; - -} // namespace mln::core - namespace { -auto buffer_view_from_string(const std::string& value) -> mln_buffer_view { - return { - .data = value.data(), - .size = value.size(), - }; -} - enum class TileSourceOptionKind : uint8_t { Vector, Raster, RasterDEM }; constexpr auto default_map_width = uint32_t{256}; constexpr auto default_map_height = uint32_t{256}; constexpr double default_scale_factor = 1.0; -auto validate_string_view(mln_buffer_view string, const char* name) -> bool { - if (string.size > 0 && string.data == nullptr) { - auto message = std::string{name} + " data must not be null"; - mln::core::set_thread_error(message.c_str()); - return false; - } - return true; -} - -auto string_from_view(mln_buffer_view string) -> std::string { - if (string.size == 0) { - return {}; - } - return std::string{static_cast(string.data), string.size}; -} - -auto string_view_from_string(const std::string& string) -> mln_buffer_view { - return mln_buffer_view{.data = string.data(), .size = string.size()}; -} - -auto string_view_from_literal(const char* string) -> mln_buffer_view { - return mln_buffer_view{.data = string, .size = std::strlen(string)}; -} - auto validate_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln_status; auto validate_lat_lng_array( const mln_lat_lng* coordinates, size_t coordinate_count, bool allow_empty @@ -163,6981 +113,3868 @@ auto to_native_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln::LatLngBounds; auto from_native_lat_lng_bounds(const mln::LatLngBounds& bounds) -> mln_lat_lng_bounds; -auto to_c_source_type(mln::style::SourceType type) -> uint32_t { - switch (type) { - case mln::style::SourceType::Vector: - return MLN_STYLE_SOURCE_TYPE_VECTOR; - case mln::style::SourceType::Raster: - return MLN_STYLE_SOURCE_TYPE_RASTER; - case mln::style::SourceType::RasterDEM: - return MLN_STYLE_SOURCE_TYPE_RASTER_DEM; - case mln::style::SourceType::GeoJSON: - return MLN_STYLE_SOURCE_TYPE_GEOJSON; - case mln::style::SourceType::Video: - return MLN_STYLE_SOURCE_TYPE_VIDEO; - case mln::style::SourceType::Annotations: - return MLN_STYLE_SOURCE_TYPE_ANNOTATIONS; - case mln::style::SourceType::Image: - return MLN_STYLE_SOURCE_TYPE_IMAGE; - case mln::style::SourceType::CustomVector: - return MLN_STYLE_SOURCE_TYPE_CUSTOM_VECTOR; - case mln::style::SourceType::CustomMVTVector: - return MLN_STYLE_SOURCE_TYPE_CUSTOM_MVT_VECTOR; - } - assert(false); - return MLN_STYLE_SOURCE_TYPE_UNKNOWN; -} - -auto to_c_tile_scheme(mln::Tileset::Scheme scheme) -> uint32_t { - switch (scheme) { - case mln::Tileset::Scheme::XYZ: - return MLN_STYLE_TILE_SCHEME_XYZ; - case mln::Tileset::Scheme::TMS: - return MLN_STYLE_TILE_SCHEME_TMS; - } - assert(false); - return MLN_STYLE_TILE_SCHEME_XYZ; -} +} // namespace -auto to_c_vector_encoding(mln::Tileset::VectorEncoding encoding) -> uint32_t { - switch (encoding) { - case mln::Tileset::VectorEncoding::Mapbox: - return MLN_STYLE_VECTOR_TILE_ENCODING_MVT; - case mln::Tileset::VectorEncoding::MLT: - return MLN_STYLE_VECTOR_TILE_ENCODING_MLT; - } - assert(false); - return MLN_STYLE_VECTOR_TILE_ENCODING_MVT; -} +namespace mln::core { -auto to_c_raster_encoding(mln::Tileset::RasterEncoding encoding) -> uint32_t { - switch (encoding) { - case mln::Tileset::RasterEncoding::Mapbox: - return MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX; - case mln::Tileset::RasterEncoding::Terrarium: - return MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM; +auto callback_source_matches( + const mln::style::Source* source, CallbackSourceKind kind +) -> bool { + if (source == nullptr) { + return false; } - assert(false); - return MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX; -} - -auto tile_source_from_source(const mln::style::Source& source) - -> const mln::style::TileSource* { - switch (source.getType()) { - case mln::style::SourceType::Vector: - return source.as(); - case mln::style::SourceType::Raster: - return source.as(); - case mln::style::SourceType::RasterDEM: - return source.as(); - default: - return nullptr; + switch (kind) { + case CallbackSourceKind::CustomGeometry: + return source->as() != nullptr; + case CallbackSourceKind::CustomMvtVector: + return source->as() != nullptr; } + return false; } -auto inline_tileset(const mln::style::TileSource& source) - -> const mln::Tileset* { - const auto& url_or_tileset = source.getURLOrTileset(); - return url_or_tileset.is() ? &url_or_tileset.get() - : nullptr; -} +// Holds the release callback owed for each tracked callback source. A host +// cannot see when a style load drops a source, so this layer tracks it. +// +// Every mutation runs on the runtime worker, which is the only thread that +// adds or removes sources and receives MapLibre style-load callbacks. +class CallbackSourceRegistry final { + public: + CallbackSourceRegistry() = default; -auto source_url(const mln::style::Source& source) - -> std::optional { - if (const auto* tile_source = tile_source_from_source(source)) { - return tile_source->getURL(); - } - if (const auto* geojson = source.as()) { - return geojson->getURL(); - } - if (const auto* image = source.as()) { - return image->getURL(); - } - return std::nullopt; -} + // Runs when the owning map is destroyed, after the mln::Map that could still + // call into the source is gone. + ~CallbackSourceRegistry() { release_all(); } -auto has_tile_source_option( - const mln_style_tile_source_options& options, uint32_t field -) -> bool { - return (options.fields & field) != 0U; -} + CallbackSourceRegistry(const CallbackSourceRegistry&) = delete; + CallbackSourceRegistry(CallbackSourceRegistry&&) = delete; + auto operator=(const CallbackSourceRegistry&) + -> CallbackSourceRegistry& = delete; + auto operator=(CallbackSourceRegistry&&) -> CallbackSourceRegistry& = delete; -auto validate_zoom_option(double zoom, const char* name) -> mln_status { - if (!std::isfinite(zoom) || zoom < 0.0 || zoom > 255.0) { - auto message = std::string{name} + " must be finite and within [0, 255]"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; + // A source with no release callback is not tracked. + auto add( + const std::string& source_id, CallbackSourceKind kind, + CallbackSourceRelease release, void* user_data + ) -> void { + // An entry already under this ID belongs to a source that the style dropped + // before reconciliation ran, so it still owes its release. Invoke it here + // rather than letting the assignment below drop it, which keeps + // exactly-once independent of when the style-loaded observer reconciles. + if (const auto stale = entries_.find(source_id); stale != entries_.end()) { + const auto owed = stale->second; + entries_.erase(stale); + invoke(owed); + } + if (release == nullptr) { + return; + } + // The caller tracks before the style takes the source, so a throw here + // leaves nothing committed and the caller still owns user_data. A failed + // add owes no release. + entries_.insert_or_assign(source_id, Entry{kind, release, user_data}); } - return MLN_STATUS_OK; -} -auto validate_tile_source_option_header( - const mln_style_tile_source_options& options -) -> mln_status { - if (options.size < sizeof(mln_style_tile_source_options)) { - mln::core::set_thread_error( - "mln_style_tile_source_options.size is too small" - ); - return MLN_STATUS_INVALID_ARGUMENT; + // Drops an entry without releasing it, for a source the style then rejected. + // The caller still owns user_data on that path. + auto untrack(const std::string& source_id) noexcept -> void { + entries_.erase(source_id); } - constexpr auto known_fields = - static_cast(MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) | - MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM | - MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION | - MLN_STYLE_TILE_SOURCE_OPTION_SCHEME | MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS | - MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE | - MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING | - MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING; - if ((options.fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_style_tile_source_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; + // The observer that reports style loads has no route to a style, so the + // registry keeps the map that it belongs to until close detaches it. + auto attach(mln::Map& map) noexcept -> void { map_ = ↦ } + + auto detach() noexcept -> void { map_ = nullptr; } + + auto release(const std::string& source_id) -> void { + const auto entry = entries_.find(source_id); + if (entry == entries_.end()) { + return; + } + const auto owed = entry->second; + entries_.erase(entry); + invoke(owed); } - return MLN_STATUS_OK; -} -auto validate_tile_source_zoom_options( - const mln_style_tile_source_options& options -) -> mln_status { - if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM)) { - const auto status = validate_zoom_option(options.min_zoom, "min_zoom"); - if (status != MLN_STATUS_OK) { - return status; + // Releases every tracked source the current style no longer holds. A style + // document cannot declare a callback source, so a source of another type + // under a tracked ID means the tracked source is gone. + auto reconcile() -> void { + if (map_ == nullptr || entries_.empty()) { + return; + } + auto& style = map_->getStyle(); + auto owed = std::vector{}; + for (auto entry = entries_.begin(); entry != entries_.end();) { + auto* source = style.getSource(entry->first); + if (callback_source_matches(source, entry->second.kind)) { + ++entry; + continue; + } + owed.push_back(entry->second); + entry = entries_.erase(entry); + } + for (const auto& release : owed) { + invoke(release); } } - if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM)) { - const auto status = validate_zoom_option(options.max_zoom, "max_zoom"); - if (status != MLN_STATUS_OK) { - return status; + + auto release_all() -> void { + auto owed = std::move(entries_); + entries_.clear(); + for (const auto& entry : owed) { + invoke(entry.second); } } - if ( - has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) && - has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM) && - options.min_zoom > options.max_zoom - ) { - mln::core::set_thread_error( - "min_zoom must be less than or equal to max_zoom" - ); - return MLN_STATUS_INVALID_ARGUMENT; + + private: + struct Entry { + CallbackSourceKind kind = CallbackSourceKind::CustomGeometry; + CallbackSourceRelease release = nullptr; + void* user_data = nullptr; + }; + + static auto invoke(const Entry& entry) noexcept -> void { + try { + entry.release(entry.user_data); + } catch (const std::exception& exception) { + mln::core::set_thread_error(exception); + } catch (...) { + mln::core::set_thread_error("callback source release threw"); + } } - return MLN_STATUS_OK; + + std::unordered_map entries_; + mln::Map* map_ = nullptr; +}; + +auto track_callback_source( + MapObject& map, const std::string& source_id, CallbackSourceKind kind, + CallbackSourceRelease release, void* user_data +) -> void { + map.callback_sources->add(source_id, kind, release, user_data); } -auto validate_tile_source_attribution_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION - )) { - return MLN_STATUS_OK; - } - return validate_string_view(options.attribution, "attribution") - ? MLN_STATUS_OK - : MLN_STATUS_INVALID_ARGUMENT; +auto untrack_callback_source( + MapObject& map, const std::string& source_id +) noexcept -> void { + map.callback_sources->untrack(source_id); } -auto validate_tile_source_scheme_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_SCHEME)) { - return MLN_STATUS_OK; - } - switch (options.scheme) { - case MLN_STYLE_TILE_SCHEME_XYZ: - case MLN_STYLE_TILE_SCHEME_TMS: - return MLN_STATUS_OK; - default: - mln::core::set_thread_error("scheme is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto release_callback_source(MapObject& map, const std::string& source_id) + -> void { + map.callback_sources->release(source_id); } -auto validate_tile_source_bounds_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { - return MLN_STATUS_OK; +} // namespace mln::core + +namespace { + +using mln::core::CallbackSourceRegistry; + +auto to_c_camera_change_mode(mln::MapObserver::CameraChangeMode mode) + -> int32_t { + switch (mode) { + case mln::MapObserver::CameraChangeMode::Immediate: + return MLN_CAMERA_CHANGE_MODE_IMMEDIATE; + case mln::MapObserver::CameraChangeMode::Animated: + return MLN_CAMERA_CHANGE_MODE_ANIMATED; } - return validate_lat_lng_bounds(options.bounds); + assert(false); + return MLN_CAMERA_CHANGE_MODE_IMMEDIATE; } -auto validate_tile_source_tile_size_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE - )) { - return MLN_STATUS_OK; - } - if (options.tile_size == 0 || options.tile_size > 65535U) { - mln::core::set_thread_error("tile_size must be within [1, 65535]"); - return MLN_STATUS_INVALID_ARGUMENT; +auto to_c_render_mode(mln::MapObserver::RenderMode mode) -> uint32_t { + switch (mode) { + case mln::MapObserver::RenderMode::Partial: + return MLN_RENDER_MODE_PARTIAL; + case mln::MapObserver::RenderMode::Full: + return MLN_RENDER_MODE_FULL; } - return MLN_STATUS_OK; + assert(false); + return MLN_RENDER_MODE_PARTIAL; } -auto validate_tile_source_vector_encoding_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING - )) { - return MLN_STATUS_OK; - } - switch (options.vector_encoding) { - case MLN_STYLE_VECTOR_TILE_ENCODING_MVT: - case MLN_STYLE_VECTOR_TILE_ENCODING_MLT: - return MLN_STATUS_OK; - default: - mln::core::set_thread_error("vector_encoding is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto to_c_rendering_stats(const mln::gfx::RenderingStats& stats) + -> mln_rendering_stats { + return mln_rendering_stats{ + .encoding_time = stats.encodingTime, + .rendering_time = stats.renderingTime, + .frame_count = stats.numFrames, + .draw_call_count = stats.numDrawCalls, + .total_draw_call_count = stats.totalDrawCalls + }; } -auto validate_tile_source_raster_encoding_option( - const mln_style_tile_source_options& options -) -> mln_status { - if (!has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING - )) { - return MLN_STATUS_OK; - } - switch (options.raster_encoding) { - case MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX: - case MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM: - return MLN_STATUS_OK; - default: - mln::core::set_thread_error("raster_encoding is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto render_frame_payload(const mln::MapObserver::RenderFrameStatus& status) + -> mln_runtime_event_payload { + auto payload = mln::core::zeroed_event_payload(); + payload.render_frame = mln_runtime_event_render_frame{ + .mode = to_c_render_mode(status.mode), + .needs_repaint = status.needsRepaint, + .placement_changed = status.placementChanged, + .stats = to_c_rendering_stats(status.renderingStats) + }; + return payload; } -auto validate_tile_source_option_kind( - const mln_style_tile_source_options& options, TileSourceOptionKind kind -) -> mln_status { - if ( - kind != TileSourceOptionKind::Vector && - has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING - ) - ) { - mln::core::set_thread_error( - "vector_encoding is only valid for vector sources" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - kind != TileSourceOptionKind::RasterDEM && - has_tile_source_option( - options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING - ) - ) { - mln::core::set_thread_error( - "raster_encoding is only valid for raster DEM sources" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; +auto render_map_payload(mln::MapObserver::RenderMode mode) + -> mln_runtime_event_payload { + auto payload = mln::core::zeroed_event_payload(); + payload.render_map = + mln_runtime_event_render_map{.mode = to_c_render_mode(mode)}; + return payload; } -auto validate_tile_source_options( - const mln_style_tile_source_options* options, TileSourceOptionKind kind -) -> mln_status { - if (options == nullptr) { - return MLN_STATUS_OK; - } - for (const auto validator : { - validate_tile_source_option_header, - validate_tile_source_zoom_options, - validate_tile_source_attribution_option, - validate_tile_source_scheme_option, - validate_tile_source_bounds_option, - validate_tile_source_tile_size_option, - validate_tile_source_vector_encoding_option, - validate_tile_source_raster_encoding_option, - }) { - const auto status = validator(*options); - if (status != MLN_STATUS_OK) { - return status; - } +auto to_c_tile_operation(mln::TileOperation operation) -> uint32_t { + switch (operation) { + case mln::TileOperation::RequestedFromCache: + return MLN_TILE_OPERATION_REQUESTED_FROM_CACHE; + case mln::TileOperation::RequestedFromNetwork: + return MLN_TILE_OPERATION_REQUESTED_FROM_NETWORK; + case mln::TileOperation::LoadFromNetwork: + return MLN_TILE_OPERATION_LOAD_FROM_NETWORK; + case mln::TileOperation::LoadFromCache: + return MLN_TILE_OPERATION_LOAD_FROM_CACHE; + case mln::TileOperation::StartParse: + return MLN_TILE_OPERATION_START_PARSE; + case mln::TileOperation::EndParse: + return MLN_TILE_OPERATION_END_PARSE; + case mln::TileOperation::Error: + return MLN_TILE_OPERATION_ERROR; + case mln::TileOperation::Cancelled: + return MLN_TILE_OPERATION_CANCELLED; + case mln::TileOperation::NullOp: + return MLN_TILE_OPERATION_NULL; } - return validate_tile_source_option_kind(*options, kind); + return MLN_TILE_OPERATION_NULL; } -auto effective_tile_source_options(const mln_style_tile_source_options* options) - -> mln_style_tile_source_options { - auto result = mln::core::style_tile_source_options_default(); - if (options == nullptr) { - return result; - } - - result.fields = options->fields; - if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM)) { - result.min_zoom = options->min_zoom; - } - if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM)) { - result.max_zoom = options->max_zoom; - } - if ( - has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION) - ) { - result.attribution = options->attribution; - } - if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_SCHEME)) { - result.scheme = options->scheme; - } - if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { - result.bounds = options->bounds; - } - if ( - has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE) - ) { - result.tile_size = options->tile_size; - } - if ( - has_tile_source_option( - *options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING - ) - ) { - result.vector_encoding = options->vector_encoding; - } - if ( - has_tile_source_option( - *options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING - ) - ) { - result.raster_encoding = options->raster_encoding; - } - return result; +auto to_c_tile_id(const mln::OverscaledTileID& tile_id) -> mln_tile_id { + return mln_tile_id{ + .overscaled_z = tile_id.overscaledZ, + .wrap = tile_id.wrap, + .canonical_z = tile_id.canonical.z, + .canonical_x = tile_id.canonical.x, + .canonical_y = tile_id.canonical.y + }; } -auto to_native_tile_scheme(uint32_t scheme) -> mln::Tileset::Scheme { - return scheme == MLN_STYLE_TILE_SCHEME_TMS ? mln::Tileset::Scheme::TMS - : mln::Tileset::Scheme::XYZ; +auto tile_action_payload( + mln::TileOperation operation, const mln::OverscaledTileID& tile_id +) -> mln_runtime_event_payload { + auto payload = mln::core::zeroed_event_payload(); + payload.tile_action = mln_runtime_event_tile_action{ + .operation = to_c_tile_operation(operation), + .tile_id = to_c_tile_id(tile_id) + }; + return payload; } -auto to_native_vector_encoding(uint32_t encoding) - -> mln::Tileset::VectorEncoding { - return encoding == MLN_STYLE_VECTOR_TILE_ENCODING_MLT - ? mln::Tileset::VectorEncoding::MLT - : mln::Tileset::VectorEncoding::Mapbox; -} +} // namespace -auto to_native_raster_encoding(uint32_t encoding) - -> mln::Tileset::RasterEncoding { - return encoding == MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM - ? mln::Tileset::RasterEncoding::Terrarium - : mln::Tileset::RasterEncoding::Mapbox; -} +namespace mln::core { -auto validate_tile_urls(const mln_buffer_view* tiles, size_t tile_count) - -> mln_status { - if (tile_count == 0) { - mln::core::set_thread_error("tile_count must be greater than 0"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (tiles == nullptr) { - mln::core::set_thread_error("tiles must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - for (const auto tile : std::span{tiles, tile_count}) { - if (!validate_string_view(tile, "tile URL")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (tile.size == 0) { - mln::core::set_thread_error("tile URLs must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; +// Every callback tests the map's subscription mask before it builds anything, +// so an unselected event allocates no payload, message, or queue node. +class HeadlessObserver final : public mln::MapObserver { + public: + HeadlessObserver( + mln_runtime runtime, mln_map map, + std::shared_ptr event_state, + std::shared_ptr callback_sources + ) + : runtime_(runtime), + map_(map), + event_state_(std::move(event_state)), + callback_sources_(std::move(callback_sources)) {} + + void onCameraWillChange(CameraChangeMode mode) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE)) { + return; } + push( + MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE, to_c_camera_change_mode(mode) + ); } - return MLN_STATUS_OK; -} -auto to_native_tile_urls(const mln_buffer_view* tiles, size_t tile_count) - -> std::vector { - auto result = std::vector{}; - result.reserve(tile_count); - for (const auto tile : std::span{tiles, tile_count}) { - result.push_back(string_from_view(tile)); + void onCameraIsChanging() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_IS_CHANGING)) { + return; + } + push(MLN_RUNTIME_EVENT_MAP_CAMERA_IS_CHANGING); } - return result; -} -auto to_native_tileset( - const mln_buffer_view* tiles, size_t tile_count, - const mln_style_tile_source_options& options, bool vector_source -) -> std::optional { - if (options.min_zoom > options.max_zoom) { - mln::core::set_thread_error( - "effective min_zoom must be less than or equal to max_zoom" + void onCameraDidChange(CameraChangeMode mode) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_DID_CHANGE)) { + return; + } + push( + MLN_RUNTIME_EVENT_MAP_CAMERA_DID_CHANGE, to_c_camera_change_mode(mode) ); - return std::nullopt; } - auto tileset = mln::Tileset{ - to_native_tile_urls(tiles, tile_count), - mln::Range{ - static_cast(options.min_zoom), - static_cast(options.max_zoom) - }, - string_from_view(options.attribution), - to_native_tile_scheme(options.scheme), - std::nullopt, - vector_source - ? std::optional{to_native_vector_encoding( - options.vector_encoding - )} - : std::nullopt - }; - if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { - tileset.bounds = to_native_lat_lng_bounds(options.bounds); + void onWillStartLoadingMap() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_STARTED)) { + return; + } + push(MLN_RUNTIME_EVENT_MAP_LOADING_STARTED); } - return tileset; -} -auto has_custom_geometry_source_option( - const mln_custom_geometry_source_options& options, uint32_t field -) -> bool { - return (options.fields & field) != 0U; -} - -auto validate_custom_geometry_zoom(double zoom, const char* name) - -> mln_status { - if ( - !std::isfinite(zoom) || zoom < 0.0 || zoom > 32.0 || - std::floor(zoom) != zoom - ) { - auto message = std::string{name} + " must be an integer within [0, 32]"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; + void onDidFinishLoadingMap() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_FINISHED)) { + return; + } + push(MLN_RUNTIME_EVENT_MAP_LOADING_FINISHED); } - return MLN_STATUS_OK; -} -auto effective_custom_geometry_source_options( - const mln_custom_geometry_source_options& options -) -> mln_custom_geometry_source_options; - -auto validate_custom_geometry_source_options( - const mln_custom_geometry_source_options* options -) -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_custom_geometry_source_options)) { - mln::core::set_thread_error( - "mln_custom_geometry_source_options.size is too small" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - constexpr auto known_fields = - static_cast(MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM) | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TOLERANCE | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TILE_SIZE | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_BUFFER | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_CLIP | - MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_WRAP; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_custom_geometry_source_options.fields contains unknown bits" + // The failure text is map state that both style setters read, so it is + // recorded whatever the mask selects. + void onDidFailLoadingMap( + mln::MapLoadError error, const std::string& message + ) override { + event_state_->style_load_failure = message; + event_state_->style_load_failed = true; + if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_FAILED)) { + return; + } + push( + MLN_RUNTIME_EVENT_MAP_LOADING_FAILED, static_cast(error), + message.c_str() ); - return MLN_STATUS_INVALID_ARGUMENT; } - if (options->fetch_tile == nullptr) { - mln::core::set_thread_error("fetch_tile must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - has_custom_geometry_source_option( - *options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM - ) - ) { - const auto status = - validate_custom_geometry_zoom(options->min_zoom, "min_zoom"); - if (status != MLN_STATUS_OK) { - return status; + + // A style load can drop custom geometry sources that the previous style held. + // The release callbacks that owes are map state rather than an event, so the + // reconciliation runs whatever the mask selects. + void onDidFinishLoadingStyle() override { + // The event is queued before the reconciliation, and the registry is held + // by a local share across it, because reconcile() runs host release + // callbacks. Nothing may touch this observer or its members after host code + // runs: a callback that destroys its map destroys this observer with it. + if (selected(MLN_RUNTIME_EVENT_MAP_STYLE_LOADED)) { + push(MLN_RUNTIME_EVENT_MAP_STYLE_LOADED); } + const auto sources = callback_sources_; + sources->reconcile(); } - if ( - has_custom_geometry_source_option( - *options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM - ) - ) { - const auto status = - validate_custom_geometry_zoom(options->max_zoom, "max_zoom"); - if (status != MLN_STATUS_OK) { - return status; + + void onWillStartRenderingFrame() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_STARTED)) { + return; } + push(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_STARTED); } - const auto effective = effective_custom_geometry_source_options(*options); - if (effective.min_zoom > effective.max_zoom) { - mln::core::set_thread_error( - "min_zoom must be less than or equal to max_zoom" + + void onDidFinishRenderingFrame(const RenderFrameStatus& status) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED)) { + return; + } + push_payload( + MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED, + MLN_RUNTIME_EVENT_PAYLOAD_RENDER_FRAME, render_frame_payload(status) ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!std::isfinite(effective.tolerance) || effective.tolerance < 0.0) { - mln::core::set_thread_error("tolerance must be finite and non-negative"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (effective.tile_size == 0 || effective.tile_size > 65535U) { - mln::core::set_thread_error("tile_size must be within [1, 65535]"); - return MLN_STATUS_INVALID_ARGUMENT; } - if (effective.buffer > 65535U) { - mln::core::set_thread_error("buffer must be at most 65535"); - return MLN_STATUS_INVALID_ARGUMENT; + + void onWillStartRenderingMap() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_STARTED)) { + return; + } + push(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_STARTED); } - return MLN_STATUS_OK; -} -auto effective_custom_geometry_source_options( - const mln_custom_geometry_source_options& options -) -> mln_custom_geometry_source_options { - auto result = mln::core::custom_geometry_source_options_default(); - result.fields = options.fields; - result.fetch_tile = options.fetch_tile; - result.cancel_tile = options.cancel_tile; - result.user_data = options.user_data; - result.release_user_data = options.release_user_data; - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM - ) - ) { - result.min_zoom = options.min_zoom; + void onDidFinishRenderingMap(RenderMode mode) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_FINISHED)) { + return; + } + push_payload( + MLN_RUNTIME_EVENT_MAP_RENDER_MAP_FINISHED, + MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP, render_map_payload(mode) + ); } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM - ) - ) { - result.max_zoom = options.max_zoom; + + void onDidBecomeIdle() override { + if (!selected(MLN_RUNTIME_EVENT_MAP_IDLE)) { + return; + } + push(MLN_RUNTIME_EVENT_MAP_IDLE); } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TOLERANCE - ) - ) { - result.tolerance = options.tolerance; - } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TILE_SIZE - ) - ) { - result.tile_size = options.tile_size; - } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_BUFFER - ) - ) { - result.buffer = options.buffer; - } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_CLIP - ) - ) { - result.clip = options.clip; - } - if ( - has_custom_geometry_source_option( - options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_WRAP - ) - ) { - result.wrap = options.wrap; - } - return result; -} - -auto to_c_canonical_tile_id(const mln::CanonicalTileID& tile_id) - -> mln_canonical_tile_id { - return mln_canonical_tile_id{.z = tile_id.z, .x = tile_id.x, .y = tile_id.y}; -} -auto to_native_tile_function( - void (*callback)(void*, mln_canonical_tile_id), void* user_data -) -> mln::style::TileFunction { - if (callback == nullptr) { - return nullptr; - } - return [callback, user_data](const mln::CanonicalTileID& tile_id) -> void { - try { - callback(user_data, to_c_canonical_tile_id(tile_id)); - } catch (const std::exception& exception) { - mln::core::set_thread_error(exception); - } catch (...) { - mln::core::set_thread_error("custom source callback threw"); + void onStyleImageMissing(const std::string& image_id) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_STYLE_IMAGE_MISSING)) { + return; } - }; -} - -auto to_native_custom_geometry_source_options( - const mln_custom_geometry_source_options& options -) -> mln::style::CustomGeometrySource::Options { - auto result = mln::style::CustomGeometrySource::Options{}; - result.fetchTileFunction = - to_native_tile_function(options.fetch_tile, options.user_data); - result.cancelTileFunction = - to_native_tile_function(options.cancel_tile, options.user_data); - result.zoomRange = mln::Range{ - static_cast(options.min_zoom), - static_cast(options.max_zoom) - }; - result.tileOptions = mln::style::CustomGeometrySource::TileOptions{ - .tolerance = options.tolerance, - .tileSize = static_cast(options.tile_size), - .buffer = static_cast(options.buffer), - .clip = options.clip, - .wrap = options.wrap - }; - return result; -} - -auto has_custom_mvt_vector_source_option( - const mln_custom_mvt_vector_source_options& options, uint32_t field -) -> bool { - return (options.fields & field) != 0U; -} - -auto effective_custom_mvt_vector_source_options( - const mln_custom_mvt_vector_source_options& options -) -> mln_custom_mvt_vector_source_options; - -auto validate_custom_mvt_vector_source_options( - const mln_custom_mvt_vector_source_options* options -) -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_custom_mvt_vector_source_options)) { - mln::core::set_thread_error( - "mln_custom_mvt_vector_source_options.size is too small" + push_payload( + MLN_RUNTIME_EVENT_MAP_STYLE_IMAGE_MISSING, MLN_RUNTIME_EVENT_PAYLOAD_NONE, + mln::core::zeroed_event_payload(), 0, image_id ); - return MLN_STATUS_INVALID_ARGUMENT; } - constexpr auto known_fields = - static_cast(MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM) | - MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_custom_mvt_vector_source_options.fields contains unknown bits" + + void onTileAction( + mln::TileOperation operation, const mln::OverscaledTileID& tile_id, + const std::string& source_id + ) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_TILE_ACTION)) { + return; + } + push_payload( + MLN_RUNTIME_EVENT_MAP_TILE_ACTION, MLN_RUNTIME_EVENT_PAYLOAD_TILE_ACTION, + tile_action_payload(operation, tile_id), 0, source_id ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->fetch_tile == nullptr) { - mln::core::set_thread_error("fetch_tile must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; } - if ( - has_custom_mvt_vector_source_option( - *options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM - ) - ) { - const auto status = - validate_custom_geometry_zoom(options->min_zoom, "min_zoom"); - if (status != MLN_STATUS_OK) { - return status; + + // The mask test precedes the try block, so a suppressed render error never + // formats the exception text. + void onRenderError(std::exception_ptr error) override { + if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR)) { + return; } - } - if ( - has_custom_mvt_vector_source_option( - *options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM - ) - ) { - const auto status = - validate_custom_geometry_zoom(options->max_zoom, "max_zoom"); - if (status != MLN_STATUS_OK) { - return status; + try { + if (error) { + std::rethrow_exception(error); + } + push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR); + } catch (const std::exception& exception) { + push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR, 0, exception.what()); + } catch (...) { + push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR, 0, "unknown render error"); } } - const auto effective = effective_custom_mvt_vector_source_options(*options); - if (effective.min_zoom > effective.max_zoom) { - mln::core::set_thread_error( - "min_zoom must be less than or equal to max_zoom" - ); - return MLN_STATUS_INVALID_ARGUMENT; + + private: + [[nodiscard]] auto selected(uint32_t type) const noexcept -> bool { + return mln::core::event_selected(event_state_->mask, type); } - return MLN_STATUS_OK; -} -auto effective_custom_mvt_vector_source_options( - const mln_custom_mvt_vector_source_options& options -) -> mln_custom_mvt_vector_source_options { - auto result = mln::core::custom_mvt_vector_source_options_default(); - result.fields = options.fields; - result.fetch_tile = options.fetch_tile; - result.cancel_tile = options.cancel_tile; - result.user_data = options.user_data; - result.release_user_data = options.release_user_data; - if ( - has_custom_mvt_vector_source_option( - options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM - ) - ) { - result.min_zoom = options.min_zoom; + auto push(uint32_t type, int32_t code = 0, const char* message = nullptr) + -> void { + mln::core::push_runtime_map_event(runtime_, map_, type, code, message); } - if ( - has_custom_mvt_vector_source_option( - options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM - ) - ) { - result.max_zoom = options.max_zoom; + + auto push_payload( + uint32_t type, uint32_t payload_type, + const mln_runtime_event_payload& payload, int32_t code = 0, + std::string message = {} + ) -> void { + mln::core::push_runtime_map_event_payload( + runtime_, map_, type, payload_type, payload, code, std::move(message) + ); } - return result; -} -auto to_native_custom_mvt_vector_source_options( - const mln_custom_mvt_vector_source_options& options -) -> mln::style::CustomVectorSource::Options { - auto result = mln::style::CustomVectorSource::Options{}; - result.fetchTileFunction = - to_native_tile_function(options.fetch_tile, options.user_data); - result.cancelTileFunction = - to_native_tile_function(options.cancel_tile, options.user_data); - result.zoomRange = mln::Range{ - static_cast(options.min_zoom), - static_cast(options.max_zoom) - }; - return result; -} + mln_runtime runtime_; + mln_map map_; + std::shared_ptr event_state_; + std::shared_ptr callback_sources_; +}; -enum class CallbackSourceKind : uint8_t { CustomGeometry, CustomMvtVector }; +} // namespace mln::core -using CallbackSourceRelease = void (*)(void*); +namespace { -auto source_matches_kind( - const mln::style::Source* source, CallbackSourceKind kind -) -> bool { - if (source == nullptr) { - return false; - } - switch (kind) { - case CallbackSourceKind::CustomGeometry: - return source->as() != nullptr; - case CallbackSourceKind::CustomMvtVector: - return source->as() != nullptr; - } - return false; -} +using mln::core::HeadlessObserver; -// Holds the release callback owed for each tracked callback source. A host -// cannot see when a style load drops a source, so this layer tracks it. -// -// Every mutation runs on the map owner thread, which is the only thread that -// adds or removes sources and the only thread mbgl reports style loads on. -class CallbackSourceRegistry final { +// Delivers mln::RendererObserver callbacks on the map's run loop instead of on +// whichever thread rendered. The delegate is mln::Map::Impl, whose handlers +// must not run concurrently. Every callback therefore becomes a mailbox message +// submitted to the runtime worker. Forwarding is unconditional, preserving +// delivery order independently of the callback's native thread. +class ForwardingRendererObserver final : public mln::RendererObserver { public: - CallbackSourceRegistry() = default; + ForwardingRendererObserver( + mln::Scheduler& map_scheduler, mln::RendererObserver& delegate + ) + : mailbox_(std::make_shared(map_scheduler)), + delegate_(delegate, mailbox_) {} - // Runs when the owning map is destroyed, after the mln::Map that could still - // call into the source is gone. - ~CallbackSourceRegistry() { release_all(); } + ForwardingRendererObserver(const ForwardingRendererObserver&) = delete; + auto operator=(const ForwardingRendererObserver&) + -> ForwardingRendererObserver& = delete; + ForwardingRendererObserver(ForwardingRendererObserver&&) = delete; + auto operator=(ForwardingRendererObserver&&) + -> ForwardingRendererObserver& = delete; - CallbackSourceRegistry(const CallbackSourceRegistry&) = delete; - CallbackSourceRegistry(CallbackSourceRegistry&&) = delete; - auto operator=(const CallbackSourceRegistry&) - -> CallbackSourceRegistry& = delete; - auto operator=(CallbackSourceRegistry&&) -> CallbackSourceRegistry& = delete; + ~ForwardingRendererObserver() override { mailbox_->close(); } - // A source with no release callback is not tracked. - auto add( - const std::string& source_id, CallbackSourceKind kind, - CallbackSourceRelease release, void* user_data - ) -> void { - // An entry already under this ID belongs to a source that the style dropped - // before reconciliation ran, so it still owes its release. Invoke it here - // rather than letting the assignment below drop it, which keeps - // exactly-once independent of when the style-loaded observer reconciles. - if (const auto stale = entries_.find(source_id); stale != entries_.end()) { - const auto owed = stale->second; - entries_.erase(stale); - invoke(owed); - } - if (release == nullptr) { - return; - } - // The caller tracks before the style takes the source, so a throw here - // leaves nothing committed and the caller still owns user_data. A failed - // add owes no release. - entries_.insert_or_assign(source_id, Entry{kind, release, user_data}); - } + // Waits out an in-flight receive and drops anything queued, so the delegate + // can be torn down once this returns. Idempotent. + auto close() -> void { mailbox_->close(); } - // Drops an entry without releasing it, for a source the style then rejected. - // The caller still owns user_data on that path. - auto untrack(const std::string& source_id) noexcept -> void { - entries_.erase(source_id); + void onInvalidate() override { + delegate_.invoke(&mln::RendererObserver::onInvalidate); } - // The observer that reports style loads has no route to a style, so the - // registry keeps the map it belongs to. destroy_map() clears it before the - // map is destroyed. - auto attach(mln::Map& map) noexcept -> void { map_ = ↦ } - - auto detach() noexcept -> void { map_ = nullptr; } - - auto release(const std::string& source_id) -> void { - const auto entry = entries_.find(source_id); - if (entry == entries_.end()) { - return; - } - const auto owed = entry->second; - entries_.erase(entry); - invoke(owed); + void onResourceError(std::exception_ptr error) override { + delegate_.invoke(&mln::RendererObserver::onResourceError, error); } - // Releases every tracked source the current style no longer holds. A style - // document cannot declare a callback source, so a source of another type - // under a tracked ID means the tracked source is gone. - auto reconcile() -> void { - if (map_ == nullptr || entries_.empty()) { - return; - } - auto& style = map_->getStyle(); - auto owed = std::vector{}; - for (auto entry = entries_.begin(); entry != entries_.end();) { - auto* source = style.getSource(entry->first); - if (source_matches_kind(source, entry->second.kind)) { - ++entry; - continue; - } - owed.push_back(entry->second); - entry = entries_.erase(entry); - } - for (const auto& release : owed) { - invoke(release); - } + void onWillStartRenderingMap() override { + delegate_.invoke(&mln::RendererObserver::onWillStartRenderingMap); } - auto release_all() -> void { - auto owed = std::move(entries_); - entries_.clear(); - for (const auto& entry : owed) { - invoke(entry.second); - } + void onWillStartRenderingFrame() override { + delegate_.invoke(&mln::RendererObserver::onWillStartRenderingFrame); } - private: - struct Entry { - CallbackSourceKind kind = CallbackSourceKind::CustomGeometry; - CallbackSourceRelease release = nullptr; - void* user_data = nullptr; - }; + void onDidFinishRenderingFrame( + RenderMode mode, bool repaint_needed, bool placement_changed, + const mln::gfx::RenderingStats& stats + ) override { + // The name carries three overloads; mln::Map::Impl implements only this + // one. + void (mln::RendererObserver::*method)( + RenderMode, bool, bool, const mln::gfx::RenderingStats& + ) = &mln::RendererObserver::onDidFinishRenderingFrame; + delegate_.invoke(method, mode, repaint_needed, placement_changed, stats); + } - static auto invoke(const Entry& entry) noexcept -> void { - try { - entry.release(entry.user_data); - } catch (const std::exception& exception) { - mln::core::set_thread_error(exception); - } catch (...) { - mln::core::set_thread_error("callback source release threw"); - } + void onDidFinishRenderingMap() override { + delegate_.invoke(&mln::RendererObserver::onDidFinishRenderingMap); } - std::unordered_map entries_; - mln::Map* map_ = nullptr; -}; + void onStyleImageMissing( + const std::string& id, const StyleImageMissingCallback& done + ) override { + delegate_.invoke(&mln::RendererObserver::onStyleImageMissing, id, done); + } -auto validate_canonical_tile_id(mln_canonical_tile_id tile_id) -> mln_status { - if (tile_id.z > 32U) { - mln::core::set_thread_error("tile_id.z must be within [0, 32]"); - return MLN_STATUS_INVALID_ARGUMENT; + void onRemoveUnusedStyleImages(const std::vector& ids) override { + delegate_.invoke(&mln::RendererObserver::onRemoveUnusedStyleImages, ids); } - const auto coordinate_limit = uint64_t{1} << tile_id.z; - if (tile_id.x >= coordinate_limit || tile_id.y >= coordinate_limit) { - mln::core::set_thread_error("tile_id x and y must be within zoom bounds"); - return MLN_STATUS_INVALID_ARGUMENT; + + void onPreCompileShader( + mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, + const std::string& defines + ) override { + delegate_.invoke( + &mln::RendererObserver::onPreCompileShader, id, type, defines + ); } - return MLN_STATUS_OK; -} -auto to_native_canonical_tile_id(mln_canonical_tile_id tile_id) - -> mln::CanonicalTileID { - return mln::CanonicalTileID{ - static_cast(tile_id.z), tile_id.x, tile_id.y - }; -} + void onPostCompileShader( + mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, + const std::string& defines + ) override { + delegate_.invoke( + &mln::RendererObserver::onPostCompileShader, id, type, defines + ); + } -auto validate_source_id(mln_buffer_view source_id) -> mln_status { - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; + void onShaderCompileFailed( + mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, + const std::string& defines + ) override { + delegate_.invoke( + &mln::RendererObserver::onShaderCompileFailed, id, type, defines + ); } - if (source_id.size == 0) { - mln::core::set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; + + void onGlyphsLoaded( + const mln::FontStack& stack, const mln::GlyphRange& range + ) override { + delegate_.invoke(&mln::RendererObserver::onGlyphsLoaded, stack, range); } - return MLN_STATUS_OK; -} -auto validate_source_can_be_added( - mln::style::Style& style, const std::string& source_id -) -> mln_status { - if (style.getSource(source_id) != nullptr) { - mln::core::set_thread_error("source already exists"); - return MLN_STATUS_INVALID_ARGUMENT; + void onGlyphsError( + const mln::FontStack& stack, const mln::GlyphRange& range, + std::exception_ptr error + ) override { + delegate_.invoke( + &mln::RendererObserver::onGlyphsError, stack, range, error + ); } - return MLN_STATUS_OK; -} -auto has_style_image_option( - const mln_style_image_options& options, uint32_t field -) -> bool { - return (options.fields & field) != 0U; -} + void onGlyphsRequested( + const mln::FontStack& stack, const mln::GlyphRange& range + ) override { + delegate_.invoke(&mln::RendererObserver::onGlyphsRequested, stack, range); + } -auto validate_style_image_options(const mln_style_image_options* options) - -> mln_status { - if (options == nullptr) { - return MLN_STATUS_OK; + void onTileAction( + mln::TileOperation operation, const mln::OverscaledTileID& id, + const std::string& source_id + ) override { + delegate_.invoke( + &mln::RendererObserver::onTileAction, operation, id, source_id + ); } - if (options->size < sizeof(mln_style_image_options)) { - mln::core::set_thread_error("mln_style_image_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; + + void onRenderError(std::exception_ptr error) override { + delegate_.invoke(&mln::RendererObserver::onRenderError, error); } - constexpr auto known_fields = - static_cast(MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO) | - MLN_STYLE_IMAGE_OPTION_SDF | MLN_STYLE_IMAGE_OPTION_STRETCH_X | - MLN_STYLE_IMAGE_OPTION_STRETCH_Y | MLN_STYLE_IMAGE_OPTION_CONTENT | - MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH | - MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_style_image_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; + + private: + std::shared_ptr mailbox_; + mln::ActorRef delegate_; +}; + +} // namespace + +namespace mln::core { + +class HeadlessFrontend final : public mln::RendererFrontend { + public: + // The thread pool tag must be a default-constructed identity, unique per map. + // SimpleIdentity::Empty pools every map's work into one bucket that + // waitForEmpty() cannot wait on, because it remaps the empty tag to the + // pool's own identity. The run loop comes in by reference because mbgl calls + // setObserver() from the map constructor; it outlives the map. + HeadlessFrontend( + mln_runtime runtime, mln_map map, mln::util::RunLoop& run_loop, + std::shared_ptr event_state + ) + : runtime_(runtime), + map_(map), + run_loop_(run_loop), + event_state_(std::move(event_state)), + thread_pool_( + mln::Scheduler::GetBackground(), mln::util::SimpleIdentity{} + ) {} + + void reset() override { + const std::scoped_lock lock(latest_update_mutex_); + latest_update_.reset(); + repaint_demand_ = false; } - if ( - has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO) && - (!std::isfinite(options->pixel_ratio) || options->pixel_ratio <= 0.0F) - ) { - mln::core::set_thread_error("pixel_ratio must be finite and positive"); - return MLN_STATUS_INVALID_ARGUMENT; + + // mln::Map calls this once from its constructor on the runtime worker. + void setObserver(mln::RendererObserver& observer) override { + observer_ = + std::make_unique(run_loop_, observer); } - for (const auto& [field, stretches, count, name] : { - std::tuple{ - uint32_t{MLN_STYLE_IMAGE_OPTION_STRETCH_X}, options->stretch_x, - options->stretch_x_count, "stretch_x" - }, - std::tuple{ - uint32_t{MLN_STYLE_IMAGE_OPTION_STRETCH_Y}, options->stretch_y, - options->stretch_y_count, "stretch_y" - }, - }) { - if (!has_style_image_option(*options, field)) { - continue; + + // Store render state before publishing it to sessions and event consumers. + void update(std::shared_ptr update) override { + std::function publish; + std::function publish_session; + { + const std::scoped_lock lock(latest_update_mutex_); + latest_update_ = std::move(update); + ++latest_update_generation_; + repaint_demand_ = true; + publish = publish_; + publish_session = session_publish_; } - if (stretches == nullptr && count != 0) { - auto message = - std::string{name} + " must not be null when its count is non-zero"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; + if (publish) { + publish(); } - for (size_t index = 0; index < count; index += 1) { - const auto& stretch = stretches[index]; - if (!std::isfinite(stretch.from) || !std::isfinite(stretch.to)) { - auto message = std::string{name} + " intervals must be finite"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } - // MapLibre divides by the summed interval width, so an axis whose - // intervals are all zero-width would divide by zero. - if (stretch.from >= stretch.to) { - auto message = - std::string{name} + " intervals must have a positive width"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (index != 0 && stretch.from < stretches[index - 1].to) { - auto message = - std::string{name} + " intervals must increase and must not overlap"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } + if (publish_session) { + publish_session(); } - } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT)) { - const auto& content = options->content; if ( - !std::isfinite(content.left) || !std::isfinite(content.top) || - !std::isfinite(content.right) || !std::isfinite(content.bottom) + mln::core::event_selected( + event_state_->mask, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE + ) ) { - mln::core::set_thread_error("content insets must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (content.left > content.right || content.top > content.bottom) { - mln::core::set_thread_error("content insets must not run backwards"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - for (const auto& [field, value, name] : { - std::tuple{ - uint32_t{MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH}, - options->text_fit_width, "text_fit_width" - }, - std::tuple{ - uint32_t{MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT}, - options->text_fit_height, "text_fit_height" - }, - }) { - if (!has_style_image_option(*options, field)) { - continue; - } - switch (value) { - case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK: - case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY: - case MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL: - break; - default: { - auto message = std::string{name} + " is invalid"; - mln::core::set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } + mln::core::push_runtime_map_event( + runtime_, map_, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE + ); } } - return MLN_STATUS_OK; -} -auto effective_style_image_options(const mln_style_image_options* options) - -> mln_style_image_options { - auto result = mln::core::style_image_options_default(); - if (options == nullptr) { - return result; - } - result.fields = options->fields; - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO)) { - result.pixel_ratio = options->pixel_ratio; - } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_SDF)) { - result.sdf = options->sdf; - } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_STRETCH_X)) { - result.stretch_x = options->stretch_x; - result.stretch_x_count = options->stretch_x_count; + [[nodiscard]] auto latest_update() const + -> std::shared_ptr { + const std::scoped_lock lock(latest_update_mutex_); + return latest_update_; } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_STRETCH_Y)) { - result.stretch_y = options->stretch_y; - result.stretch_y_count = options->stretch_y_count; + // One lock hold keeps the pair coherent: a frame result must report the + // generation of the update that was actually rendered. + [[nodiscard]] auto latest_update_snapshot(uint64_t& out_generation) const + -> std::shared_ptr { + const std::scoped_lock lock(latest_update_mutex_); + out_generation = latest_update_generation_; + return latest_update_; } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT)) { - result.content = options->content; + auto set_publish_callback(std::function publish) -> void { + const std::scoped_lock lock(latest_update_mutex_); + publish_ = std::move(publish); } - if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH)) { - result.text_fit_width = options->text_fit_width; + auto set_session_publish_callback(std::function publish) -> void { + const std::scoped_lock lock(latest_update_mutex_); + session_publish_ = std::move(publish); } - if ( - has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT) - ) { - result.text_fit_height = options->text_fit_height; + + [[nodiscard]] auto latest_update_generation() const -> uint64_t { + const std::scoped_lock lock(latest_update_mutex_); + return latest_update_generation_; } - return result; -} -auto to_native_image_stretches(const mln_image_stretch* stretches, size_t count) - -> mln::style::ImageStretches { - auto native = mln::style::ImageStretches{}; - native.reserve(count); - for (size_t index = 0; index < count; index += 1) { - native.emplace_back(stretches[index].from, stretches[index].to); + [[nodiscard]] auto repaint_demand() const -> bool { + const std::scoped_lock lock(latest_update_mutex_); + return repaint_demand_; } - return native; -} -auto to_native_text_fit(uint32_t value) -> mln::style::TextFit { - switch (value) { - case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY: - return mln::style::TextFit::stretchOnly; - case MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL: - return mln::style::TextFit::proportional; - default: - return mln::style::TextFit::stretchOrShrink; + auto run_render_jobs() -> void { thread_pool_.runRenderJobs(); } + + // Drains queued and running worker jobs without closing the queue; later + // work re-creates the pool bucket, and shutdown_thread_pool() still runs at + // close. Must not be called from a pool thread. + auto wait_thread_pool() -> void { thread_pool_.waitForEmpty(); } + + // Every map must call this: only waitForEmpty() erases the map's bucket in + // the process-global scheduler, which the worker loop otherwise keeps + // walking. + auto shutdown_thread_pool() -> void { + thread_pool_.runRenderJobs(/*closeQueue=*/true); + thread_pool_.waitForEmpty(); } -} -auto from_native_text_fit(mln::style::TextFit value) -> uint32_t { - switch (value) { - case mln::style::TextFit::stretchOnly: - return MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY; - case mln::style::TextFit::proportional: - return MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL; - default: - return MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK; + [[nodiscard]] auto renderer_observer() const -> mln::RendererObserver* { + return observer_.get(); } -} -auto required_premultiplied_rgba8_bytes( - uint32_t width, uint32_t height, uint32_t stride -) -> std::optional { - constexpr auto channels = size_t{4}; - const auto row_bytes = static_cast(width) * channels; - if (height == 0) { - return std::nullopt; + // Must run before the map that backs the delegate is torn down. + auto close_renderer_observer() -> void { + if (observer_ != nullptr) { + observer_->close(); + } } - if (height == 1) { - return row_bytes; + + [[nodiscard]] auto getThreadPool() const + -> const mln::TaggedScheduler& override { + return thread_pool_; } - const auto trailing_rows = static_cast(height - 1U); - const auto row_stride = static_cast(stride); - if ( - trailing_rows > - (std::numeric_limits::max() - row_bytes) / row_stride - ) { - return std::nullopt; + + private: + mln_runtime runtime_; + mln_map map_; + mln::util::RunLoop& run_loop_; + std::shared_ptr event_state_; + std::unique_ptr observer_; + mln::TaggedScheduler thread_pool_; + mutable std::mutex latest_update_mutex_; + std::shared_ptr latest_update_; + std::function publish_; + std::function session_publish_; + uint64_t latest_update_generation_ = 0; + bool repaint_demand_ = false; +}; + +} // namespace mln::core + +namespace { + +using mln::core::HeadlessFrontend; + +auto validate_map_options(const mln_map_options* options) -> mln_status { + if (options == nullptr) { + return MLN_STATUS_OK; } - return (trailing_rows * row_stride) + row_bytes; -} -auto validate_premultiplied_rgba8_image( - const mln_premultiplied_rgba8_image* image -) -> mln_status { - if (image == nullptr) { - mln::core::set_thread_error("image must not be null"); + if (options->size < sizeof(mln_map_options)) { + mln::core::set_thread_error("mln_map_options.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - if (image->size < sizeof(mln_premultiplied_rgba8_image)) { + + // Validated here as well as in the setter, so a mask this library cannot + // honour is rejected wherever it arrives. + constexpr auto known_mask_bits = + static_cast(MLN_RUNTIME_EVENT_MASK_ALL); + if ((options->event_mask & ~known_mask_bits) != 0U) { mln::core::set_thread_error( - "mln_premultiplied_rgba8_image.size is too small" + "mln_map_options.event_mask contains unknown bits" ); return MLN_STATUS_INVALID_ARGUMENT; } - if (image->width == 0 || image->height == 0) { - mln::core::set_thread_error("image dimensions must be positive"); - return MLN_STATUS_INVALID_ARGUMENT; - } - constexpr auto channels = uint32_t{4}; - if (image->width > std::numeric_limits::max() / channels) { - mln::core::set_thread_error("image row byte length overflows"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto row_bytes = image->width * channels; - if (image->stride < row_bytes) { - mln::core::set_thread_error("image stride must be at least width * 4"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (image->pixels == nullptr) { - mln::core::set_thread_error("image pixels must not be null"); + + if ( + options->initial_extent.width == 0 || options->initial_extent.height == 0 || + !std::isfinite(options->initial_extent.scale_factor) || + options->initial_extent.scale_factor <= 0 + ) { + mln::core::set_thread_error( + "initial extent dimensions and scale factor must be positive" + ); return MLN_STATUS_INVALID_ARGUMENT; } - const auto required = required_premultiplied_rgba8_bytes( - image->width, image->height, image->stride - ); - if (!required || image->byte_length < *required) { - mln::core::set_thread_error("image byte_length is too small"); - return MLN_STATUS_INVALID_ARGUMENT; + + switch (options->map_mode) { + case MLN_MAP_MODE_CONTINUOUS: + case MLN_MAP_MODE_STATIC: + case MLN_MAP_MODE_TILE: + break; + default: + mln::core::set_thread_error("map_mode is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; } + return MLN_STATUS_OK; } -auto to_native_premultiplied_rgba8_image( - const mln_premultiplied_rgba8_image& image -) -> mln::PremultipliedImage { - auto result = mln::PremultipliedImage{mln::Size{image.width, image.height}}; - const auto output_stride = result.stride(); - const auto row_bytes = static_cast(image.width) * 4U; - const auto input = std::span{image.pixels, image.byte_length}; - const auto output = std::span{result.data.get(), result.bytes()}; - for (auto row = uint32_t{0}; row < image.height; ++row) { - const auto input_offset = static_cast(row) * image.stride; - const auto output_offset = static_cast(row) * output_stride; - std::copy_n( - input.subspan(input_offset, row_bytes).begin(), row_bytes, - output.subspan(output_offset, row_bytes).begin() - ); +auto to_native_map_mode(uint32_t mode) -> mln::MapMode { + switch (mode) { + case MLN_MAP_MODE_STATIC: + return mln::MapMode::Static; + case MLN_MAP_MODE_TILE: + return mln::MapMode::Tile; + case MLN_MAP_MODE_CONTINUOUS: + return mln::MapMode::Continuous; + default: + assert(false); + return mln::MapMode::Continuous; } - return result; } -auto validate_image_id(mln_buffer_view image_id) -> mln_status { - if (!validate_string_view(image_id, "image_id")) { - return MLN_STATUS_INVALID_ARGUMENT; +auto is_still_map_mode(uint32_t mode) -> bool { + return mode == MLN_MAP_MODE_STATIC || mode == MLN_MAP_MODE_TILE; +} + +auto exception_message(std::exception_ptr error) -> std::string { + if (!error) { + return {}; } - if (image_id.size == 0) { - mln::core::set_thread_error("image_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; + try { + std::rethrow_exception(error); + } catch (const std::exception& exception) { + return exception.what(); + } catch (...) { + return "unknown still-image request error"; } - return MLN_STATUS_OK; } -auto style_image_info_from_native(const mln::style::Image& image) - -> mln_style_image_info { - const auto& pixels = image.getImage(); - return mln_style_image_info{ - .size = sizeof(mln_style_image_info), - .width = pixels.size.width, - .height = pixels.size.height, - .stride = static_cast(pixels.stride()), - .byte_length = pixels.bytes(), - .stretch_x_count = image.getStretchX().size(), - .stretch_y_count = image.getStretchY().size(), - .content = - image.getContent().has_value() - ? mln_image_content{ - .left = image.getContent()->left, - .top = image.getContent()->top, - .right = image.getContent()->right, - .bottom = image.getContent()->bottom - } - : mln_image_content{.left = 0, .top = 0, .right = 0, .bottom = 0}, - .text_fit_width = - image.getTextFitWidth().has_value() - ? from_native_text_fit(*image.getTextFitWidth()) - : static_cast(MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK), - .text_fit_height = - image.getTextFitHeight().has_value() - ? from_native_text_fit(*image.getTextFitHeight()) - : static_cast(MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK), - .pixel_ratio = image.getPixelRatio(), - .sdf = image.isSdf(), - .has_content = image.getContent().has_value(), - .has_text_fit_width = image.getTextFitWidth().has_value(), - .has_text_fit_height = image.getTextFitHeight().has_value() - }; -} +auto validate_lat_lng(mln_lat_lng coordinate) -> mln_status; +auto validate_edge_insets(mln_edge_insets padding) -> mln_status; +auto validate_screen_point(mln_screen_point point) -> mln_status; -auto validate_image_source_coordinates( - const mln_lat_lng* coordinates, size_t coordinate_count -) -> mln_status { - if (coordinate_count != 4) { - mln::core::set_thread_error("image source coordinate_count must be 4"); +auto validate_camera_options(const mln_camera_options* camera) -> mln_status { + if (camera == nullptr) { + mln::core::set_thread_error("camera must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto status = - validate_lat_lng_array(coordinates, coordinate_count, false); - if (status != MLN_STATUS_OK) { - return status; + + if (camera->size < sizeof(mln_camera_options)) { + mln::core::set_thread_error("mln_camera_options.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; } - const auto coordinate_span = std::span{coordinates, 4}; - const auto first = coordinate_span.front(); - const auto all_same = - std::ranges::all_of(coordinate_span, [first](mln_lat_lng value) -> bool { - return value.latitude == first.latitude && - value.longitude == first.longitude; - }); - if (all_same) { - mln::core::set_thread_error("image source coordinates must not all match"); + constexpr auto known_fields = + static_cast(MLN_CAMERA_OPTION_CENTER) | MLN_CAMERA_OPTION_ZOOM | + MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH | + MLN_CAMERA_OPTION_CENTER_ALTITUDE | MLN_CAMERA_OPTION_PADDING | + MLN_CAMERA_OPTION_ANCHOR | MLN_CAMERA_OPTION_ROLL | MLN_CAMERA_OPTION_FOV; + if ((camera->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_camera_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + if ((camera->fields & MLN_CAMERA_OPTION_CENTER) != 0U) { + const auto status = validate_lat_lng( + mln_lat_lng{.latitude = camera->latitude, .longitude = camera->longitude} + ); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ( + ((camera->fields & MLN_CAMERA_OPTION_CENTER_ALTITUDE) != 0U && + !std::isfinite(camera->center_altitude)) || + ((camera->fields & MLN_CAMERA_OPTION_ZOOM) != 0U && + !std::isfinite(camera->zoom)) || + ((camera->fields & MLN_CAMERA_OPTION_BEARING) != 0U && + !std::isfinite(camera->bearing)) || + ((camera->fields & MLN_CAMERA_OPTION_PITCH) != 0U && + !std::isfinite(camera->pitch)) || + ((camera->fields & MLN_CAMERA_OPTION_ROLL) != 0U && + !std::isfinite(camera->roll)) || + ((camera->fields & MLN_CAMERA_OPTION_FOV) != 0U && + !std::isfinite(camera->field_of_view)) + ) { + mln::core::set_thread_error("enabled camera numeric fields must be finite"); return MLN_STATUS_INVALID_ARGUMENT; } + if ((camera->fields & MLN_CAMERA_OPTION_PADDING) != 0U) { + const auto status = validate_edge_insets(camera->padding); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ((camera->fields & MLN_CAMERA_OPTION_ANCHOR) != 0U) { + const auto status = validate_screen_point(camera->anchor); + if (status != MLN_STATUS_OK) { + return status; + } + } + return MLN_STATUS_OK; } -auto to_native_image_source_coordinates(const mln_lat_lng* coordinates) - -> std::array { - auto result = std::array{}; - const auto coordinate_span = std::span{coordinates, 4}; - auto index = size_t{0}; - for (const auto coordinate : coordinate_span) { - result.at(index) = to_native_lat_lng(coordinate); - ++index; - } - return result; +using DoubleMilliseconds = std::chrono::duration; + +auto max_native_duration_ms() -> double { + return std::chrono::duration_cast(mln::Duration::max()) + .count(); } -auto from_native_image_source_coordinates( - const std::array& coordinates -) -> std::array { - auto result = std::array{}; - for (auto index = size_t{0}; index < result.size(); ++index) { - result.at(index) = from_native_lat_lng(coordinates.at(index)); - } - return result; +auto duration_from_milliseconds(double milliseconds) -> mln::Duration { + return std::chrono::duration_cast( + DoubleMilliseconds{milliseconds} + ); } -auto create_style_id_list( - std::vector ids, mln_style_id_list* out_list -) -> mln_status { - if (out_list == nullptr || *out_list != MLN_HANDLE_NULL) { +// The accepted bound is exclusive because mln::Duration::max() has no exact +// double representation: the nearest double converts back to 2^63 ticks, one +// past the largest representable count. The margin holds for a nanosecond +// duration only, so pin the representation. +static_assert( + std::is_same_v, + "the accepted duration bound is derived from a nanosecond mln::Duration" +); + +auto is_native_duration_ms(double milliseconds) -> bool { + return std::isfinite(milliseconds) && milliseconds >= 0.0 && + milliseconds < max_native_duration_ms(); +} + +auto validate_animation_options(const mln_animation_options* animation) + -> mln_status { + if (animation == nullptr) { + return MLN_STATUS_OK; + } + if (animation->size < sizeof(mln_animation_options)) { + mln::core::set_thread_error("mln_animation_options.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + constexpr auto known_fields = + static_cast(MLN_ANIMATION_OPTION_DURATION) | + MLN_ANIMATION_OPTION_VELOCITY | MLN_ANIMATION_OPTION_MIN_ZOOM | + MLN_ANIMATION_OPTION_EASING | MLN_ANIMATION_OPTION_TRANSITION_ID; + if ((animation->fields & ~known_fields) != 0U) { mln::core::set_thread_error( - "out_list must not be null and *out_list must be the null handle" + "mln_animation_options.fields contains unknown bits" ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto list = std::make_shared(); - list->ids = std::move(ids); - *out_list = mln::core::handle_table().insert( - std::move(list) - ); - return MLN_STATUS_OK; -} - -auto create_style_string_list( - std::vector values, mln_style_string_list* out_list -) -> mln_status { - if (out_list == nullptr || *out_list != MLN_HANDLE_NULL) { + if ( + (animation->fields & MLN_ANIMATION_OPTION_DURATION) != 0U && + !is_native_duration_ms(animation->duration_ms) + ) { mln::core::set_thread_error( - "out_list must not be null and *out_list must be the null handle" + "animation duration_ms must fit the native duration range" ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto list = std::make_shared(); - list->values = std::move(values); - *out_list = - mln::core::handle_table().insert( - std::move(list) + if ( + (animation->fields & MLN_ANIMATION_OPTION_VELOCITY) != 0U && + (!std::isfinite(animation->velocity) || animation->velocity <= 0.0) + ) { + mln::core::set_thread_error( + "animation velocity must be positive and finite" ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + (animation->fields & MLN_ANIMATION_OPTION_MIN_ZOOM) != 0U && + !std::isfinite(animation->min_zoom) + ) { + mln::core::set_thread_error("animation min_zoom must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ((animation->fields & MLN_ANIMATION_OPTION_EASING) != 0U) { + const auto easing = animation->easing; + if ( + !std::isfinite(easing.x1) || !std::isfinite(easing.y1) || + !std::isfinite(easing.x2) || !std::isfinite(easing.y2) || + easing.x1 < 0.0 || easing.x1 > 1.0 || easing.x2 < 0.0 || easing.x2 > 1.0 + ) { + mln::core::set_thread_error( + "animation easing x values must be within [0, 1] and all easing values " + "must be finite" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + } return MLN_STATUS_OK; } -auto to_c_camera_change_mode(mln::MapObserver::CameraChangeMode mode) - -> int32_t { - switch (mode) { - case mln::MapObserver::CameraChangeMode::Immediate: - return MLN_CAMERA_CHANGE_MODE_IMMEDIATE; - case mln::MapObserver::CameraChangeMode::Animated: - return MLN_CAMERA_CHANGE_MODE_ANIMATED; +auto validate_camera_fit_options(const mln_camera_fit_options* options) + -> mln_status { + if (options == nullptr) { + return MLN_STATUS_OK; + } + if (options->size < sizeof(mln_camera_fit_options)) { + mln::core::set_thread_error("mln_camera_fit_options.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; } - assert(false); - return MLN_CAMERA_CHANGE_MODE_IMMEDIATE; -} -auto to_c_render_mode(mln::MapObserver::RenderMode mode) -> uint32_t { - switch (mode) { - case mln::MapObserver::RenderMode::Partial: - return MLN_RENDER_MODE_PARTIAL; - case mln::MapObserver::RenderMode::Full: - return MLN_RENDER_MODE_FULL; + constexpr auto known_fields = + static_cast(MLN_CAMERA_FIT_OPTION_PADDING) | + MLN_CAMERA_FIT_OPTION_BEARING | MLN_CAMERA_FIT_OPTION_PITCH; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_camera_fit_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; } - assert(false); - return MLN_RENDER_MODE_PARTIAL; + if ((options->fields & MLN_CAMERA_FIT_OPTION_PADDING) != 0U) { + const auto status = validate_edge_insets(options->padding); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ( + ((options->fields & MLN_CAMERA_FIT_OPTION_BEARING) != 0U && + !std::isfinite(options->bearing)) || + ((options->fields & MLN_CAMERA_FIT_OPTION_PITCH) != 0U && + !std::isfinite(options->pitch)) + ) { + mln::core::set_thread_error("camera fit bearing and pitch must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; } -auto to_c_rendering_stats(const mln::gfx::RenderingStats& stats) - -> mln_rendering_stats { - return mln_rendering_stats{ - .encoding_time = stats.encodingTime, - .rendering_time = stats.renderingTime, - .frame_count = stats.numFrames, - .draw_call_count = stats.numDrawCalls, - .total_draw_call_count = stats.totalDrawCalls - }; -} - -auto render_frame_payload(const mln::MapObserver::RenderFrameStatus& status) - -> mln_runtime_event_payload { - auto payload = mln::core::zeroed_event_payload(); - payload.render_frame = mln_runtime_event_render_frame{ - .mode = to_c_render_mode(status.mode), - .needs_repaint = status.needsRepaint, - .placement_changed = status.placementChanged, - .stats = to_c_rendering_stats(status.renderingStats) - }; - return payload; -} - -auto render_map_payload(mln::MapObserver::RenderMode mode) - -> mln_runtime_event_payload { - auto payload = mln::core::zeroed_event_payload(); - payload.render_map = - mln_runtime_event_render_map{.mode = to_c_render_mode(mode)}; - return payload; -} - -auto to_c_tile_operation(mln::TileOperation operation) -> uint32_t { - switch (operation) { - case mln::TileOperation::RequestedFromCache: - return MLN_TILE_OPERATION_REQUESTED_FROM_CACHE; - case mln::TileOperation::RequestedFromNetwork: - return MLN_TILE_OPERATION_REQUESTED_FROM_NETWORK; - case mln::TileOperation::LoadFromNetwork: - return MLN_TILE_OPERATION_LOAD_FROM_NETWORK; - case mln::TileOperation::LoadFromCache: - return MLN_TILE_OPERATION_LOAD_FROM_CACHE; - case mln::TileOperation::StartParse: - return MLN_TILE_OPERATION_START_PARSE; - case mln::TileOperation::EndParse: - return MLN_TILE_OPERATION_END_PARSE; - case mln::TileOperation::Error: - return MLN_TILE_OPERATION_ERROR; - case mln::TileOperation::Cancelled: - return MLN_TILE_OPERATION_CANCELLED; - case mln::TileOperation::NullOp: - return MLN_TILE_OPERATION_NULL; - } - return MLN_TILE_OPERATION_NULL; -} - -auto to_c_tile_id(const mln::OverscaledTileID& tile_id) -> mln_tile_id { - return mln_tile_id{ - .overscaled_z = tile_id.overscaledZ, - .wrap = tile_id.wrap, - .canonical_z = tile_id.canonical.z, - .canonical_x = tile_id.canonical.x, - .canonical_y = tile_id.canonical.y - }; -} - -auto tile_action_payload( - mln::TileOperation operation, const mln::OverscaledTileID& tile_id -) -> mln_runtime_event_payload { - auto payload = mln::core::zeroed_event_payload(); - payload.tile_action = mln_runtime_event_tile_action{ - .operation = to_c_tile_operation(operation), - .tile_id = to_c_tile_id(tile_id) - }; - return payload; -} - -// Every callback tests the map's subscription mask before it builds anything, -// so an unselected event allocates no payload, message, or queue node. -class HeadlessObserver final : public mln::MapObserver { - public: - HeadlessObserver( - mln_runtime runtime, mln_map map, - std::shared_ptr event_state, - std::shared_ptr callback_sources - ) - : runtime_(runtime), - map_(map), - event_state_(std::move(event_state)), - callback_sources_(std::move(callback_sources)) {} - - void onCameraWillChange(CameraChangeMode mode) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE)) { - return; - } - push( - MLN_RUNTIME_EVENT_MAP_CAMERA_WILL_CHANGE, to_c_camera_change_mode(mode) - ); - } - - void onCameraIsChanging() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_IS_CHANGING)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_CAMERA_IS_CHANGING); - } - - void onCameraDidChange(CameraChangeMode mode) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_CAMERA_DID_CHANGE)) { - return; - } - push( - MLN_RUNTIME_EVENT_MAP_CAMERA_DID_CHANGE, to_c_camera_change_mode(mode) - ); - } - - void onWillStartLoadingMap() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_STARTED)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_LOADING_STARTED); - } - - void onDidFinishLoadingMap() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_FINISHED)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_LOADING_FINISHED); - } - - // The failure text is map state that both style setters read, so it is - // recorded whatever the mask selects. - void onDidFailLoadingMap( - mln::MapLoadError error, const std::string& message - ) override { - event_state_->style_load_failure = message; - event_state_->style_load_failed = true; - if (!selected(MLN_RUNTIME_EVENT_MAP_LOADING_FAILED)) { - return; - } - push( - MLN_RUNTIME_EVENT_MAP_LOADING_FAILED, static_cast(error), - message.c_str() - ); - } - - // A style load can drop callback sources that the previous style held. - // The release callbacks it owes are map state rather than an event, so the - // reconciliation runs whatever the mask selects. - void onDidFinishLoadingStyle() override { - // The event is queued before the reconciliation, and the registry is held - // by a local share across it, because reconcile() runs host release - // callbacks. Nothing may touch this observer or its members after host code - // runs: a callback that destroys its map destroys this observer with it. - if (selected(MLN_RUNTIME_EVENT_MAP_STYLE_LOADED)) { - push(MLN_RUNTIME_EVENT_MAP_STYLE_LOADED); - } - const auto sources = callback_sources_; - sources->reconcile(); - } - - void onWillStartRenderingFrame() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_STARTED)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_STARTED); - } - - void onDidFinishRenderingFrame(const RenderFrameStatus& status) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED)) { - return; - } - push_payload( - MLN_RUNTIME_EVENT_MAP_RENDER_FRAME_FINISHED, - MLN_RUNTIME_EVENT_PAYLOAD_RENDER_FRAME, render_frame_payload(status) - ); - } - - void onWillStartRenderingMap() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_STARTED)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_STARTED); - } - - void onDidFinishRenderingMap(RenderMode mode) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_MAP_FINISHED)) { - return; - } - push_payload( - MLN_RUNTIME_EVENT_MAP_RENDER_MAP_FINISHED, - MLN_RUNTIME_EVENT_PAYLOAD_RENDER_MAP, render_map_payload(mode) - ); - } - - void onDidBecomeIdle() override { - if (!selected(MLN_RUNTIME_EVENT_MAP_IDLE)) { - return; - } - push(MLN_RUNTIME_EVENT_MAP_IDLE); - } - - void onStyleImageMissing(const std::string& image_id) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_STYLE_IMAGE_MISSING)) { - return; - } - push_payload( - MLN_RUNTIME_EVENT_MAP_STYLE_IMAGE_MISSING, MLN_RUNTIME_EVENT_PAYLOAD_NONE, - mln::core::zeroed_event_payload(), 0, image_id - ); - } - - void onTileAction( - mln::TileOperation operation, const mln::OverscaledTileID& tile_id, - const std::string& source_id - ) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_TILE_ACTION)) { - return; - } - push_payload( - MLN_RUNTIME_EVENT_MAP_TILE_ACTION, MLN_RUNTIME_EVENT_PAYLOAD_TILE_ACTION, - tile_action_payload(operation, tile_id), 0, source_id - ); - } - - // The mask test precedes the try block, so a suppressed render error never - // formats the exception text. - void onRenderError(std::exception_ptr error) override { - if (!selected(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR)) { - return; - } - try { - if (error) { - std::rethrow_exception(error); - } - push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR); - } catch (const std::exception& exception) { - push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR, 0, exception.what()); - } catch (...) { - push(MLN_RUNTIME_EVENT_MAP_RENDER_ERROR, 0, "unknown render error"); - } - } - - private: - [[nodiscard]] auto selected(uint32_t type) const noexcept -> bool { - return mln::core::event_selected(event_state_->mask, type); - } - - auto push(uint32_t type, int32_t code = 0, const char* message = nullptr) - -> void { - mln::core::push_runtime_map_event(runtime_, map_, type, code, message); - } - - auto push_payload( - uint32_t type, uint32_t payload_type, - const mln_runtime_event_payload& payload, int32_t code = 0, - std::string message = {} - ) -> void { - mln::core::push_runtime_map_event_payload( - runtime_, map_, type, payload_type, payload, code, std::move(message) - ); - } - - mln_runtime runtime_; - mln_map map_; - std::shared_ptr event_state_; - std::shared_ptr callback_sources_; -}; - -// Delivers mln::RendererObserver callbacks on the map's run loop instead of on -// whichever thread rendered. The delegate is mln::Map::Impl, whose handlers -// touch map-thread state, so every callback becomes a mailbox message that runs -// during the host's next mln_runtime_pump(). Forwarding is unconditional, so -// delivery order is the same whether or not the session shares the map's owner -// thread. -class ForwardingRendererObserver final : public mln::RendererObserver { - public: - ForwardingRendererObserver( - mln::Scheduler& map_scheduler, mln::RendererObserver& delegate - ) - : mailbox_(std::make_shared(map_scheduler)), - delegate_(delegate, mailbox_) {} - - ForwardingRendererObserver(const ForwardingRendererObserver&) = delete; - auto operator=(const ForwardingRendererObserver&) - -> ForwardingRendererObserver& = delete; - ForwardingRendererObserver(ForwardingRendererObserver&&) = delete; - auto operator=(ForwardingRendererObserver&&) - -> ForwardingRendererObserver& = delete; - - ~ForwardingRendererObserver() override { mailbox_->close(); } - - // Waits out an in-flight receive and drops anything queued, so the delegate - // can be torn down once this returns. Idempotent. - auto close() -> void { mailbox_->close(); } - - void onInvalidate() override { - delegate_.invoke(&mln::RendererObserver::onInvalidate); - } - - void onResourceError(std::exception_ptr error) override { - delegate_.invoke(&mln::RendererObserver::onResourceError, error); - } - - void onWillStartRenderingMap() override { - delegate_.invoke(&mln::RendererObserver::onWillStartRenderingMap); - } - - void onWillStartRenderingFrame() override { - delegate_.invoke(&mln::RendererObserver::onWillStartRenderingFrame); - } - - void onDidFinishRenderingFrame( - RenderMode mode, bool repaint_needed, bool placement_changed, - const mln::gfx::RenderingStats& stats - ) override { - // The name carries three overloads; mln::Map::Impl implements only this - // one. - void (mln::RendererObserver::*method)( - RenderMode, bool, bool, const mln::gfx::RenderingStats& - ) = &mln::RendererObserver::onDidFinishRenderingFrame; - delegate_.invoke(method, mode, repaint_needed, placement_changed, stats); - } - - void onDidFinishRenderingMap() override { - delegate_.invoke(&mln::RendererObserver::onDidFinishRenderingMap); - } - - void onStyleImageMissing( - const std::string& id, const StyleImageMissingCallback& done - ) override { - delegate_.invoke(&mln::RendererObserver::onStyleImageMissing, id, done); - } - - void onRemoveUnusedStyleImages(const std::vector& ids) override { - delegate_.invoke(&mln::RendererObserver::onRemoveUnusedStyleImages, ids); - } - - void onPreCompileShader( - mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, - const std::string& defines - ) override { - delegate_.invoke( - &mln::RendererObserver::onPreCompileShader, id, type, defines - ); - } - - void onPostCompileShader( - mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, - const std::string& defines - ) override { - delegate_.invoke( - &mln::RendererObserver::onPostCompileShader, id, type, defines - ); - } - - void onShaderCompileFailed( - mln::shaders::BuiltIn id, mln::gfx::Backend::Type type, - const std::string& defines - ) override { - delegate_.invoke( - &mln::RendererObserver::onShaderCompileFailed, id, type, defines - ); - } - - void onGlyphsLoaded( - const mln::FontStack& stack, const mln::GlyphRange& range - ) override { - delegate_.invoke(&mln::RendererObserver::onGlyphsLoaded, stack, range); - } - - void onGlyphsError( - const mln::FontStack& stack, const mln::GlyphRange& range, - std::exception_ptr error - ) override { - delegate_.invoke( - &mln::RendererObserver::onGlyphsError, stack, range, error - ); - } - - void onGlyphsRequested( - const mln::FontStack& stack, const mln::GlyphRange& range - ) override { - delegate_.invoke(&mln::RendererObserver::onGlyphsRequested, stack, range); - } - - void onTileAction( - mln::TileOperation operation, const mln::OverscaledTileID& id, - const std::string& source_id - ) override { - delegate_.invoke( - &mln::RendererObserver::onTileAction, operation, id, source_id - ); - } - - void onRenderError(std::exception_ptr error) override { - delegate_.invoke(&mln::RendererObserver::onRenderError, error); - } - - private: - std::shared_ptr mailbox_; - mln::ActorRef delegate_; -}; - -// Map mutations a render session reaches for from its own owner thread. The -// mailbox on the map's run loop keeps mln::Map single-threaded, and closing it -// during map teardown turns late messages into no-ops. -class MapCommands { - public: - explicit MapCommands(mln::Map& map) : map_(map) {} - - auto set_size(uint32_t width, uint32_t height) -> void { - map_.setSize(mln::Size{width, height}); - } - - auto trigger_repaint() -> void { map_.triggerRepaint(); } - - private: - mln::Map& map_; -}; - -class HeadlessFrontend final : public mln::RendererFrontend { - public: - // The thread pool tag must be a default-constructed identity, unique per map. - // SimpleIdentity::Empty pools every map's work into one bucket that - // waitForEmpty() cannot wait on, because it remaps the empty tag to the - // pool's own identity. The run loop comes in by reference because mbgl calls - // setObserver() from the map constructor; it outlives the map. - HeadlessFrontend( - mln_runtime runtime, mln_map map, mln::util::RunLoop& run_loop, - std::shared_ptr event_state - ) - : runtime_(runtime), - map_(map), - run_loop_(run_loop), - event_state_(std::move(event_state)), - thread_pool_( - mln::Scheduler::GetBackground(), mln::util::SimpleIdentity{} - ) {} - - void reset() override { - const std::scoped_lock lock(latest_update_mutex_); - latest_update_.reset(); - } - - // mln::Map calls this once, from its constructor, on the map owner thread. - void setObserver(mln::RendererObserver& observer) override { - observer_ = - std::make_unique(run_loop_, observer); - } - - // The latest update is render state that a session reads whatever the mask - // selects, so it is stored first. Pushing outside the update lock keeps - // `latest_update_mutex_` off the handle table and `event_mutex`; only the map - // owner thread reaches this, so nothing can interleave in between. - void update(std::shared_ptr update) override { - { - const std::scoped_lock lock(latest_update_mutex_); - latest_update_ = std::move(update); - } - if (!mln::core::event_selected( - event_state_->mask, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE - )) { - return; - } - mln::core::push_runtime_map_event( - runtime_, map_, MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE - ); - } - - [[nodiscard]] auto latest_update() const - -> std::shared_ptr { - const std::scoped_lock lock(latest_update_mutex_); - return latest_update_; - } - - auto run_render_jobs() -> void { thread_pool_.runRenderJobs(); } - - // Every map must call this: only waitForEmpty() erases the map's bucket in - // the process-global scheduler, which the worker loop otherwise keeps - // walking. - auto shutdown_thread_pool() -> void { - thread_pool_.runRenderJobs(/*closeQueue=*/true); - thread_pool_.waitForEmpty(); - } - - [[nodiscard]] auto renderer_observer() const -> mln::RendererObserver* { - return observer_.get(); - } - - // Must run before the map that backs the delegate is torn down. - auto close_renderer_observer() -> void { - if (observer_ != nullptr) { - observer_->close(); - } - } - - [[nodiscard]] auto getThreadPool() const - -> const mln::TaggedScheduler& override { - return thread_pool_; - } - - private: - mln_runtime runtime_; - mln_map map_; - mln::util::RunLoop& run_loop_; - std::shared_ptr event_state_; - std::unique_ptr observer_; - mln::TaggedScheduler thread_pool_; - mutable std::mutex latest_update_mutex_; - std::shared_ptr latest_update_; -}; - -auto validate_map_options(const mln_map_options* options) -> mln_status { - if (options == nullptr) { - return MLN_STATUS_OK; - } - - if (options->size < sizeof(mln_map_options)) { - mln::core::set_thread_error("mln_map_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - // Validated here as well as in the setter, so a mask this library cannot - // honour is rejected wherever it arrives. - constexpr auto known_mask_bits = - static_cast(MLN_RUNTIME_EVENT_MASK_ALL); - if ((options->event_mask & ~known_mask_bits) != 0U) { - mln::core::set_thread_error( - "mln_map_options.event_mask contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if ( - options->width == 0 || options->height == 0 || - !std::isfinite(options->scale_factor) || options->scale_factor <= 0 - ) { - mln::core::set_thread_error( - "map dimensions and scale_factor must be positive" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - - switch (options->map_mode) { - case MLN_MAP_MODE_CONTINUOUS: - case MLN_MAP_MODE_STATIC: - case MLN_MAP_MODE_TILE: - break; - default: - mln::core::set_thread_error("map_mode is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - return MLN_STATUS_OK; -} - -auto to_native_map_mode(uint32_t mode) -> mln::MapMode { - switch (mode) { - case MLN_MAP_MODE_STATIC: - return mln::MapMode::Static; - case MLN_MAP_MODE_TILE: - return mln::MapMode::Tile; - case MLN_MAP_MODE_CONTINUOUS: - return mln::MapMode::Continuous; - default: - assert(false); - return mln::MapMode::Continuous; - } -} - -auto is_still_map_mode(uint32_t mode) -> bool { - return mode == MLN_MAP_MODE_STATIC || mode == MLN_MAP_MODE_TILE; -} - -auto exception_message(std::exception_ptr error) -> std::string { - if (!error) { - return {}; - } - try { - std::rethrow_exception(error); - } catch (const std::exception& exception) { - return exception.what(); - } catch (...) { - return "unknown still-image request error"; - } -} - -auto validate_lat_lng(mln_lat_lng coordinate) -> mln_status; -auto validate_edge_insets(mln_edge_insets padding) -> mln_status; -auto validate_screen_point(mln_screen_point point) -> mln_status; - -auto validate_camera_options(const mln_camera_options* camera) -> mln_status { - if (camera == nullptr) { - mln::core::set_thread_error("camera must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if (camera->size < sizeof(mln_camera_options)) { - mln::core::set_thread_error("mln_camera_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_CAMERA_OPTION_CENTER) | MLN_CAMERA_OPTION_ZOOM | - MLN_CAMERA_OPTION_BEARING | MLN_CAMERA_OPTION_PITCH | - MLN_CAMERA_OPTION_CENTER_ALTITUDE | MLN_CAMERA_OPTION_PADDING | - MLN_CAMERA_OPTION_ANCHOR | MLN_CAMERA_OPTION_ROLL | MLN_CAMERA_OPTION_FOV; - if ((camera->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_camera_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if ((camera->fields & MLN_CAMERA_OPTION_CENTER) != 0U) { - const auto status = validate_lat_lng( - mln_lat_lng{.latitude = camera->latitude, .longitude = camera->longitude} - ); - if (status != MLN_STATUS_OK) { - return status; - } - } - if ( - ((camera->fields & MLN_CAMERA_OPTION_CENTER_ALTITUDE) != 0U && - !std::isfinite(camera->center_altitude)) || - ((camera->fields & MLN_CAMERA_OPTION_ZOOM) != 0U && - !std::isfinite(camera->zoom)) || - ((camera->fields & MLN_CAMERA_OPTION_BEARING) != 0U && - !std::isfinite(camera->bearing)) || - ((camera->fields & MLN_CAMERA_OPTION_PITCH) != 0U && - !std::isfinite(camera->pitch)) || - ((camera->fields & MLN_CAMERA_OPTION_ROLL) != 0U && - !std::isfinite(camera->roll)) || - ((camera->fields & MLN_CAMERA_OPTION_FOV) != 0U && - !std::isfinite(camera->field_of_view)) - ) { - mln::core::set_thread_error("enabled camera numeric fields must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((camera->fields & MLN_CAMERA_OPTION_PADDING) != 0U) { - const auto status = validate_edge_insets(camera->padding); - if (status != MLN_STATUS_OK) { - return status; - } - } - if ((camera->fields & MLN_CAMERA_OPTION_ANCHOR) != 0U) { - const auto status = validate_screen_point(camera->anchor); - if (status != MLN_STATUS_OK) { - return status; - } - } - - return MLN_STATUS_OK; -} - -using DoubleMilliseconds = std::chrono::duration; - -auto max_native_duration_ms() -> double { - return std::chrono::duration_cast(mln::Duration::max()) - .count(); -} - -auto duration_from_milliseconds(double milliseconds) -> mln::Duration { - return std::chrono::duration_cast( - DoubleMilliseconds{milliseconds} - ); -} - -auto milliseconds_from_duration(mln::Duration duration) -> double { - return std::chrono::duration_cast(duration).count(); -} - -// The accepted bound is exclusive because mln::Duration::max() has no exact -// double representation: the nearest double converts back to 2^63 ticks, one -// past the largest representable count. The margin holds for a nanosecond -// duration only, so pin the representation. -static_assert( - std::is_same_v, - "the accepted duration bound is derived from a nanosecond mln::Duration" -); - -auto is_native_duration_ms(double milliseconds) -> bool { - return std::isfinite(milliseconds) && milliseconds >= 0.0 && - milliseconds < max_native_duration_ms(); -} - -auto validate_animation_options(const mln_animation_options* animation) - -> mln_status { - if (animation == nullptr) { - return MLN_STATUS_OK; - } - if (animation->size < sizeof(mln_animation_options)) { - mln::core::set_thread_error("mln_animation_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_ANIMATION_OPTION_DURATION) | - MLN_ANIMATION_OPTION_VELOCITY | MLN_ANIMATION_OPTION_MIN_ZOOM | - MLN_ANIMATION_OPTION_EASING | MLN_ANIMATION_OPTION_TRANSITION_ID; - if ((animation->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_animation_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (animation->fields & MLN_ANIMATION_OPTION_DURATION) != 0U && - !is_native_duration_ms(animation->duration_ms) - ) { - mln::core::set_thread_error( - "animation duration_ms must fit the native duration range" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (animation->fields & MLN_ANIMATION_OPTION_VELOCITY) != 0U && - (!std::isfinite(animation->velocity) || animation->velocity <= 0.0) - ) { - mln::core::set_thread_error( - "animation velocity must be positive and finite" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (animation->fields & MLN_ANIMATION_OPTION_MIN_ZOOM) != 0U && - !std::isfinite(animation->min_zoom) - ) { - mln::core::set_thread_error("animation min_zoom must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((animation->fields & MLN_ANIMATION_OPTION_EASING) != 0U) { - const auto easing = animation->easing; - if ( - !std::isfinite(easing.x1) || !std::isfinite(easing.y1) || - !std::isfinite(easing.x2) || !std::isfinite(easing.y2) || - easing.x1 < 0.0 || easing.x1 > 1.0 || easing.x2 < 0.0 || easing.x2 > 1.0 - ) { - mln::core::set_thread_error( - "animation easing x values must be within [0, 1] and all easing values " - "must be finite" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - return MLN_STATUS_OK; -} - -auto validate_camera_fit_options(const mln_camera_fit_options* options) - -> mln_status { - if (options == nullptr) { - return MLN_STATUS_OK; - } - if (options->size < sizeof(mln_camera_fit_options)) { - mln::core::set_thread_error("mln_camera_fit_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_CAMERA_FIT_OPTION_PADDING) | - MLN_CAMERA_FIT_OPTION_BEARING | MLN_CAMERA_FIT_OPTION_PITCH; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_camera_fit_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((options->fields & MLN_CAMERA_FIT_OPTION_PADDING) != 0U) { - const auto status = validate_edge_insets(options->padding); - if (status != MLN_STATUS_OK) { - return status; - } - } - if ( - ((options->fields & MLN_CAMERA_FIT_OPTION_BEARING) != 0U && - !std::isfinite(options->bearing)) || - ((options->fields & MLN_CAMERA_FIT_OPTION_PITCH) != 0U && - !std::isfinite(options->pitch)) - ) { - mln::core::set_thread_error("camera fit bearing and pitch must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_bound_options(const mln_bound_options* options) -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("bound options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_bound_options)) { - mln::core::set_thread_error("mln_bound_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_BOUND_OPTION_BOUNDS) | MLN_BOUND_OPTION_MIN_ZOOM | - MLN_BOUND_OPTION_MAX_ZOOM | MLN_BOUND_OPTION_MIN_PITCH | - MLN_BOUND_OPTION_MAX_PITCH | MLN_BOUND_OPTION_UNBOUNDED; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_bound_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (options->fields & MLN_BOUND_OPTION_BOUNDS) != 0U && - (options->fields & MLN_BOUND_OPTION_UNBOUNDED) != 0U - ) { - mln::core::set_thread_error( - "MLN_BOUND_OPTION_BOUNDS and MLN_BOUND_OPTION_UNBOUNDED are mutually " - "exclusive" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((options->fields & MLN_BOUND_OPTION_BOUNDS) != 0U) { - const auto status = validate_lat_lng_bounds(options->bounds); - if (status != MLN_STATUS_OK) { - return status; - } - } - if ( - ((options->fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U && - !std::isfinite(options->min_zoom)) || - ((options->fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U && - !std::isfinite(options->max_zoom)) || - ((options->fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U && - !std::isfinite(options->min_pitch)) || - ((options->fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U && - !std::isfinite(options->max_pitch)) - ) { - mln::core::set_thread_error("bound numeric fields must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (options->fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U && - (options->fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U && - options->min_zoom > options->max_zoom - ) { - mln::core::set_thread_error( - "min_zoom must be less than or equal to max_zoom" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (options->fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U && - (options->fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U && - options->min_pitch > options->max_pitch - ) { - mln::core::set_thread_error( - "min_pitch must be less than or equal to max_pitch" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_vec3(mln_vec3 value, const char* name) -> mln_status { - if ( - !std::isfinite(value.x) || !std::isfinite(value.y) || - !std::isfinite(value.z) - ) { - mln::core::set_thread_error(name); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_quaternion(mln_quaternion value) -> mln_status { - if ( - !std::isfinite(value.x) || !std::isfinite(value.y) || - !std::isfinite(value.z) || !std::isfinite(value.w) - ) { - mln::core::set_thread_error( - "free camera orientation values must be finite" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (value.x == 0.0 && value.y == 0.0 && value.z == 0.0 && value.w == 0.0) { - mln::core::set_thread_error( - "free camera orientation must not be zero length" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_free_camera_options(const mln_free_camera_options* options) - -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("free camera options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_free_camera_options)) { - mln::core::set_thread_error("mln_free_camera_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_FREE_CAMERA_OPTION_POSITION) | - MLN_FREE_CAMERA_OPTION_ORIENTATION; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_free_camera_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((options->fields & MLN_FREE_CAMERA_OPTION_POSITION) != 0U) { - const auto status = validate_vec3( - options->position, "free camera position values must be finite" - ); - if (status != MLN_STATUS_OK) { - return status; - } - } - if ((options->fields & MLN_FREE_CAMERA_OPTION_ORIENTATION) != 0U) { - const auto status = validate_quaternion(options->orientation); - if (status != MLN_STATUS_OK) { - return status; - } - } - return MLN_STATUS_OK; -} - -auto validate_projection_mode_options(const mln_projection_mode* mode) - -> mln_status { - if (mode == nullptr) { - mln::core::set_thread_error("projection mode must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if (mode->size < sizeof(mln_projection_mode)) { - mln::core::set_thread_error("mln_projection_mode.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_PROJECTION_MODE_AXONOMETRIC) | - MLN_PROJECTION_MODE_X_SKEW | MLN_PROJECTION_MODE_Y_SKEW; - if ((mode->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_projection_mode.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if ( - ((mode->fields & MLN_PROJECTION_MODE_X_SKEW) != 0U && - !std::isfinite(mode->x_skew)) || - ((mode->fields & MLN_PROJECTION_MODE_Y_SKEW) != 0U && - !std::isfinite(mode->y_skew)) - ) { - mln::core::set_thread_error("projection skew values must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - return MLN_STATUS_OK; -} - -auto validate_debug_options(uint32_t options) -> mln_status { - constexpr auto known_options = - static_cast(MLN_MAP_DEBUG_TILE_BORDERS) | - MLN_MAP_DEBUG_PARSE_STATUS | MLN_MAP_DEBUG_TIMESTAMPS | - MLN_MAP_DEBUG_COLLISION | MLN_MAP_DEBUG_OVERDRAW | - MLN_MAP_DEBUG_STENCIL_CLIP | MLN_MAP_DEBUG_DEPTH_BUFFER; - if ((options & ~known_options) != 0U) { - mln::core::set_thread_error("debug options contain unknown bits"); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_frustum_offset(mln_edge_insets offset) -> mln_status { - if ( - !std::isfinite(offset.top) || !std::isfinite(offset.left) || - !std::isfinite(offset.bottom) || !std::isfinite(offset.right) - ) { - mln::core::set_thread_error("frustum offset values must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - offset.top < 0.0 || offset.left < 0.0 || offset.bottom < 0.0 || - offset.right < 0.0 - ) { - mln::core::set_thread_error( - "frustum offset values must be greater than or equal to 0" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_viewport_options(const mln_map_viewport_options* options) - -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("viewport options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_map_viewport_options)) { - mln::core::set_thread_error("mln_map_viewport_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) | - MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE | - MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE | - MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_map_viewport_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((options->fields & MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) != 0U) { - switch (options->north_orientation) { - case MLN_NORTH_ORIENTATION_UP: - case MLN_NORTH_ORIENTATION_RIGHT: - case MLN_NORTH_ORIENTATION_DOWN: - case MLN_NORTH_ORIENTATION_LEFT: - break; - default: - mln::core::set_thread_error("north_orientation is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - if ((options->fields & MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE) != 0U) { - switch (options->constrain_mode) { - case MLN_CONSTRAIN_MODE_NONE: - case MLN_CONSTRAIN_MODE_HEIGHT_ONLY: - case MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT: - case MLN_CONSTRAIN_MODE_SCREEN: - break; - default: - mln::core::set_thread_error("constrain_mode is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - if ((options->fields & MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE) != 0U) { - switch (options->viewport_mode) { - case MLN_VIEWPORT_MODE_DEFAULT: - case MLN_VIEWPORT_MODE_FLIPPED_Y: - break; - default: - mln::core::set_thread_error("viewport_mode is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - if ((options->fields & MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET) != 0U) { - return validate_frustum_offset(options->frustum_offset); - } - return MLN_STATUS_OK; -} - -auto validate_tile_options(const mln_map_tile_options* options) -> mln_status { - if (options == nullptr) { - mln::core::set_thread_error("tile options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (options->size < sizeof(mln_map_tile_options)) { - mln::core::set_thread_error("mln_map_tile_options.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - constexpr auto known_fields = - static_cast(MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) | - MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS | MLN_MAP_TILE_OPTION_LOD_SCALE | - MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD | - MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT | MLN_MAP_TILE_OPTION_LOD_MODE; - if ((options->fields & ~known_fields) != 0U) { - mln::core::set_thread_error( - "mln_map_tile_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - (options->fields & MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) != 0U && - options->prefetch_zoom_delta > std::numeric_limits::max() - ) { - mln::core::set_thread_error("prefetch_zoom_delta must be at most 255"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - ((options->fields & MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS) != 0U && - !std::isfinite(options->lod_min_radius)) || - ((options->fields & MLN_MAP_TILE_OPTION_LOD_SCALE) != 0U && - !std::isfinite(options->lod_scale)) || - ((options->fields & MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD) != 0U && - !std::isfinite(options->lod_pitch_threshold)) || - ((options->fields & MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT) != 0U && - !std::isfinite(options->lod_zoom_shift)) - ) { - mln::core::set_thread_error("tile LOD values must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ((options->fields & MLN_MAP_TILE_OPTION_LOD_MODE) != 0U) { - switch (options->lod_mode) { - case MLN_TILE_LOD_MODE_DEFAULT: - case MLN_TILE_LOD_MODE_DISTANCE: - break; - default: - mln::core::set_thread_error("lod_mode is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - return MLN_STATUS_OK; -} - -auto validate_lat_lng(mln_lat_lng coordinate) -> mln_status { - if ( - !std::isfinite(coordinate.latitude) || coordinate.latitude < -90.0 || - coordinate.latitude > 90.0 || !std::isfinite(coordinate.longitude) - ) { - mln::core::set_thread_error( - "latitude must be finite and within [-90, 90], and longitude must be " - "finite" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln_status { - const auto southwest_status = validate_lat_lng(bounds.southwest); - if (southwest_status != MLN_STATUS_OK) { - return southwest_status; - } - const auto northeast_status = validate_lat_lng(bounds.northeast); - if (northeast_status != MLN_STATUS_OK) { - return northeast_status; - } - if ( - bounds.southwest.latitude > bounds.northeast.latitude || - bounds.southwest.longitude > bounds.northeast.longitude - ) { - mln::core::set_thread_error( - "bounds southwest must be less than or equal to northeast" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_lat_lng_array( - const mln_lat_lng* coordinates, size_t coordinate_count, bool allow_empty -) -> mln_status { - if (coordinate_count == 0) { - if (allow_empty) { - return MLN_STATUS_OK; - } - mln::core::set_thread_error("coordinate_count must be greater than 0"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - if (coordinates == nullptr) { - mln::core::set_thread_error("coordinates must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - const auto coordinate_span = - std::span{coordinates, coordinate_count}; - for (const auto coordinate : coordinate_span) { - const auto status = validate_lat_lng(coordinate); - if (status != MLN_STATUS_OK) { - return status; - } - } - return MLN_STATUS_OK; -} - -auto validate_screen_point(mln_screen_point point) -> mln_status { - if (!std::isfinite(point.x) || !std::isfinite(point.y)) { - mln::core::set_thread_error("screen point values must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_screen_point_array( - const mln_screen_point* points, size_t point_count -) -> mln_status { - if (point_count == 0) { - return MLN_STATUS_OK; - } - - if (points == nullptr) { - mln::core::set_thread_error("points must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - const auto point_span = - std::span{points, point_count}; - for (const auto point : point_span) { - const auto status = validate_screen_point(point); - if (status != MLN_STATUS_OK) { - return status; - } - } - return MLN_STATUS_OK; -} - -auto validate_edge_insets(mln_edge_insets padding) -> mln_status { - if ( - !std::isfinite(padding.top) || !std::isfinite(padding.left) || - !std::isfinite(padding.bottom) || !std::isfinite(padding.right) || - padding.top < 0.0 || padding.left < 0.0 || padding.bottom < 0.0 || - padding.right < 0.0 - ) { - mln::core::set_thread_error( - "padding values must be finite and greater than or equal to 0" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto validate_projected_meters(mln_projected_meters meters) -> mln_status { - if (!std::isfinite(meters.northing) || !std::isfinite(meters.easting)) { - mln::core::set_thread_error("projected meter values must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} - -auto to_native_screen_point(mln_screen_point point) -> mln::ScreenCoordinate; -auto from_native_screen_point(const mln::ScreenCoordinate& point) - -> mln_screen_point; -auto to_native_edge_insets(mln_edge_insets padding) -> mln::EdgeInsets; -auto from_native_edge_insets(const mln::EdgeInsets& insets) -> mln_edge_insets; - -auto to_native_camera(const mln_camera_options& camera) -> mln::CameraOptions { - auto result = mln::CameraOptions{}; - if ((camera.fields & MLN_CAMERA_OPTION_CENTER) != 0U) { - result.withCenter(mln::LatLng{camera.latitude, camera.longitude}); - } - if ((camera.fields & MLN_CAMERA_OPTION_CENTER_ALTITUDE) != 0U) { - result.withCenterAltitude(camera.center_altitude); - } - if ((camera.fields & MLN_CAMERA_OPTION_PADDING) != 0U) { - result.withPadding(to_native_edge_insets(camera.padding)); - } - if ((camera.fields & MLN_CAMERA_OPTION_ANCHOR) != 0U) { - result.withAnchor(to_native_screen_point(camera.anchor)); - } - if ((camera.fields & MLN_CAMERA_OPTION_ZOOM) != 0U) { - result.withZoom(camera.zoom); - } - if ((camera.fields & MLN_CAMERA_OPTION_BEARING) != 0U) { - result.withBearing(camera.bearing); - } - if ((camera.fields & MLN_CAMERA_OPTION_PITCH) != 0U) { - result.withPitch(camera.pitch); - } - if ((camera.fields & MLN_CAMERA_OPTION_ROLL) != 0U) { - result.withRoll(camera.roll); - } - if ((camera.fields & MLN_CAMERA_OPTION_FOV) != 0U) { - result.withFov(camera.field_of_view); - } - return result; -} - -auto from_native_camera(const mln::CameraOptions& camera) - -> mln_camera_options { - auto result = mln::core::camera_options_default(); - if (camera.center) { - result.fields |= MLN_CAMERA_OPTION_CENTER; - result.latitude = camera.center->latitude(); - result.longitude = camera.center->longitude(); - } - if (camera.centerAltitude) { - result.fields |= MLN_CAMERA_OPTION_CENTER_ALTITUDE; - result.center_altitude = *camera.centerAltitude; - } - if (camera.padding) { - result.fields |= MLN_CAMERA_OPTION_PADDING; - result.padding = from_native_edge_insets(*camera.padding); - } - if (camera.anchor) { - result.fields |= MLN_CAMERA_OPTION_ANCHOR; - result.anchor = from_native_screen_point(*camera.anchor); - } - if (camera.zoom) { - result.fields |= MLN_CAMERA_OPTION_ZOOM; - result.zoom = *camera.zoom; - } - if (camera.bearing) { - result.fields |= MLN_CAMERA_OPTION_BEARING; - result.bearing = *camera.bearing; - } - if (camera.pitch) { - result.fields |= MLN_CAMERA_OPTION_PITCH; - result.pitch = *camera.pitch; - } - if (camera.roll) { - result.fields |= MLN_CAMERA_OPTION_ROLL; - result.roll = *camera.roll; - } - if (camera.fov) { - result.fields |= MLN_CAMERA_OPTION_FOV; - result.field_of_view = *camera.fov; - } - return result; -} - -auto camera_transition_finished_payload(uint64_t transition_id) - -> mln_runtime_event_payload { - auto payload = mln::core::zeroed_event_payload(); - payload.camera_transition_finished = - mln_runtime_event_camera_transition_finished{ - .transition_id = transition_id - }; - return payload; -} - -// MapLibre Native owns the returned AnimationOptions for the lifetime of the -// transition, and invokes transitionFinishFn on the map owner thread. The push -// discards events for a destroyed map, so a transition outliving the map -// enqueues nothing. The lambda holds the event state by value, so it reads a -// live mask cell at completion time even for a map that is already gone. -auto to_native_animation( - mln_runtime runtime, mln_map map, - const std::shared_ptr& event_state, - const mln_animation_options* animation -) -> mln::AnimationOptions { - auto result = mln::AnimationOptions{}; - if (animation == nullptr) { - return result; - } - if ((animation->fields & MLN_ANIMATION_OPTION_TRANSITION_ID) != 0U) { - result.transitionFinishFn = [runtime, map, event_state, - transition_id = animation->transition_id] { - if (!mln::core::event_selected( - event_state->mask, MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED - )) { - return; - } - mln::core::push_runtime_map_event_payload( - runtime, map, MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, - MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED, - camera_transition_finished_payload(transition_id) - ); - }; - } - if ((animation->fields & MLN_ANIMATION_OPTION_DURATION) != 0U) { - result.duration = duration_from_milliseconds(animation->duration_ms); - } - if ((animation->fields & MLN_ANIMATION_OPTION_VELOCITY) != 0U) { - result.velocity = animation->velocity; - } - if ((animation->fields & MLN_ANIMATION_OPTION_MIN_ZOOM) != 0U) { - result.minZoom = animation->min_zoom; - } - if ((animation->fields & MLN_ANIMATION_OPTION_EASING) != 0U) { - const auto easing = animation->easing; - result.easing.emplace(easing.x1, easing.y1, easing.x2, easing.y2); - } - return result; -} - -auto camera_fit_padding(const mln_camera_fit_options* options) - -> mln::EdgeInsets { - if ( - options == nullptr || - (options->fields & MLN_CAMERA_FIT_OPTION_PADDING) == 0U - ) { - return mln::EdgeInsets{}; - } - return to_native_edge_insets(options->padding); -} - -auto camera_fit_bearing(const mln_camera_fit_options* options) - -> std::optional { - if ( - options == nullptr || - (options->fields & MLN_CAMERA_FIT_OPTION_BEARING) == 0U - ) { - return std::nullopt; - } - return options->bearing; -} - -auto camera_fit_pitch(const mln_camera_fit_options* options) - -> std::optional { - if ( - options == nullptr || (options->fields & MLN_CAMERA_FIT_OPTION_PITCH) == 0U - ) { - return std::nullopt; - } - return options->pitch; -} - -auto to_native_projection_mode(const mln_projection_mode& mode) - -> mln::ProjectionMode { - auto result = mln::ProjectionMode{}; - if ((mode.fields & MLN_PROJECTION_MODE_AXONOMETRIC) != 0U) { - result.withAxonometric(mode.axonometric); - } - if ((mode.fields & MLN_PROJECTION_MODE_X_SKEW) != 0U) { - result.withXSkew(mode.x_skew); - } - if ((mode.fields & MLN_PROJECTION_MODE_Y_SKEW) != 0U) { - result.withYSkew(mode.y_skew); - } - return result; -} - -auto to_native_debug_options(uint32_t options) -> mln::MapDebugOptions { - return static_cast(options); -} - -auto from_native_debug_options(mln::MapDebugOptions options) -> uint32_t { - return static_cast(options); -} - -auto to_native_north_orientation(uint32_t orientation) - -> mln::NorthOrientation { - switch (orientation) { - case MLN_NORTH_ORIENTATION_RIGHT: - return mln::NorthOrientation::Rightwards; - case MLN_NORTH_ORIENTATION_DOWN: - return mln::NorthOrientation::Downwards; - case MLN_NORTH_ORIENTATION_LEFT: - return mln::NorthOrientation::Leftwards; - case MLN_NORTH_ORIENTATION_UP: - return mln::NorthOrientation::Upwards; - default: - assert(false); - return mln::NorthOrientation::Upwards; - } -} - -auto from_native_north_orientation(mln::NorthOrientation orientation) - -> uint32_t { - switch (orientation) { - case mln::NorthOrientation::Rightwards: - return MLN_NORTH_ORIENTATION_RIGHT; - case mln::NorthOrientation::Downwards: - return MLN_NORTH_ORIENTATION_DOWN; - case mln::NorthOrientation::Leftwards: - return MLN_NORTH_ORIENTATION_LEFT; - case mln::NorthOrientation::Upwards: - return MLN_NORTH_ORIENTATION_UP; - } - assert(false); - return MLN_NORTH_ORIENTATION_UP; -} - -auto to_native_constrain_mode(uint32_t mode) -> mln::ConstrainMode { - switch (mode) { - case MLN_CONSTRAIN_MODE_NONE: - return mln::ConstrainMode::None; - case MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT: - return mln::ConstrainMode::WidthAndHeight; - case MLN_CONSTRAIN_MODE_SCREEN: - return mln::ConstrainMode::Screen; - case MLN_CONSTRAIN_MODE_HEIGHT_ONLY: - return mln::ConstrainMode::HeightOnly; - default: - assert(false); - return mln::ConstrainMode::HeightOnly; - } -} - -auto from_native_constrain_mode(mln::ConstrainMode mode) -> uint32_t { - switch (mode) { - case mln::ConstrainMode::None: - return MLN_CONSTRAIN_MODE_NONE; - case mln::ConstrainMode::WidthAndHeight: - return MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT; - case mln::ConstrainMode::Screen: - return MLN_CONSTRAIN_MODE_SCREEN; - case mln::ConstrainMode::HeightOnly: - return MLN_CONSTRAIN_MODE_HEIGHT_ONLY; - } - assert(false); - return MLN_CONSTRAIN_MODE_HEIGHT_ONLY; -} - -auto to_native_viewport_mode(uint32_t mode) -> mln::ViewportMode { - switch (mode) { - case MLN_VIEWPORT_MODE_FLIPPED_Y: - return mln::ViewportMode::FlippedY; - case MLN_VIEWPORT_MODE_DEFAULT: - return mln::ViewportMode::Default; - default: - assert(false); - return mln::ViewportMode::Default; - } -} - -auto from_native_viewport_mode(mln::ViewportMode mode) -> uint32_t { - switch (mode) { - case mln::ViewportMode::FlippedY: - return MLN_VIEWPORT_MODE_FLIPPED_Y; - case mln::ViewportMode::Default: - return MLN_VIEWPORT_MODE_DEFAULT; - } - assert(false); - return MLN_VIEWPORT_MODE_DEFAULT; -} - -auto from_native_edge_insets(const mln::EdgeInsets& insets) -> mln_edge_insets { - return mln_edge_insets{ - .top = insets.top(), - .left = insets.left(), - .bottom = insets.bottom(), - .right = insets.right() - }; -} - -auto to_native_tile_lod_mode(uint32_t mode) -> mln::TileLodMode { - switch (mode) { - case MLN_TILE_LOD_MODE_DISTANCE: - return mln::TileLodMode::Distance; - case MLN_TILE_LOD_MODE_DEFAULT: - return mln::TileLodMode::Default; - default: - assert(false); - return mln::TileLodMode::Default; - } -} - -auto from_native_tile_lod_mode(mln::TileLodMode mode) -> uint32_t { - switch (mode) { - case mln::TileLodMode::Distance: - return MLN_TILE_LOD_MODE_DISTANCE; - case mln::TileLodMode::Default: - return MLN_TILE_LOD_MODE_DEFAULT; - } - assert(false); - return MLN_TILE_LOD_MODE_DEFAULT; -} - -auto from_native_projection_mode(const mln::ProjectionMode& mode) - -> mln_projection_mode { - auto result = mln::core::projection_mode_default(); - if (mode.axonometric) { - result.fields |= MLN_PROJECTION_MODE_AXONOMETRIC; - result.axonometric = *mode.axonometric; - } - if (mode.xSkew) { - result.fields |= MLN_PROJECTION_MODE_X_SKEW; - result.x_skew = *mode.xSkew; - } - if (mode.ySkew) { - result.fields |= MLN_PROJECTION_MODE_Y_SKEW; - result.y_skew = *mode.ySkew; - } - return result; -} - -auto to_native_lat_lng(mln_lat_lng coordinate) -> mln::LatLng { - return mln::LatLng{coordinate.latitude, coordinate.longitude}; -} - -auto from_native_lat_lng(const mln::LatLng& coordinate) -> mln_lat_lng { - return mln_lat_lng{ - .latitude = coordinate.latitude(), .longitude = coordinate.longitude() - }; -} - -auto to_native_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln::LatLngBounds { - return mln::LatLngBounds::hull( - to_native_lat_lng(bounds.southwest), to_native_lat_lng(bounds.northeast) - ); -} - -auto from_native_lat_lng_bounds(const mln::LatLngBounds& bounds) - -> mln_lat_lng_bounds { - return mln_lat_lng_bounds{ - .southwest = - mln_lat_lng{.latitude = bounds.south(), .longitude = bounds.west()}, - .northeast = - mln_lat_lng{.latitude = bounds.north(), .longitude = bounds.east()} - }; -} - -// mbgl keeps the unbounded flag private. Its operator== treats unbounded values -// as equal to each other and distinct from every bounded one, so comparing -// against a default-constructed value tests the flag exactly. -auto is_unbounded_lat_lng_bounds(const mln::LatLngBounds& bounds) -> bool { - return bounds == mln::LatLngBounds{}; -} - -auto to_native_lat_lngs(const mln_lat_lng* coordinates, size_t coordinate_count) - -> std::vector { - auto result = std::vector{}; - result.reserve(coordinate_count); - const auto coordinate_span = - std::span{coordinates, coordinate_count}; - for (const auto coordinate : coordinate_span) { - result.emplace_back(to_native_lat_lng(coordinate)); - } - return result; -} - -auto to_native_screen_point(mln_screen_point point) -> mln::ScreenCoordinate { - return mln::ScreenCoordinate{point.x, point.y}; -} - -auto from_native_screen_point(const mln::ScreenCoordinate& point) - -> mln_screen_point { - return mln_screen_point{.x = point.x, .y = point.y}; -} - -auto to_native_screen_points(const mln_screen_point* points, size_t point_count) - -> std::vector { - auto result = std::vector{}; - result.reserve(point_count); - const auto point_span = - std::span{points, point_count}; - for (const auto point : point_span) { - result.emplace_back(to_native_screen_point(point)); - } - return result; -} - -auto to_native_edge_insets(mln_edge_insets padding) -> mln::EdgeInsets { - return mln::EdgeInsets{ - padding.top, padding.left, padding.bottom, padding.right - }; -} - -auto to_native_bound_options(const mln_bound_options& options) - -> mln::BoundOptions { - auto result = mln::BoundOptions{}; - if ((options.fields & MLN_BOUND_OPTION_BOUNDS) != 0U) { - result.withLatLngBounds(to_native_lat_lng_bounds(options.bounds)); - } - if ((options.fields & MLN_BOUND_OPTION_UNBOUNDED) != 0U) { - // A default-constructed LatLngBounds is the mbgl unbounded constraint. - result.withLatLngBounds(mln::LatLngBounds{}); - } - if ((options.fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U) { - result.withMinZoom(options.min_zoom); - } - if ((options.fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U) { - result.withMaxZoom(options.max_zoom); - } - if ((options.fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U) { - result.withMinPitch(options.min_pitch); - } - if ((options.fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U) { - result.withMaxPitch(options.max_pitch); - } - return result; -} - -auto from_native_bound_options(const mln::BoundOptions& options) - -> mln_bound_options { - auto result = mln::core::bound_options_default(); - if (options.bounds) { - if (is_unbounded_lat_lng_bounds(*options.bounds)) { - result.fields |= MLN_BOUND_OPTION_UNBOUNDED; - } else { - result.fields |= MLN_BOUND_OPTION_BOUNDS; - result.bounds = from_native_lat_lng_bounds(*options.bounds); - } - } - if (options.minZoom) { - result.fields |= MLN_BOUND_OPTION_MIN_ZOOM; - result.min_zoom = *options.minZoom; - } - if (options.maxZoom) { - result.fields |= MLN_BOUND_OPTION_MAX_ZOOM; - result.max_zoom = *options.maxZoom; - } - if (options.minPitch) { - result.fields |= MLN_BOUND_OPTION_MIN_PITCH; - result.min_pitch = *options.minPitch; - } - if (options.maxPitch) { - result.fields |= MLN_BOUND_OPTION_MAX_PITCH; - result.max_pitch = *options.maxPitch; - } - return result; -} - -auto to_native_vec3(mln_vec3 value) -> mln::vec3 { - return mln::vec3{{value.x, value.y, value.z}}; -} - -auto from_native_vec3(const mln::vec3& value) -> mln_vec3 { - const auto [x_component, y_component, z_component] = value; - return mln_vec3{.x = x_component, .y = y_component, .z = z_component}; -} - -auto to_native_vec4(mln_quaternion value) -> mln::vec4 { - return mln::vec4{{value.x, value.y, value.z, value.w}}; -} - -auto from_native_vec4(const mln::vec4& value) -> mln_quaternion { - const auto [x_component, y_component, z_component, w_component] = value; - return mln_quaternion{ - .x = x_component, .y = y_component, .z = z_component, .w = w_component - }; -} - -auto to_native_free_camera(const mln_free_camera_options& options) - -> mln::FreeCameraOptions { - auto result = mln::FreeCameraOptions{}; - if ((options.fields & MLN_FREE_CAMERA_OPTION_POSITION) != 0U) { - result.position = to_native_vec3(options.position); - } - if ((options.fields & MLN_FREE_CAMERA_OPTION_ORIENTATION) != 0U) { - result.orientation = to_native_vec4(options.orientation); - } - return result; -} - -auto from_native_free_camera(const mln::FreeCameraOptions& options) - -> mln_free_camera_options { - auto result = mln::core::free_camera_options_default(); - if (options.position) { - result.fields |= MLN_FREE_CAMERA_OPTION_POSITION; - result.position = from_native_vec3(*options.position); - } - if (options.orientation) { - result.fields |= MLN_FREE_CAMERA_OPTION_ORIENTATION; - result.orientation = from_native_vec4(*options.orientation); - } - return result; -} - -auto screen_point(mln_screen_point point) -> mln::ScreenCoordinate { - return mln::ScreenCoordinate{point.x, point.y}; -} -} // namespace - -namespace mln::core { - -struct MapObject { - mln_runtime runtime = MLN_HANDLE_NULL; - std::thread::id owner_thread; - uint32_t map_mode = MLN_MAP_MODE_CONTINUOUS; - double scale_factor = default_scale_factor; - bool still_image_request_pending = false; - // Declared first so reverse-order destruction runs the release callbacks it - // still owes after the mln::Map that could still reach a source is gone. - std::shared_ptr callback_sources; - // Declared before `observer` so reverse-order destruction retires the - // observer, which holds its own reference, before this member is destroyed. - std::shared_ptr event_state; - std::unique_ptr observer; - std::unique_ptr frontend; - std::unique_ptr map; - // Declared after `map` so reverse-order destruction retires the command - // channel before the mln::Map it targets. - std::unique_ptr commands; - std::shared_ptr command_mailbox; - std::optional> command_ref; - // Guarded by the map handle table's mutex; a render session on another thread - // clears it from map_detach_render_target_session(). - void* render_target_session = nullptr; - FeatureStateStore feature_state; -}; - -template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::Map; - static constexpr auto leasable = false; -}; - -struct MapProjectionObject { - // Serializes projection calls with each other and with destruction, so a - // standalone projection may be used from any thread. - std::mutex call_mutex; - // Null after destruction retires the handle; guarded by call_mutex. - std::unique_ptr projection; -}; - -template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::MapProjection; - static constexpr auto leasable = true; -}; - -} // namespace mln::core - -namespace mln::core { - -namespace { - -class RuntimeMapRetainGuard final { - public: - explicit RuntimeMapRetainGuard(mln_runtime runtime) noexcept - : runtime_(runtime) {} - - ~RuntimeMapRetainGuard() { release_runtime_map(runtime_); } - - RuntimeMapRetainGuard(const RuntimeMapRetainGuard&) = delete; - RuntimeMapRetainGuard(RuntimeMapRetainGuard&&) = delete; - auto operator=(const RuntimeMapRetainGuard&) - -> RuntimeMapRetainGuard& = delete; - auto operator=(RuntimeMapRetainGuard&&) -> RuntimeMapRetainGuard& = delete; - - auto dismiss() noexcept -> void { runtime_ = MLN_HANDLE_NULL; } - - private: - mln_runtime runtime_ = MLN_HANDLE_NULL; -}; - -// Runs on the map thread from mbgl's still-image continuation. It takes the -// handle rather than the object because the map can be destroyed between the -// request and the callback, and try_resolve leaves the thread-local diagnostic -// to the pump this fires under. -auto finish_still_image_request(mln_map map, std::exception_ptr error) -> void { - auto* live = handle_table().try_resolve(map); - if (live == nullptr) { - return; - } - // Clearing the pending flag is map state that the next request reads, so it - // happens whatever the mask selects. - live->still_image_request_pending = false; - if (error) { - if (!event_selected( - live->event_state->mask, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED - )) { - return; - } - const auto message = exception_message(error); - push_runtime_map_event( - live->runtime, map, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED, 0, - message.c_str() - ); - return; - } - - if (!event_selected( - live->event_state->mask, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED - )) { - return; - } - push_runtime_map_event( - live->runtime, map, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED - ); -} - -// The caller holds the map handle table's mutex, so it can act on the result -// without the handle being retired in between. -auto validate_map_live_locked(mln_map map, MapObject*& out_map) -> mln_status { - out_map = handle_table().resolve_locked(map); - return out_map == nullptr ? MLN_STATUS_INVALID_ARGUMENT : MLN_STATUS_OK; -} - -// Same locking contract as validate_map_live_locked(). -auto validate_map_locked(mln_map map, MapObject*& out_map) -> mln_status { - const auto status = validate_map_live_locked(map, out_map); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_map->owner_thread != std::this_thread::get_id()) { - set_thread_error("map call must be made on its owner thread"); - return MLN_STATUS_WRONG_THREAD; - } - return MLN_STATUS_OK; -} - -auto copy_text( - const std::string& text, char* out_text, size_t text_capacity, - size_t* out_text_size, const char* capacity_name -) -> mln_status { - if (out_text == nullptr && text_capacity > 0) { - set_thread_error( - "output buffer must not be null when capacity is non-zero" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_text_size == nullptr) { - set_thread_error("output size pointer must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - *out_text_size = text.size(); - // A null buffer with zero capacity is a size probe, so it succeeds rather - // than sharing a status with a missing object. - if (out_text == nullptr) { - return MLN_STATUS_OK; - } - if (text_capacity < text.size()) { - auto message = std::string{capacity_name} + " is too small"; - set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!text.empty()) { - std::copy(text.begin(), text.end(), out_text); - } - return MLN_STATUS_OK; -} - -} // namespace - -auto map_options_default() noexcept -> mln_map_options { - return mln_map_options{ - .size = sizeof(mln_map_options), - .width = default_map_width, - .height = default_map_height, - .scale_factor = default_scale_factor, - .map_mode = MLN_MAP_MODE_CONTINUOUS, - .fast_pfor_enabled = false, - .event_mask = MLN_RUNTIME_EVENT_MASK_ALL - }; -} - -auto camera_options_default() noexcept -> mln_camera_options { - return mln_camera_options{ - .size = sizeof(mln_camera_options), - .fields = 0, - .latitude = 0, - .longitude = 0, - .center_altitude = 0, - .padding = {.top = 0, .left = 0, .bottom = 0, .right = 0}, - .anchor = {.x = 0, .y = 0}, - .zoom = 0, - .bearing = 0, - .pitch = 0, - .roll = 0, - .field_of_view = 0 - }; -} - -auto animation_options_default() noexcept -> mln_animation_options { - return mln_animation_options{ - .size = sizeof(mln_animation_options), - .fields = 0, - .duration_ms = 0, - .velocity = 0, - .min_zoom = 0, - .easing = {.x1 = 0, .y1 = 0, .x2 = 0.25, .y2 = 1}, - .transition_id = 0 - }; -} - -auto camera_fit_options_default() noexcept -> mln_camera_fit_options { - return mln_camera_fit_options{ - .size = sizeof(mln_camera_fit_options), - .fields = 0, - .padding = {.top = 0, .left = 0, .bottom = 0, .right = 0}, - .bearing = 0, - .pitch = 0 - }; -} - -auto bound_options_default() noexcept -> mln_bound_options { - return mln_bound_options{ - .size = sizeof(mln_bound_options), - .fields = 0, - .bounds = - {.southwest = {.latitude = 0, .longitude = 0}, - .northeast = {.latitude = 0, .longitude = 0}}, - .min_zoom = 0, - .max_zoom = 0, - .min_pitch = 0, - .max_pitch = 0 - }; -} - -auto free_camera_options_default() noexcept -> mln_free_camera_options { - return mln_free_camera_options{ - .size = sizeof(mln_free_camera_options), - .fields = 0, - .position = {.x = 0, .y = 0, .z = 0}, - .orientation = {.x = 0, .y = 0, .z = 0, .w = 1} - }; -} - -auto projection_mode_default() noexcept -> mln_projection_mode { - return mln_projection_mode{ - .size = sizeof(mln_projection_mode), - .fields = 0, - .axonometric = false, - .x_skew = 0, - .y_skew = 0 - }; -} - -auto map_viewport_options_default() noexcept -> mln_map_viewport_options { - return mln_map_viewport_options{ - .size = sizeof(mln_map_viewport_options), - .fields = 0, - .north_orientation = MLN_NORTH_ORIENTATION_UP, - .constrain_mode = MLN_CONSTRAIN_MODE_HEIGHT_ONLY, - .viewport_mode = MLN_VIEWPORT_MODE_DEFAULT, - .frustum_offset = {.top = 0, .left = 0, .bottom = 0, .right = 0} - }; -} - -auto map_tile_options_default() noexcept -> mln_map_tile_options { - return mln_map_tile_options{ - .size = sizeof(mln_map_tile_options), - .fields = 0, - .prefetch_zoom_delta = 0, - .lod_min_radius = 0, - .lod_scale = 0, - .lod_pitch_threshold = 0, - .lod_zoom_shift = 0, - .lod_mode = MLN_TILE_LOD_MODE_DEFAULT - }; -} - -auto style_tile_source_options_default() noexcept - -> mln_style_tile_source_options { - return mln_style_tile_source_options{ - .size = sizeof(mln_style_tile_source_options), - .fields = 0, - .min_zoom = 0, - .max_zoom = mln::util::DEFAULT_MAX_ZOOM, - .attribution = {.data = nullptr, .size = 0}, - .scheme = MLN_STYLE_TILE_SCHEME_XYZ, - .bounds = - {.southwest = {.latitude = 0, .longitude = 0}, - .northeast = {.latitude = 0, .longitude = 0}}, - .tile_size = mln::util::tileSize_I, - .vector_encoding = MLN_STYLE_VECTOR_TILE_ENCODING_MVT, - .raster_encoding = MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX - }; -} - -auto geojson_source_options_default() noexcept -> mln_geojson_source_options { - const auto defaults = mln::style::GeoJSONOptions{}; - return mln_geojson_source_options{ - .size = sizeof(mln_geojson_source_options), - .fields = 0, - .min_zoom = static_cast(defaults.minzoom), - .max_zoom = static_cast(defaults.maxzoom), - .tolerance = defaults.tolerance, - .cluster_max_zoom = static_cast(defaults.clusterMaxZoom), - .cluster_properties = {}, - .tile_size = defaults.tileSize, - .buffer = defaults.buffer, - .cluster_radius = defaults.clusterRadius, - .cluster_min_points = static_cast(defaults.clusterMinPoints), - .line_metrics = defaults.lineMetrics, - .cluster = defaults.cluster, - .synchronous_tiling = defaults.synchronousUpdate - }; -} - -auto custom_geometry_source_options_default() noexcept - -> mln_custom_geometry_source_options { - return mln_custom_geometry_source_options{ - .size = sizeof(mln_custom_geometry_source_options), - .fields = 0, - .fetch_tile = nullptr, - .cancel_tile = nullptr, - .user_data = nullptr, - .min_zoom = 0, - .max_zoom = 18, - .tolerance = 0.375, - .tile_size = mln::util::tileSize_I, - .buffer = 128, - .clip = false, - .wrap = false, - .release_user_data = nullptr - }; -} - -auto custom_mvt_vector_source_options_default() noexcept - -> mln_custom_mvt_vector_source_options { - return mln_custom_mvt_vector_source_options{ - .size = sizeof(mln_custom_mvt_vector_source_options), - .fields = 0, - .fetch_tile = nullptr, - .cancel_tile = nullptr, - .user_data = nullptr, - .min_zoom = 0, - .max_zoom = 18, - .release_user_data = nullptr - }; -} - -auto premultiplied_rgba8_image_default() noexcept - -> mln_premultiplied_rgba8_image { - return mln_premultiplied_rgba8_image{ - .size = sizeof(mln_premultiplied_rgba8_image), - .width = 0, - .height = 0, - .stride = 0, - .pixels = nullptr, - .byte_length = 0 - }; -} - -auto style_image_options_default() noexcept -> mln_style_image_options { - return mln_style_image_options{ - .size = sizeof(mln_style_image_options), - .fields = 0, - .stretch_x = nullptr, - .stretch_x_count = 0, - .stretch_y = nullptr, - .stretch_y_count = 0, - .content = {.left = 0, .top = 0, .right = 0, .bottom = 0}, - .text_fit_width = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, - .text_fit_height = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, - .pixel_ratio = 1.0F, - .sdf = false - }; -} - -auto style_image_info_default() noexcept -> mln_style_image_info { - return mln_style_image_info{ - .size = sizeof(mln_style_image_info), - .width = 0, - .height = 0, - .stride = 0, - .byte_length = 0, - .stretch_x_count = 0, - .stretch_y_count = 0, - .content = {.left = 0, .top = 0, .right = 0, .bottom = 0}, - .text_fit_width = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, - .text_fit_height = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, - .pixel_ratio = 1.0F, - .sdf = false, - .has_content = false, - .has_text_fit_width = false, - .has_text_fit_height = false - }; -} - -auto style_transition_options_default() noexcept - -> mln_style_transition_options { - return mln_style_transition_options{ - .size = sizeof(mln_style_transition_options), - .fields = 0, - .duration_ms = 0.0, - .delay_ms = 0.0, - .enable_placement_transitions = true - }; -} - -auto validate_map_live(mln_map map, MapObject*& out_map) -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - return validate_map_live_locked(map, out_map); -} - -// Only the owner thread destroys a map, so the borrowed object stays alive for -// as long as the calling thread can use it. -auto validate_map(mln_map map, MapObject*& out_map) -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - return validate_map_locked(map, out_map); -} - -template -auto with_map_projection(mln_map_projection handle, Work&& work) -> mln_status { - auto live = handle_table().lease(handle); - if (live == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - const std::scoped_lock lock(live->call_mutex); - if (live->projection == nullptr) { - set_handle_fault_error( - HandleTraits::kind, handle, HandleFault::Stale - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - return std::invoke(std::forward(work), *live->projection); -} - -auto create_map( - mln_runtime runtime, const mln_map_options* options, mln_map* out_map -) -> mln_status { - const auto options_status = validate_map_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; - } - if (out_map == nullptr) { - set_thread_error("out_map must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (*out_map != MLN_HANDLE_NULL) { - set_thread_error("out_map must point to the null handle"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - RuntimeObject* live_runtime = nullptr; - const auto runtime_status = validate_runtime(runtime, live_runtime); - if (runtime_status != MLN_STATUS_OK) { - return runtime_status; - } - - const auto retain_status = retain_runtime_map(runtime); - if (retain_status != MLN_STATUS_OK) { - return retain_status; - } - auto retain_guard = RuntimeMapRetainGuard{runtime}; - - const auto effective = options == nullptr ? map_options_default() : *options; - auto owned_map = std::make_shared(); - // Every allocation this function owns happens before the handle is published, - // so a throw here cannot leave a registered map the caller has no handle to - // destroy. - auto event_state = std::make_shared(); - auto source_registry = std::make_shared(); - // Publish the handle first, so the observer and frontend capture an id that - // already resolves. - const auto handle = handle_table().insert(owned_map); - owned_map->runtime = runtime; - owned_map->owner_thread = std::this_thread::get_id(); - owned_map->map_mode = effective.map_mode; - owned_map->scale_factor = effective.scale_factor; - owned_map->event_state = std::move(event_state); - owned_map->callback_sources = std::move(source_registry); - owned_map->event_state->mask.store( - effective.event_mask, std::memory_order_relaxed - ); - try { - // Registering allocates, so it belongs inside the scope that unpublishes - // the handle on failure. Nothing before this point queues an event, and the - // observer and frontend below are the first producers that need it. - register_runtime_map_events(runtime, handle); - owned_map->observer = std::make_unique( - runtime, handle, owned_map->event_state, owned_map->callback_sources - ); - owned_map->frontend = std::make_unique( - runtime, handle, runtime_run_loop(live_runtime), owned_map->event_state - ); - - auto map_options = mln::MapOptions{}; - map_options.withMapMode(to_native_map_mode(effective.map_mode)) - .withSize(mln::Size{effective.width, effective.height}) - .withPixelRatio(static_cast(effective.scale_factor)) - .withFastPFOREnabled(effective.fast_pfor_enabled); - owned_map->map = std::make_unique( - *owned_map->frontend, *owned_map->observer, map_options, - resource_options_for_runtime(runtime) - ); - owned_map->callback_sources->attach(*owned_map->map); - - owned_map->commands = std::make_unique(*owned_map->map); - owned_map->command_mailbox = - std::make_shared(runtime_run_loop(live_runtime)); - owned_map->command_ref.emplace( - *owned_map->commands, owned_map->command_mailbox - ); - - } catch (...) { - static_cast(handle_table().remove(handle)); - discard_runtime_map_events(runtime, handle); - throw; - } - *out_map = handle; - retain_guard.dismiss(); - return MLN_STATUS_OK; -} - -auto destroy_map(mln_map map) -> mln_status { - auto runtime = mln_runtime{MLN_HANDLE_NULL}; - auto owned_map = std::shared_ptr{}; - { - // One critical section covers validation, the render-session check, and - // taking ownership, because a render session on another thread detaches - // under this same lock. - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - MapObject* live = nullptr; - const auto status = validate_map_locked(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (live->render_target_session != nullptr) { - set_thread_error("map still has an attached render session"); - return MLN_STATUS_INVALID_STATE; - } - runtime = live->runtime; - owned_map = table.remove_locked(map); - } - // Both cross-thread channels close before the map is destroyed. The registry - // clears its map at the same point, so nothing reconciles against a map - // that is being destroyed; the releases it still owes run from its own - // destructor once the map is gone. - owned_map->callback_sources->detach(); - owned_map->frontend->close_renderer_observer(); - owned_map->command_mailbox->close(); - // Runs outside the registry lock: it can block on in-flight background work. - owned_map->frontend->shutdown_thread_pool(); - discard_runtime_map_events(runtime, map); - owned_map.reset(); - release_runtime_map(runtime); - return MLN_STATUS_OK; -} - -auto map_request_repaint(mln_map map) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - - if (live->map_mode != MLN_MAP_MODE_CONTINUOUS) { - set_thread_error("map is not in continuous mode"); - return MLN_STATUS_INVALID_STATE; - } - - live->map->triggerRepaint(); - return MLN_STATUS_OK; -} - -auto map_set_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer_view state -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto selector_status = validate_feature_state_selector(selector, true); - if (selector_status != MLN_STATUS_OK) { - return selector_status; - } - - auto native_state = to_native_json_value(state); - if (!native_state) { - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto* state_object = native_state->getObject(); - if (state_object == nullptr) { - set_thread_error("feature state value must be a JSON object"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - live->feature_state.set( - string_from_view(selector->source_id), - feature_state_source_layer(*selector), - string_from_view(selector->feature_id), *state_object - ); - live->map->triggerRepaint(); - return MLN_STATUS_OK; -} - -auto map_get_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer* out_state -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto selector_status = validate_feature_state_selector(selector, true); - if (selector_status != MLN_STATUS_OK) { - return selector_status; - } - - auto state = live->feature_state.get( - string_from_view(selector->source_id), - feature_state_source_layer(*selector), - string_from_view(selector->feature_id) - ); - return create_buffer( - serialize_json_value(mln::Value{std::move(state)}), out_state - ); -} - -auto map_remove_feature_state( - mln_map map, const mln_feature_state_selector* selector -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto selector_status = validate_feature_state_selector(selector, false); - if (selector_status != MLN_STATUS_OK) { - return selector_status; - } - - live->feature_state.remove( - string_from_view(selector->source_id), - feature_state_source_layer(*selector), - optional_selector_string( - *selector, MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, selector->feature_id - ), - optional_selector_string( - *selector, MLN_FEATURE_STATE_SELECTOR_STATE_KEY, selector->state_key - ) - ); - live->map->triggerRepaint(); - return MLN_STATUS_OK; -} - -auto map_request_still_image(mln_map map) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - - if (!is_still_map_mode(live->map_mode)) { - set_thread_error("map is not in static or tile mode"); - return MLN_STATUS_INVALID_STATE; - } - - if (live->still_image_request_pending) { - set_thread_error("map already has a pending still-image request"); - return MLN_STATUS_INVALID_STATE; - } - - live->still_image_request_pending = true; - live->map->renderStill([map](std::exception_ptr error) -> void { - finish_still_image_request(map, error); - }); - return MLN_STATUS_OK; -} - -// The render-facing helpers below run on the session's thread while the map -// lives on its own. They are reachable only through an attached session, and -// destroy_map() returns MLN_STATUS_INVALID_STATE while a session is attached, -// so the map cannot be retired underneath them. -auto map_scale_factor(mln_map map) -> double { - const auto* live = handle_table().try_resolve(map); - return live == nullptr ? default_scale_factor : live->scale_factor; -} - -// Map-thread only. The render path posts through map_post_set_size() and -// map_post_trigger_repaint() instead. -auto map_native(MapObject* map) -> mln::Map* { return map->map.get(); } - -// Both posting helpers hold the map table's mutex across the liveness check and -// the send, so the map cannot be retired in between. Mailbox::push takes only -// its own mutex and the run loop's, so there is no path back to this lock. -auto map_post_set_size(mln_map map, uint32_t width, uint32_t height) - -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - MapObject* live = nullptr; - const auto status = validate_map_live_locked(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - live->command_ref->invoke(&MapCommands::set_size, width, height); - return MLN_STATUS_OK; -} - -auto map_post_trigger_repaint(mln_map map) -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - MapObject* live = nullptr; - const auto status = validate_map_live_locked(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - live->command_ref->invoke(&MapCommands::trigger_repaint); - return MLN_STATUS_OK; -} - -auto map_latest_update(mln_map map) -> std::shared_ptr { - auto* live = handle_table().try_resolve(map); - return live == nullptr ? nullptr : live->frontend->latest_update(); -} - -auto map_feature_state_snapshot(mln_map map) - -> std::shared_ptr { - auto* live = handle_table().try_resolve(map); - return live == nullptr ? std::make_shared() - : live->feature_state.snapshot(); -} - -auto map_renderer_observer(mln_map map) -> mln::RendererObserver* { - auto* live = handle_table().try_resolve(map); - return live == nullptr ? nullptr : live->frontend->renderer_observer(); -} - -auto map_run_render_jobs(mln_map map) -> void { - if ( - auto* live = handle_table().try_resolve(map); live != nullptr - ) { - live->frontend->run_render_jobs(); - } -} - -// Claims the map's single render-session slot. Runs on the render session's own -// thread, so it validates liveness only, and holds the map handle table's mutex -// across the check and the claim to stay race-free against destroy_map(). -auto map_attach_render_target_session(mln_map map, void* session) - -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - MapObject* live = nullptr; - const auto status = validate_map_live_locked(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (session == nullptr) { - set_thread_error("render session must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->render_target_session != nullptr) { - set_thread_error("map already has an attached render session"); - return MLN_STATUS_INVALID_STATE; - } - live->render_target_session = session; - return MLN_STATUS_OK; -} - -// Runs on the render session's owner thread, so it validates liveness only, and -// holds the map handle table's mutex across the check and the clear to stay -// race-free against destroy_map(). -auto map_detach_render_target_session(mln_map map, void* session) - -> mln_status { - const std::scoped_lock lock(handle_table().mutex()); - MapObject* live = nullptr; - const auto status = validate_map_live_locked(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (session == nullptr) { - set_thread_error("render session must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->render_target_session != session) { - set_thread_error("render session is not attached to this map"); - return MLN_STATUS_INVALID_STATE; - } - live->render_target_session = nullptr; - return MLN_STATUS_OK; -} - -auto map_set_style_url(mln_map map, const char* url) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (url == nullptr) { - set_thread_error("url must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - // A style that fails to parse inside this call reaches - // HeadlessObserver::onDidFailLoadingMap on this stack, so the flag below is - // owner-thread state that needs no lock and no queued event. - live->event_state->style_load_failed = false; - live->event_state->style_load_failure.clear(); - live->map->getStyle().loadURL(url); - if (live->event_state->style_load_failed) { - set_thread_error(live->event_state->style_load_failure.c_str()); - return MLN_STATUS_NATIVE_ERROR; - } - return MLN_STATUS_OK; -} - -auto map_set_style_json(mln_map map, mln_buffer_view json) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_bytes(json, "style JSON")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - try { - live->event_state->style_load_failed = false; - live->event_state->style_load_failure.clear(); - live->map->getStyle().loadJSON( - std::string{reinterpret_cast(json.data), json.size} - ); - } catch (const std::exception& exception) { - // The diagnostic is this call's own status text, so it is set whatever the - // mask selects; only the event is gated. - set_thread_error(exception.what()); - if ( - event_selected( - live->event_state->mask, MLN_RUNTIME_EVENT_MAP_LOADING_FAILED - ) - ) { - push_runtime_map_event( - live->runtime, map, MLN_RUNTIME_EVENT_MAP_LOADING_FAILED, 0, - exception.what() - ); - } - return MLN_STATUS_NATIVE_ERROR; - } - if (live->event_state->style_load_failed) { - set_thread_error(live->event_state->style_load_failure.c_str()); - return MLN_STATUS_NATIVE_ERROR; - } - return MLN_STATUS_OK; -} - -// Reports the document the style loader last parsed, not the live style, so -// runtime style mutations never reach it. -auto map_copy_loaded_style_json( - mln_map map, uint8_t* out_json, size_t json_capacity, size_t* out_json_size -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - return copy_text( - live->map->getStyle().getJSON(), reinterpret_cast(out_json), - json_capacity, out_json_size, "json_capacity" - ); -} - -// Reports live state: MapLibre records the style URL when the request is made -// and clears it when a JSON style replaces it. -auto map_copy_style_url( - mln_map map, char* out_url, size_t url_capacity, size_t* out_url_size -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - return copy_text( - live->map->getStyle().getURL(), out_url, url_capacity, out_url_size, - "url_capacity" - ); -} - -auto map_set_event_mask(mln_map map, uint64_t mask) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ((mask & ~static_cast(MLN_RUNTIME_EVENT_MASK_ALL)) != 0U) { - set_thread_error("mask contains unknown bits"); - return MLN_STATUS_INVALID_ARGUMENT; - } - // The whole value is stored, including the runtime-event bits this map's - // producers never test, so a getter reports what a host wrote. - live->event_state->mask.store(mask, std::memory_order_relaxed); - return MLN_STATUS_OK; -} - -auto map_get_event_mask(mln_map map, uint64_t* out_mask) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_mask == nullptr) { - set_thread_error("out_mask must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_mask = live->event_state->mask.load(std::memory_order_relaxed); - return MLN_STATUS_OK; -} - -auto style_id_list_count(mln_style_id_list list, size_t* out_count) - -> mln_status { - if (out_count == nullptr) { - set_thread_error("out_count must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - // A style ID list carries no thread affinity, so another thread may destroy - // it mid-read; the lock spans the read. - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_count = live_list->ids.size(); - return MLN_STATUS_OK; -} - -auto style_id_list_get( - mln_style_id_list list, size_t index, mln_buffer_view* out_id -) -> mln_status { - if (out_id == nullptr) { - set_thread_error("out_id must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (index >= live_list->ids.size()) { - set_thread_error("index is out of range"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_id = string_view_from_string(live_list->ids.at(index)); - return MLN_STATUS_OK; -} - -auto style_id_list_destroy(mln_style_id_list list) -> void { - static_cast(handle_table().remove(list)); -} - -auto style_string_list_count(mln_style_string_list list, size_t* out_count) - -> mln_status { - if (out_count == nullptr) { - set_thread_error("out_count must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_count = live_list->values.size(); - return MLN_STATUS_OK; -} - -auto style_string_list_get( - mln_style_string_list list, size_t index, mln_buffer_view* out_value -) -> mln_status { - if (out_value == nullptr) { - set_thread_error("out_value must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (index >= live_list->values.size()) { - set_thread_error("index is out of range"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_value = string_view_from_string(live_list->values.at(index)); - return MLN_STATUS_OK; -} - -auto style_string_list_destroy(mln_style_string_list list) -> void { - static_cast(handle_table().remove(list)); -} - -auto map_add_style_source_json( - mln_map map, mln_buffer_view source_id, mln_buffer_view source_json -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); +auto validate_bound_options(const mln_bound_options* options) -> mln_status { + if (options == nullptr) { + mln::core::set_thread_error("bound options must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (!validate_bytes(source_json, "style source")) { + if (options->size < sizeof(mln_bound_options)) { + mln::core::set_thread_error("mln_bound_options.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - if (style.getSource(id) != nullptr) { - set_thread_error("source already exists"); + constexpr auto known_fields = + static_cast(MLN_BOUND_OPTION_BOUNDS) | MLN_BOUND_OPTION_MIN_ZOOM | + MLN_BOUND_OPTION_MAX_ZOOM | MLN_BOUND_OPTION_MIN_PITCH | + MLN_BOUND_OPTION_MAX_PITCH | MLN_BOUND_OPTION_UNBOUNDED; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_bound_options.fields contains unknown bits" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto error = mln::style::conversion::Error{}; - auto source = - mln::style::conversion::convertJSON>( - string_from_view(source_json), error, id + if ( + (options->fields & MLN_BOUND_OPTION_BOUNDS) != 0U && + (options->fields & MLN_BOUND_OPTION_UNBOUNDED) != 0U + ) { + mln::core::set_thread_error( + "MLN_BOUND_OPTION_BOUNDS and MLN_BOUND_OPTION_UNBOUNDED are mutually " + "exclusive" ); - if (!source) { - set_style_conversion_error("style source", error); return MLN_STATUS_INVALID_ARGUMENT; } - - style.addSource(std::move(*source)); - return MLN_STATUS_OK; -} - -auto map_remove_style_source( - mln_map map, mln_buffer_view source_id, bool* out_removed -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if ((options->fields & MLN_BOUND_OPTION_BOUNDS) != 0U) { + const auto status = validate_lat_lng_bounds(options->bounds); + if (status != MLN_STATUS_OK) { + return status; + } } - if (!validate_string_view(source_id, "source_id")) { + if ( + ((options->fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U && + !std::isfinite(options->min_zoom)) || + ((options->fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U && + !std::isfinite(options->max_zoom)) || + ((options->fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U && + !std::isfinite(options->min_pitch)) || + ((options->fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U && + !std::isfinite(options->max_pitch)) + ) { + mln::core::set_thread_error("bound numeric fields must be finite"); return MLN_STATUS_INVALID_ARGUMENT; } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); + if ( + (options->fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U && + (options->fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U && + options->min_zoom > options->max_zoom + ) { + mln::core::set_thread_error( + "min_zoom must be less than or equal to max_zoom" + ); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_removed == nullptr) { - set_thread_error("out_removed must not be null"); + if ( + (options->fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U && + (options->fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U && + options->min_pitch > options->max_pitch + ) { + mln::core::set_thread_error( + "min_pitch must be less than or equal to max_pitch" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - if (style.getSource(id) == nullptr) { - *out_removed = false; - return MLN_STATUS_OK; - } - - auto removed = style.removeSource(id); - if (!removed) { - set_thread_error("source is used by a layer"); - return MLN_STATUS_INVALID_STATE; - } - // The detached source is dropped before the release runs, so the style no - // longer holds the callbacks that read the host's state. - removed.reset(); - live->callback_sources->release(id); - *out_removed = true; return MLN_STATUS_OK; } -auto map_style_source_exists( - mln_map map, mln_buffer_view source_id, bool* out_exists -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_exists == nullptr) { - set_thread_error("out_exists must not be null"); +auto validate_vec3(mln_vec3 value, const char* name) -> mln_status { + if ( + !std::isfinite(value.x) || !std::isfinite(value.y) || + !std::isfinite(value.z) + ) { + mln::core::set_thread_error(name); return MLN_STATUS_INVALID_ARGUMENT; } - - *out_exists = - live->map->getStyle().getSource(string_from_view(source_id)) != nullptr; return MLN_STATUS_OK; } -auto map_get_style_source_type( - mln_map map, mln_buffer_view source_id, uint32_t* out_source_type, - bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); +auto validate_quaternion(mln_quaternion value) -> mln_status { + if ( + !std::isfinite(value.x) || !std::isfinite(value.y) || + !std::isfinite(value.z) || !std::isfinite(value.w) + ) { + mln::core::set_thread_error( + "free camera orientation values must be finite" + ); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_source_type == nullptr || out_found == nullptr) { - set_thread_error("out_source_type and out_found must not be null"); + if (value.x == 0.0 && value.y == 0.0 && value.z == 0.0 && value.w == 0.0) { + mln::core::set_thread_error( + "free camera orientation must not be zero length" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - const auto* source = - live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - *out_source_type = MLN_STYLE_SOURCE_TYPE_UNKNOWN; - if (source != nullptr) { - *out_source_type = to_c_source_type(source->getType()); - } return MLN_STATUS_OK; } -auto map_get_style_source_info( - mln_map map, mln_buffer_view source_id, mln_style_source_info* out_info, - bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_info == nullptr || out_info->size < sizeof(mln_style_source_info)) { - set_thread_error("out_info must not be null and must have a valid size"); +auto validate_free_camera_options(const mln_free_camera_options* options) + -> mln_status { + if (options == nullptr) { + mln::core::set_thread_error("free camera options must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_found == nullptr) { - set_thread_error("out_found must not be null"); + if (options->size < sizeof(mln_free_camera_options)) { + mln::core::set_thread_error("mln_free_camera_options.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto* source = - live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - *out_info = mln_style_source_info{}; - out_info->size = sizeof(mln_style_source_info); - out_info->type = MLN_STYLE_SOURCE_TYPE_UNKNOWN; - if (source == nullptr) { - return MLN_STATUS_OK; - } - - const auto attribution = source->getAttribution(); - out_info->type = to_c_source_type(source->getType()); - out_info->id_size = source->getID().size(); - out_info->is_volatile = source->isVolatile(); - out_info->has_attribution = attribution.has_value(); - out_info->attribution_size = attribution ? attribution->size() : 0; - - const auto url = source_url(*source); - if (url) { - out_info->fields |= MLN_STYLE_SOURCE_INFO_URL; - out_info->url_size = url->size(); - } - - const auto* tile_source = tile_source_from_source(*source); - if (tile_source == nullptr) { - return MLN_STATUS_OK; - } - - out_info->fields |= MLN_STYLE_SOURCE_INFO_TILE_SIZE; - out_info->tile_size = tile_source->getTileSize(); - if (const auto* vector_source = source->as()) { - out_info->fields |= MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING; - out_info->vector_encoding = - to_c_vector_encoding(vector_source->getEncoding()); - } - - const auto* tileset = inline_tileset(*tile_source); - if (tileset == nullptr) { - return MLN_STATUS_OK; - } - - out_info->fields |= MLN_STYLE_SOURCE_INFO_TILEJSON; - out_info->tile_count = tileset->tiles.size(); - out_info->min_zoom = tileset->zoomRange.min; - out_info->max_zoom = tileset->zoomRange.max; - out_info->scheme = to_c_tile_scheme(tileset->scheme); - if (tileset->bounds) { - out_info->fields |= MLN_STYLE_SOURCE_INFO_BOUNDS; - out_info->bounds = from_native_lat_lng_bounds(*tileset->bounds); - } - if (tileset->vectorEncoding) { - out_info->fields |= MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING; - out_info->vector_encoding = to_c_vector_encoding(*tileset->vectorEncoding); - } - if (tileset->rasterEncoding) { - out_info->fields |= MLN_STYLE_SOURCE_INFO_RASTER_ENCODING; - out_info->raster_encoding = to_c_raster_encoding(*tileset->rasterEncoding); - } - return MLN_STATUS_OK; -} - -auto map_copy_style_source_attribution( - mln_map map, mln_buffer_view source_id, char* out_attribution, - size_t attribution_capacity, size_t* out_attribution_size, bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_attribution == nullptr && attribution_capacity > 0) { - set_thread_error( - "out_attribution must not be null when capacity is non-zero" + constexpr auto known_fields = + static_cast(MLN_FREE_CAMERA_OPTION_POSITION) | + MLN_FREE_CAMERA_OPTION_ORIENTATION; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_free_camera_options.fields contains unknown bits" ); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_attribution_size == nullptr || out_found == nullptr) { - set_thread_error("out_attribution_size and out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - const auto* source = - live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - *out_attribution_size = 0; - if (source == nullptr) { - return MLN_STATUS_OK; - } - - const auto attribution = source->getAttribution(); - if (!attribution) { - return MLN_STATUS_OK; - } - *out_attribution_size = attribution->size(); - // A null buffer with zero capacity is a size probe, so it reports the length - // and succeeds rather than sharing a status with a missing source. - if (out_attribution == nullptr) { - return MLN_STATUS_OK; - } - if (attribution_capacity < attribution->size()) { - set_thread_error("attribution_capacity is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!attribution->empty()) { - std::copy(attribution->begin(), attribution->end(), out_attribution); - } - return MLN_STATUS_OK; -} - -auto map_copy_style_source_url( - mln_map map, mln_buffer_view source_id, char* out_url, size_t url_capacity, - size_t* out_url_size, bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_url == nullptr && url_capacity > 0) { - set_thread_error("out_url must not be null when capacity is non-zero"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_url_size == nullptr || out_found == nullptr) { - set_thread_error("out_url_size and out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; + if ((options->fields & MLN_FREE_CAMERA_OPTION_POSITION) != 0U) { + const auto status = validate_vec3( + options->position, "free camera position values must be finite" + ); + if (status != MLN_STATUS_OK) { + return status; + } } - - const auto* source = - live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - if (source == nullptr) { - *out_url_size = 0; - return MLN_STATUS_OK; + if ((options->fields & MLN_FREE_CAMERA_OPTION_ORIENTATION) != 0U) { + const auto status = validate_quaternion(options->orientation); + if (status != MLN_STATUS_OK) { + return status; + } } - - return copy_text( - source_url(*source).value_or(std::string{}), out_url, url_capacity, - out_url_size, "url_capacity" - ); + return MLN_STATUS_OK; } -auto map_get_style_source_tile_urls( - mln_map map, mln_buffer_view source_id, mln_style_string_list* out_tile_urls, - bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(source_id, "source_id")) { +auto validate_debug_options(uint32_t options) -> mln_status { + constexpr auto known_options = + static_cast(MLN_MAP_DEBUG_TILE_BORDERS) | + MLN_MAP_DEBUG_PARSE_STATUS | MLN_MAP_DEBUG_TIMESTAMPS | + MLN_MAP_DEBUG_COLLISION | MLN_MAP_DEBUG_OVERDRAW | + MLN_MAP_DEBUG_STENCIL_CLIP | MLN_MAP_DEBUG_DEPTH_BUFFER; + if ((options & ~known_options) != 0U) { + mln::core::set_thread_error("debug options contain unknown bits"); return MLN_STATUS_INVALID_ARGUMENT; } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); + return MLN_STATUS_OK; +} + +auto validate_frustum_offset(mln_edge_insets offset) -> mln_status { + if ( + !std::isfinite(offset.top) || !std::isfinite(offset.left) || + !std::isfinite(offset.bottom) || !std::isfinite(offset.right) + ) { + mln::core::set_thread_error("frustum offset values must be finite"); return MLN_STATUS_INVALID_ARGUMENT; } if ( - out_tile_urls == nullptr || *out_tile_urls != MLN_HANDLE_NULL || - out_found == nullptr + offset.top < 0.0 || offset.left < 0.0 || offset.bottom < 0.0 || + offset.right < 0.0 ) { - set_thread_error( - "out_tile_urls must not be null, *out_tile_urls must be the null handle, " - "and out_found must not be null" + mln::core::set_thread_error( + "frustum offset values must be greater than or equal to 0" ); return MLN_STATUS_INVALID_ARGUMENT; } - - const auto* source = - live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - if (source == nullptr) { - return MLN_STATUS_OK; - } - - auto tile_urls = std::vector{}; - if (const auto* tile_source = tile_source_from_source(*source)) { - if (const auto* tileset = inline_tileset(*tile_source)) { - tile_urls = tileset->tiles; - } - } - return create_style_string_list(std::move(tile_urls), out_tile_urls); + return MLN_STATUS_OK; } -auto map_list_style_source_ids(mln_map map, mln_style_id_list* out_source_ids) +auto validate_viewport_options(const mln_map_viewport_options* options) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - - auto ids = std::vector{}; - for (const auto* source : live->map->getStyle().getSources()) { - ids.push_back(source->getID()); - } - return create_style_id_list(std::move(ids), out_source_ids); -} - -auto map_add_geojson_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_geojson_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - if (!validate_string_view(url, "url")) { + if (options == nullptr) { + mln::core::set_thread_error("viewport options must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (url.size == 0) { - set_thread_error("url must not be empty"); + if (options->size < sizeof(mln_map_viewport_options)) { + mln::core::set_thread_error("mln_map_viewport_options.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto options_status = validate_geojson_source_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; - } - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; - } - - auto native_options = - to_native_geojson_source_options(effective_geojson_source_options(options)); - if (!native_options) { + constexpr auto known_fields = + static_cast(MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) | + MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE | + MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE | + MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_map_viewport_options.fields contains unknown bits" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto source = - std::make_unique(id, std::move(*native_options)); - source->setURL(string_from_view(url)); - style.addSource(std::move(source)); - return MLN_STATUS_OK; -} - -auto map_add_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if ((options->fields & MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) != 0U) { + switch (options->north_orientation) { + case MLN_NORTH_ORIENTATION_UP: + case MLN_NORTH_ORIENTATION_RIGHT: + case MLN_NORTH_ORIENTATION_DOWN: + case MLN_NORTH_ORIENTATION_LEFT: + break; + default: + mln::core::set_thread_error("north_orientation is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if ((options->fields & MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE) != 0U) { + switch (options->constrain_mode) { + case MLN_CONSTRAIN_MODE_NONE: + case MLN_CONSTRAIN_MODE_HEIGHT_ONLY: + case MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT: + case MLN_CONSTRAIN_MODE_SCREEN: + break; + default: + mln::core::set_thread_error("constrain_mode is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } } - - // A lease keeps the prepared data alive across a concurrent destroy from - // another thread, which the data handle's contract allows. - const auto prepared = geojson_source_data_table().lease(data); - if (prepared == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; + if ((options->fields & MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE) != 0U) { + switch (options->viewport_mode) { + case MLN_VIEWPORT_MODE_DEFAULT: + case MLN_VIEWPORT_MODE_FLIPPED_Y: + break; + default: + mln::core::set_thread_error("viewport_mode is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; + if ((options->fields & MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET) != 0U) { + return validate_frustum_offset(options->frustum_offset); } - - auto source = - std::make_unique(id, prepared->options); - source->setGeoJSONData(prepared->data); - style.addSource(std::move(source)); return MLN_STATUS_OK; } -auto map_set_geojson_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - if (!validate_string_view(url, "url")) { +auto validate_tile_options(const mln_map_tile_options* options) -> mln_status { + if (options == nullptr) { + mln::core::set_thread_error("tile options must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (url.size == 0) { - set_thread_error("url must not be empty"); + if (options->size < sizeof(mln_map_tile_options)) { + mln::core::set_thread_error("mln_map_tile_options.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); + constexpr auto known_fields = + static_cast(MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) | + MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS | MLN_MAP_TILE_OPTION_LOD_SCALE | + MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD | + MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT | MLN_MAP_TILE_OPTION_LOD_MODE; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_map_tile_options.fields contains unknown bits" + ); return MLN_STATUS_INVALID_ARGUMENT; } - auto* geojson_source = source->as(); - if (geojson_source == nullptr) { - set_thread_error("source is not a GeoJSON source"); + if ( + (options->fields & MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) != 0U && + options->prefetch_zoom_delta > std::numeric_limits::max() + ) { + mln::core::set_thread_error("prefetch_zoom_delta must be at most 255"); return MLN_STATUS_INVALID_ARGUMENT; } - - geojson_source->setURL(string_from_view(url)); - return MLN_STATUS_OK; -} - -auto map_set_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if ( + ((options->fields & MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS) != 0U && + !std::isfinite(options->lod_min_radius)) || + ((options->fields & MLN_MAP_TILE_OPTION_LOD_SCALE) != 0U && + !std::isfinite(options->lod_scale)) || + ((options->fields & MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD) != 0U && + !std::isfinite(options->lod_pitch_threshold)) || + ((options->fields & MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT) != 0U && + !std::isfinite(options->lod_zoom_shift)) + ) { + mln::core::set_thread_error("tile LOD values must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if ((options->fields & MLN_MAP_TILE_OPTION_LOD_MODE) != 0U) { + switch (options->lod_mode) { + case MLN_TILE_LOD_MODE_DEFAULT: + case MLN_TILE_LOD_MODE_DISTANCE: + break; + default: + mln::core::set_thread_error("lod_mode is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } } + return MLN_STATUS_OK; +} - // A lease keeps the prepared data alive across a concurrent destroy from - // another thread, which the data handle's contract allows. - const auto prepared = geojson_source_data_table().lease(data); - if (prepared == nullptr) { +auto validate_lat_lng(mln_lat_lng coordinate) -> mln_status { + if ( + !std::isfinite(coordinate.latitude) || coordinate.latitude < -90.0 || + coordinate.latitude > 90.0 || !std::isfinite(coordinate.longitude) + ) { + mln::core::set_thread_error( + "latitude must be finite and within [-90, 90], and longitude must be " + "finite" + ); return MLN_STATUS_INVALID_ARGUMENT; } + return MLN_STATUS_OK; +} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; +auto validate_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln_status { + const auto southwest_status = validate_lat_lng(bounds.southwest); + if (southwest_status != MLN_STATUS_OK) { + return southwest_status; } - auto* geojson_source = source->as(); - if (geojson_source == nullptr) { - set_thread_error("source is not a GeoJSON source"); - return MLN_STATUS_INVALID_ARGUMENT; + const auto northeast_status = validate_lat_lng(bounds.northeast); + if (northeast_status != MLN_STATUS_OK) { + return northeast_status; } - if (!geojson_source_options_equal( - geojson_source->getOptions(), *prepared->options - )) { - const auto message = - "prepared GeoJSON data options do not match the options that GeoJSON " - "source \"" + - string_from_view(source_id) + "\" was added with"; - set_thread_error(message.c_str()); + if ( + bounds.southwest.latitude > bounds.northeast.latitude || + bounds.southwest.longitude > bounds.northeast.longitude + ) { + mln::core::set_thread_error( + "bounds southwest must be less than or equal to northeast" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - geojson_source->setGeoJSONData(prepared->data); return MLN_STATUS_OK; } -auto map_set_geojson_source_synchronous_tiling( - mln_map map, mln_buffer_view source_id, bool enabled +auto validate_lat_lng_array( + const mln_lat_lng* coordinates, size_t coordinate_count, bool allow_empty ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); + if (coordinate_count == 0) { + if (allow_empty) { + return MLN_STATUS_OK; + } + mln::core::set_thread_error("coordinate_count must be greater than 0"); return MLN_STATUS_INVALID_ARGUMENT; } - auto* geojson_source = source->as(); - if (geojson_source == nullptr) { - set_thread_error("source is not a GeoJSON source"); + + if (coordinates == nullptr) { + mln::core::set_thread_error("coordinates must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - geojson_source->setOverrideSynchronousUpdate(enabled); + const auto coordinate_span = + std::span{coordinates, coordinate_count}; + for (const auto coordinate : coordinate_span) { + const auto status = validate_lat_lng(coordinate); + if (status != MLN_STATUS_OK) { + return status; + } + } return MLN_STATUS_OK; } -auto map_add_vector_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - if (!validate_string_view(url, "url")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (url.size == 0) { - set_thread_error("url must not be empty"); +auto validate_screen_point(mln_screen_point point) -> mln_status { + if (!std::isfinite(point.x) || !std::isfinite(point.y)) { + mln::core::set_thread_error("screen point values must be finite"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::Vector); - if (options_status != MLN_STATUS_OK) { - return options_status; - } + return MLN_STATUS_OK; +} - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; +auto validate_screen_point_array( + const mln_screen_point* points, size_t point_count +) -> mln_status { + if (point_count == 0) { + return MLN_STATUS_OK; } - const auto effective = effective_tile_source_options(options); - auto min_zoom = std::optional{}; - if ( - has_tile_source_option(effective, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) - ) { - min_zoom = static_cast(effective.min_zoom); + if (points == nullptr) { + mln::core::set_thread_error("points must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; } - auto max_zoom = std::optional{}; - if ( - has_tile_source_option(effective, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM) - ) { - max_zoom = static_cast(effective.max_zoom); + + const auto point_span = + std::span{points, point_count}; + for (const auto point : point_span) { + const auto status = validate_screen_point(point); + if (status != MLN_STATUS_OK) { + return status; + } } + return MLN_STATUS_OK; +} +auto validate_edge_insets(mln_edge_insets padding) -> mln_status { if ( - has_tile_source_option( - effective, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING - ) + !std::isfinite(padding.top) || !std::isfinite(padding.left) || + !std::isfinite(padding.bottom) || !std::isfinite(padding.right) || + padding.top < 0.0 || padding.left < 0.0 || padding.bottom < 0.0 || + padding.right < 0.0 ) { - style.addSource( - std::make_unique( - id, string_from_view(url), max_zoom, min_zoom, - to_native_vector_encoding(effective.vector_encoding) - ) - ); - } else { - style.addSource( - std::make_unique( - id, string_from_view(url), max_zoom, min_zoom - ) + mln::core::set_thread_error( + "padding values must be finite and greater than or equal to 0" ); + return MLN_STATUS_INVALID_ARGUMENT; } return MLN_STATUS_OK; } -auto map_add_vector_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto tiles_status = validate_tile_urls(tiles, tile_count); - if (tiles_status != MLN_STATUS_OK) { - return tiles_status; - } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::Vector); - if (options_status != MLN_STATUS_OK) { - return options_status; - } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; - } - - const auto effective = effective_tile_source_options(options); - auto tileset = to_native_tileset(tiles, tile_count, effective, true); - if (!tileset) { +auto validate_projected_meters(mln_projected_meters meters) -> mln_status { + if (!std::isfinite(meters.northing) || !std::isfinite(meters.easting)) { + mln::core::set_thread_error("projected meter values must be finite"); return MLN_STATUS_INVALID_ARGUMENT; } - style.addSource( - std::make_unique( - id, *tileset, std::nullopt, std::nullopt, - to_native_vector_encoding(effective.vector_encoding) - ) - ); return MLN_STATUS_OK; } -auto map_add_raster_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; +auto to_native_screen_point(mln_screen_point point) -> mln::ScreenCoordinate; +auto from_native_screen_point(const mln::ScreenCoordinate& point) + -> mln_screen_point; +auto to_native_edge_insets(mln_edge_insets padding) -> mln::EdgeInsets; +auto from_native_edge_insets(const mln::EdgeInsets& insets) -> mln_edge_insets; + +auto to_native_camera(const mln_camera_options& camera) -> mln::CameraOptions { + auto result = mln::CameraOptions{}; + if ((camera.fields & MLN_CAMERA_OPTION_CENTER) != 0U) { + result.withCenter(mln::LatLng{camera.latitude, camera.longitude}); } - if (!validate_string_view(url, "url")) { - return MLN_STATUS_INVALID_ARGUMENT; + if ((camera.fields & MLN_CAMERA_OPTION_CENTER_ALTITUDE) != 0U) { + result.withCenterAltitude(camera.center_altitude); } - if (url.size == 0) { - set_thread_error("url must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; + if ((camera.fields & MLN_CAMERA_OPTION_PADDING) != 0U) { + result.withPadding(to_native_edge_insets(camera.padding)); } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::Raster); - if (options_status != MLN_STATUS_OK) { - return options_status; + if ((camera.fields & MLN_CAMERA_OPTION_ANCHOR) != 0U) { + result.withAnchor(to_native_screen_point(camera.anchor)); } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; + if ((camera.fields & MLN_CAMERA_OPTION_ZOOM) != 0U) { + result.withZoom(camera.zoom); } - - const auto effective = effective_tile_source_options(options); - style.addSource( - std::make_unique( - id, string_from_view(url), static_cast(effective.tile_size) - ) - ); - return MLN_STATUS_OK; -} - -auto map_add_raster_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if ((camera.fields & MLN_CAMERA_OPTION_BEARING) != 0U) { + result.withBearing(camera.bearing); } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if ((camera.fields & MLN_CAMERA_OPTION_PITCH) != 0U) { + result.withPitch(camera.pitch); } - const auto tiles_status = validate_tile_urls(tiles, tile_count); - if (tiles_status != MLN_STATUS_OK) { - return tiles_status; + if ((camera.fields & MLN_CAMERA_OPTION_ROLL) != 0U) { + result.withRoll(camera.roll); } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::Raster); - if (options_status != MLN_STATUS_OK) { - return options_status; + if ((camera.fields & MLN_CAMERA_OPTION_FOV) != 0U) { + result.withFov(camera.field_of_view); } + return result; +} - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; +auto from_native_camera(const mln::CameraOptions& camera) + -> mln_camera_options { + auto result = mln::core::camera_options_default(); + if (camera.center) { + result.fields |= MLN_CAMERA_OPTION_CENTER; + result.latitude = camera.center->latitude(); + result.longitude = camera.center->longitude(); } - - const auto effective = effective_tile_source_options(options); - auto tileset = to_native_tileset(tiles, tile_count, effective, false); - if (!tileset) { - return MLN_STATUS_INVALID_ARGUMENT; + if (camera.centerAltitude) { + result.fields |= MLN_CAMERA_OPTION_CENTER_ALTITUDE; + result.center_altitude = *camera.centerAltitude; } - style.addSource( - std::make_unique( - id, *tileset, static_cast(effective.tile_size) - ) - ); - return MLN_STATUS_OK; -} - -auto map_add_raster_dem_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url, - const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if (camera.padding) { + result.fields |= MLN_CAMERA_OPTION_PADDING; + result.padding = from_native_edge_insets(*camera.padding); } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if (camera.anchor) { + result.fields |= MLN_CAMERA_OPTION_ANCHOR; + result.anchor = from_native_screen_point(*camera.anchor); } - if (!validate_string_view(url, "url")) { - return MLN_STATUS_INVALID_ARGUMENT; + if (camera.zoom) { + result.fields |= MLN_CAMERA_OPTION_ZOOM; + result.zoom = *camera.zoom; } - if (url.size == 0) { - set_thread_error("url must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; + if (camera.bearing) { + result.fields |= MLN_CAMERA_OPTION_BEARING; + result.bearing = *camera.bearing; } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::RasterDEM); - if (options_status != MLN_STATUS_OK) { - return options_status; + if (camera.pitch) { + result.fields |= MLN_CAMERA_OPTION_PITCH; + result.pitch = *camera.pitch; } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; + if (camera.roll) { + result.fields |= MLN_CAMERA_OPTION_ROLL; + result.roll = *camera.roll; + } + if (camera.fov) { + result.fields |= MLN_CAMERA_OPTION_FOV; + result.field_of_view = *camera.fov; } + return result; +} - const auto effective = effective_tile_source_options(options); - auto source_options = std::optional{}; - if ( - has_tile_source_option( - effective, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING - ) - ) { - source_options = mln::style::SourceOptions{ - .rasterEncoding = to_native_raster_encoding(effective.raster_encoding) +auto camera_transition_finished_payload(uint64_t transition_id) + -> mln_runtime_event_payload { + auto payload = mln::core::zeroed_event_payload(); + payload.camera_transition_finished = + mln_runtime_event_camera_transition_finished{ + .transition_id = transition_id }; - } - style.addSource( - std::make_unique( - id, string_from_view(url), static_cast(effective.tile_size), - source_options - ) - ); - return MLN_STATUS_OK; + return payload; } -auto map_add_raster_dem_source_tiles( - mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, - size_t tile_count, const mln_style_tile_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +// MapLibre Native owns the returned AnimationOptions for the transition +// lifetime and invokes transitionFinishFn on the runtime worker. Event +// publication stops for a closed map. The lambda holds event state by value, so +// it can still inspect the selected mask after map close. +auto to_native_animation( + mln_runtime runtime, mln_map map, + const std::shared_ptr& event_state, + const mln_animation_options* animation +) -> mln::AnimationOptions { + auto result = mln::AnimationOptions{}; + if (animation == nullptr) { + return result; + } + if ((animation->fields & MLN_ANIMATION_OPTION_TRANSITION_ID) != 0U) { + result.transitionFinishFn = [runtime, map, event_state, + transition_id = animation->transition_id] { + if (!mln::core::event_selected( + event_state->mask, MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED + )) { + return; + } + mln::core::push_runtime_map_event_payload( + runtime, map, MLN_RUNTIME_EVENT_MAP_CAMERA_TRANSITION_FINISHED, + MLN_RUNTIME_EVENT_PAYLOAD_CAMERA_TRANSITION_FINISHED, + camera_transition_finished_payload(transition_id) + ); + }; } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if ((animation->fields & MLN_ANIMATION_OPTION_DURATION) != 0U) { + result.duration = duration_from_milliseconds(animation->duration_ms); } - const auto tiles_status = validate_tile_urls(tiles, tile_count); - if (tiles_status != MLN_STATUS_OK) { - return tiles_status; + if ((animation->fields & MLN_ANIMATION_OPTION_VELOCITY) != 0U) { + result.velocity = animation->velocity; } - const auto options_status = - validate_tile_source_options(options, TileSourceOptionKind::RasterDEM); - if (options_status != MLN_STATUS_OK) { - return options_status; + if ((animation->fields & MLN_ANIMATION_OPTION_MIN_ZOOM) != 0U) { + result.minZoom = animation->min_zoom; } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; + if ((animation->fields & MLN_ANIMATION_OPTION_EASING) != 0U) { + const auto easing = animation->easing; + result.easing.emplace(easing.x1, easing.y1, easing.x2, easing.y2); } + return result; +} - const auto effective = effective_tile_source_options(options); - auto tileset = to_native_tileset(tiles, tile_count, effective, false); - if (!tileset) { - return MLN_STATUS_INVALID_ARGUMENT; - } +auto camera_fit_padding(const mln_camera_fit_options* options) + -> mln::EdgeInsets { if ( - has_tile_source_option( - effective, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING - ) + options == nullptr || + (options->fields & MLN_CAMERA_FIT_OPTION_PADDING) == 0U ) { - tileset->rasterEncoding = - to_native_raster_encoding(effective.raster_encoding); + return mln::EdgeInsets{}; } - style.addSource( - std::make_unique( - id, *tileset, static_cast(effective.tile_size) - ) - ); - return MLN_STATUS_OK; + return to_native_edge_insets(options->padding); } -auto map_add_custom_geometry_source( - mln_map map, mln_buffer_view source_id, - const mln_custom_geometry_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto options_status = validate_custom_geometry_source_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; - } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; +auto camera_fit_bearing(const mln_camera_fit_options* options) + -> std::optional { + if ( + options == nullptr || + (options->fields & MLN_CAMERA_FIT_OPTION_BEARING) == 0U + ) { + return std::nullopt; } + return options->bearing; +} - const auto effective = effective_custom_geometry_source_options(*options); - // Tracked before the style takes the source, so the two cannot disagree. A - // throw while tracking leaves no source in the style, and a style that - // rejects the source untracks it again; either way the add failed and the - // caller still owns user_data. Tracking afterwards would leave a live source - // whose release never runs. - live->callback_sources->add( - id, CallbackSourceKind::CustomGeometry, effective.release_user_data, - effective.user_data - ); - try { - style.addSource( - std::make_unique( - id, to_native_custom_geometry_source_options(effective) - ) - ); - } catch (...) { - // Mirrors add()'s own early return: a source with no release callback was - // never tracked, so there is nothing to untrack and no entry of another - // source's to erase. - if (effective.release_user_data != nullptr) { - live->callback_sources->untrack(id); - } - throw; +auto camera_fit_pitch(const mln_camera_fit_options* options) + -> std::optional { + if ( + options == nullptr || (options->fields & MLN_CAMERA_FIT_OPTION_PITCH) == 0U + ) { + return std::nullopt; } - return MLN_STATUS_OK; + return options->pitch; } -auto map_set_custom_geometry_source_tile_data( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto tile_status = validate_canonical_tile_id(tile_id); - if (tile_status != MLN_STATUS_OK) { - return tile_status; - } - auto geojson = to_native_geojson(data); - if (!geojson) { - return MLN_STATUS_INVALID_ARGUMENT; - } +auto to_native_debug_options(uint32_t options) -> mln::MapDebugOptions { + return static_cast(options); +} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* custom_source = source->as(); - if (custom_source == nullptr) { - set_thread_error("source is not a custom geometry source"); - return MLN_STATUS_INVALID_ARGUMENT; - } - custom_source->setTileData(to_native_canonical_tile_id(tile_id), *geojson); - return MLN_STATUS_OK; +auto from_native_debug_options(mln::MapDebugOptions options) -> uint32_t { + return static_cast(options); } -auto map_invalidate_custom_geometry_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto tile_status = validate_canonical_tile_id(tile_id); - if (tile_status != MLN_STATUS_OK) { - return tile_status; +auto to_native_north_orientation(uint32_t orientation) + -> mln::NorthOrientation { + switch (orientation) { + case MLN_NORTH_ORIENTATION_RIGHT: + return mln::NorthOrientation::Rightwards; + case MLN_NORTH_ORIENTATION_DOWN: + return mln::NorthOrientation::Downwards; + case MLN_NORTH_ORIENTATION_LEFT: + return mln::NorthOrientation::Leftwards; + case MLN_NORTH_ORIENTATION_UP: + return mln::NorthOrientation::Upwards; + default: + assert(false); + return mln::NorthOrientation::Upwards; } +} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* custom_source = source->as(); - if (custom_source == nullptr) { - set_thread_error("source is not a custom geometry source"); - return MLN_STATUS_INVALID_ARGUMENT; +auto from_native_north_orientation(mln::NorthOrientation orientation) + -> uint32_t { + switch (orientation) { + case mln::NorthOrientation::Rightwards: + return MLN_NORTH_ORIENTATION_RIGHT; + case mln::NorthOrientation::Downwards: + return MLN_NORTH_ORIENTATION_DOWN; + case mln::NorthOrientation::Leftwards: + return MLN_NORTH_ORIENTATION_LEFT; + case mln::NorthOrientation::Upwards: + return MLN_NORTH_ORIENTATION_UP; } - custom_source->invalidateTile(to_native_canonical_tile_id(tile_id)); - return MLN_STATUS_OK; + assert(false); + return MLN_NORTH_ORIENTATION_UP; } -auto map_invalidate_custom_geometry_source_region( - mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto bounds_status = validate_lat_lng_bounds(bounds); - if (bounds_status != MLN_STATUS_OK) { - return bounds_status; +auto to_native_constrain_mode(uint32_t mode) -> mln::ConstrainMode { + switch (mode) { + case MLN_CONSTRAIN_MODE_NONE: + return mln::ConstrainMode::None; + case MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT: + return mln::ConstrainMode::WidthAndHeight; + case MLN_CONSTRAIN_MODE_SCREEN: + return mln::ConstrainMode::Screen; + case MLN_CONSTRAIN_MODE_HEIGHT_ONLY: + return mln::ConstrainMode::HeightOnly; + default: + assert(false); + return mln::ConstrainMode::HeightOnly; } +} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* custom_source = source->as(); - if (custom_source == nullptr) { - set_thread_error("source is not a custom geometry source"); - return MLN_STATUS_INVALID_ARGUMENT; +auto from_native_constrain_mode(mln::ConstrainMode mode) -> uint32_t { + switch (mode) { + case mln::ConstrainMode::None: + return MLN_CONSTRAIN_MODE_NONE; + case mln::ConstrainMode::WidthAndHeight: + return MLN_CONSTRAIN_MODE_WIDTH_AND_HEIGHT; + case mln::ConstrainMode::Screen: + return MLN_CONSTRAIN_MODE_SCREEN; + case mln::ConstrainMode::HeightOnly: + return MLN_CONSTRAIN_MODE_HEIGHT_ONLY; } - custom_source->invalidateRegion(to_native_lat_lng_bounds(bounds)); - return MLN_STATUS_OK; + assert(false); + return MLN_CONSTRAIN_MODE_HEIGHT_ONLY; } -auto lookup_custom_mvt_vector_source( - MapObject* live, mln_buffer_view source_id, - mln::style::CustomVectorSource*& out_source -) -> mln_status { - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - out_source = source->as(); - if (out_source == nullptr) { - set_thread_error("source is not a custom MVT vector source"); - return MLN_STATUS_INVALID_ARGUMENT; +auto to_native_viewport_mode(uint32_t mode) -> mln::ViewportMode { + switch (mode) { + case MLN_VIEWPORT_MODE_FLIPPED_Y: + return mln::ViewportMode::FlippedY; + case MLN_VIEWPORT_MODE_DEFAULT: + return mln::ViewportMode::Default; + default: + assert(false); + return mln::ViewportMode::Default; } - return MLN_STATUS_OK; } -auto map_add_custom_mvt_vector_source( - mln_map map, mln_buffer_view source_id, - const mln_custom_mvt_vector_source_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto options_status = - validate_custom_mvt_vector_source_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; +auto from_native_viewport_mode(mln::ViewportMode mode) -> uint32_t { + switch (mode) { + case mln::ViewportMode::FlippedY: + return MLN_VIEWPORT_MODE_FLIPPED_Y; + case mln::ViewportMode::Default: + return MLN_VIEWPORT_MODE_DEFAULT; } + assert(false); + return MLN_VIEWPORT_MODE_DEFAULT; +} - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; - } +auto from_native_edge_insets(const mln::EdgeInsets& insets) -> mln_edge_insets { + return mln_edge_insets{ + .top = insets.top(), + .left = insets.left(), + .bottom = insets.bottom(), + .right = insets.right() + }; +} - const auto effective = effective_custom_mvt_vector_source_options(*options); - live->callback_sources->add( - id, CallbackSourceKind::CustomMvtVector, effective.release_user_data, - effective.user_data - ); - try { - style.addSource( - std::make_unique( - id, to_native_custom_mvt_vector_source_options(effective) - ) - ); - } catch (...) { - if (effective.release_user_data != nullptr) { - live->callback_sources->untrack(id); - } - throw; +auto to_native_tile_lod_mode(uint32_t mode) -> mln::TileLodMode { + switch (mode) { + case MLN_TILE_LOD_MODE_DISTANCE: + return mln::TileLodMode::Distance; + case MLN_TILE_LOD_MODE_DEFAULT: + return mln::TileLodMode::Default; + default: + assert(false); + return mln::TileLodMode::Default; } - return MLN_STATUS_OK; } -auto map_set_custom_mvt_vector_source_tile_data( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view data -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +auto from_native_tile_lod_mode(mln::TileLodMode mode) -> uint32_t { + switch (mode) { + case mln::TileLodMode::Distance: + return MLN_TILE_LOD_MODE_DISTANCE; + case mln::TileLodMode::Default: + return MLN_TILE_LOD_MODE_DEFAULT; } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + assert(false); + return MLN_TILE_LOD_MODE_DEFAULT; +} + +auto from_native_projection_mode(const mln::ProjectionMode& mode) + -> mln_projection_mode { + auto result = mln::core::projection_mode_default(); + if (mode.axonometric) { + result.fields |= MLN_PROJECTION_MODE_AXONOMETRIC; + result.axonometric = *mode.axonometric; } - const auto tile_status = validate_canonical_tile_id(tile_id); - if (tile_status != MLN_STATUS_OK) { - return tile_status; + if (mode.xSkew) { + result.fields |= MLN_PROJECTION_MODE_X_SKEW; + result.x_skew = *mode.xSkew; } - if (!validate_string_view(data, "data")) { - return MLN_STATUS_INVALID_ARGUMENT; + if (mode.ySkew) { + result.fields |= MLN_PROJECTION_MODE_Y_SKEW; + result.y_skew = *mode.ySkew; } + return result; +} - mln::style::CustomVectorSource* custom_source = nullptr; - const auto source_status = - lookup_custom_mvt_vector_source(live, source_id, custom_source); - if (source_status != MLN_STATUS_OK) { - return source_status; - } +auto to_native_lat_lng(mln_lat_lng coordinate) -> mln::LatLng { + return mln::LatLng{coordinate.latitude, coordinate.longitude}; +} - auto native_data = std::shared_ptr{}; - if (data.size != 0) { - native_data = std::make_shared( - static_cast(data.data), data.size - ); - } - custom_source->setTileData( - to_native_canonical_tile_id(tile_id), native_data, - mln::style::TileDataFormat::MVT +auto from_native_lat_lng(const mln::LatLng& coordinate) -> mln_lat_lng { + return mln_lat_lng{ + .latitude = coordinate.latitude(), .longitude = coordinate.longitude() + }; +} + +auto to_native_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln::LatLngBounds { + return mln::LatLngBounds::hull( + to_native_lat_lng(bounds.southwest), to_native_lat_lng(bounds.northeast) ); - return MLN_STATUS_OK; } -auto map_set_custom_mvt_vector_source_tile_error( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, - mln_buffer_view message -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto tile_status = validate_canonical_tile_id(tile_id); - if (tile_status != MLN_STATUS_OK) { - return tile_status; - } - if (!validate_string_view(message, "message")) { - return MLN_STATUS_INVALID_ARGUMENT; - } +auto from_native_lat_lng_bounds(const mln::LatLngBounds& bounds) + -> mln_lat_lng_bounds { + return mln_lat_lng_bounds{ + .southwest = + mln_lat_lng{.latitude = bounds.south(), .longitude = bounds.west()}, + .northeast = + mln_lat_lng{.latitude = bounds.north(), .longitude = bounds.east()} + }; +} + +// mbgl keeps the unbounded flag private. Its operator== treats unbounded values +// as equal to each other and distinct from every bounded one, so comparing +// against a default-constructed value tests the flag exactly. +auto is_unbounded_lat_lng_bounds(const mln::LatLngBounds& bounds) -> bool { + return bounds == mln::LatLngBounds{}; +} - mln::style::CustomVectorSource* custom_source = nullptr; - const auto source_status = - lookup_custom_mvt_vector_source(live, source_id, custom_source); - if (source_status != MLN_STATUS_OK) { - return source_status; +auto to_native_lat_lngs(const mln_lat_lng* coordinates, size_t coordinate_count) + -> std::vector { + auto result = std::vector{}; + result.reserve(coordinate_count); + const auto coordinate_span = + std::span{coordinates, coordinate_count}; + for (const auto coordinate : coordinate_span) { + result.emplace_back(to_native_lat_lng(coordinate)); } + return result; +} - custom_source->setTileError( - to_native_canonical_tile_id(tile_id), - std::make_exception_ptr(std::runtime_error(string_from_view(message))) - ); - return MLN_STATUS_OK; +auto to_native_screen_point(mln_screen_point point) -> mln::ScreenCoordinate { + return mln::ScreenCoordinate{point.x, point.y}; +} + +auto from_native_screen_point(const mln::ScreenCoordinate& point) + -> mln_screen_point { + return mln_screen_point{.x = point.x, .y = point.y}; } -auto map_invalidate_custom_mvt_vector_source_tile( - mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto tile_status = validate_canonical_tile_id(tile_id); - if (tile_status != MLN_STATUS_OK) { - return tile_status; +auto to_native_screen_points(const mln_screen_point* points, size_t point_count) + -> std::vector { + auto result = std::vector{}; + result.reserve(point_count); + const auto point_span = + std::span{points, point_count}; + for (const auto point : point_span) { + result.emplace_back(to_native_screen_point(point)); } + return result; +} - mln::style::CustomVectorSource* custom_source = nullptr; - const auto source_status = - lookup_custom_mvt_vector_source(live, source_id, custom_source); - if (source_status != MLN_STATUS_OK) { - return source_status; - } - custom_source->invalidateTile(to_native_canonical_tile_id(tile_id)); - return MLN_STATUS_OK; +auto to_native_edge_insets(mln_edge_insets padding) -> mln::EdgeInsets { + return mln::EdgeInsets{ + padding.top, padding.left, padding.bottom, padding.right + }; } -auto map_set_style_image( - mln_map map, mln_buffer_view image_id, - const mln_premultiplied_rgba8_image* image, - const mln_style_image_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; +auto to_native_bound_options(const mln_bound_options& options) + -> mln::BoundOptions { + auto result = mln::BoundOptions{}; + if ((options.fields & MLN_BOUND_OPTION_BOUNDS) != 0U) { + result.withLatLngBounds(to_native_lat_lng_bounds(options.bounds)); } - const auto image_status = validate_premultiplied_rgba8_image(image); - if (image_status != MLN_STATUS_OK) { - return image_status; + if ((options.fields & MLN_BOUND_OPTION_UNBOUNDED) != 0U) { + // A default-constructed LatLngBounds is the mbgl unbounded constraint. + result.withLatLngBounds(mln::LatLngBounds{}); } - const auto options_status = validate_style_image_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; + if ((options.fields & MLN_BOUND_OPTION_MIN_ZOOM) != 0U) { + result.withMinZoom(options.min_zoom); } - - const auto effective = effective_style_image_options(options); - auto content = std::optional{}; - if ( - options != nullptr && - has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT) - ) { - content = mln::style::ImageContent{ - .left = effective.content.left, - .top = effective.content.top, - .right = effective.content.right, - .bottom = effective.content.bottom - }; + if ((options.fields & MLN_BOUND_OPTION_MAX_ZOOM) != 0U) { + result.withMaxZoom(options.max_zoom); } - auto text_fit_width = std::optional{}; - if ( - options != nullptr && - has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH) - ) { - text_fit_width = to_native_text_fit(effective.text_fit_width); + if ((options.fields & MLN_BOUND_OPTION_MIN_PITCH) != 0U) { + result.withMinPitch(options.min_pitch); } - auto text_fit_height = std::optional{}; - if ( - options != nullptr && - has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT) - ) { - text_fit_height = to_native_text_fit(effective.text_fit_height); + if ((options.fields & MLN_BOUND_OPTION_MAX_PITCH) != 0U) { + result.withMaxPitch(options.max_pitch); } - - auto style_image = std::make_unique( - string_from_view(image_id), to_native_premultiplied_rgba8_image(*image), - effective.pixel_ratio, effective.sdf, - to_native_image_stretches(effective.stretch_x, effective.stretch_x_count), - to_native_image_stretches(effective.stretch_y, effective.stretch_y_count), - content, text_fit_width, text_fit_height - ); - live->map->getStyle().addImage(std::move(style_image)); - return MLN_STATUS_OK; + return result; } -auto map_remove_style_image( - mln_map map, mln_buffer_view image_id, bool* out_removed -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +auto from_native_bound_options(const mln::BoundOptions& options) + -> mln_bound_options { + auto result = mln::core::bound_options_default(); + if (options.bounds) { + if (is_unbounded_lat_lng_bounds(*options.bounds)) { + result.fields |= MLN_BOUND_OPTION_UNBOUNDED; + } else { + result.fields |= MLN_BOUND_OPTION_BOUNDS; + result.bounds = from_native_lat_lng_bounds(*options.bounds); + } } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; + if (options.minZoom) { + result.fields |= MLN_BOUND_OPTION_MIN_ZOOM; + result.min_zoom = *options.minZoom; } - if (out_removed == nullptr) { - set_thread_error("out_removed must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; + if (options.maxZoom) { + result.fields |= MLN_BOUND_OPTION_MAX_ZOOM; + result.max_zoom = *options.maxZoom; } - - auto& style = live->map->getStyle(); - const auto id = string_from_view(image_id); - *out_removed = style.getImage(id).has_value(); - if (*out_removed) { - style.removeImage(id); + if (options.minPitch) { + result.fields |= MLN_BOUND_OPTION_MIN_PITCH; + result.min_pitch = *options.minPitch; } - return MLN_STATUS_OK; + if (options.maxPitch) { + result.fields |= MLN_BOUND_OPTION_MAX_PITCH; + result.max_pitch = *options.maxPitch; + } + return result; } -auto map_style_image_exists( - mln_map map, mln_buffer_view image_id, bool* out_exists -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; - } - if (out_exists == nullptr) { - set_thread_error("out_exists must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto to_native_vec3(mln_vec3 value) -> mln::vec3 { + return mln::vec3{{value.x, value.y, value.z}}; +} - *out_exists = - live->map->getStyle().getImage(string_from_view(image_id)).has_value(); - return MLN_STATUS_OK; +auto from_native_vec3(const mln::vec3& value) -> mln_vec3 { + const auto [x_component, y_component, z_component] = value; + return mln_vec3{.x = x_component, .y = y_component, .z = z_component}; } -auto map_get_style_image_info( - mln_map map, mln_buffer_view image_id, mln_style_image_info* out_info, - bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; - } - if (out_info == nullptr || out_info->size < sizeof(mln_style_image_info)) { - set_thread_error("out_info must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_found == nullptr) { - set_thread_error("out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto to_native_vec4(mln_quaternion value) -> mln::vec4 { + return mln::vec4{{value.x, value.y, value.z, value.w}}; +} - const auto image = live->map->getStyle().getImage(string_from_view(image_id)); - *out_found = image.has_value(); - *out_info = - image ? style_image_info_from_native(*image) : style_image_info_default(); - return MLN_STATUS_OK; +auto from_native_vec4(const mln::vec4& value) -> mln_quaternion { + const auto [x_component, y_component, z_component, w_component] = value; + return mln_quaternion{ + .x = x_component, .y = y_component, .z = z_component, .w = w_component + }; } -auto map_copy_style_image_stretches( - mln_map map, mln_buffer_view image_id, mln_image_stretch* out_stretch_x, - size_t stretch_x_capacity, size_t* out_stretch_x_count, - mln_image_stretch* out_stretch_y, size_t stretch_y_capacity, - size_t* out_stretch_y_count, bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; - } - if ( - (out_stretch_x == nullptr && stretch_x_capacity > 0) || - (out_stretch_y == nullptr && stretch_y_capacity > 0) - ) { - set_thread_error( - "stretch output arrays must not be null when their capacity is non-zero" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - out_stretch_x_count == nullptr || out_stretch_y_count == nullptr || - out_found == nullptr - ) { - set_thread_error("stretch output counts and out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; +auto to_native_free_camera(const mln_free_camera_options& options) + -> mln::FreeCameraOptions { + auto result = mln::FreeCameraOptions{}; + if ((options.fields & MLN_FREE_CAMERA_OPTION_POSITION) != 0U) { + result.position = to_native_vec3(options.position); } - - const auto image = live->map->getStyle().getImage(string_from_view(image_id)); - *out_found = image.has_value(); - *out_stretch_x_count = 0; - *out_stretch_y_count = 0; - if (!image) { - return MLN_STATUS_OK; + if ((options.fields & MLN_FREE_CAMERA_OPTION_ORIENTATION) != 0U) { + result.orientation = to_native_vec4(options.orientation); } + return result; +} - const auto& stretch_x = image->getStretchX(); - const auto& stretch_y = image->getStretchY(); - *out_stretch_x_count = stretch_x.size(); - *out_stretch_y_count = stretch_y.size(); - - // A null array with zero capacity is a size probe, so it reports the counts - // and succeeds rather than sharing a status with a missing image. - for (const auto& [out, capacity, stretches, name] : { - std::tuple{ - out_stretch_x, stretch_x_capacity, &stretch_x, "stretch_x_capacity" - }, - std::tuple{ - out_stretch_y, stretch_y_capacity, &stretch_y, "stretch_y_capacity" - }, - }) { - if (out == nullptr) { - continue; - } - if (capacity < stretches->size()) { - auto message = std::string{name} + " is too small"; - set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto from_native_free_camera(const mln::FreeCameraOptions& options) + -> mln_free_camera_options { + auto result = mln::core::free_camera_options_default(); + if (options.position) { + result.fields |= MLN_FREE_CAMERA_OPTION_POSITION; + result.position = from_native_vec3(*options.position); } - for (const auto& [out, stretches] : { - std::pair{out_stretch_x, &stretch_x}, - std::pair{out_stretch_y, &stretch_y}, - }) { - if (out == nullptr) { - continue; - } - for (size_t index = 0; index < stretches->size(); index += 1) { - out[index] = mln_image_stretch{ - .from = (*stretches)[index].first, .to = (*stretches)[index].second - }; - } + if (options.orientation) { + result.fields |= MLN_FREE_CAMERA_OPTION_ORIENTATION; + result.orientation = from_native_vec4(*options.orientation); } - return MLN_STATUS_OK; + return result; } -auto map_copy_style_image_premultiplied_rgba8( - mln_map map, mln_buffer_view image_id, uint8_t* out_pixels, - size_t pixel_capacity, size_t* out_byte_length, bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto image_id_status = validate_image_id(image_id); - if (image_id_status != MLN_STATUS_OK) { - return image_id_status; - } - if (out_pixels == nullptr && pixel_capacity > 0) { - set_thread_error("out_pixels must not be null when capacity is non-zero"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_byte_length == nullptr || out_found == nullptr) { - set_thread_error("out_byte_length and out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } +} // namespace - const auto image = live->map->getStyle().getImage(string_from_view(image_id)); - *out_found = image.has_value(); - *out_byte_length = 0; - if (!image) { - return MLN_STATUS_OK; - } +namespace mln::core { +auto validate_debug_options_input(uint32_t options) -> mln_status { + return validate_debug_options(options); +} - const auto& pixels = image->getImage(); - *out_byte_length = pixels.bytes(); - // A null buffer with zero capacity is a size probe, so it reports the length - // and succeeds rather than sharing a status with a missing image. - if (out_pixels == nullptr) { - return MLN_STATUS_OK; - } - if (pixel_capacity < pixels.bytes()) { - set_thread_error("pixel_capacity is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (pixels.bytes() > 0) { - std::copy_n(pixels.data.get(), pixels.bytes(), out_pixels); - } - return MLN_STATUS_OK; +auto validate_viewport_options_input(const mln_map_viewport_options* options) + -> mln_status { + return validate_viewport_options(options); } -auto map_add_image_source_url( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, mln_buffer_view url -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto coordinate_status = - validate_image_source_coordinates(coordinates, coordinate_count); - if (coordinate_status != MLN_STATUS_OK) { - return coordinate_status; - } - if (!validate_string_view(url, "url")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (url.size == 0) { - set_thread_error("url must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto validate_tile_options_input(const mln_map_tile_options* options) + -> mln_status { + return validate_tile_options(options); +} - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; - } +auto validate_bound_options_input(const mln_bound_options* options) + -> mln_status { + return validate_bound_options(options); +} - auto source = std::make_unique( - id, to_native_image_source_coordinates(coordinates) - ); - source->setURL(string_from_view(url)); - style.addSource(std::move(source)); - return MLN_STATUS_OK; +auto validate_free_camera_options_input(const mln_free_camera_options* options) + -> mln_status { + return validate_free_camera_options(options); } -auto map_add_image_source_image( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count, const mln_premultiplied_rgba8_image* image -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto coordinate_status = - validate_image_source_coordinates(coordinates, coordinate_count); - if (coordinate_status != MLN_STATUS_OK) { - return coordinate_status; - } - const auto image_status = validate_premultiplied_rgba8_image(image); - if (image_status != MLN_STATUS_OK) { - return image_status; - } +MapObject::~MapObject() = default; - auto& style = live->map->getStyle(); - const auto id = string_from_view(source_id); - const auto add_status = validate_source_can_be_added(style, id); - if (add_status != MLN_STATUS_OK) { - return add_status; - } +MapProjectionObject::~MapProjectionObject() = default; - auto native_image = to_native_premultiplied_rgba8_image(*image); - style.addSource( - std::make_unique( - id, to_native_image_source_coordinates(coordinates) - ) - ); - auto* added_source = style.getSource(id); - auto* image_source = added_source == nullptr - ? nullptr - : added_source->as(); - if (image_source == nullptr) { - set_thread_error("added source is not an image source"); - return MLN_STATUS_NATIVE_ERROR; - } - image_source->setImage(std::move(native_image)); - return MLN_STATUS_OK; -} +class PendingMapResult final { + public: + explicit PendingMapResult(mln_map value) : value_(value) {} + ~PendingMapResult(); + PendingMapResult(const PendingMapResult&) = delete; + PendingMapResult(PendingMapResult&&) = delete; + auto operator=(const PendingMapResult&) -> PendingMapResult& = delete; + auto operator=(PendingMapResult&&) -> PendingMapResult& = delete; -auto map_set_image_source_url( - mln_map map, mln_buffer_view source_id, mln_buffer_view url -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - if (!validate_string_view(url, "url")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (url.size == 0) { - set_thread_error("url must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } + [[nodiscard]] auto value() const noexcept -> mln_map { return value_; } + auto transfer() noexcept -> void { value_ = MLN_HANDLE_NULL; } + + private: + mln_map value_ = MLN_HANDLE_NULL; +}; + +} // namespace mln::core + +namespace mln::core { + +namespace { + +class RuntimeMapRetainGuard final { + public: + explicit RuntimeMapRetainGuard(mln_runtime runtime) noexcept + : runtime_(runtime) {} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* image_source = source->as(); - if (image_source == nullptr) { - set_thread_error("source is not an image source"); - return MLN_STATUS_INVALID_ARGUMENT; - } - image_source->setURL(string_from_view(url)); - return MLN_STATUS_OK; -} + ~RuntimeMapRetainGuard() { release_runtime_map(runtime_); } -auto map_set_image_source_image( - mln_map map, mln_buffer_view source_id, - const mln_premultiplied_rgba8_image* image -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto image_status = validate_premultiplied_rgba8_image(image); - if (image_status != MLN_STATUS_OK) { - return image_status; - } + RuntimeMapRetainGuard(const RuntimeMapRetainGuard&) = delete; + RuntimeMapRetainGuard(RuntimeMapRetainGuard&&) = delete; + auto operator=(const RuntimeMapRetainGuard&) + -> RuntimeMapRetainGuard& = delete; + auto operator=(RuntimeMapRetainGuard&&) -> RuntimeMapRetainGuard& = delete; - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* image_source = source->as(); - if (image_source == nullptr) { - set_thread_error("source is not an image source"); - return MLN_STATUS_INVALID_ARGUMENT; - } - image_source->setImage(to_native_premultiplied_rgba8_image(*image)); - return MLN_STATUS_OK; -} + auto dismiss() noexcept -> void { runtime_ = MLN_HANDLE_NULL; } -auto map_set_image_source_coordinates( - mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, - size_t coordinate_count -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; - } - const auto coordinate_status = - validate_image_source_coordinates(coordinates, coordinate_count); - if (coordinate_status != MLN_STATUS_OK) { - return coordinate_status; - } + private: + mln_runtime runtime_ = MLN_HANDLE_NULL; +}; - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - if (source == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; +// Runs on the runtime worker from MapLibre's still-image continuation and +// resolves the handle that identifies the pending request. +auto finish_still_image_request(mln_map map, std::exception_ptr error) -> void { + auto* live = handle_table().try_resolve(map); + if (live == nullptr) { + return; } - auto* image_source = source->as(); - if (image_source == nullptr) { - set_thread_error("source is not an image source"); - return MLN_STATUS_INVALID_ARGUMENT; + live->still_image_request_pending = false; + auto operation = std::exchange(live->still_image_operation, {}); + if (auto release = std::exchange(live->still_image_release_submission, {})) { + release(); } - image_source->setCoordinates(to_native_image_source_coordinates(coordinates)); - return MLN_STATUS_OK; -} - -auto map_get_image_source_coordinates( - mln_map map, mln_buffer_view source_id, mln_lat_lng* out_coordinates, - size_t coordinate_capacity, size_t* out_coordinate_count, bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if (error) { + const auto message = exception_message(error); + if (operation) { + operation->complete( + MLN_STATUS_NATIVE_ERROR, message, std::any{std::monostate{}} + ); + } + if ( + event_selected( + live->event_state->mask, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED + ) + ) { + push_runtime_map_event( + live->runtime, map, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FAILED, 0, + message.c_str() + ); + } + return; } - const auto source_id_status = validate_source_id(source_id); - if (source_id_status != MLN_STATUS_OK) { - return source_id_status; + if (operation) { + operation->complete(MLN_STATUS_OK, {}, std::any{std::monostate{}}); } - if (out_coordinates == nullptr && coordinate_capacity > 0) { - set_thread_error( - "out_coordinates must not be null when capacity is non-zero" + if ( + event_selected( + live->event_state->mask, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED + ) + ) { + push_runtime_map_event( + live->runtime, map, MLN_RUNTIME_EVENT_MAP_STILL_IMAGE_FINISHED ); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_coordinate_count == nullptr || out_found == nullptr) { - set_thread_error("out_coordinate_count and out_found must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; } +} - auto* source = live->map->getStyle().getSource(string_from_view(source_id)); - *out_found = source != nullptr; - *out_coordinate_count = 0; - if (source == nullptr) { - return MLN_STATUS_OK; - } - auto* image_source = source->as(); - if (image_source == nullptr) { - set_thread_error("source is not an image source"); - return MLN_STATUS_INVALID_ARGUMENT; - } +// The caller holds the map handle table's mutex, so it can act on the result +// without the handle being retired in between. +auto validate_map_live_locked(mln_map map, MapObject*& out_map) -> mln_status { + out_map = handle_table().resolve_locked(map); + return out_map == nullptr ? MLN_STATUS_INVALID_ARGUMENT : MLN_STATUS_OK; +} - constexpr auto image_source_coordinate_count = size_t{4}; - *out_coordinate_count = image_source_coordinate_count; - if (coordinate_capacity < image_source_coordinate_count) { - set_thread_error("coordinate_capacity is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto coordinates = - from_native_image_source_coordinates(image_source->getCoordinates()); - auto output = std::span{out_coordinates, coordinate_capacity}; - std::ranges::copy(coordinates, output.begin()); - return MLN_STATUS_OK; +// Same locking contract as validate_map_live_locked(). +auto validate_map_locked(mln_map map, MapObject*& out_map) -> mln_status { + return validate_map_live_locked(map, out_map); } -auto map_add_hillshade_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(source_id, "source_id") || - !validate_string_view(before_layer_id, "before_layer_id") - ) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0 || source_id.size == 0) { - set_thread_error("layer_id and source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; +auto publish_map_snapshot(MapObject& live) -> uint64_t { + const auto options = live.map->getMapOptions(); + auto snapshot = mln_map_snapshot{ + .size = sizeof(mln_map_snapshot), + .debug_options = from_native_debug_options(live.map->getDebug()), + .generation = live.next_snapshot_generation++, + .camera = from_native_camera(live.map->getCameraOptions()), + .logical_extent = live.logical_extent, + .projection_mode = + from_native_projection_mode(live.map->getProjectionMode()), + .viewport = + {.size = sizeof(mln_map_viewport_options), + .fields = + static_cast(MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) | + MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE | + MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE | + MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET, + .north_orientation = + from_native_north_orientation(options.northOrientation()), + .constrain_mode = from_native_constrain_mode(options.constrainMode()), + .viewport_mode = from_native_viewport_mode(options.viewportMode()), + .frustum_offset = from_native_edge_insets(live.map->getFrustumOffset())}, + .fully_loaded = live.map->isFullyLoaded(), + .rendering_stats_view_enabled = live.map->isRenderingStatsViewEnabled(), + .repaint_demand = live.frontend->repaint_demand(), + .reserved_flags = 0, + .event_mask = live.event_state->mask.load(std::memory_order_relaxed), + .latest_render_update_generation = + live.frontend->latest_update_generation(), + .tile = + {.size = sizeof(mln_map_tile_options), + .fields = + static_cast(MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) | + MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS | MLN_MAP_TILE_OPTION_LOD_SCALE | + MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD | + MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT | MLN_MAP_TILE_OPTION_LOD_MODE, + .prefetch_zoom_delta = live.map->getPrefetchZoomDelta(), + .lod_min_radius = live.map->getTileLodMinRadius(), + .lod_scale = live.map->getTileLodScale(), + .lod_pitch_threshold = live.map->getTileLodPitchThreshold(), + .lod_zoom_shift = live.map->getTileLodZoomShift(), + .lod_mode = from_native_tile_lod_mode(live.map->getTileLodMode())}, + .bounds = from_native_bound_options(live.map->getBounds()), + .free_camera = from_native_free_camera(live.map->getFreeCameraOptions()) + }; + const auto generation = snapshot.generation; + { + const std::scoped_lock lock(live.snapshot_mutex); + live.snapshot = snapshot; } + return generation; +} - auto& style = live->map->getStyle(); - const auto layer = string_from_view(layer_id); - const auto source = string_from_view(source_id); - if (style.getLayer(layer) != nullptr) { - set_thread_error("layer already exists"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* source_ptr = style.getSource(source); - if (source_ptr == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!source_ptr->is()) { - set_thread_error("source is not a raster DEM source"); - return MLN_STATUS_INVALID_ARGUMENT; - } +} // namespace - auto before = std::optional{}; - if (before_layer_id.size > 0) { - before = string_from_view(before_layer_id); - if (style.getLayer(*before) == nullptr) { - set_thread_error("before_layer_id does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - style.addLayer( - std::make_unique(layer, source), before - ); - return MLN_STATUS_OK; +auto map_options_default() noexcept -> mln_map_options { + return mln_map_options{ + .size = sizeof(mln_map_options), + .initial_extent = + {.width = default_map_width, + .height = default_map_height, + .scale_factor = default_scale_factor}, + .map_mode = MLN_MAP_MODE_CONTINUOUS, + .fast_pfor_enabled = false, + .event_mask = MLN_RUNTIME_EVENT_MASK_ALL + }; } -auto map_add_color_relief_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, - mln_buffer_view before_layer_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(source_id, "source_id") || - !validate_string_view(before_layer_id, "before_layer_id") - ) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0 || source_id.size == 0) { - set_thread_error("layer_id and source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - auto& style = live->map->getStyle(); - const auto layer = string_from_view(layer_id); - const auto source = string_from_view(source_id); - if (style.getLayer(layer) != nullptr) { - set_thread_error("layer already exists"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto* source_ptr = style.getSource(source); - if (source_ptr == nullptr) { - set_thread_error("source does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!source_ptr->is()) { - set_thread_error("source is not a raster DEM source"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto camera_options_default() noexcept -> mln_camera_options { + return mln_camera_options{ + .size = sizeof(mln_camera_options), + .fields = 0, + .latitude = 0, + .longitude = 0, + .center_altitude = 0, + .padding = {.top = 0, .left = 0, .bottom = 0, .right = 0}, + .anchor = {.x = 0, .y = 0}, + .zoom = 0, + .bearing = 0, + .pitch = 0, + .roll = 0, + .field_of_view = 0 + }; +} - auto before = std::optional{}; - if (before_layer_id.size > 0) { - before = string_from_view(before_layer_id); - if (style.getLayer(*before) == nullptr) { - set_thread_error("before_layer_id does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - style.addLayer( - std::make_unique(layer, source), before - ); - return MLN_STATUS_OK; +auto animation_options_default() noexcept -> mln_animation_options { + return mln_animation_options{ + .size = sizeof(mln_animation_options), + .fields = 0, + .duration_ms = 0, + .velocity = 0, + .min_zoom = 0, + .easing = {.x1 = 0, .y1 = 0, .x2 = 0.25, .y2 = 1}, + .transition_id = 0 + }; } -auto map_add_location_indicator_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(before_layer_id, "before_layer_id") - ) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto camera_delta_default() noexcept -> mln_camera_delta { + return mln_camera_delta{ + .size = sizeof(mln_camera_delta), + .kind = MLN_CAMERA_DELTA_MOVE, + .offset = {}, + .amount = 0, + .has_anchor = false, + .anchor = {}, + .animation = animation_options_default() + }; +} - auto& style = live->map->getStyle(); - const auto layer = string_from_view(layer_id); - if (style.getLayer(layer) != nullptr) { - set_thread_error("layer already exists"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto before = std::optional{}; - if (before_layer_id.size > 0) { - before = string_from_view(before_layer_id); - if (style.getLayer(*before) == nullptr) { - set_thread_error("before_layer_id does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } - style.addLayer( - std::make_unique(layer), before - ); - return MLN_STATUS_OK; +auto camera_update_default() noexcept -> mln_camera_update { + return mln_camera_update{ + .size = sizeof(mln_camera_update), + .mode = MLN_CAMERA_UPDATE_MODE_JUMP, + .camera = camera_options_default(), + .animation = animation_options_default(), + .gesture_phase = MLN_GESTURE_PHASE_NONE, + .reserved = 0 + }; } -auto validate_location_indicator_layer(MapObject* map, mln_buffer_view layer_id) - -> mln_status { - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto* layer = map->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (std::strcmp(layer->getTypeInfo()->type, "location-indicator") != 0) { - set_thread_error("layer is not a location indicator layer"); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; +auto camera_fit_options_default() noexcept -> mln_camera_fit_options { + return mln_camera_fit_options{ + .size = sizeof(mln_camera_fit_options), + .fields = 0, + .padding = {.top = 0, .left = 0, .bottom = 0, .right = 0}, + .bearing = 0, + .pitch = 0 + }; } -auto validate_float64_to_float32(double value, const char* name) -> mln_status { - if ( - !std::isfinite(value) || value < -std::numeric_limits::max() || - value > std::numeric_limits::max() - ) { - const auto message = std::string{name} + " must fit in finite float32"; - set_thread_error(message.c_str()); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; +auto bound_options_default() noexcept -> mln_bound_options { + return mln_bound_options{ + .size = sizeof(mln_bound_options), + .fields = 0, + .bounds = + {.southwest = {.latitude = 0, .longitude = 0}, + .northeast = {.latitude = 0, .longitude = 0}}, + .min_zoom = 0, + .max_zoom = 0, + .min_pitch = 0, + .max_pitch = 0 + }; } -auto map_set_location_indicator_location( - mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto layer_status = validate_location_indicator_layer(live, layer_id); - if (layer_status != MLN_STATUS_OK) { - return layer_status; - } - const auto coordinate_status = validate_lat_lng(coordinate); - if (coordinate_status != MLN_STATUS_OK) { - return coordinate_status; - } - if (!std::isfinite(altitude)) { - set_thread_error("altitude must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto free_camera_options_default() noexcept -> mln_free_camera_options { + return mln_free_camera_options{ + .size = sizeof(mln_free_camera_options), + .fields = 0, + .position = {.x = 0, .y = 0, .z = 0}, + .orientation = {.x = 0, .y = 0, .z = 0, .w = 1} + }; +} - // The style property is [latitude, longitude, altitude]; the renderer reads - // it back as LatLng{values[0], values[1]}. - const auto location = serialize_json_value( - mln::Value{mapbox::base::ValueArray{ - mln::Value{coordinate.latitude}, mln::Value{coordinate.longitude}, - mln::Value{altitude} - }} - ); - return map_set_layer_property( - map, layer_id, string_view_from_literal("location"), - buffer_view_from_string(location) - ); +auto projection_mode_default() noexcept -> mln_projection_mode { + return mln_projection_mode{ + .size = sizeof(mln_projection_mode), + .fields = 0, + .axonometric = false, + .x_skew = 0, + .y_skew = 0 + }; } -auto map_set_location_indicator_bearing( - mln_map map, mln_buffer_view layer_id, double bearing -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto layer_status = validate_location_indicator_layer(live, layer_id); - if (layer_status != MLN_STATUS_OK) { - return layer_status; - } - const auto bearing_status = validate_float64_to_float32(bearing, "bearing"); - if (bearing_status != MLN_STATUS_OK) { - return bearing_status; - } - const auto value = serialize_json_value(mln::Value{bearing}); - return map_set_layer_property( - map, layer_id, string_view_from_literal("bearing"), - buffer_view_from_string(value) - ); +auto map_viewport_options_default() noexcept -> mln_map_viewport_options { + return mln_map_viewport_options{ + .size = sizeof(mln_map_viewport_options), + .fields = 0, + .north_orientation = MLN_NORTH_ORIENTATION_UP, + .constrain_mode = MLN_CONSTRAIN_MODE_HEIGHT_ONLY, + .viewport_mode = MLN_VIEWPORT_MODE_DEFAULT, + .frustum_offset = {.top = 0, .left = 0, .bottom = 0, .right = 0} + }; } -auto map_set_location_indicator_accuracy_radius( - mln_map map, mln_buffer_view layer_id, double radius -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto layer_status = validate_location_indicator_layer(live, layer_id); - if (layer_status != MLN_STATUS_OK) { - return layer_status; - } - const auto radius_status = validate_float64_to_float32(radius, "radius"); - if (radius_status != MLN_STATUS_OK) { - return radius_status; - } - if (radius < 0.0) { - set_thread_error("radius must be non-negative"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto value = serialize_json_value(mln::Value{radius}); - return map_set_layer_property( - map, layer_id, string_view_from_literal("accuracy-radius"), - buffer_view_from_string(value) - ); +auto map_tile_options_default() noexcept -> mln_map_tile_options { + return mln_map_tile_options{ + .size = sizeof(mln_map_tile_options), + .fields = 0, + .prefetch_zoom_delta = 0, + .lod_min_radius = 0, + .lod_scale = 0, + .lod_pitch_threshold = 0, + .lod_zoom_shift = 0, + .lod_mode = MLN_TILE_LOD_MODE_DEFAULT + }; } -auto location_indicator_image_property(uint32_t image_kind) - -> std::optional { - switch (image_kind) { - case MLN_LOCATION_INDICATOR_IMAGE_KIND_TOP: - return string_view_from_literal("top-image"); - case MLN_LOCATION_INDICATOR_IMAGE_KIND_BEARING: - return string_view_from_literal("bearing-image"); - case MLN_LOCATION_INDICATOR_IMAGE_KIND_SHADOW: - return string_view_from_literal("shadow-image"); - default: - return std::nullopt; - } +auto validate_map_live(mln_map map, MapObject*& out_map) -> mln_status { + const std::scoped_lock lock(handle_table().mutex()); + return validate_map_live_locked(map, out_map); } -auto map_set_location_indicator_image_name( - mln_map map, mln_buffer_view layer_id, uint32_t image_kind, - mln_buffer_view image_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto layer_status = validate_location_indicator_layer(live, layer_id); - if (layer_status != MLN_STATUS_OK) { - return layer_status; - } - if (!validate_string_view(image_id, "image_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (image_id.size == 0) { - set_thread_error("image_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto property = location_indicator_image_property(image_kind); - if (!property) { - set_thread_error("image_kind is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto value = - serialize_json_value(mln::Value{string_from_view(image_id)}); - return map_set_layer_property( - map, layer_id, *property, buffer_view_from_string(value) - ); +auto validate_map(mln_map map, MapObject*& out_map) -> mln_status { + const std::scoped_lock lock(handle_table().mutex()); + return validate_map_locked(map, out_map); } -auto map_add_style_layer_json( - mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(before_layer_id, "before_layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (!validate_bytes(layer_json, "style layer")) { - return MLN_STATUS_INVALID_ARGUMENT; - } +namespace { - auto& style = live->map->getStyle(); - auto before = std::optional{}; - if (before_layer_id.size > 0) { - before = string_from_view(before_layer_id); - if (style.getLayer(*before) == nullptr) { - set_thread_error("before_layer_id does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } - } +struct MapSubmissionContext { + std::shared_ptr map; + std::shared_ptr control; + std::shared_ptr runtime; +}; - auto error = mln::style::conversion::Error{}; - auto layer = - mln::style::conversion::convertJSON>( - string_from_view(layer_json), error - ); - if (!layer) { - set_style_conversion_error("style layer", error); +auto acquire_map_submission(mln_map map, MapSubmissionContext& out_context) + -> mln_status { + auto live = handle_table().lease(map); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - - const auto id = (*layer)->getID(); - if (style.getLayer(id) != nullptr) { - set_thread_error("layer already exists"); - return MLN_STATUS_INVALID_ARGUMENT; + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - if ( - (*layer)->getTypeInfo()->source == - mln::style::LayerTypeInfo::Source::Required && - style.getSource((*layer)->getSourceID()) == nullptr - ) { - set_thread_error("layer source does not exist"); + auto guard = ControlLease{&live->control}; + auto control = std::make_shared(std::move(guard)); + auto runtime = lease_runtime(live->runtime); + if (runtime == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - - style.addLayer(std::move(*layer), before); + out_context = MapSubmissionContext{ + .map = std::move(live), + .control = std::move(control), + .runtime = std::move(runtime), + }; return MLN_STATUS_OK; } -auto map_remove_style_layer( - mln_map map, mln_buffer_view layer_id, bool* out_removed -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_removed == nullptr) { - set_thread_error("out_removed must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } +} // namespace - auto removed = live->map->getStyle().removeLayer(string_from_view(layer_id)); - *out_removed = removed != nullptr; - return MLN_STATUS_OK; +auto submit_map_command( + mln_map map, std::function work, + const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto context = MapSubmissionContext{}; + const auto acquire_status = acquire_map_submission(map, context); + if (acquire_status != MLN_STATUS_OK) { + return acquire_status; + } + auto live = std::move(context.map); + auto map_lease = std::move(context.control); + auto runtime = std::move(context.runtime); + auto completion_state = std::make_shared(*completion); + return submit_runtime_command( + runtime, + [live = std::move(live), map_lease = std::move(map_lease), completion_state, + work = std::move(work)](uint64_t) mutable -> void { + clear_thread_error(); + auto status = MLN_STATUS_NATIVE_ERROR; + auto message = std::string{}; + auto generation = uint64_t{0}; + try { + status = std::invoke(std::move(work)); + message = thread_last_error_message(); + // Committed commands republish so snapshot reads observe the commit. + if (status == MLN_STATUS_OK) { + generation = publish_map_snapshot(*live); + } + } catch (const std::exception& exception) { + status = MLN_STATUS_NATIVE_ERROR; + generation = 0; + message = exception.what(); + } catch (...) { + status = MLN_STATUS_NATIVE_ERROR; + generation = 0; + message = "map command failed"; + } + complete_command( + completion_state, + status == MLN_STATUS_OK ? MLN_COMMAND_DISPOSITION_COMMITTED + : MLN_COMMAND_DISPOSITION_FAILED, + status, generation, std::move(message) + ); + }, + completion_state + ); } -auto map_style_layer_exists( - mln_map map, mln_buffer_view layer_id, bool* out_exists -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_exists == nullptr) { - set_thread_error("out_exists must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - *out_exists = - live->map->getStyle().getLayer(string_from_view(layer_id)) != nullptr; - return MLN_STATUS_OK; +auto start_style_operation( + mln_map map, StyleOperationKind kind, StyleWork work, + const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto completion_state = std::make_shared(*completion); + auto result_callback = [completion_state, kind]( + mln_status status, std::string diagnostic, + std::any result + ) mutable { + auto shared = std::any_cast>(&result); + if (status != MLN_STATUS_OK || shared == nullptr || *shared == nullptr) { + complete( + completion_state, + status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK ? "style operation produced an invalid result" + : std::move(diagnostic) + ); + return; + } + completion_state->resolve([kind, result = std::move(*shared)]( + const mln_completion& descriptor + ) { + const void* value = nullptr; + auto count = std::size_t{0}; + auto view = mln_buffer_view{}; + auto views = std::vector{}; + auto stretches = mln_style_image_stretches_result{}; + auto source = mln_style_source_result{}; + auto layer = mln_style_layer_result{}; + auto image = mln_style_image_result{}; + switch (kind) { + case StyleOperationKind::SourceInfo: + if (result->found) { + views.reserve(result->strings.size()); + for (const auto& string : result->strings) { + views.push_back({.data = string.data(), .size = string.size()}); + } + source = { + .size = sizeof(mln_style_source_result), + .reserved = 0, + .info = result->source_info, + .attribution = + {.data = result->attribution.data(), + .size = result->attribution.size()}, + .url = {.data = result->url.data(), .size = result->url.size()}, + .tile_urls = views.data(), + .tile_url_count = views.size() + }; + value = &source; + count = 1; + } + break; + case StyleOperationKind::LayerInfo: + if (result->found) { + layer = { + .size = sizeof(mln_style_layer_result), + .reserved = 0, + .info = result->layer_info, + .source_id = + {.data = result->source_id.data(), + .size = result->source_id.size()}, + .source_layer = { + .data = result->source_layer.data(), + .size = result->source_layer.size() + } + }; + value = &layer; + count = 1; + } + break; + case StyleOperationKind::ImageInfo: + if (result->found) { + image = { + .size = sizeof(mln_style_image_result), + .reserved = 0, + .info = result->image_info, + .pixels = + {.data = result->bytes.data(), .size = result->bytes.size()}, + .stretch_x = result->stretch_x.data(), + .stretch_x_count = result->stretch_x.size(), + .stretch_y = result->stretch_y.data(), + .stretch_y_count = result->stretch_y.size() + }; + value = ℑ + count = 1; + } + break; + case StyleOperationKind::TransitionOptions: + value = &result->transition_options; + count = 1; + break; + case StyleOperationKind::SourceIds: + case StyleOperationKind::LayerIds: + views.reserve(result->strings.size()); + for (const auto& string : result->strings) { + views.push_back({.data = string.data(), .size = string.size()}); + } + value = views.data(); + count = views.size(); + break; + case StyleOperationKind::SourceTileUrls: + if (result->found) { + views.reserve(result->strings.size()); + for (const auto& string : result->strings) { + views.push_back({.data = string.data(), .size = string.size()}); + } + value = views.data(); + count = views.size(); + } + break; + case StyleOperationKind::ImageStretches: + if (result->found) { + stretches = mln_style_image_stretches_result{ + .size = sizeof(mln_style_image_stretches_result), + .reserved = 0, + .stretch_x = result->stretch_x.data(), + .stretch_x_count = result->stretch_x.size(), + .stretch_y = result->stretch_y.data(), + .stretch_y_count = result->stretch_y.size(), + }; + value = &stretches; + count = 1; + } + break; + case StyleOperationKind::ImageCoordinates: + value = result->found ? result->coordinates.data() : nullptr; + count = result->found ? result->coordinates.size() : 0; + break; + case StyleOperationKind::SourceAttribution: + case StyleOperationKind::SourceUrl: + case StyleOperationKind::LayerJson: + case StyleOperationKind::LightProperty: + case StyleOperationKind::LayerProperty: + if (result->found) { + view = {.data = result->bytes.data(), .size = result->bytes.size()}; + value = &view; + count = 1; + } + break; + default: + if ( + result->found || (kind != StyleOperationKind::ImagePixels && + kind != StyleOperationKind::LayerFilter) + ) { + view = {.data = result->bytes.data(), .size = result->bytes.size()}; + value = &view; + count = 1; + } + break; + } + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + value, count + ); + }); + }; + auto context = MapSubmissionContext{}; + const auto acquire_status = acquire_map_submission(map, context); + if (acquire_status != MLN_STATUS_OK) return acquire_status; + auto state = std::make_shared(std::move(result_callback)); + const auto submission = submit_runtime_operation( + context.runtime, state, + [map = std::move(context.map), control = std::move(context.control), state, + work = std::move(work)]() mutable -> void { + static_cast(map); + static_cast(control); + auto result = std::make_shared(); + clear_thread_error(); + try { + const auto status = std::invoke(std::move(work), *result); + state->complete(status, thread_last_error_message(), std::any{result}); + } catch (const std::exception& exception) { + state->complete(MLN_STATUS_NATIVE_ERROR, exception.what(), result); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, "style operation failed", result + ); + } + } + ); + if (submission == MLN_STATUS_OK) + completion_state->accept(); + else + completion_state->reject(); + return submission; +} + +auto start_geometry_operation( + mln_map map, GeometryOperationKind kind, GeometryWork work, + const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto completion_state = std::make_shared(*completion); + auto context = MapSubmissionContext{}; + const auto acquire_status = acquire_map_submission(map, context); + if (acquire_status != MLN_STATUS_OK) return acquire_status; + auto state = std::make_shared( + [completion_state, + kind](mln_status status, std::string diagnostic, std::any result) mutable { + auto* value = std::any_cast(&result); + if (status != MLN_STATUS_OK || value == nullptr) { + complete( + completion_state, + status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK + ? "geometry operation produced an invalid result" + : std::move(diagnostic) + ); + return; + } + completion_state->resolve( + [kind, value = std::move(*value)](const mln_completion& descriptor) { + const void* pointer = nullptr; + auto count = std::size_t{1}; + switch (kind) { + case GeometryOperationKind::CameraForBounds: + case GeometryOperationKind::CameraForCoordinates: + case GeometryOperationKind::CameraForGeometry: + pointer = &value.camera; + break; + case GeometryOperationKind::BoundsForCamera: + case GeometryOperationKind::UnwrappedBoundsForCamera: + pointer = &value.bounds; + break; + case GeometryOperationKind::PixelForCoordinate: + pointer = &value.point; + break; + case GeometryOperationKind::CoordinateForPixel: + pointer = &value.coordinate; + break; + case GeometryOperationKind::PixelsForCoordinates: + pointer = value.points.data(); + count = value.points.size(); + break; + case GeometryOperationKind::CoordinatesForPixels: + pointer = value.coordinates.data(); + count = value.coordinates.size(); + break; + } + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + pointer, count + ); + } + ); + } + ); + const auto submission = submit_runtime_operation( + context.runtime, state, + [map = std::move(context.map), control = std::move(context.control), state, + work = std::move(work)]() mutable { + static_cast(map); + static_cast(control); + auto result = GeometryOperationResult{}; + clear_thread_error(); + try { + const auto status = std::invoke(std::move(work), result); + state->complete( + status, thread_last_error_message(), std::any{std::move(result)} + ); + } catch (const std::exception& exception) { + state->complete( + MLN_STATUS_NATIVE_ERROR, exception.what(), std::any{std::move(result)} + ); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, "geometry operation failed", + std::any{std::move(result)} + ); + } + } + ); + if (submission == MLN_STATUS_OK) + completion_state->accept(); + else + completion_state->reject(); + return submission; } -auto map_get_style_layer_type( - mln_map map, mln_buffer_view layer_id, mln_buffer_view* out_layer_type, - bool* out_found -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); +namespace { + +// Runs work against a live projection on the calling thread, serialized with +// every other projection call, including close, by the per-projection mutex. +template +auto with_projection(mln_map_projection projection, Work work) -> mln_status { + auto live = handle_table().lease(projection); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - if (out_layer_type == nullptr || out_found == nullptr) { - set_thread_error("out_layer_type and out_found must not be null"); + const std::scoped_lock call_lock(live->call_mutex); + if (live->projection == nullptr) { + set_handle_fault_error( + HandleTraits::kind, projection, HandleFault::Stale + ); return MLN_STATUS_INVALID_ARGUMENT; } - - const auto* layer = - live->map->getStyle().getLayer(string_from_view(layer_id)); - *out_found = layer != nullptr; - *out_layer_type = {}; - if (layer != nullptr) { - *out_layer_type = string_view_from_literal(layer->getTypeInfo()->type); - } + work(*live->projection); return MLN_STATUS_OK; } -auto map_list_style_layer_ids(mln_map map, mln_style_id_list* out_layer_ids) - -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } +} // namespace - auto ids = std::vector{}; - for (const auto* layer : live->map->getStyle().getLayers()) { - ids.push_back(layer->getID()); - } - return create_style_id_list(std::move(ids), out_layer_ids); +namespace { +auto ensure_map_teardown_lane() -> void; } -auto map_move_style_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id +auto create_map( + mln_runtime runtime, const mln_map_options* options, mln_map* out_map ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + const auto options_status = validate_map_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(before_layer_id, "before_layer_id") - ) { + if (out_map == nullptr) { + set_thread_error("out_map must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); + if (*out_map != MLN_HANDLE_NULL) { + set_thread_error("out_map must point to the null handle"); return MLN_STATUS_INVALID_ARGUMENT; } - auto& style = live->map->getStyle(); - const auto id = string_from_view(layer_id); - if (style.getLayer(id) == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; + RuntimeObject* live_runtime = nullptr; + const auto runtime_status = validate_runtime(runtime, live_runtime); + if (runtime_status != MLN_STATUS_OK) { + return runtime_status; } + ensure_map_teardown_lane(); - auto before = std::optional{}; - if (before_layer_id.size > 0) { - before = string_from_view(before_layer_id); - if (*before == id) { - return MLN_STATUS_OK; - } - if (style.getLayer(*before) == nullptr) { - set_thread_error("before_layer_id does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } + const auto retain_status = retain_runtime_map(runtime); + if (retain_status != MLN_STATUS_OK) { + return retain_status; } + auto retain_guard = RuntimeMapRetainGuard{runtime}; + + const auto effective = options == nullptr ? map_options_default() : *options; + auto owned_map = std::make_shared(); + // Every allocation this function owns happens before the handle is published, + // so a throw here cannot leave a registered map the caller has no handle to + // destroy. + auto event_state = std::make_shared(); + auto source_registry = std::make_shared(); + // Publish the handle first, so the observer and frontend capture an id that + // already resolves. + const auto handle = handle_table().insert(owned_map); + owned_map->runtime = runtime; + owned_map->runtime_state = lease_runtime(runtime); + owned_map->map_mode = effective.map_mode; + owned_map->logical_extent = effective.initial_extent; + owned_map->event_state = std::move(event_state); + owned_map->callback_sources = std::move(source_registry); + owned_map->event_state->mask.store( + effective.event_mask, std::memory_order_relaxed + ); + try { + // Registering allocates, so it belongs inside the scope that unpublishes + // the handle on failure. Nothing before this point queues an event, and the + // observer and frontend below are the first producers that need it. + register_runtime_map_events(runtime, handle, owned_map->event_state); + owned_map->observer = std::make_unique( + runtime, handle, owned_map->event_state, owned_map->callback_sources + ); + owned_map->frontend = std::make_unique( + runtime, handle, runtime_run_loop(live_runtime), owned_map->event_state + ); + + auto map_options = mln::MapOptions{}; + map_options.withMapMode(to_native_map_mode(effective.map_mode)) + .withSize( + mln::Size{ + effective.initial_extent.width, effective.initial_extent.height + } + ) + .withPixelRatio(static_cast(effective.initial_extent.scale_factor)) + .withFastPFOREnabled(effective.fast_pfor_enabled); + owned_map->map = std::make_unique( + *owned_map->frontend, *owned_map->observer, map_options, + resource_options_for_runtime(runtime) + ); + owned_map->callback_sources->attach(*owned_map->map); + owned_map->frontend->set_publish_callback( + [weak = std::weak_ptr{owned_map}]() -> void { + if (const auto locked = weak.lock(); locked && locked->map) { + publish_map_snapshot(*locked); + } + } + ); + publish_map_snapshot(*owned_map); - auto layer = style.removeLayer(id); - style.addLayer(std::move(layer), before); + } catch (...) { + static_cast(handle_table().remove(handle)); + unregister_runtime_map_events(runtime, handle); + throw; + } + *out_map = handle; + retain_guard.dismiss(); return MLN_STATUS_OK; } - -auto map_get_style_layer_json( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_layer, bool* out_found +auto create_map_start( + mln_runtime runtime, const mln_map_options* options, + const mln_completion* completion ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; + const auto options_status = validate_map_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); + auto runtime_state = lease_runtime(runtime); + if (runtime_state == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - if ( - out_layer == nullptr || *out_layer != MLN_HANDLE_NULL || - out_found == nullptr - ) { - set_thread_error( - "out_layer must not be null, *out_layer must be the null handle, and " - "out_found must not be null" - ); - return MLN_STATUS_INVALID_ARGUMENT; + const auto reserve_status = retain_runtime_map(runtime); + if (reserve_status != MLN_STATUS_OK) { + return reserve_status; } - - const auto* layer = - live->map->getStyle().getLayer(string_from_view(layer_id)); - *out_found = layer != nullptr; - if (layer == nullptr) { - return MLN_STATUS_OK; + const auto effective = options == nullptr ? map_options_default() : *options; + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) { + release_runtime_map(runtime); + return completion_status; + } + auto completion_state = std::make_shared(*completion); + auto state = + std::make_shared([completion_state]( + mln_status status, + std::string diagnostic, std::any result + ) mutable { + if (status != MLN_STATUS_OK) { + complete(completion_state, status, std::move(diagnostic)); + return; + } + auto* pending = std::any_cast>(&result); + if (pending == nullptr || *pending == nullptr) { + complete( + completion_state, MLN_STATUS_NATIVE_ERROR, + "map creation produced an invalid result" + ); + return; + } + const auto map = (*pending)->value(); + (*pending)->transfer(); + complete_value(completion_state, MLN_STATUS_OK, {}, map); + }); + const auto submit_status = submit_runtime_operation( + runtime_state, state, [runtime, effective, state]() mutable -> void { + auto result = mln_map{MLN_HANDLE_NULL}; + auto status = MLN_STATUS_NATIVE_ERROR; + auto diagnostic = std::string{}; + try { + status = create_map(runtime, &effective, &result); + } catch (...) { + diagnostic = exception_message(std::current_exception()); + } + // Drop the creation reservation before publishing completion. Once the + // result is observable, the map itself is the runtime's only child and + // callers may release the map and runtime back to back. + release_runtime_map(runtime); + if (status == MLN_STATUS_OK) { + state->complete( + status, {}, std::any{std::make_shared(result)} + ); + } else { + state->complete( + status, + diagnostic.empty() ? "map creation failed" : std::move(diagnostic), {} + ); + } + } + ); + if (submit_status != MLN_STATUS_OK) { + completion_state->reject(); + release_runtime_map(runtime); + } else { + completion_state->accept(); } - return create_buffer(serialize_json_value(layer->serialize()), out_layer); + return submit_status; } -auto map_set_style_light_json(mln_map map, mln_buffer_view light_json) +auto map_snapshot_get(mln_map map, mln_map_snapshot* out_snapshot) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_bytes(light_json, "style light")) { + if ( + out_snapshot == nullptr || out_snapshot->size < sizeof(mln_map_snapshot) + ) { + set_thread_error( + "out_snapshot must not be null and must have a valid size" + ); return MLN_STATUS_INVALID_ARGUMENT; } - - auto error = mln::style::conversion::Error{}; - auto light = mln::style::conversion::convertJSON( - string_from_view(light_json), error - ); - if (!light) { - set_style_conversion_error("style light", error); - return MLN_STATUS_INVALID_ARGUMENT; + auto live = handle_table().lease(map); + if (live == nullptr || live->control.is_closing()) { + return live == nullptr ? MLN_STATUS_INVALID_ARGUMENT + : MLN_STATUS_INVALID_STATE; } - - live->map->getStyle().setLight(std::make_unique(*light)); + const std::scoped_lock lock(live->snapshot_mutex); + *out_snapshot = live->snapshot; return MLN_STATUS_OK; } -auto map_set_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer_view value +auto map_resize( + mln_map map, mln_logical_extent extent, const mln_completion* completion ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(property_name, "property_name")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (property_name.size == 0) { - set_thread_error("property_name must not be empty"); + const auto completion_status = validate_completion(completion); + if ( + completion_status != MLN_STATUS_OK || extent.width == 0 || + extent.height == 0 || !std::isfinite(extent.scale_factor) || + extent.scale_factor <= 0 + ) { + if (completion_status == MLN_STATUS_OK) + set_thread_error("extent must be valid"); return MLN_STATUS_INVALID_ARGUMENT; } - auto document = mln::JSDocument{}; - if (!parse_json_document(value, "style light property", document)) { + auto live = handle_table().lease(map); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - - auto* light = live->map->getStyle().getLight(); - if (light == nullptr) { - set_thread_error("style light does not exist"); + if (!live->control.acquire()) { return MLN_STATUS_INVALID_STATE; } - - auto error = light->setProperty( - string_from_view(property_name), - mln::style::conversion::Convertible{ - static_cast(&document) - } + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + const auto status = submit_runtime_command( + live->runtime_state, + [live, submission = std::move(submission), completion_state, + extent](uint64_t sequence) mutable -> void { + if (sequence != live->latest_resize_submission.load()) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_SUPERSEDED, MLN_STATUS_OK + ); + return; + } + try { + live->logical_extent = extent; + live->map->setSize(mln::Size{extent.width, extent.height}); + const auto generation = publish_map_snapshot(*live); + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_COMMITTED, MLN_STATUS_OK, + generation + ); + } catch (...) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_FAILED, + MLN_STATUS_NATIVE_ERROR, 0, + exception_message(std::current_exception()) + ); + } + }, + completion_state, &live->latest_resize_submission ); - if (error) { - set_style_conversion_error("style light property", *error); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; + return status; } -auto map_get_style_light_property( - mln_map map, mln_buffer_view property_name, mln_buffer* out_value -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(property_name, "property_name")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (property_name.size == 0) { - set_thread_error("property_name must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_value == nullptr || *out_value != MLN_HANDLE_NULL) { - set_thread_error( - "out_value must not be null and *out_value must be the null handle" - ); - return MLN_STATUS_INVALID_ARGUMENT; +namespace { +auto discarded_completion() noexcept -> mln_completion; + +// Serializes blocking map destruction away from runtime executors. One shared +// worker is reserved before a map creates its own worker pool, so close never +// depends on creating a thread after that pool is saturated. +class MapTeardownLane { + public: + MapTeardownLane() { + std::thread([this]() noexcept -> void { run(); }).detach(); } - auto* light = live->map->getStyle().getLight(); - if (light == nullptr) { - set_thread_error("style light does not exist"); - return MLN_STATUS_INVALID_STATE; + auto submit(std::function teardown) -> void { + { + const std::scoped_lock lock(mutex_); + pending_.push_back(std::move(teardown)); + } + condition_.notify_one(); } - const auto property = light->getProperty(string_from_view(property_name)); - if (property.getKind() == mln::style::StyleProperty::Kind::Undefined) { - return MLN_STATUS_OK; + private: + auto run() noexcept -> void { + for (;;) { + auto teardown = std::function{}; + { + auto lock = std::unique_lock{mutex_}; + condition_.wait(lock, [this]() noexcept -> bool { + return !pending_.empty(); + }); + teardown = std::move(pending_.front()); + pending_.pop_front(); + } + try { + teardown(); + } catch (...) { + } + } } - return create_buffer(serialize_json_value(property.getValue()), out_value); + + std::mutex mutex_; + std::condition_variable condition_; + std::deque> pending_; +}; + +auto map_teardown_lane() -> MapTeardownLane& { + // Process lifetime avoids a static-destruction race with the retiring lane. + static auto* lane = new MapTeardownLane{}; + return *lane; } -auto map_set_style_transition_options( - mln_map map, const mln_style_transition_options* options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - options == nullptr || options->size < sizeof(mln_style_transition_options) - ) { - set_thread_error("options must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto ensure_map_teardown_lane() -> void { + static_cast(map_teardown_lane()); +} +} // namespace - constexpr auto known_fields = - static_cast(MLN_STYLE_TRANSITION_OPTION_DURATION) | - MLN_STYLE_TRANSITION_OPTION_DELAY | - MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; - if ((options->fields & ~known_fields) != 0U) { - set_thread_error( - "mln_style_transition_options.fields contains unknown bits" - ); - return MLN_STATUS_INVALID_ARGUMENT; +auto release_map(mln_map map, const mln_completion* completion) -> mln_status { + CompletionOperation teardown; + const auto completion_status = + create_completion_operation(completion, {}, teardown); + if (completion_status != MLN_STATUS_OK) { + return completion_status; } - - // The native default is already on, so an omitted field leaves it alone. - auto native = mln::style::TransitionOptions{}; - if ( - (options->fields & - MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS) != 0U - ) { - native.enablePlacementTransitions = options->enable_placement_transitions; + auto owned_map = handle_table().lease(map); + if (owned_map == nullptr) { + teardown.completion->reject(); + return MLN_STATUS_INVALID_ARGUMENT; } - if ((options->fields & MLN_STYLE_TRANSITION_OPTION_DURATION) != 0U) { - if (!is_native_duration_ms(options->duration_ms)) { - set_thread_error( - "transition duration_ms must fit the native duration range" - ); + { + const std::scoped_lock lock(handle_table().mutex()); + auto* live = handle_table().resolve_locked(map); + if (live == nullptr || live != owned_map.get()) { + teardown.completion->reject(); return MLN_STATUS_INVALID_ARGUMENT; } - native.duration = duration_from_milliseconds(options->duration_ms); + if (live->render_target_session != nullptr) { + teardown.completion->reject(); + set_thread_error("map still has an attached render session"); + return MLN_STATUS_INVALID_STATE; + } + const auto close_status = live->control.begin_close(); + if (close_status != MLN_STATUS_OK) { + teardown.completion->reject(); + return close_status; + } + } + struct CloseState { + std::mutex mutex; + bool ordered = false; + bool drained = false; + bool scheduled = false; + mln_map map = MLN_HANDLE_NULL; + std::shared_ptr owned_map; + std::shared_ptr operation; + }; + auto close = std::shared_ptr{}; + try { + close = std::make_shared(); + } catch (...) { + owned_map->control.abort_close(); + teardown.completion->reject(); + set_thread_error("map release could not allocate its teardown gate"); + return MLN_STATUS_NATIVE_ERROR; } - if ((options->fields & MLN_STYLE_TRANSITION_OPTION_DELAY) != 0U) { - if (!is_native_duration_ms(options->delay_ms)) { - set_thread_error( - "transition delay_ms must fit the native duration range" + close->map = map; + close->owned_map = owned_map; + close->operation = teardown.operation; + auto schedule_if_ready = [close]() noexcept -> void { + auto schedule = false; + { + const std::scoped_lock lock(close->mutex); + if (close->ordered && close->drained && !close->scheduled) { + close->scheduled = true; + schedule = true; + } + } + if (!schedule) return; + try { + map_teardown_lane().submit([close]() mutable { + auto owned = std::move(close->owned_map); + owned->callback_sources->detach(); + owned->frontend->close_renderer_observer(); + // Retire the map before reporting completion, so it can no longer + // reach host callback state. Browser backends can require main-thread + // service while their worker pool shuts down, so that backend cleanup + // continues after the public retirement boundary. + owned->map.reset(); + owned->callback_sources->release_all(); + { + const std::scoped_lock event_lock( + owned->runtime_state->event_queue->mutex + ); + owned->runtime_state->event_queue->event_maps.erase(close->map); + } + close->operation->complete( + MLN_STATUS_OK, {}, std::any{std::monostate{}} + ); + owned->frontend->shutdown_thread_pool(); + owned.reset(); + }); + } catch (...) { + close->operation->complete( + MLN_STATUS_NATIVE_ERROR, exception_message(std::current_exception()), {} ); - return MLN_STATUS_INVALID_ARGUMENT; } - native.delay = duration_from_milliseconds(options->delay_ms); + }; + const auto submit_status = submit_runtime_operation( + owned_map->runtime_state, teardown.operation, + [close, schedule_if_ready]() mutable -> void { + if (close->owned_map->still_image_operation != nullptr) { + close->owned_map->still_image_operation->complete( + MLN_STATUS_CANCELLED, "map closed before still image completed", {} + ); + } + if ( + auto release = + std::exchange(close->owned_map->still_image_release_submission, {}) + ) { + release(); + } + { + const std::scoped_lock lock(close->mutex); + close->ordered = true; + } + schedule_if_ready(); + } + ); + if (submit_status != MLN_STATUS_OK) { + owned_map->control.abort_close(); + teardown.completion->reject(); + return submit_status; } - - live->map->getStyle().setTransitionOptions(native); + { + const std::scoped_lock lock(handle_table().mutex()); + static_cast(handle_table().remove_locked(map)); + } + release_runtime_map(owned_map->runtime); + owned_map->control.notify_when_drained([close, schedule_if_ready]() { + { + const std::scoped_lock lock(close->mutex); + close->drained = true; + } + schedule_if_ready(); + }); + teardown.completion->accept(); return MLN_STATUS_OK; } - -auto map_get_style_transition_options( - mln_map map, mln_style_transition_options* out_options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +PendingMapResult::~PendingMapResult() { + if (value_ == MLN_HANDLE_NULL) { + return; } - if ( - out_options == nullptr || - out_options->size < sizeof(mln_style_transition_options) - ) { - set_thread_error("out_options must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; + try { + const auto completion = discarded_completion(); + static_cast(release_map(value_, &completion)); + } catch (...) { } +} - const auto native = live->map->getStyle().getTransitionOptions(); - auto result = style_transition_options_default(); - // MapLibre Native always holds this one, so it always reports as present. - result.fields |= MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; - result.enable_placement_transitions = native.enablePlacementTransitions; - if (native.duration) { - result.fields |= MLN_STYLE_TRANSITION_OPTION_DURATION; - result.duration_ms = milliseconds_from_duration(*native.duration); +auto map_request_repaint(mln_map map, const mln_completion* completion) + -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = handle_table().lease(map); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (native.delay) { - result.fields |= MLN_STYLE_TRANSITION_OPTION_DELAY; - result.delay_ms = milliseconds_from_duration(*native.delay); + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - *out_options = result; - return MLN_STATUS_OK; + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + return submit_runtime_command( + live->runtime_state, + [live, submission = std::move(submission), + completion_state](uint64_t) mutable -> void { + if (live->map_mode != MLN_MAP_MODE_CONTINUOUS) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_FAILED, + MLN_STATUS_INVALID_STATE, 0, "map is not in continuous mode" + ); + return; + } + try { + live->map->triggerRepaint(); + const auto generation = publish_map_snapshot(*live); + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_COMMITTED, MLN_STATUS_OK, + generation + ); + } catch (...) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_FAILED, + MLN_STATUS_NATIVE_ERROR, 0, + exception_message(std::current_exception()) + ); + } + }, + completion_state + ); } -auto map_set_layer_property( - mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer_view value +auto map_request_still_image_start( + mln_map map, const mln_completion* completion ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(property_name, "property_name") - ) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0 || property_name.size == 0) { - set_thread_error("layer_id and property_name must not be empty"); + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = handle_table().lease(map); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - - auto document = mln::JSDocument{}; - if (!parse_json_document(value, "layer property", document)) { - return MLN_STATUS_INVALID_ARGUMENT; + if (!is_still_map_mode(live->map_mode)) { + set_thread_error("map is not in static or tile mode"); + return MLN_STATUS_INVALID_STATE; } - - auto* layer = live->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - - auto error = layer->setProperty( - string_from_view(property_name), - mln::style::conversion::Convertible{ - static_cast(&document) + auto control_lease = ControlLease{&live->control}; + auto submission = std::make_shared(std::move(control_lease)); + auto submission_released = std::make_shared(false); + auto release_submission = [submission, + submission_released]() noexcept -> void { + if (!submission_released->exchange(true)) { + submission->reset(); + } + }; + auto completion_state = std::make_shared(*completion); + auto state = std::make_shared( + [completion_state](mln_status status, std::string diagnostic, std::any) { + complete(completion_state, status, std::move(diagnostic)); } ); - if (error) { - set_style_conversion_error("layer property", *error); - return MLN_STATUS_INVALID_ARGUMENT; + const auto submit_status = submit_runtime_operation( + live->runtime_state, state, + [map, live = std::move(live), state, release_submission]() mutable -> void { + if (live->still_image_request_pending) { + state->complete( + MLN_STATUS_INVALID_STATE, + "map already has a pending still-image request", {} + ); + return; + } + try { + live->still_image_request_pending = true; + live->still_image_operation = state; + live->still_image_release_submission = release_submission; + live->map->renderStill( + [map, release_submission](std::exception_ptr error) mutable -> void { + finish_still_image_request(map, error); + release_submission(); + } + ); + } catch (...) { + live->still_image_request_pending = false; + live->still_image_operation.reset(); + state->complete( + MLN_STATUS_NATIVE_ERROR, exception_message(std::current_exception()), + {} + ); + } + } + ); + if (submit_status != MLN_STATUS_OK) { + completion_state->reject(); + } else { + completion_state->accept(); } + return submit_status; +} - return MLN_STATUS_OK; +// Render-facing helpers hold an attached-session child relationship, so +// asynchronous map close cannot retire their state. +auto map_scale_factor(mln_map map) -> double { + const auto* live = handle_table().try_resolve(map); + return live == nullptr ? default_scale_factor + : live->logical_extent.scale_factor; } -auto map_get_layer_property( - mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, - mln_buffer* out_value -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - !validate_string_view(layer_id, "layer_id") || - !validate_string_view(property_name, "property_name") - ) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0 || property_name.size == 0) { - set_thread_error("layer_id and property_name must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_value == nullptr || *out_value != MLN_HANDLE_NULL) { - set_thread_error( - "out_value must not be null and *out_value must be the null handle" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } +// Returns worker-owned native state. Callers must already run on the runtime +// worker or use the posting helpers below. +auto map_native(MapObject* map) -> mln::Map* { return map->map.get(); } - auto* layer = live->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; +namespace { + +void discard_completion(void*, const mln_completion_result*) noexcept {} + +auto discarded_completion() noexcept -> mln_completion { + return mln_completion{ + .size = sizeof(mln_completion), + .callback = discard_completion, + .user_data = nullptr, + .release_user_data = nullptr, + }; +} + +} // namespace + +// Render-session resize enters the same ordered extent command as public +// resize, so one path owns logical extent and scale after map creation. +auto map_post_resize(mln_map map, mln_logical_extent extent) -> mln_status { + const auto completion = discarded_completion(); + return map_resize(map, extent, &completion); +} + +auto map_post_trigger_repaint(mln_map map) -> mln_status { + const auto completion = discarded_completion(); + return map_request_repaint(map, &completion); +} + +auto map_latest_update(mln_map map) -> std::shared_ptr { + auto* live = handle_table().try_resolve(map); + return live == nullptr ? nullptr : live->frontend->latest_update(); +} +auto map_latest_update_generation(mln_map map) noexcept -> uint64_t { + auto* live = handle_table().try_resolve(map); + return live == nullptr ? 0 : live->frontend->latest_update_generation(); +} +auto map_latest_update_snapshot(mln_map map, uint64_t& out_generation) + -> std::shared_ptr { + auto* live = handle_table().try_resolve(map); + if (live == nullptr) { + out_generation = 0; + return nullptr; } + return live->frontend->latest_update_snapshot(out_generation); +} - const auto property = layer->getProperty(string_from_view(property_name)); - if (property.getKind() == mln::style::StyleProperty::Kind::Undefined) { - return MLN_STATUS_OK; - } - return create_buffer(serialize_json_value(property.getValue()), out_value); +auto map_feature_state_snapshot(mln_map map) + -> std::shared_ptr { + auto* live = handle_table().try_resolve(map); + return live == nullptr ? std::make_shared() + : live->feature_state.snapshot(); } -auto map_set_layer_filter( - mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter +auto map_set_render_session_publish_callback( + mln_map map, std::function callback ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); + const auto live = handle_table().lease(map); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } + live->frontend->set_session_publish_callback(std::move(callback)); + return MLN_STATUS_OK; +} - auto* layer = live->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto map_renderer_observer(mln_map map) -> mln::RendererObserver* { + auto* live = handle_table().try_resolve(map); + return live == nullptr ? nullptr : live->frontend->renderer_observer(); +} - if (filter == nullptr) { - layer->setFilter(mln::style::Filter{}); - return MLN_STATUS_OK; +auto map_run_render_jobs(mln_map map) -> void { + if ( + auto* live = handle_table().try_resolve(map); live != nullptr + ) { + live->frontend->run_render_jobs(); } +} - auto native_filter = to_native_style_filter(filter); - if (!native_filter) { - return MLN_STATUS_INVALID_ARGUMENT; +auto map_quiesce_render_workers(mln_map map) -> void { + auto* live = handle_table().try_resolve(map); + if (live != nullptr) { + live->frontend->wait_thread_pool(); } - - layer->setFilter(*native_filter); - return MLN_STATUS_OK; } -auto map_get_layer_filter( - mln_map map, mln_buffer_view layer_id, mln_buffer* out_filter -) -> mln_status { +// The handle-table lock keeps the map live across the attached-session claim. +auto map_attach_render_target_session(mln_map map, void* session) + -> mln_status { + const std::scoped_lock lock(handle_table().mutex()); MapObject* live = nullptr; - const auto status = validate_map(map, live); + const auto status = validate_map_live_locked(map, live); if (status != MLN_STATUS_OK) { return status; } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_filter == nullptr || *out_filter != MLN_HANDLE_NULL) { - set_thread_error( - "out_filter must not be null and *out_filter must be the null handle" - ); + if (session == nullptr) { + set_thread_error("render session must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - - auto* layer = live->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; + // begin_close() runs under this same lock, so this check makes the claim + // atomic with close's empty-session preflight. + if (live->control.is_closing()) { + set_thread_error("map is closing"); + return MLN_STATUS_INVALID_STATE; } - - const auto filter = layer->getFilter().serialize(); - if (filter.is()) { - return MLN_STATUS_OK; + if (live->render_target_session != nullptr) { + set_thread_error("map already has an attached render session"); + return MLN_STATUS_INVALID_STATE; } - return create_buffer(serialize_json_value(filter), out_filter); + live->render_target_session = session; + return MLN_STATUS_OK; } -namespace { - -auto resolve_layer_for_access( - mln_map map, mln_buffer_view layer_id, mln::style::Layer*& out_layer -) -> mln_status { +// The handle-table lock keeps the map live across the attached-session clear. +auto map_detach_render_target_session(mln_map map, void* session) + -> mln_status { + const std::scoped_lock lock(handle_table().mutex()); MapObject* live = nullptr; - const auto status = validate_map(map, live); + const auto status = validate_map_live_locked(map, live); if (status != MLN_STATUS_OK) { return status; } - if (!validate_string_view(layer_id, "layer_id")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (layer_id.size == 0) { - set_thread_error("layer_id must not be empty"); + if (session == nullptr) { + set_thread_error("render session must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - - auto* layer = live->map->getStyle().getLayer(string_from_view(layer_id)); - if (layer == nullptr) { - set_thread_error("layer does not exist"); - return MLN_STATUS_INVALID_ARGUMENT; + if (live->render_target_session != session) { + set_thread_error("render session is not attached to this map"); + return MLN_STATUS_INVALID_STATE; } - out_layer = layer; + live->render_target_session = nullptr; return MLN_STATUS_OK; } -// MapLibre's setProperty path logs a warning and does nothing when a layer type -// takes no source, so the typed setters reject that case instead. -auto require_layer_takes_source( - const mln::style::Layer& layer, const char* field -) -> bool { - if ( - layer.getTypeInfo()->source == mln::style::LayerTypeInfo::Source::Required - ) { - return true; - } - auto message = std::string{"layer type does not take a "} + field + - "; layer id is " + layer.getID(); - set_thread_error(message.c_str()); - return false; -} - -// MapLibre stores the layer zoom range as floats, and infinities survive the -// narrowing that bounds a layer to one end of the range. -auto validate_layer_zoom(double zoom, const char* field) -> bool { - if (!std::isnan(zoom)) { - return true; - } - auto message = std::string{field} + " must not be NaN"; - set_thread_error(message.c_str()); - return false; -} - -} // namespace - -auto map_set_layer_source_layer( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); +auto map_set_style_url(mln_map map, const char* url) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); if (status != MLN_STATUS_OK) { return status; } - if (!validate_string_view(source_layer, "source_layer")) { + if (url == nullptr) { + set_thread_error("url must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (!require_layer_takes_source(*layer, "source-layer")) { - return MLN_STATUS_INVALID_ARGUMENT; + // Parse failures reported on this stack use runtime-worker state, so no + // additional lock or queued diagnostic is needed. + live->event_state->style_load_failed = false; + live->event_state->style_load_failure.clear(); + live->map->getStyle().loadURL(url); + if (live->event_state->style_load_failed) { + set_thread_error(live->event_state->style_load_failure.c_str()); + return MLN_STATUS_NATIVE_ERROR; } - - layer->setSourceLayer(string_from_view(source_layer)); return MLN_STATUS_OK; } -auto map_copy_layer_source_layer( - mln_map map, mln_buffer_view layer_id, char* out_source_layer, - size_t source_layer_capacity, size_t* out_source_layer_size -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; - } - return copy_text( - layer->getSourceLayer(), out_source_layer, source_layer_capacity, - out_source_layer_size, "source_layer_capacity" - ); -} - -auto map_set_layer_source_id( - mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); +auto map_set_style_json(mln_map map, mln_buffer_view json) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); if (status != MLN_STATUS_OK) { return status; } - if (!validate_string_view(source_id, "source_id")) { + if (!validate_bytes(json, "style JSON")) { return MLN_STATUS_INVALID_ARGUMENT; } - if (source_id.size == 0) { - set_thread_error("source_id must not be empty"); - return MLN_STATUS_INVALID_ARGUMENT; + try { + live->event_state->style_load_failed = false; + live->event_state->style_load_failure.clear(); + live->map->getStyle().loadJSON( + std::string{reinterpret_cast(json.data), json.size} + ); + } catch (const std::exception& exception) { + // The diagnostic is this call's own status text, so it is set whatever the + // mask selects; only the event is gated. + set_thread_error(exception.what()); + if ( + event_selected( + live->event_state->mask, MLN_RUNTIME_EVENT_MAP_LOADING_FAILED + ) + ) { + push_runtime_map_event( + live->runtime, map, MLN_RUNTIME_EVENT_MAP_LOADING_FAILED, 0, + exception.what() + ); + } + return MLN_STATUS_NATIVE_ERROR; } - if (!require_layer_takes_source(*layer, "source")) { - return MLN_STATUS_INVALID_ARGUMENT; + if (live->event_state->style_load_failed) { + set_thread_error(live->event_state->style_load_failure.c_str()); + return MLN_STATUS_NATIVE_ERROR; } - - layer->setSourceID(string_from_view(source_id)); return MLN_STATUS_OK; } -auto map_copy_layer_source_id( - mln_map map, mln_buffer_view layer_id, char* out_source_id, - size_t source_id_capacity, size_t* out_source_id_size +auto start_map_string_operation( + mln_map map, const mln_completion* completion, + std::function read ) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = handle_table().lease(map); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - return copy_text( - layer->getSourceID(), out_source_id, source_id_capacity, out_source_id_size, - "source_id_capacity" + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + auto state = + std::make_shared([completion_state]( + mln_status status, + std::string diagnostic, std::any result + ) { + if (status != MLN_STATUS_OK) { + complete(completion_state, status, std::move(diagnostic)); + return; + } + auto* text = std::any_cast(&result); + if (text == nullptr) { + complete( + completion_state, MLN_STATUS_NATIVE_ERROR, + "map string operation produced an invalid result" + ); + return; + } + completion_state->resolve( + [text = std::move(*text)](const mln_completion& descriptor) { + const auto view = + mln_buffer_view{.data = text.data(), .size = text.size()}; + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + &view, 1 + ); + } + ); + }); + const auto submit_status = submit_runtime_operation( + live->runtime_state, state, + [live = std::move(live), state, read = std::move(read), + submission = std::move(submission)]() mutable -> void { + try { + state->complete(MLN_STATUS_OK, {}, std::any{read(*live)}); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, exception_message(std::current_exception()), + {} + ); + } + } ); + if (submit_status == MLN_STATUS_OK) + completion_state->accept(); + else + completion_state->reject(); + return submit_status; } -auto map_set_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double min_zoom -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; - } - if (!validate_layer_zoom(min_zoom, "min_zoom")) { - return MLN_STATUS_INVALID_ARGUMENT; - } - - layer->setMinZoom(static_cast(min_zoom)); - return MLN_STATUS_OK; +namespace { +auto feature_state_string_from_view(mln_buffer_view value) -> std::string { + return value.data == nullptr + ? std::string{} + : std::string{static_cast(value.data), value.size}; } +} // namespace -auto map_get_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double* out_min_zoom +auto map_set_feature_state( + mln_map map, const mln_feature_state_selector* selector, mln_buffer_view state ) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); + MapObject* live = nullptr; + const auto status = validate_map(map, live); if (status != MLN_STATUS_OK) { return status; } - if (out_min_zoom == nullptr) { - set_thread_error("out_min_zoom must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; + const auto selector_status = validate_feature_state_selector(selector, true); + if (selector_status != MLN_STATUS_OK) { + return selector_status; } - *out_min_zoom = static_cast(layer->getMinZoom()); - return MLN_STATUS_OK; -} - -auto map_set_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double max_zoom -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; + auto native_state = to_native_json_value(state); + if (!native_state) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (!validate_layer_zoom(max_zoom, "max_zoom")) { + const auto* state_object = native_state->getObject(); + if (state_object == nullptr) { + set_thread_error("feature state value must be a JSON object"); return MLN_STATUS_INVALID_ARGUMENT; } - layer->setMaxZoom(static_cast(max_zoom)); + live->feature_state.set( + feature_state_string_from_view(selector->source_id), + feature_state_source_layer(*selector), + feature_state_string_from_view(selector->feature_id), *state_object + ); + live->map->triggerRepaint(); return MLN_STATUS_OK; } -auto map_get_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double* out_max_zoom +auto map_get_feature_state_start( + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion ) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_max_zoom == nullptr) { - set_thread_error("out_max_zoom must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; + const auto selector_status = validate_feature_state_selector(selector, true); + if (selector_status != MLN_STATUS_OK) { + return selector_status; } - - *out_max_zoom = static_cast(layer->getMaxZoom()); - return MLN_STATUS_OK; + auto source_id = feature_state_string_from_view(selector->source_id); + auto source_layer = feature_state_source_layer(*selector); + auto feature_id = feature_state_string_from_view(selector->feature_id); + return start_map_string_operation( + map, completion, + [source_id = std::move(source_id), source_layer = std::move(source_layer), + feature_id = std::move(feature_id)](MapObject& live) -> std::string { + auto state = live.feature_state.get(source_id, source_layer, feature_id); + return serialize_json_value(mln::Value{std::move(state)}); + } + ); } -auto map_set_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t visibility +auto map_remove_feature_state( + mln_map map, const mln_feature_state_selector* selector ) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); + MapObject* live = nullptr; + const auto status = validate_map(map, live); if (status != MLN_STATUS_OK) { return status; } - auto native_visibility = mln::style::VisibilityType::Visible; - switch (visibility) { - case MLN_STYLE_LAYER_VISIBILITY_VISIBLE: - native_visibility = mln::style::VisibilityType::Visible; - break; - case MLN_STYLE_LAYER_VISIBILITY_NONE: - native_visibility = mln::style::VisibilityType::None; - break; - default: - set_thread_error("visibility is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; + const auto selector_status = validate_feature_state_selector(selector, false); + if (selector_status != MLN_STATUS_OK) { + return selector_status; } - layer->setVisibility(native_visibility); + live->feature_state.remove( + feature_state_string_from_view(selector->source_id), + feature_state_source_layer(*selector), + optional_selector_string( + *selector, MLN_FEATURE_STATE_SELECTOR_FEATURE_ID, selector->feature_id + ), + optional_selector_string( + *selector, MLN_FEATURE_STATE_SELECTOR_STATE_KEY, selector->state_key + ) + ); + live->map->triggerRepaint(); return MLN_STATUS_OK; } -auto map_get_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t* out_visibility -) -> mln_status { - mln::style::Layer* layer = nullptr; - const auto status = resolve_layer_for_access(map, layer_id, layer); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_visibility == nullptr) { - set_thread_error("out_visibility must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } +auto map_loaded_style_json_start(mln_map map, const mln_completion* completion) + -> mln_status { + return start_map_string_operation( + map, completion, [](MapObject& live) -> std::string { + return live.map->getStyle().getJSON(); + } + ); +} - *out_visibility = layer->getVisibility() == mln::style::VisibilityType::None - ? MLN_STYLE_LAYER_VISIBILITY_NONE - : MLN_STYLE_LAYER_VISIBILITY_VISIBLE; - return MLN_STATUS_OK; +auto map_style_url_start(mln_map map, const mln_completion* completion) + -> mln_status { + return start_map_string_operation( + map, completion, + [](MapObject& live) -> std::string { return live.map->getStyle().getURL(); } + ); } -auto map_get_camera(mln_map map, mln_camera_options* out_camera) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +auto map_set_event_mask( + mln_map map, uint64_t mask, const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if ( + completion_status != MLN_STATUS_OK || + (mask & ~static_cast(MLN_RUNTIME_EVENT_MASK_ALL)) != 0U + ) { + if (completion_status == MLN_STATUS_OK) + set_thread_error("mask must be valid"); + return MLN_STATUS_INVALID_ARGUMENT; } - if (out_camera == nullptr || out_camera->size < sizeof(mln_camera_options)) { - set_thread_error("out_camera must not be null and must have a valid size"); + auto live = handle_table().lease(map); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - *out_camera = from_native_camera(live->map->getCameraOptions()); - return MLN_STATUS_OK; + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; + } + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + return submit_runtime_command( + live->runtime_state, + [live, mask, submission = std::move(submission), + completion_state](uint64_t) mutable -> void { + try { + live->event_state->mask.store(mask, std::memory_order_relaxed); + const auto generation = publish_map_snapshot(*live); + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_COMMITTED, MLN_STATUS_OK, + generation + ); + } catch (...) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_FAILED, + MLN_STATUS_NATIVE_ERROR, 0, + exception_message(std::current_exception()) + ); + } + }, + completion_state + ); } -auto map_jump_to(mln_map map, const mln_camera_options* camera) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); +auto map_camera_snapshot_get( + mln_map map, mln_camera_options* out_camera, uint64_t* out_generation +) -> mln_status { + if ( + out_camera == nullptr || out_camera->size < sizeof(mln_camera_options) || + out_generation == nullptr + ) { + set_thread_error("camera output and generation must be valid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto snapshot = mln_map_snapshot{}; + snapshot.size = sizeof(mln_map_snapshot); + const auto status = map_snapshot_get(map, &snapshot); if (status != MLN_STATUS_OK) { return status; } - const auto camera_status = validate_camera_options(camera); - if (camera_status != MLN_STATUS_OK) { - return camera_status; - } - live->map->jumpTo(to_native_camera(*camera)); + *out_camera = snapshot.camera; + *out_generation = snapshot.generation; return MLN_STATUS_OK; } -auto map_ease_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto camera_status = validate_camera_options(camera); - if (camera_status != MLN_STATUS_OK) { - return camera_status; - } - const auto animation_status = validate_animation_options(animation); - if (animation_status != MLN_STATUS_OK) { - return animation_status; - } +namespace { - live->map->easeTo( - to_native_camera(*camera), - to_native_animation(live->runtime, map, live->event_state, animation) +template +auto submit_camera_command( + mln_map map, Mutation mutation, const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = handle_table().lease(map); + if (live == nullptr) return MLN_STATUS_INVALID_ARGUMENT; + if (!live->control.acquire()) return MLN_STATUS_INVALID_STATE; + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + return submit_runtime_command( + live->runtime_state, + [map, live, mutation = std::move(mutation), + submission = std::move(submission), + completion_state](uint64_t) mutable -> void { + try { + mutation(*live, map); + const auto generation = publish_map_snapshot(*live); + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_COMMITTED, MLN_STATUS_OK, + generation + ); + } catch (...) { + complete_command( + completion_state, MLN_COMMAND_DISPOSITION_FAILED, + MLN_STATUS_NATIVE_ERROR, 0, + exception_message(std::current_exception()) + ); + } + }, + completion_state ); - return MLN_STATUS_OK; } -auto map_fly_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation +} // namespace + +auto map_update_camera( + mln_map map, const mln_camera_update* update, const mln_completion* completion ) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + if (update == nullptr || update->size < sizeof(mln_camera_update)) { + set_thread_error("camera update must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; } - const auto camera_status = validate_camera_options(camera); + if ( + update->mode > MLN_CAMERA_UPDATE_MODE_FLY || + update->gesture_phase > MLN_GESTURE_PHASE_CANCEL + ) { + set_thread_error("camera update mode or gesture phase is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto camera_status = validate_camera_options(&update->camera); if (camera_status != MLN_STATUS_OK) { return camera_status; } - const auto animation_status = validate_animation_options(animation); + const auto animation_status = validate_animation_options(&update->animation); if (animation_status != MLN_STATUS_OK) { return animation_status; } - - live->map->flyTo( - to_native_camera(*camera), - to_native_animation(live->runtime, map, live->event_state, animation) + const auto copied = *update; + return submit_camera_command( + map, + [copied](MapObject& live, mln_map map_handle) -> void { + if ( + copied.gesture_phase == MLN_GESTURE_PHASE_BEGIN || + copied.gesture_phase == MLN_GESTURE_PHASE_UPDATE + ) { + live.map->setGestureInProgress(true); + } + switch (copied.mode) { + case MLN_CAMERA_UPDATE_MODE_JUMP: + live.map->jumpTo(to_native_camera(copied.camera)); + break; + case MLN_CAMERA_UPDATE_MODE_EASE: + live.map->easeTo( + to_native_camera(copied.camera), + to_native_animation( + live.runtime, map_handle, live.event_state, &copied.animation + ) + ); + break; + case MLN_CAMERA_UPDATE_MODE_FLY: + live.map->flyTo( + to_native_camera(copied.camera), + to_native_animation( + live.runtime, map_handle, live.event_state, &copied.animation + ) + ); + break; + default: + break; + } + if (copied.gesture_phase == MLN_GESTURE_PHASE_CANCEL) { + live.map->cancelTransitions(); + } + if ( + copied.gesture_phase == MLN_GESTURE_PHASE_END || + copied.gesture_phase == MLN_GESTURE_PHASE_CANCEL + ) { + live.map->setGestureInProgress(false); + } + }, + completion ); - return MLN_STATUS_OK; } -auto map_get_projection_mode(mln_map map, mln_projection_mode* out_mode) - -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +auto map_apply_camera_delta( + mln_map map, const mln_camera_delta* delta, const mln_completion* completion +) -> mln_status { + if (delta == nullptr || delta->size < sizeof(mln_camera_delta)) { + set_thread_error("camera delta must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; } - if (out_mode == nullptr || out_mode->size < sizeof(mln_projection_mode)) { - set_thread_error("out_mode must not be null and must have a valid size"); + if (delta->kind > MLN_CAMERA_DELTA_PITCH) { + set_thread_error("camera delta kind is invalid"); return MLN_STATUS_INVALID_ARGUMENT; } - - *out_mode = from_native_projection_mode(live->map->getProjectionMode()); - return MLN_STATUS_OK; + if ( + delta->kind == MLN_CAMERA_DELTA_MOVE && + validate_screen_point(delta->offset) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + delta->kind == MLN_CAMERA_DELTA_SCALE && + (!std::isfinite(delta->amount) || delta->amount <= 0) + ) { + set_thread_error("camera scale must be finite and positive"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + (delta->kind == MLN_CAMERA_DELTA_BEARING || + delta->kind == MLN_CAMERA_DELTA_PITCH) && + !std::isfinite(delta->amount) + ) { + set_thread_error("camera angle delta must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + delta->has_anchor && delta->kind != MLN_CAMERA_DELTA_SCALE && + delta->kind != MLN_CAMERA_DELTA_BEARING + ) { + set_thread_error("only scale and bearing camera deltas accept an anchor"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + delta->has_anchor && validate_screen_point(delta->anchor) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (validate_animation_options(&delta->animation) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto copied = *delta; + return submit_camera_command( + map, + [copied](MapObject& live, mln_map map_handle) -> void { + const auto animation = to_native_animation( + live.runtime, map_handle, live.event_state, &copied.animation + ); + const auto anchor = + copied.has_anchor + ? std::optional{to_native_screen_point( + copied.anchor + )} + : std::nullopt; + switch (copied.kind) { + case MLN_CAMERA_DELTA_MOVE: + live.map->moveBy(to_native_screen_point(copied.offset), animation); + break; + case MLN_CAMERA_DELTA_SCALE: + live.map->scaleBy(copied.amount, anchor, animation); + break; + case MLN_CAMERA_DELTA_BEARING: { + const auto current = live.map->getCameraOptions(); + auto camera = mln::CameraOptions{}.withBearing( + current.bearing.value_or(0) + copied.amount + ); + if (anchor.has_value()) camera.withAnchor(*anchor); + live.map->easeTo(camera, animation); + break; + } + case MLN_CAMERA_DELTA_PITCH: + live.map->pitchBy(-copied.amount, animation); + break; + default: + break; + } + }, + completion + ); } -auto map_set_projection_mode(mln_map map, const mln_projection_mode* mode) +auto map_camera_query_start(mln_map map, const mln_completion* completion) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = handle_table().lease(map); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - const auto mode_status = validate_projection_mode_options(mode); - if (mode_status != MLN_STATUS_OK) { - return mode_status; + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - - live->map->setProjectionMode(to_native_projection_mode(*mode)); - return MLN_STATUS_OK; + auto submission = std::make_shared(&live->control); + auto completion_state = std::make_shared(*completion); + auto state = + std::make_shared([completion_state]( + mln_status status, + std::string diagnostic, std::any result + ) { + auto* value = std::any_cast(&result); + if (status != MLN_STATUS_OK || value == nullptr) { + complete( + completion_state, + status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK ? "camera query produced an invalid result" + : std::move(diagnostic) + ); + return; + } + complete_value(completion_state, MLN_STATUS_OK, {}, *value); + }); + const auto submit_status = submit_runtime_operation( + live->runtime_state, state, + [live = std::move(live), state, + submission = std::move(submission)]() mutable -> void { + try { + const auto generation = publish_map_snapshot(*live); + state->complete( + MLN_STATUS_OK, {}, + std::any{mln_camera_query_result{ + .size = sizeof(mln_camera_query_result), + .reserved = 0, + .generation = generation, + .camera = from_native_camera(live->map->getCameraOptions()) + }} + ); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, exception_message(std::current_exception()), + {} + ); + } + } + ); + if (submit_status == MLN_STATUS_OK) + completion_state->accept(); + else + completion_state->reject(); + return submit_status; } auto map_set_debug_options(mln_map map, uint32_t options) -> mln_status { @@ -7154,20 +3991,6 @@ auto map_set_debug_options(mln_map map, uint32_t options) -> mln_status { return MLN_STATUS_OK; } -auto map_get_debug_options(mln_map map, uint32_t* out_options) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_options == nullptr) { - set_thread_error("out_options must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_options = from_native_debug_options(live->map->getDebug()); - return MLN_STATUS_OK; -} - auto map_set_rendering_stats_view_enabled(mln_map map, bool enabled) -> mln_status { MapObject* live = nullptr; @@ -7179,35 +4002,6 @@ auto map_set_rendering_stats_view_enabled(mln_map map, bool enabled) return MLN_STATUS_OK; } -auto map_get_rendering_stats_view_enabled(mln_map map, bool* out_enabled) - -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_enabled == nullptr) { - set_thread_error("out_enabled must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_enabled = live->map->isRenderingStatsViewEnabled(); - return MLN_STATUS_OK; -} - -auto map_is_fully_loaded(mln_map map, bool* out_loaded) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_loaded == nullptr) { - set_thread_error("out_loaded must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_loaded = live->map->isFullyLoaded(); - return MLN_STATUS_OK; -} - auto map_dump_debug_logs(mln_map map) -> mln_status { MapObject* live = nullptr; const auto status = validate_map(map, live); @@ -7218,64 +4012,6 @@ auto map_dump_debug_logs(mln_map map) -> mln_status { return MLN_STATUS_OK; } -auto map_get_size( - mln_map map, uint32_t* out_width, uint32_t* out_height, - double* out_scale_factor -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_width == nullptr || out_height == nullptr || out_scale_factor == nullptr - ) { - set_thread_error( - "out_width, out_height, and out_scale_factor must not be null" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - - const auto options = live->map->getMapOptions(); - const auto size = options.size(); - *out_width = size.width; - *out_height = size.height; - *out_scale_factor = live->scale_factor; - return MLN_STATUS_OK; -} - -auto map_get_viewport_options( - mln_map map, mln_map_viewport_options* out_options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_options == nullptr || - out_options->size < sizeof(mln_map_viewport_options) - ) { - set_thread_error("out_options must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - const auto options = live->map->getMapOptions(); - *out_options = mln_map_viewport_options{ - .size = sizeof(mln_map_viewport_options), - .fields = static_cast(MLN_MAP_VIEWPORT_OPTION_NORTH_ORIENTATION) | - MLN_MAP_VIEWPORT_OPTION_CONSTRAIN_MODE | - MLN_MAP_VIEWPORT_OPTION_VIEWPORT_MODE | - MLN_MAP_VIEWPORT_OPTION_FRUSTUM_OFFSET, - .north_orientation = - from_native_north_orientation(options.northOrientation()), - .constrain_mode = from_native_constrain_mode(options.constrainMode()), - .viewport_mode = from_native_viewport_mode(options.viewportMode()), - .frustum_offset = from_native_edge_insets(live->map->getFrustumOffset()) - }; - return MLN_STATUS_OK; -} - auto map_set_viewport_options( mln_map map, const mln_map_viewport_options* options ) -> mln_status { @@ -7308,37 +4044,6 @@ auto map_set_viewport_options( return MLN_STATUS_OK; } -auto map_get_tile_options(mln_map map, mln_map_tile_options* out_options) - -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_options == nullptr || out_options->size < sizeof(mln_map_tile_options) - ) { - set_thread_error("out_options must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - *out_options = mln_map_tile_options{ - .size = sizeof(mln_map_tile_options), - .fields = static_cast(MLN_MAP_TILE_OPTION_PREFETCH_ZOOM_DELTA) | - MLN_MAP_TILE_OPTION_LOD_MIN_RADIUS | - MLN_MAP_TILE_OPTION_LOD_SCALE | - MLN_MAP_TILE_OPTION_LOD_PITCH_THRESHOLD | - MLN_MAP_TILE_OPTION_LOD_ZOOM_SHIFT | MLN_MAP_TILE_OPTION_LOD_MODE, - .prefetch_zoom_delta = live->map->getPrefetchZoomDelta(), - .lod_min_radius = live->map->getTileLodMinRadius(), - .lod_scale = live->map->getTileLodScale(), - .lod_pitch_threshold = live->map->getTileLodPitchThreshold(), - .lod_zoom_shift = live->map->getTileLodZoomShift(), - .lod_mode = from_native_tile_lod_mode(live->map->getTileLodMode()) - }; - return MLN_STATUS_OK; -} - auto map_set_tile_options(mln_map map, const mln_map_tile_options* options) -> mln_status { MapObject* live = nullptr; @@ -7490,45 +4195,159 @@ auto map_lat_lngs_for_pixels( return MLN_STATUS_OK; } -auto map_projection_create(mln_map map, mln_map_projection* out_projection) - -> mln_status { - if (out_projection == nullptr) { - set_thread_error("out_projection must not be null"); +auto map_pixel_for_lat_lng_start( + mln_map map, mln_lat_lng coordinate, const mln_completion* completion +) -> mln_status { + if (validate_lat_lng(coordinate) != MLN_STATUS_OK) { return MLN_STATUS_INVALID_ARGUMENT; } - if (*out_projection != MLN_HANDLE_NULL) { - set_thread_error("out_projection must point to the null handle"); + return start_geometry_operation( + map, GeometryOperationKind::PixelForCoordinate, + [map, coordinate](GeometryOperationResult& result) { + return map_pixel_for_lat_lng(map, coordinate, &result.point); + }, + completion + ); +} + +auto map_lat_lng_for_pixel_start( + mln_map map, mln_screen_point point, const mln_completion* completion +) -> mln_status { + if (validate_screen_point(point) != MLN_STATUS_OK) { return MLN_STATUS_INVALID_ARGUMENT; } - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; + return start_geometry_operation( + map, GeometryOperationKind::CoordinateForPixel, + [map, point](GeometryOperationResult& result) { + return map_lat_lng_for_pixel(map, point, &result.coordinate); + }, + completion + ); +} + +auto map_pixels_for_lat_lngs_start( + mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, + const mln_completion* completion +) -> mln_status { + if ( + validate_lat_lng_array(coordinates, coordinate_count, true) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto copied = std::vector{}; + if (coordinate_count != 0) { + copied.assign(coordinates, coordinates + coordinate_count); + } + return start_geometry_operation( + map, GeometryOperationKind::PixelsForCoordinates, + [map, copied = std::move(copied)](GeometryOperationResult& result) { + result.points.resize(copied.size()); + return map_pixels_for_lat_lngs( + map, copied.data(), copied.size(), result.points.data() + ); + }, + completion + ); +} + +auto map_lat_lngs_for_pixels_start( + mln_map map, const mln_screen_point* points, size_t point_count, + const mln_completion* completion +) -> mln_status { + if (validate_screen_point_array(points, point_count) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto copied = std::vector{}; + if (point_count != 0) { + copied.assign(points, points + point_count); } + return start_geometry_operation( + map, GeometryOperationKind::CoordinatesForPixels, + [map, copied = std::move(copied)](GeometryOperationResult& result) { + result.coordinates.resize(copied.size()); + return map_lat_lngs_for_pixels( + map, copied.data(), copied.size(), result.coordinates.data() + ); + }, + completion + ); +} - auto owned_projection = std::make_shared(); - owned_projection->projection = - std::make_unique(*live->map); +auto map_projection_create_start(mln_map map, const mln_completion* completion) + -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; - *out_projection = - handle_table().insert(std::move(owned_projection)); - return MLN_STATUS_OK; + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + auto parent = table.lease_locked(map); + if (parent == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto runtime = lease_runtime(parent->runtime); + if (runtime == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto completion_state = std::make_shared(*completion); + auto state = std::make_shared([completion_state]( + mln_status status, + std::string diagnostic, + std::any result + ) { + auto* projection = + std::any_cast>(&result); + if ( + status != MLN_STATUS_OK || projection == nullptr || *projection == nullptr + ) { + complete( + completion_state, + status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK + ? "projection creation produced an invalid result" + : std::move(diagnostic) + ); + return; + } + const auto handle = handle_table().insert(*projection); + complete_value(completion_state, MLN_STATUS_OK, {}, handle); + }); + const auto submit_status = + submit_runtime_operation(runtime, state, [parent, state]() mutable -> void { + try { + auto projection = std::make_shared(); + projection->projection = + std::make_unique(*parent->map); + state->complete(MLN_STATUS_OK, {}, std::any{std::move(projection)}); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, exception_message(std::current_exception()), + {} + ); + } + }); + if (submit_status == MLN_STATUS_OK) + completion_state->accept(); + else + completion_state->reject(); + return submit_status; } -auto map_projection_destroy(mln_map_projection projection) -> mln_status { +auto map_projection_close(mln_map_projection projection) -> mln_status { auto& table = handle_table(); std::shared_ptr owned; { const std::scoped_lock lock(table.mutex()); - if (table.resolve_locked(projection) == nullptr) { + owned = table.lease_locked(projection); + if (owned == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - owned = table.remove_locked(projection); + static_cast(table.remove_locked(projection)); } { - // A call that leased the handle before retirement either completes first - // or observes the retired projection after this lock releases. - const std::scoped_lock lock(owned->call_mutex); + // Waits for projection calls already running on other threads, then + // destroys the projection. A racing call that leased the handle before the + // removal observes the null projection and reports a stale handle. + const std::scoped_lock call_lock(owned->call_mutex); owned->projection.reset(); } return MLN_STATUS_OK; @@ -7537,33 +4356,26 @@ auto map_projection_destroy(mln_map_projection projection) -> mln_status { auto map_projection_get_camera( mln_map_projection projection, mln_camera_options* out_camera ) -> mln_status { - return with_map_projection( - projection, [out_camera](mln::MapProjection& live) -> mln_status { - if ( - out_camera == nullptr || out_camera->size < sizeof(mln_camera_options) - ) { - set_thread_error( - "out_camera must not be null and must have a valid size" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_camera = from_native_camera(live.getCamera()); - return MLN_STATUS_OK; - } - ); + if (out_camera == nullptr || out_camera->size < sizeof(mln_camera_options)) { + set_thread_error("out_camera must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return with_projection(projection, [out_camera](mln::MapProjection& value) { + *out_camera = from_native_camera(value.getCamera()); + }); } auto map_projection_set_camera( mln_map_projection projection, const mln_camera_options* camera ) -> mln_status { - return with_map_projection( - projection, [camera](mln::MapProjection& live) -> mln_status { - const auto camera_status = validate_camera_options(camera); - if (camera_status != MLN_STATUS_OK) { - return camera_status; - } - live.setCamera(to_native_camera(*camera)); - return MLN_STATUS_OK; + const auto camera_status = validate_camera_options(camera); + if (camera_status != MLN_STATUS_OK) { + return camera_status; + } + const auto native_camera = to_native_camera(*camera); + return with_projection( + projection, [&native_camera](mln::MapProjection& value) { + value.setCamera(native_camera); } ); } @@ -7572,24 +4384,22 @@ auto map_projection_set_visible_coordinates( mln_map_projection projection, const mln_lat_lng* coordinates, size_t coordinate_count, mln_edge_insets padding ) -> mln_status { - return with_map_projection( + const auto coordinates_status = + validate_lat_lng_array(coordinates, coordinate_count, false); + if (coordinates_status != MLN_STATUS_OK) { + return coordinates_status; + } + const auto padding_status = validate_edge_insets(padding); + if (padding_status != MLN_STATUS_OK) { + return padding_status; + } + const auto native_coordinates = + to_native_lat_lngs(coordinates, coordinate_count); + const auto native_padding = to_native_edge_insets(padding); + return with_projection( projection, - [coordinates, coordinate_count, - padding](mln::MapProjection& live) -> mln_status { - const auto coordinates_status = - validate_lat_lng_array(coordinates, coordinate_count, false); - if (coordinates_status != MLN_STATUS_OK) { - return coordinates_status; - } - const auto padding_status = validate_edge_insets(padding); - if (padding_status != MLN_STATUS_OK) { - return padding_status; - } - live.setVisibleCoordinates( - to_native_lat_lngs(coordinates, coordinate_count), - to_native_edge_insets(padding) - ); - return MLN_STATUS_OK; + [&native_coordinates, &native_padding](mln::MapProjection& value) { + value.setVisibleCoordinates(native_coordinates, native_padding); } ); } @@ -7598,23 +4408,23 @@ auto map_projection_set_visible_geometry( mln_map_projection projection, mln_buffer_view geometry, mln_edge_insets padding ) -> mln_status { - return with_map_projection( - projection, [geometry, padding](mln::MapProjection& live) -> mln_status { - const auto padding_status = validate_edge_insets(padding); - if (padding_status != MLN_STATUS_OK) { - return padding_status; - } - auto native_geometry = to_native_geometry(geometry); - if (!native_geometry) { - return MLN_STATUS_INVALID_ARGUMENT; - } - auto coordinates = geometry_lat_lngs(*native_geometry); - if (coordinates.empty()) { - set_thread_error("geometry must contain at least one coordinate"); - return MLN_STATUS_INVALID_ARGUMENT; - } - live.setVisibleCoordinates(coordinates, to_native_edge_insets(padding)); - return MLN_STATUS_OK; + const auto padding_status = validate_edge_insets(padding); + if (padding_status != MLN_STATUS_OK) { + return padding_status; + } + const auto native_geometry = to_native_geometry(geometry); + if (!native_geometry) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto coordinates = geometry_lat_lngs(*native_geometry); + if (coordinates.empty()) { + set_thread_error("geometry must contain at least one coordinate"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto native_padding = to_native_edge_insets(padding); + return with_projection( + projection, [&coordinates, &native_padding](mln::MapProjection& value) { + value.setVisibleCoordinates(coordinates, native_padding); } ); } @@ -7623,21 +4433,19 @@ auto map_projection_pixel_for_lat_lng( mln_map_projection projection, mln_lat_lng coordinate, mln_screen_point* out_point ) -> mln_status { - return with_map_projection( - projection, - [coordinate, out_point](mln::MapProjection& live) -> mln_status { - if (out_point == nullptr) { - set_thread_error("out_point must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto coordinate_status = validate_lat_lng(coordinate); - if (coordinate_status != MLN_STATUS_OK) { - return coordinate_status; - } - *out_point = from_native_screen_point( - live.pixelForLatLng(to_native_lat_lng(coordinate)) - ); - return MLN_STATUS_OK; + if (out_point == nullptr) { + set_thread_error("out_point must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto coordinate_status = validate_lat_lng(coordinate); + if (coordinate_status != MLN_STATUS_OK) { + return coordinate_status; + } + const auto native_coordinate = to_native_lat_lng(coordinate); + return with_projection( + projection, [&native_coordinate, out_point](mln::MapProjection& value) { + *out_point = + from_native_screen_point(value.pixelForLatLng(native_coordinate)); } ); } @@ -7646,20 +4454,18 @@ auto map_projection_lat_lng_for_pixel( mln_map_projection projection, mln_screen_point point, mln_lat_lng* out_coordinate ) -> mln_status { - return with_map_projection( - projection, - [point, out_coordinate](mln::MapProjection& live) -> mln_status { - if (out_coordinate == nullptr) { - set_thread_error("out_coordinate must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto point_status = validate_screen_point(point); - if (point_status != MLN_STATUS_OK) { - return point_status; - } - *out_coordinate = - from_native_lat_lng(live.latLngForPixel(to_native_screen_point(point))); - return MLN_STATUS_OK; + if (out_coordinate == nullptr) { + set_thread_error("out_coordinate must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto point_status = validate_screen_point(point); + if (point_status != MLN_STATUS_OK) { + return point_status; + } + const auto native_point = to_native_screen_point(point); + return with_projection( + projection, [&native_point, out_coordinate](mln::MapProjection& value) { + *out_coordinate = from_native_lat_lng(value.latLngForPixel(native_point)); } ); } @@ -7704,169 +4510,6 @@ auto lat_lng_for_projected_meters( return MLN_STATUS_OK; } -auto map_move_by(mln_map map, double delta_x, double delta_y) -> mln_status { - return map_move_by_animated(map, delta_x, delta_y, nullptr); -} - -auto map_move_by_animated( - mln_map map, double delta_x, double delta_y, - const mln_animation_options* animation -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!std::isfinite(delta_x) || !std::isfinite(delta_y)) { - set_thread_error("move deltas must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto animation_status = validate_animation_options(animation); - if (animation_status != MLN_STATUS_OK) { - return animation_status; - } - - live->map->moveBy( - mln::ScreenCoordinate{delta_x, delta_y}, - to_native_animation(live->runtime, map, live->event_state, animation) - ); - return MLN_STATUS_OK; -} - -auto map_scale_by(mln_map map, double scale, const mln_screen_point* anchor) - -> mln_status { - return map_scale_by_animated(map, scale, anchor, nullptr); -} - -auto map_scale_by_animated( - mln_map map, double scale, const mln_screen_point* anchor, - const mln_animation_options* animation -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!std::isfinite(scale) || scale <= 0.0) { - set_thread_error("scale must be positive and finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto native_anchor = std::optional{}; - if (anchor != nullptr) { - const auto anchor_status = validate_screen_point(*anchor); - if (anchor_status != MLN_STATUS_OK) { - return anchor_status; - } - native_anchor = screen_point(*anchor); - } - const auto animation_status = validate_animation_options(animation); - if (animation_status != MLN_STATUS_OK) { - return animation_status; - } - - live->map->scaleBy( - scale, native_anchor, - to_native_animation(live->runtime, map, live->event_state, animation) - ); - return MLN_STATUS_OK; -} - -auto map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) - -> mln_status { - return map_rotate_by_animated(map, first, second, nullptr); -} - -auto map_rotate_by_animated( - mln_map map, mln_screen_point first, mln_screen_point second, - const mln_animation_options* animation -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - const auto first_status = validate_screen_point(first); - if (first_status != MLN_STATUS_OK) { - return first_status; - } - const auto second_status = validate_screen_point(second); - if (second_status != MLN_STATUS_OK) { - return second_status; - } - const auto animation_status = validate_animation_options(animation); - if (animation_status != MLN_STATUS_OK) { - return animation_status; - } - - live->map->rotateBy( - screen_point(first), screen_point(second), - to_native_animation(live->runtime, map, live->event_state, animation) - ); - return MLN_STATUS_OK; -} - -auto map_pitch_by(mln_map map, double pitch) -> mln_status { - return map_pitch_by_animated(map, pitch, nullptr); -} - -auto map_pitch_by_animated( - mln_map map, double pitch, const mln_animation_options* animation -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (!std::isfinite(pitch)) { - set_thread_error("pitch must be finite"); - return MLN_STATUS_INVALID_ARGUMENT; - } - const auto animation_status = validate_animation_options(animation); - if (animation_status != MLN_STATUS_OK) { - return animation_status; - } - - live->map->pitchBy( - pitch, to_native_animation(live->runtime, map, live->event_state, animation) - ); - return MLN_STATUS_OK; -} - -auto map_cancel_transitions(mln_map map) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - live->map->cancelTransitions(); - return MLN_STATUS_OK; -} - -auto map_set_gesture_in_progress(mln_map map, bool in_progress) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - live->map->setGestureInProgress(in_progress); - return MLN_STATUS_OK; -} - -auto map_is_gesture_in_progress(mln_map map, bool* out_in_progress) - -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_in_progress == nullptr) { - set_thread_error("out_in_progress must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_in_progress = live->map->isGestureInProgress(); - return MLN_STATUS_OK; -} - auto validate_camera_output(mln_camera_options* out_camera) -> mln_status { if (out_camera == nullptr || out_camera->size < sizeof(mln_camera_options)) { set_thread_error("out_camera must not be null and must have a valid size"); @@ -8014,19 +4657,131 @@ auto map_lat_lng_bounds_for_camera_unwrapped( return MLN_STATUS_OK; } -auto map_get_bounds(mln_map map, mln_bound_options* out_options) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; +auto map_camera_for_lat_lng_bounds_start( + mln_map map, mln_lat_lng_bounds bounds, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status { + if ( + validate_lat_lng_bounds(bounds) != MLN_STATUS_OK || + validate_camera_fit_options(fit_options) != MLN_STATUS_OK + ) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (out_options == nullptr || out_options->size < sizeof(mln_bound_options)) { - set_thread_error("out_options must not be null and must have a valid size"); + const auto fit = + fit_options == nullptr ? camera_fit_options_default() : *fit_options; + const auto has_fit = fit_options != nullptr; + return start_geometry_operation( + map, GeometryOperationKind::CameraForBounds, + [map, bounds, fit, has_fit](GeometryOperationResult& result) { + result.camera = camera_options_default(); + return map_camera_for_lat_lng_bounds( + map, bounds, has_fit ? &fit : nullptr, &result.camera + ); + }, + completion + ); +} + +auto map_camera_for_lat_lngs_start( + mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status { + if ( + validate_lat_lng_array(coordinates, coordinate_count, false) != + MLN_STATUS_OK || + validate_camera_fit_options(fit_options) != MLN_STATUS_OK + ) { return MLN_STATUS_INVALID_ARGUMENT; } + auto copied = + std::vector{coordinates, coordinates + coordinate_count}; + const auto fit = + fit_options == nullptr ? camera_fit_options_default() : *fit_options; + const auto has_fit = fit_options != nullptr; + return start_geometry_operation( + map, GeometryOperationKind::CameraForCoordinates, + [map, copied = std::move(copied), fit, + has_fit](GeometryOperationResult& result) { + result.camera = camera_options_default(); + return map_camera_for_lat_lngs( + map, copied.data(), copied.size(), has_fit ? &fit : nullptr, + &result.camera + ); + }, + completion + ); +} - *out_options = from_native_bound_options(live->map->getBounds()); - return MLN_STATUS_OK; +auto map_camera_for_geometry_start( + mln_map map, mln_buffer_view geometry, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status { + const auto parsed = to_native_geometry(geometry); + if (!parsed || geometry_lat_lngs(*parsed).empty()) { + if (parsed) { + set_thread_error("geometry must contain at least one coordinate"); + } + return MLN_STATUS_INVALID_ARGUMENT; + } + if (validate_camera_fit_options(fit_options) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto* geometry_bytes = static_cast(geometry.data); + auto bytes = + std::vector{geometry_bytes, geometry_bytes + geometry.size}; + const auto fit = + fit_options == nullptr ? camera_fit_options_default() : *fit_options; + const auto has_fit = fit_options != nullptr; + return start_geometry_operation( + map, GeometryOperationKind::CameraForGeometry, + [map, bytes = std::move(bytes), fit, + has_fit](GeometryOperationResult& result) -> mln_status { + result.camera = camera_options_default(); + const auto view = + mln_buffer_view{.data = bytes.data(), .size = bytes.size()}; + return map_camera_for_geometry( + map, view, has_fit ? &fit : nullptr, &result.camera + ); + }, + completion + ); +} + +auto start_bounds_for_camera( + mln_map map, const mln_camera_options* camera, bool unwrapped, + const mln_completion* completion +) -> mln_status { + if (validate_camera_options(camera) != MLN_STATUS_OK) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto copied = *camera; + const auto kind = unwrapped ? GeometryOperationKind::UnwrappedBoundsForCamera + : GeometryOperationKind::BoundsForCamera; + return start_geometry_operation( + map, kind, + [map, copied, unwrapped](GeometryOperationResult& result) { + return unwrapped + ? map_lat_lng_bounds_for_camera_unwrapped( + map, &copied, &result.bounds + ) + : map_lat_lng_bounds_for_camera(map, &copied, &result.bounds); + }, + completion + ); +} + +auto map_lat_lng_bounds_for_camera_start( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion +) -> mln_status { + return start_bounds_for_camera(map, camera, false, completion); +} + +auto map_lat_lng_bounds_for_camera_unwrapped_start( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion +) -> mln_status { + return start_bounds_for_camera(map, camera, true, completion); } auto map_set_bounds(mln_map map, const mln_bound_options* options) @@ -8047,26 +4802,6 @@ auto map_set_bounds(mln_map map, const mln_bound_options* options) return MLN_STATUS_OK; } -auto map_get_free_camera_options( - mln_map map, mln_free_camera_options* out_options -) -> mln_status { - MapObject* live = nullptr; - const auto status = validate_map(map, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_options == nullptr || - out_options->size < sizeof(mln_free_camera_options) - ) { - set_thread_error("out_options must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - *out_options = from_native_free_camera(live->map->getFreeCameraOptions()); - return MLN_STATUS_OK; -} - auto map_set_free_camera_options( mln_map map, const mln_free_camera_options* options ) -> mln_status { @@ -8084,4 +4819,53 @@ auto map_set_free_camera_options( return MLN_STATUS_OK; } +auto map_set_projection_mode( + mln_map map, const mln_projection_mode* mode, const mln_completion* completion +) -> mln_status { + if (mode == nullptr || mode->size < sizeof(mln_projection_mode)) { + set_thread_error("mode must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = + static_cast(MLN_PROJECTION_MODE_AXONOMETRIC) | + MLN_PROJECTION_MODE_X_SKEW | MLN_PROJECTION_MODE_Y_SKEW; + if ((mode->fields & ~known_fields) != 0U) { + set_thread_error("mode fields contain unknown bits"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + ((mode->fields & MLN_PROJECTION_MODE_X_SKEW) != 0U && + !std::isfinite(mode->x_skew)) || + ((mode->fields & MLN_PROJECTION_MODE_Y_SKEW) != 0U && + !std::isfinite(mode->y_skew)) + ) { + set_thread_error("projection skew values must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto copied = mln::ProjectionMode{}; + if ((mode->fields & MLN_PROJECTION_MODE_AXONOMETRIC) != 0U) { + copied.withAxonometric(mode->axonometric); + } + if ((mode->fields & MLN_PROJECTION_MODE_X_SKEW) != 0U) { + copied.withXSkew(mode->x_skew); + } + if ((mode->fields & MLN_PROJECTION_MODE_Y_SKEW) != 0U) { + copied.withYSkew(mode->y_skew); + } + return submit_map_command( + map, + [map, copied = std::move(copied)]() mutable -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + live->map->setProjectionMode(std::move(copied)); + return MLN_STATUS_OK; + }, + completion + ); +} + } // namespace mln::core diff --git a/src/map/map.hpp b/src/map/map.hpp index 35b29b950..adf4e4e6a 100644 --- a/src/map/map.hpp +++ b/src/map/map.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include #include +#include #include "maplibre_native_c.h" @@ -15,12 +18,15 @@ class UpdateParameters; namespace mln::core { +struct GeoJsonSourceDataObject; struct MapObject; struct FeatureStateSnapshot; auto map_options_default() noexcept -> mln_map_options; auto camera_options_default() noexcept -> mln_camera_options; auto animation_options_default() noexcept -> mln_animation_options; +auto camera_delta_default() noexcept -> mln_camera_delta; +auto camera_update_default() noexcept -> mln_camera_update; auto camera_fit_options_default() noexcept -> mln_camera_fit_options; auto bound_options_default() noexcept -> mln_bound_options; auto free_camera_options_default() noexcept -> mln_free_camera_options; @@ -40,31 +46,137 @@ auto style_image_options_default() noexcept -> mln_style_image_options; auto style_image_info_default() noexcept -> mln_style_image_info; auto style_transition_options_default() noexcept -> mln_style_transition_options; -auto create_map( - mln_runtime runtime, const mln_map_options* options, mln_map* out_map + +enum class StyleOperationKind : uint32_t { + SourceInfo = 0x5301, + SourceAttribution, + SourceUrl, + SourceTileUrls, + SourceIds, + ImageInfo, + ImageStretches, + ImagePixels, + ImageCoordinates, + LayerInfo, + LayerIds, + LayerJson, + LightProperty, + TransitionOptions, + LayerProperty, + LayerFilter, + LayerSourceLayer, + LayerSourceId +}; + +struct StyleOperationResult { + bool found = false; + mln_style_source_info source_info{}; + mln_style_layer_info layer_info{}; + mln_style_image_info image_info{}; + mln_style_transition_options transition_options{}; + std::string bytes; + std::vector strings; + std::string attribution; + std::string url; + std::string source_id; + std::string source_layer; + std::vector stretch_x; + std::vector stretch_y; + std::vector coordinates; + + StyleOperationResult() = default; +}; + +using StyleWork = std::function; + +auto submit_map_command( + mln_map map, std::function work, + const mln_completion* completion +) -> mln_status; +auto start_style_operation( + mln_map map, StyleOperationKind kind, StyleWork work, + const mln_completion* completion +) -> mln_status; +auto validate_geojson_command_options(const mln_geojson_source_options* options) + -> mln_status; +auto validate_tile_command_options( + const mln_style_tile_source_options* options, uint32_t kind +) -> mln_status; +auto validate_custom_geometry_command_options( + const mln_custom_geometry_source_options* options +) -> mln_status; +auto validate_custom_mvt_command_options( + const mln_custom_mvt_vector_source_options* options +) -> mln_status; +auto validate_style_image_command_input( + const mln_premultiplied_rgba8_image* image, + const mln_style_image_options* options +) -> mln_status; +auto validate_image_source_command_coordinates( + const mln_lat_lng* coordinates, size_t coordinate_count +) -> mln_status; + +enum class GeometryOperationKind : uint32_t { + CameraForBounds = 0x4701, + CameraForCoordinates, + CameraForGeometry, + BoundsForCamera, + UnwrappedBoundsForCamera, + PixelForCoordinate, + CoordinateForPixel, + PixelsForCoordinates, + CoordinatesForPixels +}; + +struct GeometryOperationResult { + mln_camera_options camera{}; + mln_lat_lng_bounds bounds{}; + mln_screen_point point{}; + mln_lat_lng coordinate{}; + std::vector points; + std::vector coordinates; +}; + +using GeometryWork = std::function; + +auto start_geometry_operation( + mln_map map, GeometryOperationKind kind, GeometryWork work, + const mln_completion* completion +) -> mln_status; +auto create_map_start( + mln_runtime runtime, const mln_map_options* options, + const mln_completion* completion +) -> mln_status; +auto release_map(mln_map map, const mln_completion* completion) -> mln_status; +auto map_snapshot_get(mln_map map, mln_map_snapshot* out_snapshot) + -> mln_status; +auto map_resize( + mln_map map, mln_logical_extent extent, const mln_completion* completion +) -> mln_status; +auto map_request_repaint(mln_map map, const mln_completion* completion) + -> mln_status; +auto map_request_still_image_start( + mln_map map, const mln_completion* completion ) -> mln_status; -auto destroy_map(mln_map map) -> mln_status; -auto map_request_repaint(mln_map map) -> mln_status; auto map_set_feature_state( mln_map map, const mln_feature_state_selector* selector, mln_buffer_view state ) -> mln_status; -auto map_get_feature_state( - mln_map map, const mln_feature_state_selector* selector, mln_buffer* out_state +auto map_get_feature_state_start( + mln_map map, const mln_feature_state_selector* selector, + const mln_completion* completion ) -> mln_status; auto map_remove_feature_state( mln_map map, const mln_feature_state_selector* selector ) -> mln_status; -auto map_request_still_image(mln_map map) -> mln_status; auto map_set_style_url(mln_map map, const char* url) -> mln_status; auto map_set_style_json(mln_map map, mln_buffer_view json) -> mln_status; -auto map_copy_loaded_style_json( - mln_map map, uint8_t* out_json, size_t json_capacity, size_t* out_json_size -) -> mln_status; -auto map_copy_style_url( - mln_map map, char* out_url, size_t url_capacity, size_t* out_url_size +auto map_loaded_style_json_start(mln_map map, const mln_completion* completion) + -> mln_status; +auto map_style_url_start(mln_map map, const mln_completion* completion) + -> mln_status; +auto map_set_event_mask( + mln_map map, uint64_t mask, const mln_completion* completion ) -> mln_status; -auto map_set_event_mask(mln_map map, uint64_t mask) -> mln_status; -auto map_get_event_mask(mln_map map, uint64_t* out_mask) -> mln_status; auto style_id_list_count(mln_style_id_list list, size_t* out_count) -> mln_status; auto style_id_list_get( @@ -80,16 +192,8 @@ auto style_string_list_destroy(mln_style_string_list list) -> void; auto map_add_style_source_json( mln_map map, mln_buffer_view source_id, mln_buffer_view source_json ) -> mln_status; -auto map_remove_style_source( - mln_map map, mln_buffer_view source_id, bool* out_removed -) -> mln_status; -auto map_style_source_exists( - mln_map map, mln_buffer_view source_id, bool* out_exists -) -> mln_status; -auto map_get_style_source_type( - mln_map map, mln_buffer_view source_id, uint32_t* out_source_type, - bool* out_found -) -> mln_status; +auto map_remove_style_source(mln_map map, mln_buffer_view source_id) + -> mln_status; auto map_get_style_source_info( mln_map map, mln_buffer_view source_id, mln_style_source_info* out_info, bool* out_found @@ -113,13 +217,15 @@ auto map_add_geojson_source_url( const mln_geojson_source_options* options ) -> mln_status; auto map_add_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data + mln_map map, mln_buffer_view source_id, + const std::shared_ptr& data ) -> mln_status; auto map_set_geojson_source_url( mln_map map, mln_buffer_view source_id, mln_buffer_view url ) -> mln_status; auto map_set_geojson_source_data( - mln_map map, mln_buffer_view source_id, mln_geojson_source_data data + mln_map map, mln_buffer_view source_id, + const std::shared_ptr& data ) -> mln_status; auto map_set_geojson_source_synchronous_tiling( mln_map map, mln_buffer_view source_id, bool enabled @@ -182,12 +288,8 @@ auto map_set_style_image( const mln_premultiplied_rgba8_image* image, const mln_style_image_options* options ) -> mln_status; -auto map_remove_style_image( - mln_map map, mln_buffer_view image_id, bool* out_removed -) -> mln_status; -auto map_style_image_exists( - mln_map map, mln_buffer_view image_id, bool* out_exists -) -> mln_status; +auto map_remove_style_image(mln_map map, mln_buffer_view image_id) + -> mln_status; auto map_get_style_image_info( mln_map map, mln_buffer_view image_id, mln_style_image_info* out_info, bool* out_found @@ -252,14 +354,10 @@ auto map_set_location_indicator_image_name( auto map_add_style_layer_json( mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id ) -> mln_status; -auto map_remove_style_layer( - mln_map map, mln_buffer_view layer_id, bool* out_removed -) -> mln_status; -auto map_style_layer_exists( - mln_map map, mln_buffer_view layer_id, bool* out_exists -) -> mln_status; -auto map_get_style_layer_type( - mln_map map, mln_buffer_view layer_id, mln_buffer_view* out_layer_type, +auto map_remove_style_layer(mln_map map, mln_buffer_view layer_id) + -> mln_status; +auto map_get_style_layer_info( + mln_map map, mln_buffer_view layer_id, mln_style_layer_info* out_info, bool* out_found ) -> mln_status; auto map_list_style_layer_ids(mln_map map, mln_style_id_list* out_layer_ids) @@ -315,74 +413,69 @@ auto map_copy_layer_source_id( auto map_set_layer_min_zoom( mln_map map, mln_buffer_view layer_id, double min_zoom ) -> mln_status; -auto map_get_layer_min_zoom( - mln_map map, mln_buffer_view layer_id, double* out_min_zoom -) -> mln_status; auto map_set_layer_max_zoom( mln_map map, mln_buffer_view layer_id, double max_zoom ) -> mln_status; -auto map_get_layer_max_zoom( - mln_map map, mln_buffer_view layer_id, double* out_max_zoom -) -> mln_status; auto map_set_layer_visibility( mln_map map, mln_buffer_view layer_id, uint32_t visibility ) -> mln_status; -auto map_get_layer_visibility( - mln_map map, mln_buffer_view layer_id, uint32_t* out_visibility +auto map_camera_snapshot_get( + mln_map map, mln_camera_options* out_camera, uint64_t* out_generation ) -> mln_status; -auto map_get_camera(mln_map map, mln_camera_options* out_camera) -> mln_status; -auto map_jump_to(mln_map map, const mln_camera_options* camera) -> mln_status; -auto map_ease_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation +auto map_update_camera( + mln_map map, const mln_camera_update* update, const mln_completion* completion ) -> mln_status; -auto map_fly_to( - mln_map map, const mln_camera_options* camera, - const mln_animation_options* animation +auto map_apply_camera_delta( + mln_map map, const mln_camera_delta* delta, const mln_completion* completion ) -> mln_status; -auto map_get_projection_mode(mln_map map, mln_projection_mode* out_mode) - -> mln_status; -auto map_set_projection_mode(mln_map map, const mln_projection_mode* mode) +auto map_camera_query_start(mln_map map, const mln_completion* completion) -> mln_status; auto map_set_debug_options(mln_map map, uint32_t options) -> mln_status; -auto map_get_debug_options(mln_map map, uint32_t* out_options) -> mln_status; auto map_set_rendering_stats_view_enabled(mln_map map, bool enabled) -> mln_status; -auto map_get_rendering_stats_view_enabled(mln_map map, bool* out_enabled) - -> mln_status; -auto map_is_fully_loaded(mln_map map, bool* out_loaded) -> mln_status; auto map_dump_debug_logs(mln_map map) -> mln_status; -auto map_get_size( - mln_map map, uint32_t* out_width, uint32_t* out_height, - double* out_scale_factor -) -> mln_status; -auto map_get_viewport_options( - mln_map map, mln_map_viewport_options* out_options -) -> mln_status; auto map_set_viewport_options( mln_map map, const mln_map_viewport_options* options ) -> mln_status; -auto map_get_tile_options(mln_map map, mln_map_tile_options* out_options) - -> mln_status; auto map_set_tile_options(mln_map map, const mln_map_tile_options* options) -> mln_status; -auto map_pixel_for_lat_lng( - mln_map map, mln_lat_lng coordinate, mln_screen_point* out_point +auto map_pixel_for_lat_lng_start( + mln_map map, mln_lat_lng coordinate, const mln_completion* completion ) -> mln_status; -auto map_lat_lng_for_pixel( - mln_map map, mln_screen_point point, mln_lat_lng* out_coordinate +auto map_lat_lng_for_pixel_start( + mln_map map, mln_screen_point point, const mln_completion* completion ) -> mln_status; -auto map_pixels_for_lat_lngs( +auto map_pixels_for_lat_lngs_start( mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, - mln_screen_point* out_points + const mln_completion* completion ) -> mln_status; -auto map_lat_lngs_for_pixels( +auto map_lat_lngs_for_pixels_start( mln_map map, const mln_screen_point* points, size_t point_count, - mln_lat_lng* out_coordinates + const mln_completion* completion +) -> mln_status; +auto map_camera_for_lat_lng_bounds_start( + mln_map map, mln_lat_lng_bounds bounds, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status; +auto map_camera_for_lat_lngs_start( + mln_map map, const mln_lat_lng* coordinates, size_t coordinate_count, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status; +auto map_camera_for_geometry_start( + mln_map map, mln_buffer_view geometry, + const mln_camera_fit_options* fit_options, const mln_completion* completion +) -> mln_status; +auto map_lat_lng_bounds_for_camera_start( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion +) -> mln_status; +auto map_lat_lng_bounds_for_camera_unwrapped_start( + mln_map map, const mln_camera_options* camera, + const mln_completion* completion ) -> mln_status; -auto map_projection_create(mln_map map, mln_map_projection* out_projection) +auto map_projection_create_start(mln_map map, const mln_completion* completion) -> mln_status; -auto map_projection_destroy(mln_map_projection projection) -> mln_status; +auto map_projection_close(mln_map_projection projection) -> mln_status; auto map_projection_get_camera( mln_map_projection projection, mln_camera_options* out_camera ) -> mln_status; @@ -411,31 +504,7 @@ auto projected_meters_for_lat_lng( auto lat_lng_for_projected_meters( mln_projected_meters meters, mln_lat_lng* out_coordinate ) -> mln_status; -auto map_move_by(mln_map map, double delta_x, double delta_y) -> mln_status; -auto map_move_by_animated( - mln_map map, double delta_x, double delta_y, - const mln_animation_options* animation -) -> mln_status; -auto map_scale_by(mln_map map, double scale, const mln_screen_point* anchor) - -> mln_status; -auto map_scale_by_animated( - mln_map map, double scale, const mln_screen_point* anchor, - const mln_animation_options* animation -) -> mln_status; -auto map_rotate_by(mln_map map, mln_screen_point first, mln_screen_point second) - -> mln_status; -auto map_rotate_by_animated( - mln_map map, mln_screen_point first, mln_screen_point second, - const mln_animation_options* animation -) -> mln_status; -auto map_pitch_by(mln_map map, double pitch) -> mln_status; -auto map_pitch_by_animated( - mln_map map, double pitch, const mln_animation_options* animation -) -> mln_status; -auto map_cancel_transitions(mln_map map) -> mln_status; -auto map_set_gesture_in_progress(mln_map map, bool in_progress) -> mln_status; -auto map_is_gesture_in_progress(mln_map map, bool* out_in_progress) - -> mln_status; + auto map_camera_for_lat_lng_bounds( mln_map map, mln_lat_lng_bounds bounds, const mln_camera_fit_options* fit_options, mln_camera_options* out_camera @@ -454,38 +523,50 @@ auto map_lat_lng_bounds_for_camera( auto map_lat_lng_bounds_for_camera_unwrapped( mln_map map, const mln_camera_options* camera, mln_lat_lng_bounds* out_bounds ) -> mln_status; -auto map_get_bounds(mln_map map, mln_bound_options* out_options) -> mln_status; auto map_set_bounds(mln_map map, const mln_bound_options* options) -> mln_status; -auto map_get_free_camera_options( - mln_map map, mln_free_camera_options* out_options -) -> mln_status; auto map_set_free_camera_options( mln_map map, const mln_free_camera_options* options ) -> mln_status; -// Validates a map handle is non-null and live, from any thread. Use this only -// from a caller that runs off the map owner thread; anything touching -// thread-affine map state uses validate_map(). +auto validate_debug_options_input(uint32_t options) -> mln_status; +auto validate_viewport_options_input(const mln_map_viewport_options* options) + -> mln_status; +auto validate_tile_options_input(const mln_map_tile_options* options) + -> mln_status; +auto validate_bound_options_input(const mln_bound_options* options) + -> mln_status; +auto validate_free_camera_options_input(const mln_free_camera_options* options) + -> mln_status; +auto map_set_projection_mode( + mln_map map, const mln_projection_mode* mode, const mln_completion* completion +) -> mln_status; +// Validates that a map handle is non-null and live. auto validate_map_live(mln_map map, MapObject*& out_map) -> mln_status; auto validate_map(mln_map map, MapObject*& out_map) -> mln_status; auto map_scale_factor(mln_map map) -> double; -// Map-thread only. Code reachable from a render session's owner thread uses the -// posting helpers below instead. +// Returns worker-owned native state. Callers must already run on the runtime +// worker or use the posting helpers below. auto map_native(MapObject* map) -> mln::Map*; -// Queue a map mutation for the map's owner thread. Callable from any thread; -// the effect lands on the map's next mln_runtime_pump(). Returns -// MLN_STATUS_INVALID_ARGUMENT when the map handle is null or no longer live. -auto map_post_set_size(mln_map map, uint32_t width, uint32_t height) - -> mln_status; +auto map_post_resize(mln_map map, mln_logical_extent extent) -> mln_status; auto map_post_trigger_repaint(mln_map map) -> mln_status; auto map_latest_update(mln_map map) -> std::shared_ptr; +auto map_latest_update_generation(mln_map map) noexcept -> uint64_t; +auto map_latest_update_snapshot(mln_map map, uint64_t& out_generation) + -> std::shared_ptr; +auto map_set_render_session_publish_callback( + mln_map map, std::function callback +) -> mln_status; // Copies the map's coalesced feature-state snapshot. Callable from a render // session's owner thread while the map is attached to that session. auto map_feature_state_snapshot(mln_map map) -> std::shared_ptr; auto map_renderer_observer(mln_map map) -> mln::RendererObserver*; auto map_run_render_jobs(mln_map map) -> void; +// Blocks until the map's queued and running tile-worker jobs drain. Abandon +// uses it so a host may destroy its graphics device as soon as abandon +// returns. Must not be called from a MapLibre worker thread. +auto map_quiesce_render_workers(mln_map map) -> void; auto map_attach_render_target_session(mln_map map, void* session) -> mln_status; auto map_detach_render_target_session(mln_map map, void* session) -> mln_status; diff --git a/src/map/map_internal.hpp b/src/map/map_internal.hpp new file mode 100644 index 000000000..6a3489d27 --- /dev/null +++ b/src/map/map_internal.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "execution/control_state.hpp" +#include "handles/handle_table.hpp" +#include "map/feature_state.hpp" +#include "maplibre_native_c.h" + +namespace mln { +class Map; +class MapProjection; +} // namespace mln + +namespace mln::core { + +class CallbackSourceRegistry; +class HeadlessFrontend; +class HeadlessObserver; +struct MapEventState; +class OperationObject; +struct RuntimeObject; + +struct MapObject { + ControlState control; + mln_runtime runtime = MLN_HANDLE_NULL; + std::shared_ptr runtime_state; + uint32_t map_mode = MLN_MAP_MODE_CONTINUOUS; + mln_logical_extent logical_extent{256, 256, 1.0}; + mutable std::mutex snapshot_mutex; + mln_map_snapshot snapshot{}; + uint64_t next_snapshot_generation = 1; + std::atomic latest_resize_submission{0}; + bool still_image_request_pending = false; + // Any-thread map-owned feature state; render sessions pull coalesced + // snapshots and push diffs into their renderer. + FeatureStateStore feature_state; + std::shared_ptr still_image_operation; + // Releases the pending still request's submission lease exactly once. + // Cancellation paths invoke it directly, because the mbgl callback that + // otherwise releases it only fires when the mln::Map is destroyed — after + // teardown already waits for submissions. + std::function still_image_release_submission; + // Declared first so reverse-order destruction runs the release callbacks it + // still owes after the mln::Map that could still reach a source is gone. + std::shared_ptr callback_sources; + // Declared before `observer` so reverse-order destruction retires the + // observer, which holds its own reference, before this member is destroyed. + std::shared_ptr event_state; + std::unique_ptr observer; + std::unique_ptr frontend; + std::unique_ptr map; + // Guarded by the map handle table's mutex; a render session on another thread + // clears it from map_detach_render_target_session(). + void* render_target_session = nullptr; + + MapObject() = default; + MapObject(const MapObject&) = delete; + MapObject(MapObject&&) = delete; + auto operator=(const MapObject&) -> MapObject& = delete; + auto operator=(MapObject&&) -> MapObject& = delete; + + ~MapObject(); +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::Map; + static constexpr auto leasable = true; +}; + +struct MapProjectionObject { + // Serializes every synchronous projection call, including close, so callers + // may use one handle from any thread. + std::mutex call_mutex; + // Null once close has destroyed the projection; guarded by call_mutex. + std::unique_ptr projection; + + MapProjectionObject() = default; + MapProjectionObject(const MapProjectionObject&) = delete; + MapProjectionObject(MapProjectionObject&&) = delete; + auto operator=(const MapProjectionObject&) -> MapProjectionObject& = delete; + auto operator=(MapProjectionObject&&) -> MapProjectionObject& = delete; + ~MapProjectionObject(); +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::MapProjection; + static constexpr auto leasable = true; +}; + +// The kinds of host-callback-backed sources the registry tracks. +enum class CallbackSourceKind : uint8_t { CustomGeometry, CustomMvtVector }; + +using CallbackSourceRelease = void (*)(void*); + +auto track_callback_source( + MapObject& map, const std::string& source_id, CallbackSourceKind kind, + CallbackSourceRelease release, void* user_data +) -> void; +auto untrack_callback_source( + MapObject& map, const std::string& source_id +) noexcept -> void; +auto release_callback_source(MapObject& map, const std::string& source_id) + -> void; + +} // namespace mln::core diff --git a/src/map/style.cpp b/src/map/style.cpp new file mode 100644 index 000000000..6f92a3f4c --- /dev/null +++ b/src/map/style.cpp @@ -0,0 +1,4465 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytes/buffer.hpp" +#include "diagnostics/diagnostics.hpp" +#include "geojson/geojson.hpp" +#include "geojson/geojson_source_data.hpp" +#include "handles/handle_table.hpp" +#include "map/map.hpp" +#include "map/map_internal.hpp" +#include "maplibre_native_c.h" +#include "operation/operation.hpp" +#include "runtime/runtime.hpp" +#include "style/style_value.hpp" + +namespace mln::core { + +struct StyleIdListObject { + std::vector ids; +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::StyleIdList; + static constexpr auto leasable = false; +}; + +struct StyleStringListObject { + std::vector values; +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::StyleStringList; + static constexpr auto leasable = false; +}; + +auto validate_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln_status; +auto validate_lat_lng_array( + const mln_lat_lng* coordinates, size_t coordinate_count, bool allow_empty +) -> mln_status; +auto to_native_lat_lng(mln_lat_lng coordinate) -> mln::LatLng; +auto from_native_lat_lng(const mln::LatLng& coordinate) -> mln_lat_lng; +auto to_native_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln::LatLngBounds; +auto from_native_lat_lng_bounds(const mln::LatLngBounds& bounds) + -> mln_lat_lng_bounds; + +} // namespace mln::core + +namespace { + +auto buffer_view_from_string(const std::string& value) -> mln_buffer_view { + return { + .data = value.data(), + .size = value.size(), + }; +} + +enum class TileSourceOptionKind : uint8_t { Vector, Raster, RasterDEM }; + +auto validate_string_view(mln_buffer_view string, const char* name) -> bool { + if (string.size > 0 && string.data == nullptr) { + auto message = std::string{name} + " data must not be null"; + mln::core::set_thread_error(message.c_str()); + return false; + } + return true; +} + +auto string_from_view(mln_buffer_view string) -> std::string { + if (string.size == 0) { + return {}; + } + return std::string{static_cast(string.data), string.size}; +} + +auto string_view_from_string(const std::string& string) -> mln_buffer_view { + return mln_buffer_view{.data = string.data(), .size = string.size()}; +} + +auto string_view_from_literal(const char* string) -> mln_buffer_view { + return mln_buffer_view{.data = string, .size = std::strlen(string)}; +} + +auto to_c_source_type(mln::style::SourceType type) -> uint32_t { + switch (type) { + case mln::style::SourceType::Vector: + return MLN_STYLE_SOURCE_TYPE_VECTOR; + case mln::style::SourceType::Raster: + return MLN_STYLE_SOURCE_TYPE_RASTER; + case mln::style::SourceType::RasterDEM: + return MLN_STYLE_SOURCE_TYPE_RASTER_DEM; + case mln::style::SourceType::GeoJSON: + return MLN_STYLE_SOURCE_TYPE_GEOJSON; + case mln::style::SourceType::Video: + return MLN_STYLE_SOURCE_TYPE_VIDEO; + case mln::style::SourceType::Annotations: + return MLN_STYLE_SOURCE_TYPE_ANNOTATIONS; + case mln::style::SourceType::Image: + return MLN_STYLE_SOURCE_TYPE_IMAGE; + case mln::style::SourceType::CustomVector: + return MLN_STYLE_SOURCE_TYPE_CUSTOM_VECTOR; + case mln::style::SourceType::CustomMVTVector: + return MLN_STYLE_SOURCE_TYPE_CUSTOM_MVT_VECTOR; + } + assert(false); + return MLN_STYLE_SOURCE_TYPE_UNKNOWN; +} + +auto to_c_tile_scheme(mln::Tileset::Scheme scheme) -> uint32_t { + switch (scheme) { + case mln::Tileset::Scheme::XYZ: + return MLN_STYLE_TILE_SCHEME_XYZ; + case mln::Tileset::Scheme::TMS: + return MLN_STYLE_TILE_SCHEME_TMS; + } + assert(false); + return MLN_STYLE_TILE_SCHEME_XYZ; +} + +auto to_c_vector_encoding(mln::Tileset::VectorEncoding encoding) -> uint32_t { + switch (encoding) { + case mln::Tileset::VectorEncoding::Mapbox: + return MLN_STYLE_VECTOR_TILE_ENCODING_MVT; + case mln::Tileset::VectorEncoding::MLT: + return MLN_STYLE_VECTOR_TILE_ENCODING_MLT; + } + assert(false); + return MLN_STYLE_VECTOR_TILE_ENCODING_MVT; +} + +auto to_c_raster_encoding(mln::Tileset::RasterEncoding encoding) -> uint32_t { + switch (encoding) { + case mln::Tileset::RasterEncoding::Mapbox: + return MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX; + case mln::Tileset::RasterEncoding::Terrarium: + return MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM; + } + assert(false); + return MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX; +} + +auto tile_source_from_source(const mln::style::Source& source) + -> const mln::style::TileSource* { + switch (source.getType()) { + case mln::style::SourceType::Vector: + return source.as(); + case mln::style::SourceType::Raster: + return source.as(); + case mln::style::SourceType::RasterDEM: + return source.as(); + default: + return nullptr; + } +} + +auto inline_tileset(const mln::style::TileSource& source) + -> const mln::Tileset* { + const auto& url_or_tileset = source.getURLOrTileset(); + return url_or_tileset.is() ? &url_or_tileset.get() + : nullptr; +} + +auto source_url(const mln::style::Source& source) + -> std::optional { + if (const auto* tile_source = tile_source_from_source(source)) { + return tile_source->getURL(); + } + if (const auto* geojson = source.as()) { + return geojson->getURL(); + } + if (const auto* image = source.as()) { + return image->getURL(); + } + return std::nullopt; +} + +auto has_tile_source_option( + const mln_style_tile_source_options& options, uint32_t field +) -> bool { + return (options.fields & field) != 0U; +} + +auto validate_zoom_option(double zoom, const char* name) -> mln_status { + if (!std::isfinite(zoom) || zoom < 0.0 || zoom > 255.0) { + auto message = std::string{name} + " must be finite and within [0, 255]"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_tile_source_option_header( + const mln_style_tile_source_options& options +) -> mln_status { + if (options.size < sizeof(mln_style_tile_source_options)) { + mln::core::set_thread_error( + "mln_style_tile_source_options.size is too small" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + constexpr auto known_fields = + static_cast(MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) | + MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM | + MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION | + MLN_STYLE_TILE_SOURCE_OPTION_SCHEME | MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS | + MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE | + MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING | + MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING; + if ((options.fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_style_tile_source_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_tile_source_zoom_options( + const mln_style_tile_source_options& options +) -> mln_status { + if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM)) { + const auto status = validate_zoom_option(options.min_zoom, "min_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM)) { + const auto status = validate_zoom_option(options.max_zoom, "max_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ( + has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) && + has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM) && + options.min_zoom > options.max_zoom + ) { + mln::core::set_thread_error( + "min_zoom must be less than or equal to max_zoom" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_tile_source_attribution_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION + )) { + return MLN_STATUS_OK; + } + return validate_string_view(options.attribution, "attribution") + ? MLN_STATUS_OK + : MLN_STATUS_INVALID_ARGUMENT; +} + +auto validate_tile_source_scheme_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_SCHEME)) { + return MLN_STATUS_OK; + } + switch (options.scheme) { + case MLN_STYLE_TILE_SCHEME_XYZ: + case MLN_STYLE_TILE_SCHEME_TMS: + return MLN_STATUS_OK; + default: + mln::core::set_thread_error("scheme is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } +} + +auto validate_tile_source_bounds_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { + return MLN_STATUS_OK; + } + return mln::core::validate_lat_lng_bounds(options.bounds); +} + +auto validate_tile_source_tile_size_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE + )) { + return MLN_STATUS_OK; + } + if (options.tile_size == 0 || options.tile_size > 65535U) { + mln::core::set_thread_error("tile_size must be within [1, 65535]"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_tile_source_vector_encoding_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING + )) { + return MLN_STATUS_OK; + } + switch (options.vector_encoding) { + case MLN_STYLE_VECTOR_TILE_ENCODING_MVT: + case MLN_STYLE_VECTOR_TILE_ENCODING_MLT: + return MLN_STATUS_OK; + default: + mln::core::set_thread_error("vector_encoding is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } +} + +auto validate_tile_source_raster_encoding_option( + const mln_style_tile_source_options& options +) -> mln_status { + if (!has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING + )) { + return MLN_STATUS_OK; + } + switch (options.raster_encoding) { + case MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX: + case MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM: + return MLN_STATUS_OK; + default: + mln::core::set_thread_error("raster_encoding is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } +} + +auto validate_tile_source_option_kind( + const mln_style_tile_source_options& options, TileSourceOptionKind kind +) -> mln_status { + if ( + kind != TileSourceOptionKind::Vector && + has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING + ) + ) { + mln::core::set_thread_error( + "vector_encoding is only valid for vector sources" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + kind != TileSourceOptionKind::RasterDEM && + has_tile_source_option( + options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING + ) + ) { + mln::core::set_thread_error( + "raster_encoding is only valid for raster DEM sources" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_tile_source_options( + const mln_style_tile_source_options* options, TileSourceOptionKind kind +) -> mln_status { + if (options == nullptr) { + return MLN_STATUS_OK; + } + for (const auto validator : { + validate_tile_source_option_header, + validate_tile_source_zoom_options, + validate_tile_source_attribution_option, + validate_tile_source_scheme_option, + validate_tile_source_bounds_option, + validate_tile_source_tile_size_option, + validate_tile_source_vector_encoding_option, + validate_tile_source_raster_encoding_option, + }) { + const auto status = validator(*options); + if (status != MLN_STATUS_OK) { + return status; + } + } + return validate_tile_source_option_kind(*options, kind); +} + +auto effective_tile_source_options(const mln_style_tile_source_options* options) + -> mln_style_tile_source_options { + auto result = mln::core::style_tile_source_options_default(); + if (options == nullptr) { + return result; + } + + result.fields = options->fields; + if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM)) { + result.min_zoom = options->min_zoom; + } + if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM)) { + result.max_zoom = options->max_zoom; + } + if ( + has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_ATTRIBUTION) + ) { + result.attribution = options->attribution; + } + if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_SCHEME)) { + result.scheme = options->scheme; + } + if (has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { + result.bounds = options->bounds; + } + if ( + has_tile_source_option(*options, MLN_STYLE_TILE_SOURCE_OPTION_TILE_SIZE) + ) { + result.tile_size = options->tile_size; + } + if ( + has_tile_source_option( + *options, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING + ) + ) { + result.vector_encoding = options->vector_encoding; + } + if ( + has_tile_source_option( + *options, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING + ) + ) { + result.raster_encoding = options->raster_encoding; + } + return result; +} + +auto to_native_tile_scheme(uint32_t scheme) -> mln::Tileset::Scheme { + return scheme == MLN_STYLE_TILE_SCHEME_TMS ? mln::Tileset::Scheme::TMS + : mln::Tileset::Scheme::XYZ; +} + +auto to_native_vector_encoding(uint32_t encoding) + -> mln::Tileset::VectorEncoding { + return encoding == MLN_STYLE_VECTOR_TILE_ENCODING_MLT + ? mln::Tileset::VectorEncoding::MLT + : mln::Tileset::VectorEncoding::Mapbox; +} + +auto to_native_raster_encoding(uint32_t encoding) + -> mln::Tileset::RasterEncoding { + return encoding == MLN_STYLE_RASTER_DEM_ENCODING_TERRARIUM + ? mln::Tileset::RasterEncoding::Terrarium + : mln::Tileset::RasterEncoding::Mapbox; +} + +auto validate_tile_urls(const mln_buffer_view* tiles, size_t tile_count) + -> mln_status { + if (tile_count == 0) { + mln::core::set_thread_error("tile_count must be greater than 0"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (tiles == nullptr) { + mln::core::set_thread_error("tiles must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + for (const auto tile : std::span{tiles, tile_count}) { + if (!validate_string_view(tile, "tile URL")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (tile.size == 0) { + mln::core::set_thread_error("tile URLs must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + return MLN_STATUS_OK; +} + +auto to_native_tile_urls(const mln_buffer_view* tiles, size_t tile_count) + -> std::vector { + auto result = std::vector{}; + result.reserve(tile_count); + for (const auto tile : std::span{tiles, tile_count}) { + result.push_back(string_from_view(tile)); + } + return result; +} + +auto to_native_tileset( + const mln_buffer_view* tiles, size_t tile_count, + const mln_style_tile_source_options& options, bool vector_source +) -> std::optional { + if (options.min_zoom > options.max_zoom) { + mln::core::set_thread_error( + "effective min_zoom must be less than or equal to max_zoom" + ); + return std::nullopt; + } + + auto tileset = mln::Tileset{ + to_native_tile_urls(tiles, tile_count), + mln::Range{ + static_cast(options.min_zoom), + static_cast(options.max_zoom) + }, + string_from_view(options.attribution), + to_native_tile_scheme(options.scheme), + std::nullopt, + vector_source + ? std::optional{to_native_vector_encoding( + options.vector_encoding + )} + : std::nullopt + }; + if (has_tile_source_option(options, MLN_STYLE_TILE_SOURCE_OPTION_BOUNDS)) { + tileset.bounds = mln::core::to_native_lat_lng_bounds(options.bounds); + } + return tileset; +} + +auto has_custom_geometry_source_option( + const mln_custom_geometry_source_options& options, uint32_t field +) -> bool { + return (options.fields & field) != 0U; +} + +auto validate_custom_geometry_zoom(double zoom, const char* name) + -> mln_status { + if ( + !std::isfinite(zoom) || zoom < 0.0 || zoom > 32.0 || + std::floor(zoom) != zoom + ) { + auto message = std::string{name} + " must be an integer within [0, 32]"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto effective_custom_geometry_source_options( + const mln_custom_geometry_source_options& options +) -> mln_custom_geometry_source_options; + +auto validate_custom_geometry_source_options( + const mln_custom_geometry_source_options* options +) -> mln_status { + if (options == nullptr) { + mln::core::set_thread_error("options must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (options->size < sizeof(mln_custom_geometry_source_options)) { + mln::core::set_thread_error( + "mln_custom_geometry_source_options.size is too small" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = + static_cast(MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM) | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TOLERANCE | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TILE_SIZE | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_BUFFER | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_CLIP | + MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_WRAP; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_custom_geometry_source_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (options->fetch_tile == nullptr) { + mln::core::set_thread_error("fetch_tile must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + has_custom_geometry_source_option( + *options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM + ) + ) { + const auto status = + validate_custom_geometry_zoom(options->min_zoom, "min_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ( + has_custom_geometry_source_option( + *options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM + ) + ) { + const auto status = + validate_custom_geometry_zoom(options->max_zoom, "max_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + const auto effective = effective_custom_geometry_source_options(*options); + if (effective.min_zoom > effective.max_zoom) { + mln::core::set_thread_error( + "min_zoom must be less than or equal to max_zoom" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!std::isfinite(effective.tolerance) || effective.tolerance < 0.0) { + mln::core::set_thread_error("tolerance must be finite and non-negative"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (effective.tile_size == 0 || effective.tile_size > 65535U) { + mln::core::set_thread_error("tile_size must be within [1, 65535]"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (effective.buffer > 65535U) { + mln::core::set_thread_error("buffer must be at most 65535"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto effective_custom_geometry_source_options( + const mln_custom_geometry_source_options& options +) -> mln_custom_geometry_source_options { + auto result = mln::core::custom_geometry_source_options_default(); + result.fields = options.fields; + result.fetch_tile = options.fetch_tile; + result.cancel_tile = options.cancel_tile; + result.user_data = options.user_data; + result.release_user_data = options.release_user_data; + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MIN_ZOOM + ) + ) { + result.min_zoom = options.min_zoom; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_MAX_ZOOM + ) + ) { + result.max_zoom = options.max_zoom; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TOLERANCE + ) + ) { + result.tolerance = options.tolerance; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_TILE_SIZE + ) + ) { + result.tile_size = options.tile_size; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_BUFFER + ) + ) { + result.buffer = options.buffer; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_CLIP + ) + ) { + result.clip = options.clip; + } + if ( + has_custom_geometry_source_option( + options, MLN_CUSTOM_GEOMETRY_SOURCE_OPTION_WRAP + ) + ) { + result.wrap = options.wrap; + } + return result; +} + +auto to_c_canonical_tile_id(const mln::CanonicalTileID& tile_id) + -> mln_canonical_tile_id { + return mln_canonical_tile_id{.z = tile_id.z, .x = tile_id.x, .y = tile_id.y}; +} + +auto to_native_tile_function( + mln_custom_geometry_source_tile_callback callback, void* user_data +) -> mln::style::TileFunction { + if (callback == nullptr) { + return nullptr; + } + return [callback, user_data](const mln::CanonicalTileID& tile_id) -> void { + try { + callback(user_data, to_c_canonical_tile_id(tile_id)); + } catch (const std::exception& exception) { + mln::core::set_thread_error(exception); + } catch (...) { + mln::core::set_thread_error("custom geometry source callback threw"); + } + }; +} + +auto to_native_custom_geometry_source_options( + const mln_custom_geometry_source_options& options +) -> mln::style::CustomGeometrySource::Options { + auto result = mln::style::CustomGeometrySource::Options{}; + result.fetchTileFunction = + to_native_tile_function(options.fetch_tile, options.user_data); + result.cancelTileFunction = + to_native_tile_function(options.cancel_tile, options.user_data); + result.zoomRange = mln::Range{ + static_cast(options.min_zoom), + static_cast(options.max_zoom) + }; + result.tileOptions = mln::style::CustomGeometrySource::TileOptions{ + .tolerance = options.tolerance, + .tileSize = static_cast(options.tile_size), + .buffer = static_cast(options.buffer), + .clip = options.clip, + .wrap = options.wrap + }; + return result; +} + +auto has_custom_mvt_vector_source_option( + const mln_custom_mvt_vector_source_options& options, uint32_t field +) -> bool { + return (options.fields & field) != 0U; +} + +auto effective_custom_mvt_vector_source_options( + const mln_custom_mvt_vector_source_options& options +) -> mln_custom_mvt_vector_source_options; + +auto validate_custom_mvt_vector_source_options( + const mln_custom_mvt_vector_source_options* options +) -> mln_status { + if (options == nullptr) { + mln::core::set_thread_error("options must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (options->size < sizeof(mln_custom_mvt_vector_source_options)) { + mln::core::set_thread_error( + "mln_custom_mvt_vector_source_options.size is too small" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = + static_cast(MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM) | + MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_custom_mvt_vector_source_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (options->fetch_tile == nullptr) { + mln::core::set_thread_error("fetch_tile must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + has_custom_mvt_vector_source_option( + *options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM + ) + ) { + const auto status = + validate_custom_geometry_zoom(options->min_zoom, "min_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + if ( + has_custom_mvt_vector_source_option( + *options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM + ) + ) { + const auto status = + validate_custom_geometry_zoom(options->max_zoom, "max_zoom"); + if (status != MLN_STATUS_OK) { + return status; + } + } + const auto effective = effective_custom_mvt_vector_source_options(*options); + if (effective.min_zoom > effective.max_zoom) { + mln::core::set_thread_error( + "min_zoom must be less than or equal to max_zoom" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto effective_custom_mvt_vector_source_options( + const mln_custom_mvt_vector_source_options& options +) -> mln_custom_mvt_vector_source_options { + auto result = mln::core::custom_mvt_vector_source_options_default(); + result.fields = options.fields; + result.fetch_tile = options.fetch_tile; + result.cancel_tile = options.cancel_tile; + result.user_data = options.user_data; + result.release_user_data = options.release_user_data; + if ( + has_custom_mvt_vector_source_option( + options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MIN_ZOOM + ) + ) { + result.min_zoom = options.min_zoom; + } + if ( + has_custom_mvt_vector_source_option( + options, MLN_CUSTOM_MVT_VECTOR_SOURCE_OPTION_MAX_ZOOM + ) + ) { + result.max_zoom = options.max_zoom; + } + return result; +} + +auto to_native_custom_mvt_vector_source_options( + const mln_custom_mvt_vector_source_options& options +) -> mln::style::CustomVectorSource::Options { + auto result = mln::style::CustomVectorSource::Options{}; + result.fetchTileFunction = + to_native_tile_function(options.fetch_tile, options.user_data); + result.cancelTileFunction = + to_native_tile_function(options.cancel_tile, options.user_data); + result.zoomRange = mln::Range{ + static_cast(options.min_zoom), + static_cast(options.max_zoom) + }; + return result; +} + +} // namespace + +namespace mln::core { + +auto validate_canonical_tile_id(mln_canonical_tile_id tile_id) -> mln_status { + if (tile_id.z > 32U) { + mln::core::set_thread_error("tile_id.z must be within [0, 32]"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto coordinate_limit = uint64_t{1} << tile_id.z; + if (tile_id.x >= coordinate_limit || tile_id.y >= coordinate_limit) { + mln::core::set_thread_error("tile_id x and y must be within zoom bounds"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto to_native_canonical_tile_id(mln_canonical_tile_id tile_id) + -> mln::CanonicalTileID { + return mln::CanonicalTileID{ + static_cast(tile_id.z), tile_id.x, tile_id.y + }; +} + +auto validate_source_id(mln_buffer_view source_id) -> mln_status { + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + mln::core::set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_source_can_be_added( + mln::style::Style& style, const std::string& source_id +) -> mln_status { + if (style.getSource(source_id) != nullptr) { + mln::core::set_thread_error("source already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto has_style_image_option( + const mln_style_image_options& options, uint32_t field +) -> bool { + return (options.fields & field) != 0U; +} + +auto validate_style_image_options(const mln_style_image_options* options) + -> mln_status { + if (options == nullptr) { + return MLN_STATUS_OK; + } + if (options->size < sizeof(mln_style_image_options)) { + mln::core::set_thread_error("mln_style_image_options.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto known_fields = + static_cast(MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO) | + MLN_STYLE_IMAGE_OPTION_SDF | MLN_STYLE_IMAGE_OPTION_STRETCH_X | + MLN_STYLE_IMAGE_OPTION_STRETCH_Y | MLN_STYLE_IMAGE_OPTION_CONTENT | + MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH | + MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT; + if ((options->fields & ~known_fields) != 0U) { + mln::core::set_thread_error( + "mln_style_image_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO) && + (!std::isfinite(options->pixel_ratio) || options->pixel_ratio <= 0.0F) + ) { + mln::core::set_thread_error("pixel_ratio must be finite and positive"); + return MLN_STATUS_INVALID_ARGUMENT; + } + for (const auto& [field, stretches, count, name] : { + std::tuple{ + uint32_t{MLN_STYLE_IMAGE_OPTION_STRETCH_X}, options->stretch_x, + options->stretch_x_count, "stretch_x" + }, + std::tuple{ + uint32_t{MLN_STYLE_IMAGE_OPTION_STRETCH_Y}, options->stretch_y, + options->stretch_y_count, "stretch_y" + }, + }) { + if (!has_style_image_option(*options, field)) { + continue; + } + if (stretches == nullptr && count != 0) { + auto message = + std::string{name} + " must not be null when its count is non-zero"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + for (size_t index = 0; index < count; index += 1) { + const auto& stretch = stretches[index]; + if (!std::isfinite(stretch.from) || !std::isfinite(stretch.to)) { + auto message = std::string{name} + " intervals must be finite"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + // MapLibre divides by the summed interval width, so an axis whose + // intervals are all zero-width would divide by zero. + if (stretch.from >= stretch.to) { + auto message = + std::string{name} + " intervals must have a positive width"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (index != 0 && stretch.from < stretches[index - 1].to) { + auto message = + std::string{name} + " intervals must increase and must not overlap"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT)) { + const auto& content = options->content; + if ( + !std::isfinite(content.left) || !std::isfinite(content.top) || + !std::isfinite(content.right) || !std::isfinite(content.bottom) + ) { + mln::core::set_thread_error("content insets must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (content.left > content.right || content.top > content.bottom) { + mln::core::set_thread_error("content insets must not run backwards"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + for (const auto& [field, value, name] : { + std::tuple{ + uint32_t{MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH}, + options->text_fit_width, "text_fit_width" + }, + std::tuple{ + uint32_t{MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT}, + options->text_fit_height, "text_fit_height" + }, + }) { + if (!has_style_image_option(*options, field)) { + continue; + } + switch (value) { + case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK: + case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY: + case MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL: + break; + default: { + auto message = std::string{name} + " is invalid"; + mln::core::set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + } + return MLN_STATUS_OK; +} + +auto effective_style_image_options(const mln_style_image_options* options) + -> mln_style_image_options { + auto result = mln::core::style_image_options_default(); + if (options == nullptr) { + return result; + } + result.fields = options->fields; + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_PIXEL_RATIO)) { + result.pixel_ratio = options->pixel_ratio; + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_SDF)) { + result.sdf = options->sdf; + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_STRETCH_X)) { + result.stretch_x = options->stretch_x; + result.stretch_x_count = options->stretch_x_count; + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_STRETCH_Y)) { + result.stretch_y = options->stretch_y; + result.stretch_y_count = options->stretch_y_count; + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT)) { + result.content = options->content; + } + if (has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH)) { + result.text_fit_width = options->text_fit_width; + } + if ( + has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT) + ) { + result.text_fit_height = options->text_fit_height; + } + return result; +} + +auto to_native_image_stretches(const mln_image_stretch* stretches, size_t count) + -> mln::style::ImageStretches { + auto native = mln::style::ImageStretches{}; + native.reserve(count); + for (size_t index = 0; index < count; index += 1) { + native.emplace_back(stretches[index].from, stretches[index].to); + } + return native; +} + +auto to_native_text_fit(uint32_t value) -> mln::style::TextFit { + switch (value) { + case MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY: + return mln::style::TextFit::stretchOnly; + case MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL: + return mln::style::TextFit::proportional; + default: + return mln::style::TextFit::stretchOrShrink; + } +} + +auto from_native_text_fit(mln::style::TextFit value) -> uint32_t { + switch (value) { + case mln::style::TextFit::stretchOnly: + return MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_ONLY; + case mln::style::TextFit::proportional: + return MLN_STYLE_IMAGE_TEXT_FIT_PROPORTIONAL; + default: + return MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK; + } +} + +auto required_premultiplied_rgba8_bytes( + uint32_t width, uint32_t height, uint32_t stride +) -> std::optional { + constexpr auto channels = size_t{4}; + const auto row_bytes = static_cast(width) * channels; + if (height == 0) { + return std::nullopt; + } + if (height == 1) { + return row_bytes; + } + const auto trailing_rows = static_cast(height - 1U); + const auto row_stride = static_cast(stride); + if ( + trailing_rows > + (std::numeric_limits::max() - row_bytes) / row_stride + ) { + return std::nullopt; + } + return (trailing_rows * row_stride) + row_bytes; +} + +auto validate_premultiplied_rgba8_image( + const mln_premultiplied_rgba8_image* image +) -> mln_status { + if (image == nullptr) { + mln::core::set_thread_error("image must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (image->size < sizeof(mln_premultiplied_rgba8_image)) { + mln::core::set_thread_error( + "mln_premultiplied_rgba8_image.size is too small" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (image->width == 0 || image->height == 0) { + mln::core::set_thread_error("image dimensions must be positive"); + return MLN_STATUS_INVALID_ARGUMENT; + } + constexpr auto channels = uint32_t{4}; + if (image->width > std::numeric_limits::max() / channels) { + mln::core::set_thread_error("image row byte length overflows"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto row_bytes = image->width * channels; + if (image->stride < row_bytes) { + mln::core::set_thread_error("image stride must be at least width * 4"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (image->pixels == nullptr) { + mln::core::set_thread_error("image pixels must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto required = required_premultiplied_rgba8_bytes( + image->width, image->height, image->stride + ); + if (!required || image->byte_length < *required) { + mln::core::set_thread_error("image byte_length is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto to_native_premultiplied_rgba8_image( + const mln_premultiplied_rgba8_image& image +) -> mln::PremultipliedImage { + auto result = mln::PremultipliedImage{mln::Size{image.width, image.height}}; + const auto output_stride = result.stride(); + const auto row_bytes = static_cast(image.width) * 4U; + const auto input = std::span{image.pixels, image.byte_length}; + const auto output = std::span{result.data.get(), result.bytes()}; + for (auto row = uint32_t{0}; row < image.height; ++row) { + const auto input_offset = static_cast(row) * image.stride; + const auto output_offset = static_cast(row) * output_stride; + std::copy_n( + input.subspan(input_offset, row_bytes).begin(), row_bytes, + output.subspan(output_offset, row_bytes).begin() + ); + } + return result; +} + +auto validate_image_id(mln_buffer_view image_id) -> mln_status { + if (!validate_string_view(image_id, "image_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (image_id.size == 0) { + mln::core::set_thread_error("image_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto style_image_info_from_native(const mln::style::Image& image) + -> mln_style_image_info { + const auto& pixels = image.getImage(); + return mln_style_image_info{ + .size = sizeof(mln_style_image_info), + .width = pixels.size.width, + .height = pixels.size.height, + .stride = static_cast(pixels.stride()), + .byte_length = pixels.bytes(), + .stretch_x_count = image.getStretchX().size(), + .stretch_y_count = image.getStretchY().size(), + .content = + image.getContent().has_value() + ? mln_image_content{ + .left = image.getContent()->left, + .top = image.getContent()->top, + .right = image.getContent()->right, + .bottom = image.getContent()->bottom + } + : mln_image_content{.left = 0, .top = 0, .right = 0, .bottom = 0}, + .text_fit_width = + image.getTextFitWidth().has_value() + ? from_native_text_fit(*image.getTextFitWidth()) + : static_cast(MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK), + .text_fit_height = + image.getTextFitHeight().has_value() + ? from_native_text_fit(*image.getTextFitHeight()) + : static_cast(MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK), + .pixel_ratio = image.getPixelRatio(), + .sdf = image.isSdf(), + .has_content = image.getContent().has_value(), + .has_text_fit_width = image.getTextFitWidth().has_value(), + .has_text_fit_height = image.getTextFitHeight().has_value() + }; +} + +auto validate_image_source_coordinates( + const mln_lat_lng* coordinates, size_t coordinate_count +) -> mln_status { + if (coordinate_count != 4) { + mln::core::set_thread_error("image source coordinate_count must be 4"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto status = + validate_lat_lng_array(coordinates, coordinate_count, false); + if (status != MLN_STATUS_OK) { + return status; + } + + const auto coordinate_span = std::span{coordinates, 4}; + const auto first = coordinate_span.front(); + const auto all_same = + std::ranges::all_of(coordinate_span, [first](mln_lat_lng value) -> bool { + return value.latitude == first.latitude && + value.longitude == first.longitude; + }); + if (all_same) { + mln::core::set_thread_error("image source coordinates must not all match"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto to_native_image_source_coordinates(const mln_lat_lng* coordinates) + -> std::array { + auto result = std::array{}; + const auto coordinate_span = std::span{coordinates, 4}; + auto index = size_t{0}; + for (const auto coordinate : coordinate_span) { + result.at(index) = to_native_lat_lng(coordinate); + ++index; + } + return result; +} + +auto from_native_image_source_coordinates( + const std::array& coordinates +) -> std::array { + auto result = std::array{}; + for (auto index = size_t{0}; index < result.size(); ++index) { + result.at(index) = from_native_lat_lng(coordinates.at(index)); + } + return result; +} + +auto create_style_id_list( + std::vector ids, mln_style_id_list* out_list +) -> mln_status { + if (out_list == nullptr || *out_list != MLN_HANDLE_NULL) { + mln::core::set_thread_error( + "out_list must not be null and *out_list must be the null handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto list = std::make_shared(); + list->ids = std::move(ids); + *out_list = mln::core::handle_table().insert( + std::move(list) + ); + return MLN_STATUS_OK; +} + +auto create_style_string_list( + std::vector values, mln_style_string_list* out_list +) -> mln_status { + if (out_list == nullptr || *out_list != MLN_HANDLE_NULL) { + mln::core::set_thread_error( + "out_list must not be null and *out_list must be the null handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto list = std::make_shared(); + list->values = std::move(values); + *out_list = + mln::core::handle_table().insert( + std::move(list) + ); + return MLN_STATUS_OK; +} + +auto validate_lat_lng(mln_lat_lng coordinate) -> mln_status { + if ( + !std::isfinite(coordinate.latitude) || coordinate.latitude < -90.0 || + coordinate.latitude > 90.0 || !std::isfinite(coordinate.longitude) + ) { + mln::core::set_thread_error( + "latitude must be finite and within [-90, 90], and longitude must be " + "finite" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln_status { + const auto southwest_status = validate_lat_lng(bounds.southwest); + if (southwest_status != MLN_STATUS_OK) { + return southwest_status; + } + const auto northeast_status = validate_lat_lng(bounds.northeast); + if (northeast_status != MLN_STATUS_OK) { + return northeast_status; + } + if ( + bounds.southwest.latitude > bounds.northeast.latitude || + bounds.southwest.longitude > bounds.northeast.longitude + ) { + mln::core::set_thread_error( + "bounds southwest must be less than or equal to northeast" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_lat_lng_array( + const mln_lat_lng* coordinates, size_t coordinate_count, bool allow_empty +) -> mln_status { + if (coordinate_count == 0) { + if (allow_empty) { + return MLN_STATUS_OK; + } + mln::core::set_thread_error("coordinate_count must be greater than 0"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + if (coordinates == nullptr) { + mln::core::set_thread_error("coordinates must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto coordinate_span = + std::span{coordinates, coordinate_count}; + for (const auto coordinate : coordinate_span) { + const auto status = validate_lat_lng(coordinate); + if (status != MLN_STATUS_OK) { + return status; + } + } + return MLN_STATUS_OK; +} + +auto to_native_lat_lng(mln_lat_lng coordinate) -> mln::LatLng { + return mln::LatLng{coordinate.latitude, coordinate.longitude}; +} + +auto from_native_lat_lng(const mln::LatLng& coordinate) -> mln_lat_lng { + return mln_lat_lng{ + .latitude = coordinate.latitude(), .longitude = coordinate.longitude() + }; +} + +auto to_native_lat_lng_bounds(mln_lat_lng_bounds bounds) -> mln::LatLngBounds { + return mln::LatLngBounds::hull( + to_native_lat_lng(bounds.southwest), to_native_lat_lng(bounds.northeast) + ); +} + +auto from_native_lat_lng_bounds(const mln::LatLngBounds& bounds) + -> mln_lat_lng_bounds { + return mln_lat_lng_bounds{ + .southwest = + mln_lat_lng{.latitude = bounds.south(), .longitude = bounds.west()}, + .northeast = + mln_lat_lng{.latitude = bounds.north(), .longitude = bounds.east()} + }; +} + +auto copy_text( + const std::string& text, char* out_text, size_t text_capacity, + size_t* out_text_size, const char* capacity_name +) -> mln_status { + if (out_text == nullptr && text_capacity > 0) { + set_thread_error( + "output buffer must not be null when capacity is non-zero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_text_size == nullptr) { + set_thread_error("output size pointer must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + *out_text_size = text.size(); + // A null buffer with zero capacity is a size probe, so it succeeds rather + // than sharing a status with a missing object. + if (out_text == nullptr) { + return MLN_STATUS_OK; + } + if (text_capacity < text.size()) { + auto message = std::string{capacity_name} + " is too small"; + set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!text.empty()) { + std::copy(text.begin(), text.end(), out_text); + } + return MLN_STATUS_OK; +} + +using DoubleMilliseconds = std::chrono::duration; + +auto max_native_duration_ms() -> double { + return std::chrono::duration_cast(mln::Duration::max()) + .count(); +} + +auto duration_from_milliseconds(double milliseconds) -> mln::Duration { + return std::chrono::duration_cast( + DoubleMilliseconds{milliseconds} + ); +} + +auto milliseconds_from_duration(mln::Duration duration) -> double { + return std::chrono::duration_cast(duration).count(); +} + +// The accepted bound is exclusive because mln::Duration::max() has no exact +// double representation: the nearest double converts back to 2^63 ticks, one +// past the largest representable count. The margin holds for a nanosecond +// duration only, so pin the representation. +static_assert( + std::is_same_v, + "the accepted duration bound is derived from a nanosecond mln::Duration" +); + +auto is_native_duration_ms(double milliseconds) -> bool { + return std::isfinite(milliseconds) && milliseconds >= 0.0 && + milliseconds < max_native_duration_ms(); +} + +} // namespace mln::core + +namespace mln::core { + +auto style_tile_source_options_default() noexcept + -> mln_style_tile_source_options { + return mln_style_tile_source_options{ + .size = sizeof(mln_style_tile_source_options), + .fields = 0, + .min_zoom = 0, + .max_zoom = mln::util::DEFAULT_MAX_ZOOM, + .attribution = {.data = nullptr, .size = 0}, + .scheme = MLN_STYLE_TILE_SCHEME_XYZ, + .bounds = + {.southwest = {.latitude = 0, .longitude = 0}, + .northeast = {.latitude = 0, .longitude = 0}}, + .tile_size = mln::util::tileSize_I, + .vector_encoding = MLN_STYLE_VECTOR_TILE_ENCODING_MVT, + .raster_encoding = MLN_STYLE_RASTER_DEM_ENCODING_MAPBOX + }; +} + +auto geojson_source_options_default() noexcept -> mln_geojson_source_options { + const auto defaults = mln::style::GeoJSONOptions{}; + return mln_geojson_source_options{ + .size = sizeof(mln_geojson_source_options), + .fields = 0, + .min_zoom = static_cast(defaults.minzoom), + .max_zoom = static_cast(defaults.maxzoom), + .tolerance = defaults.tolerance, + .cluster_max_zoom = static_cast(defaults.clusterMaxZoom), + .cluster_properties = {}, + .tile_size = defaults.tileSize, + .buffer = defaults.buffer, + .cluster_radius = defaults.clusterRadius, + .cluster_min_points = static_cast(defaults.clusterMinPoints), + .line_metrics = defaults.lineMetrics, + .cluster = defaults.cluster, + .synchronous_tiling = defaults.synchronousUpdate + }; +} + +auto custom_geometry_source_options_default() noexcept + -> mln_custom_geometry_source_options { + return mln_custom_geometry_source_options{ + .size = sizeof(mln_custom_geometry_source_options), + .fields = 0, + .fetch_tile = nullptr, + .cancel_tile = nullptr, + .user_data = nullptr, + .min_zoom = 0, + .max_zoom = 18, + .tolerance = 0.375, + .tile_size = mln::util::tileSize_I, + .buffer = 128, + .clip = false, + .wrap = false, + .release_user_data = nullptr + }; +} + +auto custom_mvt_vector_source_options_default() noexcept + -> mln_custom_mvt_vector_source_options { + return mln_custom_mvt_vector_source_options{ + .size = sizeof(mln_custom_mvt_vector_source_options), + .fields = 0, + .fetch_tile = nullptr, + .cancel_tile = nullptr, + .user_data = nullptr, + .min_zoom = 0, + .max_zoom = 18, + .release_user_data = nullptr + }; +} + +auto premultiplied_rgba8_image_default() noexcept + -> mln_premultiplied_rgba8_image { + return mln_premultiplied_rgba8_image{ + .size = sizeof(mln_premultiplied_rgba8_image), + .width = 0, + .height = 0, + .stride = 0, + .pixels = nullptr, + .byte_length = 0 + }; +} + +auto style_image_options_default() noexcept -> mln_style_image_options { + return mln_style_image_options{ + .size = sizeof(mln_style_image_options), + .fields = 0, + .stretch_x = nullptr, + .stretch_x_count = 0, + .stretch_y = nullptr, + .stretch_y_count = 0, + .content = {.left = 0, .top = 0, .right = 0, .bottom = 0}, + .text_fit_width = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, + .text_fit_height = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, + .pixel_ratio = 1.0F, + .sdf = false + }; +} + +auto style_image_info_default() noexcept -> mln_style_image_info { + return mln_style_image_info{ + .size = sizeof(mln_style_image_info), + .width = 0, + .height = 0, + .stride = 0, + .byte_length = 0, + .stretch_x_count = 0, + .stretch_y_count = 0, + .content = {.left = 0, .top = 0, .right = 0, .bottom = 0}, + .text_fit_width = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, + .text_fit_height = MLN_STYLE_IMAGE_TEXT_FIT_STRETCH_OR_SHRINK, + .pixel_ratio = 1.0F, + .sdf = false, + .has_content = false, + .has_text_fit_width = false, + .has_text_fit_height = false + }; +} + +auto style_transition_options_default() noexcept + -> mln_style_transition_options { + return mln_style_transition_options{ + .size = sizeof(mln_style_transition_options), + .fields = 0, + .duration_ms = 0.0, + .delay_ms = 0.0, + .enable_placement_transitions = true + }; +} + +auto validate_geojson_command_options(const mln_geojson_source_options* options) + -> mln_status { + return validate_geojson_source_options(options); +} + +auto validate_tile_command_options( + const mln_style_tile_source_options* options, uint32_t kind +) -> mln_status { + switch (kind) { + case 0: + return validate_tile_source_options( + options, TileSourceOptionKind::Vector + ); + case 1: + return validate_tile_source_options( + options, TileSourceOptionKind::Raster + ); + case 2: + return validate_tile_source_options( + options, TileSourceOptionKind::RasterDEM + ); + default: + set_thread_error("tile source kind is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } +} + +auto validate_custom_geometry_command_options( + const mln_custom_geometry_source_options* options +) -> mln_status { + return validate_custom_geometry_source_options(options); +} + +auto validate_custom_mvt_command_options( + const mln_custom_mvt_vector_source_options* options +) -> mln_status { + return validate_custom_mvt_vector_source_options(options); +} + +auto validate_style_image_command_input( + const mln_premultiplied_rgba8_image* image, + const mln_style_image_options* options +) -> mln_status { + const auto image_status = validate_premultiplied_rgba8_image(image); + return image_status == MLN_STATUS_OK ? validate_style_image_options(options) + : image_status; +} + +auto validate_image_source_command_coordinates( + const mln_lat_lng* coordinates, size_t coordinate_count +) -> mln_status { + return validate_image_source_coordinates(coordinates, coordinate_count); +} + +auto style_id_list_count(mln_style_id_list list, size_t* out_count) + -> mln_status { + if (out_count == nullptr) { + set_thread_error("out_count must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + // A style ID list carries no thread affinity, so another thread may destroy + // it mid-read; the lock spans the read. + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + *out_count = live_list->ids.size(); + return MLN_STATUS_OK; +} + +auto style_id_list_get( + mln_style_id_list list, size_t index, mln_buffer_view* out_id +) -> mln_status { + if (out_id == nullptr) { + set_thread_error("out_id must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (index >= live_list->ids.size()) { + set_thread_error("index is out of range"); + return MLN_STATUS_INVALID_ARGUMENT; + } + *out_id = string_view_from_string(live_list->ids.at(index)); + return MLN_STATUS_OK; +} + +auto style_id_list_destroy(mln_style_id_list list) -> void { + static_cast(handle_table().remove(list)); +} + +auto style_string_list_count(mln_style_string_list list, size_t* out_count) + -> mln_status { + if (out_count == nullptr) { + set_thread_error("out_count must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + *out_count = live_list->values.size(); + return MLN_STATUS_OK; +} + +auto style_string_list_get( + mln_style_string_list list, size_t index, mln_buffer_view* out_value +) -> mln_status { + if (out_value == nullptr) { + set_thread_error("out_value must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (index >= live_list->values.size()) { + set_thread_error("index is out of range"); + return MLN_STATUS_INVALID_ARGUMENT; + } + *out_value = string_view_from_string(live_list->values.at(index)); + return MLN_STATUS_OK; +} + +auto style_string_list_destroy(mln_style_string_list list) -> void { + static_cast(handle_table().remove(list)); +} + +auto map_add_style_source_json( + mln_map map, mln_buffer_view source_id, mln_buffer_view source_json +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!validate_bytes(source_json, "style source")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + if (style.getSource(id) != nullptr) { + set_thread_error("source already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto error = mln::style::conversion::Error{}; + auto source = + mln::style::conversion::convertJSON>( + string_from_view(source_json), error, id + ); + if (!source) { + set_style_conversion_error("style source", error); + return MLN_STATUS_INVALID_ARGUMENT; + } + + style.addSource(std::move(*source)); + return MLN_STATUS_OK; +} + +auto map_remove_style_source(mln_map map, mln_buffer_view source_id) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + if (style.getSource(id) == nullptr) { + set_thread_error(("no style source has ID " + id).c_str()); + return MLN_STATUS_NOT_FOUND; + } + + auto removed = style.removeSource(id); + if (!removed) { + set_thread_error("source is used by a layer"); + return MLN_STATUS_INVALID_STATE; + } + // The detached source is dropped before the release runs, so the style no + // longer holds the callbacks that read the host's state. + removed.reset(); + release_callback_source(*live, id); + return MLN_STATUS_OK; +} + +auto map_get_style_source_info( + mln_map map, mln_buffer_view source_id, mln_style_source_info* out_info, + bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_info == nullptr || out_info->size < sizeof(mln_style_source_info)) { + set_thread_error("out_info must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_found == nullptr) { + set_thread_error("out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + *out_found = source != nullptr; + *out_info = mln_style_source_info{}; + out_info->size = sizeof(mln_style_source_info); + out_info->type = MLN_STYLE_SOURCE_TYPE_UNKNOWN; + if (source == nullptr) { + return MLN_STATUS_OK; + } + + const auto attribution = source->getAttribution(); + out_info->type = to_c_source_type(source->getType()); + out_info->id_size = source->getID().size(); + out_info->is_volatile = source->isVolatile(); + out_info->has_attribution = attribution.has_value(); + out_info->attribution_size = attribution ? attribution->size() : 0; + + const auto url = source_url(*source); + if (url) { + out_info->fields |= MLN_STYLE_SOURCE_INFO_URL; + out_info->url_size = url->size(); + } + + const auto* tile_source = tile_source_from_source(*source); + if (tile_source == nullptr) { + return MLN_STATUS_OK; + } + + out_info->fields |= MLN_STYLE_SOURCE_INFO_TILE_SIZE; + out_info->tile_size = tile_source->getTileSize(); + if (const auto* vector_source = source->as()) { + out_info->fields |= MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING; + out_info->vector_encoding = + to_c_vector_encoding(vector_source->getEncoding()); + } + + const auto* tileset = inline_tileset(*tile_source); + if (tileset == nullptr) { + return MLN_STATUS_OK; + } + + out_info->fields |= MLN_STYLE_SOURCE_INFO_TILEJSON; + out_info->tile_count = tileset->tiles.size(); + out_info->min_zoom = tileset->zoomRange.min; + out_info->max_zoom = tileset->zoomRange.max; + out_info->scheme = to_c_tile_scheme(tileset->scheme); + if (tileset->bounds) { + out_info->fields |= MLN_STYLE_SOURCE_INFO_BOUNDS; + out_info->bounds = from_native_lat_lng_bounds(*tileset->bounds); + } + if (tileset->vectorEncoding) { + out_info->fields |= MLN_STYLE_SOURCE_INFO_VECTOR_ENCODING; + out_info->vector_encoding = to_c_vector_encoding(*tileset->vectorEncoding); + } + if (tileset->rasterEncoding) { + out_info->fields |= MLN_STYLE_SOURCE_INFO_RASTER_ENCODING; + out_info->raster_encoding = to_c_raster_encoding(*tileset->rasterEncoding); + } + return MLN_STATUS_OK; +} + +auto map_copy_style_source_attribution( + mln_map map, mln_buffer_view source_id, char* out_attribution, + size_t attribution_capacity, size_t* out_attribution_size, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_attribution == nullptr && attribution_capacity > 0) { + set_thread_error( + "out_attribution must not be null when capacity is non-zero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_attribution_size == nullptr || out_found == nullptr) { + set_thread_error("out_attribution_size and out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + *out_found = false; + *out_attribution_size = 0; + if (source == nullptr) { + return MLN_STATUS_OK; + } + + const auto attribution = source->getAttribution(); + if (!attribution) { + return MLN_STATUS_OK; + } + *out_found = true; + *out_attribution_size = attribution->size(); + // A null buffer with zero capacity is a size probe, so it reports the length + // and succeeds rather than sharing a status with a missing source. + if (out_attribution == nullptr) { + return MLN_STATUS_OK; + } + if (attribution_capacity < attribution->size()) { + set_thread_error("attribution_capacity is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!attribution->empty()) { + std::copy(attribution->begin(), attribution->end(), out_attribution); + } + return MLN_STATUS_OK; +} + +auto map_copy_style_source_url( + mln_map map, mln_buffer_view source_id, char* out_url, size_t url_capacity, + size_t* out_url_size, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_url == nullptr && url_capacity > 0) { + set_thread_error("out_url must not be null when capacity is non-zero"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_url_size == nullptr || out_found == nullptr) { + set_thread_error("out_url_size and out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + const auto url = source == nullptr ? std::nullopt : source_url(*source); + *out_found = url.has_value(); + if (!url) { + *out_url_size = 0; + return MLN_STATUS_OK; + } + + return copy_text(*url, out_url, url_capacity, out_url_size, "url_capacity"); +} + +auto map_get_style_source_tile_urls( + mln_map map, mln_buffer_view source_id, mln_style_string_list* out_tile_urls, + bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + out_tile_urls == nullptr || *out_tile_urls != MLN_HANDLE_NULL || + out_found == nullptr + ) { + set_thread_error( + "out_tile_urls must not be null, *out_tile_urls must be the null handle, " + "and out_found must not be null" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + *out_found = source != nullptr; + if (source == nullptr) { + return MLN_STATUS_OK; + } + + auto tile_urls = std::vector{}; + if (const auto* tile_source = tile_source_from_source(*source)) { + if (const auto* tileset = inline_tileset(*tile_source)) { + tile_urls = tileset->tiles; + } + } + return create_style_string_list(std::move(tile_urls), out_tile_urls); +} + +auto map_list_style_source_ids(mln_map map, mln_style_id_list* out_source_ids) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + + auto ids = std::vector{}; + for (const auto* source : map_native(live)->getStyle().getSources()) { + ids.push_back(source->getID()); + } + return create_style_id_list(std::move(ids), out_source_ids); +} + +auto map_add_geojson_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_geojson_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto options_status = validate_geojson_source_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + auto native_options = + to_native_geojson_source_options(effective_geojson_source_options(options)); + if (!native_options) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto source = + std::make_unique(id, std::move(*native_options)); + source->setURL(string_from_view(url)); + style.addSource(std::move(source)); + return MLN_STATUS_OK; +} + +auto map_add_geojson_source_data( + mln_map map, mln_buffer_view source_id, + const std::shared_ptr& data +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + + if (data == nullptr) { + set_thread_error("prepared GeoJSON data is not live"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + auto source = std::make_unique(id, data->options); + source->setGeoJSONData(data->data); + style.addSource(std::move(source)); + return MLN_STATUS_OK; +} + +auto map_set_geojson_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* geojson_source = source->as(); + if (geojson_source == nullptr) { + set_thread_error("source is not a GeoJSON source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + geojson_source->setURL(string_from_view(url)); + return MLN_STATUS_OK; +} + +auto map_set_geojson_source_data( + mln_map map, mln_buffer_view source_id, + const std::shared_ptr& data +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + + if (data == nullptr) { + set_thread_error("prepared GeoJSON data is not live"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* geojson_source = source->as(); + if (geojson_source == nullptr) { + set_thread_error("source is not a GeoJSON source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!geojson_source_options_equal( + geojson_source->getOptions(), *data->options + )) { + const auto message = + "prepared GeoJSON data options do not match the options that GeoJSON " + "source \"" + + string_from_view(source_id) + "\" was added with"; + set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + + geojson_source->setGeoJSONData(data->data); + return MLN_STATUS_OK; +} + +auto map_set_geojson_source_synchronous_tiling( + mln_map map, mln_buffer_view source_id, bool enabled +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* geojson_source = source->as(); + if (geojson_source == nullptr) { + set_thread_error("source is not a GeoJSON source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + geojson_source->setOverrideSynchronousUpdate(enabled); + return MLN_STATUS_OK; +} + +auto map_add_vector_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::Vector); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + auto min_zoom = std::optional{}; + if ( + has_tile_source_option(effective, MLN_STYLE_TILE_SOURCE_OPTION_MIN_ZOOM) + ) { + min_zoom = static_cast(effective.min_zoom); + } + auto max_zoom = std::optional{}; + if ( + has_tile_source_option(effective, MLN_STYLE_TILE_SOURCE_OPTION_MAX_ZOOM) + ) { + max_zoom = static_cast(effective.max_zoom); + } + + if ( + has_tile_source_option( + effective, MLN_STYLE_TILE_SOURCE_OPTION_VECTOR_ENCODING + ) + ) { + style.addSource( + std::make_unique( + id, string_from_view(url), max_zoom, min_zoom, + to_native_vector_encoding(effective.vector_encoding) + ) + ); + } else { + style.addSource( + std::make_unique( + id, string_from_view(url), max_zoom, min_zoom + ) + ); + } + return MLN_STATUS_OK; +} + +auto map_add_vector_source_tiles( + mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, + size_t tile_count, const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tiles_status = validate_tile_urls(tiles, tile_count); + if (tiles_status != MLN_STATUS_OK) { + return tiles_status; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::Vector); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + auto tileset = to_native_tileset(tiles, tile_count, effective, true); + if (!tileset) { + return MLN_STATUS_INVALID_ARGUMENT; + } + style.addSource( + std::make_unique( + id, *tileset, std::nullopt, std::nullopt, + to_native_vector_encoding(effective.vector_encoding) + ) + ); + return MLN_STATUS_OK; +} + +auto map_add_raster_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::Raster); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + style.addSource( + std::make_unique( + id, string_from_view(url), static_cast(effective.tile_size) + ) + ); + return MLN_STATUS_OK; +} + +auto map_add_raster_source_tiles( + mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, + size_t tile_count, const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tiles_status = validate_tile_urls(tiles, tile_count); + if (tiles_status != MLN_STATUS_OK) { + return tiles_status; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::Raster); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + auto tileset = to_native_tileset(tiles, tile_count, effective, false); + if (!tileset) { + return MLN_STATUS_INVALID_ARGUMENT; + } + style.addSource( + std::make_unique( + id, *tileset, static_cast(effective.tile_size) + ) + ); + return MLN_STATUS_OK; +} + +auto map_add_raster_dem_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url, + const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::RasterDEM); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + auto source_options = std::optional{}; + if ( + has_tile_source_option( + effective, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING + ) + ) { + source_options = mln::style::SourceOptions{ + .rasterEncoding = to_native_raster_encoding(effective.raster_encoding) + }; + } + style.addSource( + std::make_unique( + id, string_from_view(url), static_cast(effective.tile_size), + source_options + ) + ); + return MLN_STATUS_OK; +} + +auto map_add_raster_dem_source_tiles( + mln_map map, mln_buffer_view source_id, const mln_buffer_view* tiles, + size_t tile_count, const mln_style_tile_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tiles_status = validate_tile_urls(tiles, tile_count); + if (tiles_status != MLN_STATUS_OK) { + return tiles_status; + } + const auto options_status = + validate_tile_source_options(options, TileSourceOptionKind::RasterDEM); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_tile_source_options(options); + auto tileset = to_native_tileset(tiles, tile_count, effective, false); + if (!tileset) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + has_tile_source_option( + effective, MLN_STYLE_TILE_SOURCE_OPTION_RASTER_ENCODING + ) + ) { + tileset->rasterEncoding = + to_native_raster_encoding(effective.raster_encoding); + } + style.addSource( + std::make_unique( + id, *tileset, static_cast(effective.tile_size) + ) + ); + return MLN_STATUS_OK; +} + +auto map_add_custom_geometry_source( + mln_map map, mln_buffer_view source_id, + const mln_custom_geometry_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto options_status = validate_custom_geometry_source_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_custom_geometry_source_options(*options); + // Tracked before the style takes the source, so the two cannot disagree. A + // throw while tracking leaves no source in the style, and a style that + // rejects the source untracks it again; either way the add failed and the + // caller still owns user_data. Tracking afterwards would leave a live source + // whose release never runs. + track_callback_source( + *live, id, CallbackSourceKind::CustomGeometry, effective.release_user_data, + effective.user_data + ); + try { + style.addSource( + std::make_unique( + id, to_native_custom_geometry_source_options(effective) + ) + ); + } catch (...) { + // Mirrors add()'s own early return: a source with no release callback was + // never tracked, so there is nothing to untrack and no entry of another + // source's to erase. + if (effective.release_user_data != nullptr) { + untrack_callback_source(*live, id); + } + throw; + } + return MLN_STATUS_OK; +} + +auto map_set_custom_geometry_source_tile_data( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view data +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tile_status = validate_canonical_tile_id(tile_id); + if (tile_status != MLN_STATUS_OK) { + return tile_status; + } + auto geojson = to_native_geojson(data); + if (!geojson) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* custom_source = source->as(); + if (custom_source == nullptr) { + set_thread_error("source is not a custom geometry source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + custom_source->setTileData(to_native_canonical_tile_id(tile_id), *geojson); + return MLN_STATUS_OK; +} + +auto map_invalidate_custom_geometry_source_tile( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tile_status = validate_canonical_tile_id(tile_id); + if (tile_status != MLN_STATUS_OK) { + return tile_status; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* custom_source = source->as(); + if (custom_source == nullptr) { + set_thread_error("source is not a custom geometry source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + custom_source->invalidateTile(to_native_canonical_tile_id(tile_id)); + return MLN_STATUS_OK; +} + +auto map_invalidate_custom_geometry_source_region( + mln_map map, mln_buffer_view source_id, mln_lat_lng_bounds bounds +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto bounds_status = validate_lat_lng_bounds(bounds); + if (bounds_status != MLN_STATUS_OK) { + return bounds_status; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* custom_source = source->as(); + if (custom_source == nullptr) { + set_thread_error("source is not a custom geometry source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + custom_source->invalidateRegion(to_native_lat_lng_bounds(bounds)); + return MLN_STATUS_OK; +} + +auto lookup_custom_mvt_vector_source( + MapObject* live, mln_buffer_view source_id, + mln::style::CustomVectorSource*& out_source +) -> mln_status { + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + out_source = source->as(); + if (out_source == nullptr) { + set_thread_error("source is not a custom MVT vector source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto map_add_custom_mvt_vector_source( + mln_map map, mln_buffer_view source_id, + const mln_custom_mvt_vector_source_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto options_status = + validate_custom_mvt_vector_source_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + const auto effective = effective_custom_mvt_vector_source_options(*options); + // Tracked before the style takes the source, so the two cannot disagree. A + // throw while tracking leaves no source in the style, and a style that + // rejects the source untracks it again; either way the add failed and the + // caller still owns user_data. Tracking afterwards would leave a live source + // whose release never runs. + track_callback_source( + *live, id, CallbackSourceKind::CustomMvtVector, effective.release_user_data, + effective.user_data + ); + try { + style.addSource( + std::make_unique( + id, to_native_custom_mvt_vector_source_options(effective) + ) + ); + } catch (...) { + // Mirrors add()'s own early return: a source with no release callback was + // never tracked, so there is nothing to untrack and no entry of another + // source's to erase. + if (effective.release_user_data != nullptr) { + untrack_callback_source(*live, id); + } + throw; + } + return MLN_STATUS_OK; +} + +auto map_set_custom_mvt_vector_source_tile_data( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view data +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tile_status = validate_canonical_tile_id(tile_id); + if (tile_status != MLN_STATUS_OK) { + return tile_status; + } + if (!validate_string_view(data, "data")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + mln::style::CustomVectorSource* custom_source = nullptr; + const auto source_status = + lookup_custom_mvt_vector_source(live, source_id, custom_source); + if (source_status != MLN_STATUS_OK) { + return source_status; + } + + auto native_data = std::shared_ptr{}; + if (data.size != 0) { + native_data = std::make_shared( + static_cast(data.data), data.size + ); + } + custom_source->setTileData( + to_native_canonical_tile_id(tile_id), native_data, + mln::style::TileDataFormat::MVT + ); + return MLN_STATUS_OK; +} + +auto map_set_custom_mvt_vector_source_tile_error( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id, + mln_buffer_view message +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tile_status = validate_canonical_tile_id(tile_id); + if (tile_status != MLN_STATUS_OK) { + return tile_status; + } + if (!validate_string_view(message, "message")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + mln::style::CustomVectorSource* custom_source = nullptr; + const auto source_status = + lookup_custom_mvt_vector_source(live, source_id, custom_source); + if (source_status != MLN_STATUS_OK) { + return source_status; + } + + custom_source->setTileError( + to_native_canonical_tile_id(tile_id), + std::make_exception_ptr(std::runtime_error(string_from_view(message))) + ); + return MLN_STATUS_OK; +} + +auto map_invalidate_custom_mvt_vector_source_tile( + mln_map map, mln_buffer_view source_id, mln_canonical_tile_id tile_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto tile_status = validate_canonical_tile_id(tile_id); + if (tile_status != MLN_STATUS_OK) { + return tile_status; + } + + mln::style::CustomVectorSource* custom_source = nullptr; + const auto source_status = + lookup_custom_mvt_vector_source(live, source_id, custom_source); + if (source_status != MLN_STATUS_OK) { + return source_status; + } + custom_source->invalidateTile(to_native_canonical_tile_id(tile_id)); + return MLN_STATUS_OK; +} + +auto map_set_style_image( + mln_map map, mln_buffer_view image_id, + const mln_premultiplied_rgba8_image* image, + const mln_style_image_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto image_id_status = validate_image_id(image_id); + if (image_id_status != MLN_STATUS_OK) { + return image_id_status; + } + const auto image_status = validate_premultiplied_rgba8_image(image); + if (image_status != MLN_STATUS_OK) { + return image_status; + } + const auto options_status = validate_style_image_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + + const auto effective = effective_style_image_options(options); + auto content = std::optional{}; + if ( + options != nullptr && + has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_CONTENT) + ) { + content = mln::style::ImageContent{ + .left = effective.content.left, + .top = effective.content.top, + .right = effective.content.right, + .bottom = effective.content.bottom + }; + } + auto text_fit_width = std::optional{}; + if ( + options != nullptr && + has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_WIDTH) + ) { + text_fit_width = to_native_text_fit(effective.text_fit_width); + } + auto text_fit_height = std::optional{}; + if ( + options != nullptr && + has_style_image_option(*options, MLN_STYLE_IMAGE_OPTION_TEXT_FIT_HEIGHT) + ) { + text_fit_height = to_native_text_fit(effective.text_fit_height); + } + + auto style_image = std::make_unique( + string_from_view(image_id), to_native_premultiplied_rgba8_image(*image), + effective.pixel_ratio, effective.sdf, + to_native_image_stretches(effective.stretch_x, effective.stretch_x_count), + to_native_image_stretches(effective.stretch_y, effective.stretch_y_count), + content, text_fit_width, text_fit_height + ); + map_native(live)->getStyle().addImage(std::move(style_image)); + return MLN_STATUS_OK; +} + +auto map_remove_style_image(mln_map map, mln_buffer_view image_id) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto image_id_status = validate_image_id(image_id); + if (image_id_status != MLN_STATUS_OK) { + return image_id_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(image_id); + if (!style.getImage(id).has_value()) { + set_thread_error(("no style image has ID " + id).c_str()); + return MLN_STATUS_NOT_FOUND; + } + style.removeImage(id); + return MLN_STATUS_OK; +} + +auto map_get_style_image_info( + mln_map map, mln_buffer_view image_id, mln_style_image_info* out_info, + bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto image_id_status = validate_image_id(image_id); + if (image_id_status != MLN_STATUS_OK) { + return image_id_status; + } + if (out_info == nullptr || out_info->size < sizeof(mln_style_image_info)) { + set_thread_error("out_info must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_found == nullptr) { + set_thread_error("out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto image = + map_native(live)->getStyle().getImage(string_from_view(image_id)); + *out_found = image.has_value(); + *out_info = + image ? style_image_info_from_native(*image) : style_image_info_default(); + return MLN_STATUS_OK; +} + +auto map_copy_style_image_stretches( + mln_map map, mln_buffer_view image_id, mln_image_stretch* out_stretch_x, + size_t stretch_x_capacity, size_t* out_stretch_x_count, + mln_image_stretch* out_stretch_y, size_t stretch_y_capacity, + size_t* out_stretch_y_count, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto image_id_status = validate_image_id(image_id); + if (image_id_status != MLN_STATUS_OK) { + return image_id_status; + } + if ( + (out_stretch_x == nullptr && stretch_x_capacity > 0) || + (out_stretch_y == nullptr && stretch_y_capacity > 0) + ) { + set_thread_error( + "stretch output arrays must not be null when their capacity is non-zero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + out_stretch_x_count == nullptr || out_stretch_y_count == nullptr || + out_found == nullptr + ) { + set_thread_error("stretch output counts and out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto image = + map_native(live)->getStyle().getImage(string_from_view(image_id)); + *out_found = image.has_value(); + *out_stretch_x_count = 0; + *out_stretch_y_count = 0; + if (!image) { + return MLN_STATUS_OK; + } + + const auto& stretch_x = image->getStretchX(); + const auto& stretch_y = image->getStretchY(); + *out_stretch_x_count = stretch_x.size(); + *out_stretch_y_count = stretch_y.size(); + + // A null array with zero capacity is a size probe, so it reports the counts + // and succeeds rather than sharing a status with a missing image. + for (const auto& [out, capacity, stretches, name] : { + std::tuple{ + out_stretch_x, stretch_x_capacity, &stretch_x, "stretch_x_capacity" + }, + std::tuple{ + out_stretch_y, stretch_y_capacity, &stretch_y, "stretch_y_capacity" + }, + }) { + if (out == nullptr) { + continue; + } + if (capacity < stretches->size()) { + auto message = std::string{name} + " is too small"; + set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + for (const auto& [out, stretches] : { + std::pair{out_stretch_x, &stretch_x}, + std::pair{out_stretch_y, &stretch_y}, + }) { + if (out == nullptr) { + continue; + } + for (size_t index = 0; index < stretches->size(); index += 1) { + out[index] = mln_image_stretch{ + .from = (*stretches)[index].first, .to = (*stretches)[index].second + }; + } + } + return MLN_STATUS_OK; +} + +auto map_copy_style_image_premultiplied_rgba8( + mln_map map, mln_buffer_view image_id, uint8_t* out_pixels, + size_t pixel_capacity, size_t* out_byte_length, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto image_id_status = validate_image_id(image_id); + if (image_id_status != MLN_STATUS_OK) { + return image_id_status; + } + if (out_pixels == nullptr && pixel_capacity > 0) { + set_thread_error("out_pixels must not be null when capacity is non-zero"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_byte_length == nullptr || out_found == nullptr) { + set_thread_error("out_byte_length and out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto image = + map_native(live)->getStyle().getImage(string_from_view(image_id)); + *out_found = image.has_value(); + *out_byte_length = 0; + if (!image) { + return MLN_STATUS_OK; + } + + const auto& pixels = image->getImage(); + *out_byte_length = pixels.bytes(); + // A null buffer with zero capacity is a size probe, so it reports the length + // and succeeds rather than sharing a status with a missing image. + if (out_pixels == nullptr) { + return MLN_STATUS_OK; + } + if (pixel_capacity < pixels.bytes()) { + set_thread_error("pixel_capacity is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (pixels.bytes() > 0) { + std::copy_n(pixels.data.get(), pixels.bytes(), out_pixels); + } + return MLN_STATUS_OK; +} + +auto map_add_image_source_url( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count, mln_buffer_view url +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto coordinate_status = + validate_image_source_coordinates(coordinates, coordinate_count); + if (coordinate_status != MLN_STATUS_OK) { + return coordinate_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + auto source = std::make_unique( + id, to_native_image_source_coordinates(coordinates) + ); + source->setURL(string_from_view(url)); + style.addSource(std::move(source)); + return MLN_STATUS_OK; +} + +auto map_add_image_source_image( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count, const mln_premultiplied_rgba8_image* image +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto coordinate_status = + validate_image_source_coordinates(coordinates, coordinate_count); + if (coordinate_status != MLN_STATUS_OK) { + return coordinate_status; + } + const auto image_status = validate_premultiplied_rgba8_image(image); + if (image_status != MLN_STATUS_OK) { + return image_status; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(source_id); + const auto add_status = validate_source_can_be_added(style, id); + if (add_status != MLN_STATUS_OK) { + return add_status; + } + + auto native_image = to_native_premultiplied_rgba8_image(*image); + style.addSource( + std::make_unique( + id, to_native_image_source_coordinates(coordinates) + ) + ); + auto* added_source = style.getSource(id); + auto* image_source = added_source == nullptr + ? nullptr + : added_source->as(); + if (image_source == nullptr) { + set_thread_error("added source is not an image source"); + return MLN_STATUS_NATIVE_ERROR; + } + image_source->setImage(std::move(native_image)); + return MLN_STATUS_OK; +} + +auto map_set_image_source_url( + mln_map map, mln_buffer_view source_id, mln_buffer_view url +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (!validate_string_view(url, "url")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url.size == 0) { + set_thread_error("url must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* image_source = source->as(); + if (image_source == nullptr) { + set_thread_error("source is not an image source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + image_source->setURL(string_from_view(url)); + return MLN_STATUS_OK; +} + +auto map_set_image_source_image( + mln_map map, mln_buffer_view source_id, + const mln_premultiplied_rgba8_image* image +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto image_status = validate_premultiplied_rgba8_image(image); + if (image_status != MLN_STATUS_OK) { + return image_status; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* image_source = source->as(); + if (image_source == nullptr) { + set_thread_error("source is not an image source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + image_source->setImage(to_native_premultiplied_rgba8_image(*image)); + return MLN_STATUS_OK; +} + +auto map_set_image_source_coordinates( + mln_map map, mln_buffer_view source_id, const mln_lat_lng* coordinates, + size_t coordinate_count +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + const auto coordinate_status = + validate_image_source_coordinates(coordinates, coordinate_count); + if (coordinate_status != MLN_STATUS_OK) { + return coordinate_status; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + if (source == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* image_source = source->as(); + if (image_source == nullptr) { + set_thread_error("source is not an image source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + image_source->setCoordinates(to_native_image_source_coordinates(coordinates)); + return MLN_STATUS_OK; +} + +auto map_get_image_source_coordinates( + mln_map map, mln_buffer_view source_id, mln_lat_lng* out_coordinates, + size_t coordinate_capacity, size_t* out_coordinate_count, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto source_id_status = validate_source_id(source_id); + if (source_id_status != MLN_STATUS_OK) { + return source_id_status; + } + if (out_coordinates == nullptr && coordinate_capacity > 0) { + set_thread_error( + "out_coordinates must not be null when capacity is non-zero" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_coordinate_count == nullptr || out_found == nullptr) { + set_thread_error("out_coordinate_count and out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* source = + map_native(live)->getStyle().getSource(string_from_view(source_id)); + *out_found = source != nullptr; + *out_coordinate_count = 0; + if (source == nullptr) { + return MLN_STATUS_OK; + } + auto* image_source = source->as(); + if (image_source == nullptr) { + set_thread_error("source is not an image source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + constexpr auto image_source_coordinate_count = size_t{4}; + *out_coordinate_count = image_source_coordinate_count; + if (out_coordinates == nullptr) { + return MLN_STATUS_OK; + } + if (coordinate_capacity < image_source_coordinate_count) { + set_thread_error("coordinate_capacity is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto coordinates = + from_native_image_source_coordinates(image_source->getCoordinates()); + auto output = std::span{out_coordinates, coordinate_capacity}; + std::ranges::copy(coordinates, output.begin()); + return MLN_STATUS_OK; +} + +auto map_add_hillshade_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, + mln_buffer_view before_layer_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(source_id, "source_id") || + !validate_string_view(before_layer_id, "before_layer_id") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0 || source_id.size == 0) { + set_thread_error("layer_id and source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto layer = string_from_view(layer_id); + const auto source = string_from_view(source_id); + if (style.getLayer(layer) != nullptr) { + set_thread_error("layer already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* source_ptr = style.getSource(source); + if (source_ptr == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!source_ptr->is()) { + set_thread_error("source is not a raster DEM source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto before = std::optional{}; + if (before_layer_id.size > 0) { + before = string_from_view(before_layer_id); + if (style.getLayer(*before) == nullptr) { + set_thread_error("before_layer_id does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + style.addLayer( + std::make_unique(layer, source), before + ); + return MLN_STATUS_OK; +} + +auto map_add_color_relief_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id, + mln_buffer_view before_layer_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(source_id, "source_id") || + !validate_string_view(before_layer_id, "before_layer_id") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0 || source_id.size == 0) { + set_thread_error("layer_id and source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto layer = string_from_view(layer_id); + const auto source = string_from_view(source_id); + if (style.getLayer(layer) != nullptr) { + set_thread_error("layer already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* source_ptr = style.getSource(source); + if (source_ptr == nullptr) { + set_thread_error("source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!source_ptr->is()) { + set_thread_error("source is not a raster DEM source"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto before = std::optional{}; + if (before_layer_id.size > 0) { + before = string_from_view(before_layer_id); + if (style.getLayer(*before) == nullptr) { + set_thread_error("before_layer_id does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + style.addLayer( + std::make_unique(layer, source), before + ); + return MLN_STATUS_OK; +} + +auto map_add_location_indicator_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(before_layer_id, "before_layer_id") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto layer = string_from_view(layer_id); + if (style.getLayer(layer) != nullptr) { + set_thread_error("layer already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto before = std::optional{}; + if (before_layer_id.size > 0) { + before = string_from_view(before_layer_id); + if (style.getLayer(*before) == nullptr) { + set_thread_error("before_layer_id does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + style.addLayer( + std::make_unique(layer), before + ); + return MLN_STATUS_OK; +} + +auto validate_location_indicator_layer(MapObject* map, mln_buffer_view layer_id) + -> mln_status { + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto* layer = + map_native(map)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (std::strcmp(layer->getTypeInfo()->type, "location-indicator") != 0) { + set_thread_error("layer is not a location indicator layer"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto validate_float64_to_float32(double value, const char* name) -> mln_status { + if ( + !std::isfinite(value) || value < -std::numeric_limits::max() || + value > std::numeric_limits::max() + ) { + const auto message = std::string{name} + " must fit in finite float32"; + set_thread_error(message.c_str()); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto map_set_location_indicator_location( + mln_map map, mln_buffer_view layer_id, mln_lat_lng coordinate, double altitude +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto layer_status = validate_location_indicator_layer(live, layer_id); + if (layer_status != MLN_STATUS_OK) { + return layer_status; + } + const auto coordinate_status = validate_lat_lng(coordinate); + if (coordinate_status != MLN_STATUS_OK) { + return coordinate_status; + } + if (!std::isfinite(altitude)) { + set_thread_error("altitude must be finite"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + // The style property is [latitude, longitude, altitude]; the renderer reads + // it back as LatLng{values[0], values[1]}. + const auto location = serialize_json_value( + mln::Value{mapbox::base::ValueArray{ + mln::Value{coordinate.latitude}, mln::Value{coordinate.longitude}, + mln::Value{altitude} + }} + ); + return map_set_layer_property( + map, layer_id, string_view_from_literal("location"), + buffer_view_from_string(location) + ); +} + +auto map_set_location_indicator_bearing( + mln_map map, mln_buffer_view layer_id, double bearing +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto layer_status = validate_location_indicator_layer(live, layer_id); + if (layer_status != MLN_STATUS_OK) { + return layer_status; + } + const auto bearing_status = validate_float64_to_float32(bearing, "bearing"); + if (bearing_status != MLN_STATUS_OK) { + return bearing_status; + } + const auto value = serialize_json_value(mln::Value{bearing}); + return map_set_layer_property( + map, layer_id, string_view_from_literal("bearing"), + buffer_view_from_string(value) + ); +} + +auto map_set_location_indicator_accuracy_radius( + mln_map map, mln_buffer_view layer_id, double radius +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto layer_status = validate_location_indicator_layer(live, layer_id); + if (layer_status != MLN_STATUS_OK) { + return layer_status; + } + const auto radius_status = validate_float64_to_float32(radius, "radius"); + if (radius_status != MLN_STATUS_OK) { + return radius_status; + } + if (radius < 0.0) { + set_thread_error("radius must be non-negative"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto value = serialize_json_value(mln::Value{radius}); + return map_set_layer_property( + map, layer_id, string_view_from_literal("accuracy-radius"), + buffer_view_from_string(value) + ); +} + +auto location_indicator_image_property(uint32_t image_kind) + -> std::optional { + switch (image_kind) { + case MLN_LOCATION_INDICATOR_IMAGE_KIND_TOP: + return string_view_from_literal("top-image"); + case MLN_LOCATION_INDICATOR_IMAGE_KIND_BEARING: + return string_view_from_literal("bearing-image"); + case MLN_LOCATION_INDICATOR_IMAGE_KIND_SHADOW: + return string_view_from_literal("shadow-image"); + default: + return std::nullopt; + } +} + +auto map_set_location_indicator_image_name( + mln_map map, mln_buffer_view layer_id, uint32_t image_kind, + mln_buffer_view image_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + const auto layer_status = validate_location_indicator_layer(live, layer_id); + if (layer_status != MLN_STATUS_OK) { + return layer_status; + } + if (!validate_string_view(image_id, "image_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (image_id.size == 0) { + set_thread_error("image_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto property = location_indicator_image_property(image_kind); + if (!property) { + set_thread_error("image_kind is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto value = + serialize_json_value(mln::Value{string_from_view(image_id)}); + return map_set_layer_property( + map, layer_id, *property, buffer_view_from_string(value) + ); +} + +auto map_add_style_layer_json( + mln_map map, mln_buffer_view layer_json, mln_buffer_view before_layer_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(before_layer_id, "before_layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!validate_bytes(layer_json, "style layer")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + auto before = std::optional{}; + if (before_layer_id.size > 0) { + before = string_from_view(before_layer_id); + if (style.getLayer(*before) == nullptr) { + set_thread_error("before_layer_id does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + + auto error = mln::style::conversion::Error{}; + auto layer = + mln::style::conversion::convertJSON>( + string_from_view(layer_json), error + ); + if (!layer) { + set_style_conversion_error("style layer", error); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto id = (*layer)->getID(); + if (style.getLayer(id) != nullptr) { + set_thread_error("layer already exists"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + (*layer)->getTypeInfo()->source == + mln::style::LayerTypeInfo::Source::Required && + style.getSource((*layer)->getSourceID()) == nullptr + ) { + set_thread_error("layer source does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + style.addLayer(std::move(*layer), before); + return MLN_STATUS_OK; +} + +auto map_remove_style_layer(mln_map map, mln_buffer_view layer_id) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto id = string_from_view(layer_id); + auto removed = map_native(live)->getStyle().removeLayer(id); + if (removed == nullptr) { + set_thread_error(("no style layer has ID " + id).c_str()); + return MLN_STATUS_NOT_FOUND; + } + return MLN_STATUS_OK; +} + +auto map_get_style_layer_info( + mln_map map, mln_buffer_view layer_id, mln_style_layer_info* out_info, + bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_info == nullptr || out_info->size < sizeof(mln_style_layer_info)) { + set_thread_error("out_info must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_found == nullptr) { + set_thread_error("out_found must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + *out_found = layer != nullptr; + *out_info = mln_style_layer_info{}; + out_info->size = sizeof(mln_style_layer_info); + if (layer == nullptr) { + return MLN_STATUS_OK; + } + + out_info->type = string_view_from_literal(layer->getTypeInfo()->type); + out_info->min_zoom = static_cast(layer->getMinZoom()); + out_info->max_zoom = static_cast(layer->getMaxZoom()); + out_info->visibility = + layer->getVisibility() == mln::style::VisibilityType::None + ? MLN_STYLE_LAYER_VISIBILITY_NONE + : MLN_STYLE_LAYER_VISIBILITY_VISIBLE; + + const auto& source_id = layer->getSourceID(); + if (!source_id.empty()) { + out_info->fields |= MLN_STYLE_LAYER_INFO_SOURCE_ID; + out_info->source_id_size = source_id.size(); + } + const auto& source_layer = layer->getSourceLayer(); + if (!source_layer.empty()) { + out_info->fields |= MLN_STYLE_LAYER_INFO_SOURCE_LAYER; + out_info->source_layer_size = source_layer.size(); + } + return MLN_STATUS_OK; +} + +auto map_list_style_layer_ids(mln_map map, mln_style_id_list* out_layer_ids) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + + auto ids = std::vector{}; + for (const auto* layer : map_native(live)->getStyle().getLayers()) { + ids.push_back(layer->getID()); + } + return create_style_id_list(std::move(ids), out_layer_ids); +} + +auto map_move_style_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view before_layer_id +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(before_layer_id, "before_layer_id") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto& style = map_native(live)->getStyle(); + const auto id = string_from_view(layer_id); + if (style.getLayer(id) == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto before = std::optional{}; + if (before_layer_id.size > 0) { + before = string_from_view(before_layer_id); + if (*before == id) { + return MLN_STATUS_OK; + } + if (style.getLayer(*before) == nullptr) { + set_thread_error("before_layer_id does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + } + + auto layer = style.removeLayer(id); + style.addLayer(std::move(layer), before); + return MLN_STATUS_OK; +} + +auto map_get_style_layer_json( + mln_map map, mln_buffer_view layer_id, mln_buffer* out_layer, bool* out_found +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + out_layer == nullptr || *out_layer != MLN_HANDLE_NULL || + out_found == nullptr + ) { + set_thread_error( + "out_layer must not be null, *out_layer must be the null handle, and " + "out_found must not be null" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + *out_found = layer != nullptr; + if (layer == nullptr) { + return MLN_STATUS_OK; + } + return create_buffer(serialize_json_value(layer->serialize()), out_layer); +} + +auto map_set_style_light_json(mln_map map, mln_buffer_view light_json) + -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_bytes(light_json, "style light")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto error = mln::style::conversion::Error{}; + auto light = mln::style::conversion::convertJSON( + string_from_view(light_json), error + ); + if (!light) { + set_style_conversion_error("style light", error); + return MLN_STATUS_INVALID_ARGUMENT; + } + + map_native(live)->getStyle().setLight( + std::make_unique(*light) + ); + return MLN_STATUS_OK; +} + +auto map_set_style_light_property( + mln_map map, mln_buffer_view property_name, mln_buffer_view value +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(property_name, "property_name")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (property_name.size == 0) { + set_thread_error("property_name must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto document = mln::JSDocument{}; + if (!parse_json_document(value, "style light property", document)) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* light = map_native(live)->getStyle().getLight(); + if (light == nullptr) { + set_thread_error("style light does not exist"); + return MLN_STATUS_INVALID_STATE; + } + + auto error = light->setProperty( + string_from_view(property_name), + mln::style::conversion::Convertible{ + static_cast(&document) + } + ); + if (error) { + set_style_conversion_error("style light property", *error); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +auto map_get_style_light_property( + mln_map map, mln_buffer_view property_name, mln_buffer* out_value +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(property_name, "property_name")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (property_name.size == 0) { + set_thread_error("property_name must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_value == nullptr || *out_value != MLN_HANDLE_NULL) { + set_thread_error( + "out_value must not be null and *out_value must be the null handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* light = map_native(live)->getStyle().getLight(); + if (light == nullptr) { + set_thread_error("style light does not exist"); + return MLN_STATUS_INVALID_STATE; + } + + const auto property = light->getProperty(string_from_view(property_name)); + if (property.getKind() == mln::style::StyleProperty::Kind::Undefined) { + return MLN_STATUS_OK; + } + return create_buffer(serialize_json_value(property.getValue()), out_value); +} + +auto map_set_style_transition_options( + mln_map map, const mln_style_transition_options* options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + options == nullptr || options->size < sizeof(mln_style_transition_options) + ) { + set_thread_error("options must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + constexpr auto known_fields = + static_cast(MLN_STYLE_TRANSITION_OPTION_DURATION) | + MLN_STYLE_TRANSITION_OPTION_DELAY | + MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; + if ((options->fields & ~known_fields) != 0U) { + set_thread_error( + "mln_style_transition_options.fields contains unknown bits" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + // The native default is already on, so an omitted field leaves it alone. + auto native = mln::style::TransitionOptions{}; + if ( + (options->fields & + MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS) != 0U + ) { + native.enablePlacementTransitions = options->enable_placement_transitions; + } + if ((options->fields & MLN_STYLE_TRANSITION_OPTION_DURATION) != 0U) { + if (!is_native_duration_ms(options->duration_ms)) { + set_thread_error( + "transition duration_ms must fit the native duration range" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + native.duration = duration_from_milliseconds(options->duration_ms); + } + if ((options->fields & MLN_STYLE_TRANSITION_OPTION_DELAY) != 0U) { + if (!is_native_duration_ms(options->delay_ms)) { + set_thread_error( + "transition delay_ms must fit the native duration range" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + native.delay = duration_from_milliseconds(options->delay_ms); + } + + map_native(live)->getStyle().setTransitionOptions(native); + return MLN_STATUS_OK; +} + +auto map_get_style_transition_options( + mln_map map, mln_style_transition_options* out_options +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + out_options == nullptr || + out_options->size < sizeof(mln_style_transition_options) + ) { + set_thread_error("out_options must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto native = map_native(live)->getStyle().getTransitionOptions(); + auto result = style_transition_options_default(); + // MapLibre Native always holds this one, so it always reports as present. + result.fields |= MLN_STYLE_TRANSITION_OPTION_ENABLE_PLACEMENT_TRANSITIONS; + result.enable_placement_transitions = native.enablePlacementTransitions; + if (native.duration) { + result.fields |= MLN_STYLE_TRANSITION_OPTION_DURATION; + result.duration_ms = milliseconds_from_duration(*native.duration); + } + if (native.delay) { + result.fields |= MLN_STYLE_TRANSITION_OPTION_DELAY; + result.delay_ms = milliseconds_from_duration(*native.delay); + } + *out_options = result; + return MLN_STATUS_OK; +} + +auto map_set_layer_property( + mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, + mln_buffer_view value +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(property_name, "property_name") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0 || property_name.size == 0) { + set_thread_error("layer_id and property_name must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto document = mln::JSDocument{}; + if (!parse_json_document(value, "layer property", document)) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto error = layer->setProperty( + string_from_view(property_name), + mln::style::conversion::Convertible{ + static_cast(&document) + } + ); + if (error) { + set_style_conversion_error("layer property", *error); + return MLN_STATUS_INVALID_ARGUMENT; + } + + return MLN_STATUS_OK; +} + +auto map_get_layer_property( + mln_map map, mln_buffer_view layer_id, mln_buffer_view property_name, + mln_buffer* out_value +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if ( + !validate_string_view(layer_id, "layer_id") || + !validate_string_view(property_name, "property_name") + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0 || property_name.size == 0) { + set_thread_error("layer_id and property_name must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_value == nullptr || *out_value != MLN_HANDLE_NULL) { + set_thread_error( + "out_value must not be null and *out_value must be the null handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto property = layer->getProperty(string_from_view(property_name)); + if (property.getKind() == mln::style::StyleProperty::Kind::Undefined) { + return MLN_STATUS_OK; + } + return create_buffer(serialize_json_value(property.getValue()), out_value); +} + +auto map_set_layer_filter( + mln_map map, mln_buffer_view layer_id, const mln_buffer_view* filter +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + if (filter == nullptr) { + layer->setFilter(mln::style::Filter{}); + return MLN_STATUS_OK; + } + + auto native_filter = to_native_style_filter(filter); + if (!native_filter) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setFilter(*native_filter); + return MLN_STATUS_OK; +} + +auto map_get_layer_filter( + mln_map map, mln_buffer_view layer_id, mln_buffer* out_filter +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_filter == nullptr || *out_filter != MLN_HANDLE_NULL) { + set_thread_error( + "out_filter must not be null and *out_filter must be the null handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto filter = layer->getFilter().serialize(); + if (filter.is()) { + return MLN_STATUS_OK; + } + return create_buffer(serialize_json_value(filter), out_filter); +} + +namespace { + +auto resolve_layer_for_access( + mln_map map, mln_buffer_view layer_id, mln::style::Layer*& out_layer +) -> mln_status { + MapObject* live = nullptr; + const auto status = validate_map(map, live); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(layer_id, "layer_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (layer_id.size == 0) { + set_thread_error("layer_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + auto* layer = + map_native(live)->getStyle().getLayer(string_from_view(layer_id)); + if (layer == nullptr) { + set_thread_error("layer does not exist"); + return MLN_STATUS_INVALID_ARGUMENT; + } + out_layer = layer; + return MLN_STATUS_OK; +} + +// MapLibre's setProperty path logs a warning and does nothing when a layer type +// takes no source, so the typed setters reject that case instead. +auto require_layer_takes_source( + const mln::style::Layer& layer, const char* field +) -> bool { + if ( + layer.getTypeInfo()->source == mln::style::LayerTypeInfo::Source::Required + ) { + return true; + } + auto message = std::string{"layer type does not take a "} + field + + "; layer id is " + layer.getID(); + set_thread_error(message.c_str()); + return false; +} + +// MapLibre stores the layer zoom range as floats, and infinities survive the +// narrowing that bounds a layer to one end of the range. +auto validate_layer_zoom(double zoom, const char* field) -> bool { + if (!std::isnan(zoom)) { + return true; + } + auto message = std::string{field} + " must not be NaN"; + set_thread_error(message.c_str()); + return false; +} + +} // namespace + +auto map_set_layer_source_layer( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_layer +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_layer, "source_layer")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!require_layer_takes_source(*layer, "source-layer")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setSourceLayer(string_from_view(source_layer)); + return MLN_STATUS_OK; +} + +auto map_copy_layer_source_layer( + mln_map map, mln_buffer_view layer_id, char* out_source_layer, + size_t source_layer_capacity, size_t* out_source_layer_size +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + return copy_text( + layer->getSourceLayer(), out_source_layer, source_layer_capacity, + out_source_layer_size, "source_layer_capacity" + ); +} + +auto map_set_layer_source_id( + mln_map map, mln_buffer_view layer_id, mln_buffer_view source_id +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_string_view(source_id, "source_id")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (source_id.size == 0) { + set_thread_error("source_id must not be empty"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (!require_layer_takes_source(*layer, "source")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setSourceID(string_from_view(source_id)); + return MLN_STATUS_OK; +} + +auto map_copy_layer_source_id( + mln_map map, mln_buffer_view layer_id, char* out_source_id, + size_t source_id_capacity, size_t* out_source_id_size +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + return copy_text( + layer->getSourceID(), out_source_id, source_id_capacity, out_source_id_size, + "source_id_capacity" + ); +} + +auto map_set_layer_min_zoom( + mln_map map, mln_buffer_view layer_id, double min_zoom +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_layer_zoom(min_zoom, "min_zoom")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setMinZoom(static_cast(min_zoom)); + return MLN_STATUS_OK; +} + +auto map_set_layer_max_zoom( + mln_map map, mln_buffer_view layer_id, double max_zoom +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + if (!validate_layer_zoom(max_zoom, "max_zoom")) { + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setMaxZoom(static_cast(max_zoom)); + return MLN_STATUS_OK; +} + +auto map_set_layer_visibility( + mln_map map, mln_buffer_view layer_id, uint32_t visibility +) -> mln_status { + mln::style::Layer* layer = nullptr; + const auto status = resolve_layer_for_access(map, layer_id, layer); + if (status != MLN_STATUS_OK) { + return status; + } + auto native_visibility = mln::style::VisibilityType::Visible; + switch (visibility) { + case MLN_STYLE_LAYER_VISIBILITY_VISIBLE: + native_visibility = mln::style::VisibilityType::Visible; + break; + case MLN_STYLE_LAYER_VISIBILITY_NONE: + native_visibility = mln::style::VisibilityType::None; + break; + default: + set_thread_error("visibility is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + layer->setVisibility(native_visibility); + return MLN_STATUS_OK; +} + +} // namespace mln::core diff --git a/src/operation/operation.cpp b/src/operation/operation.cpp new file mode 100644 index 000000000..f58cdf37e --- /dev/null +++ b/src/operation/operation.cpp @@ -0,0 +1,84 @@ +#include + +#include "operation/operation.hpp" + +#include "diagnostics/diagnostics.hpp" + +namespace mln::core { + +OperationObject::OperationObject(ResultCallback result) + : result_callback_(std::move(result)) {} + +auto OperationObject::set_terminal_callback(TerminalCallback callback) noexcept + -> void { + auto invoke_now = false; + { + const auto lock = std::scoped_lock{mutex_}; + if (completed_) { + invoke_now = true; + } else { + terminal_callback_ = std::move(callback); + } + } + if (invoke_now && callback) { + try { + callback(); + } catch (...) { + // Terminal observation cannot reopen completed native work. + } + } +} + +auto OperationObject::complete( + mln_status status, std::string diagnostic, std::any result +) noexcept -> void { + auto terminal = TerminalCallback{}; + auto deliver = ResultCallback{}; + { + const auto lock = std::scoped_lock{mutex_}; + if (completed_) return; + completed_ = true; + terminal = std::move(terminal_callback_); + deliver = std::move(result_callback_); + } + if (deliver) { + try { + deliver(status, std::move(diagnostic), std::move(result)); + } catch (...) { + // Host completion delivery cannot reopen completed native work. + } + } + if (terminal) { + try { + terminal(); + } catch (...) { + // Terminal observation cannot reopen completed native work. + } + } +} + +auto create_completion_operation( + const mln_completion* descriptor, OperationObject::ResultCallback result, + CompletionOperation& out +) -> mln_status { + const auto status = validate_completion(descriptor); + if (status != MLN_STATUS_OK) return status; + try { + out.completion = std::make_shared(*descriptor); + if (!result) { + result = [completion = out.completion]( + mln_status completion_status, std::string diagnostic, std::any + ) { + complete(completion, completion_status, std::move(diagnostic)); + }; + } + out.operation = std::make_shared(std::move(result)); + } catch (...) { + out = {}; + set_thread_error("asynchronous completion state could not be allocated"); + return MLN_STATUS_NATIVE_ERROR; + } + return MLN_STATUS_OK; +} + +} // namespace mln::core diff --git a/src/operation/operation.hpp b/src/operation/operation.hpp new file mode 100644 index 000000000..acf11c71e --- /dev/null +++ b/src/operation/operation.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "completion/completion.hpp" + +namespace mln::core { + +/** Internal terminal state shared by queued native work. */ +class OperationObject final { + public: + using TerminalCallback = std::function; + using ResultCallback = std::function; + + explicit OperationObject(ResultCallback result = {}); + OperationObject(const OperationObject&) = delete; + OperationObject(OperationObject&&) = delete; + auto operator=(const OperationObject&) -> OperationObject& = delete; + auto operator=(OperationObject&&) -> OperationObject& = delete; + ~OperationObject() = default; + + auto set_terminal_callback(TerminalCallback callback) noexcept -> void; + auto complete( + mln_status status, std::string diagnostic, std::any result + ) noexcept -> void; + + private: + std::mutex mutex_; + bool completed_ = false; + TerminalCallback terminal_callback_; + ResultCallback result_callback_; +}; + +struct CompletionOperation { + std::shared_ptr operation; + std::shared_ptr completion; +}; + +auto create_completion_operation( + const mln_completion* descriptor, OperationObject::ResultCallback result, + CompletionOperation& out +) -> mln_status; + +} // namespace mln::core diff --git a/src/platform/emscripten/http_file_source.cpp b/src/platform/emscripten/http_file_source.cpp index bb5a7fb14..59013d11a 100644 --- a/src/platform/emscripten/http_file_source.cpp +++ b/src/platform/emscripten/http_file_source.cpp @@ -646,8 +646,8 @@ void FetchRequestState::cancel() { } canceled_ = true; // Dropping the task also removes any delivery it already queued from the - // run loop, so a response cannot sit in the wasm heap awaiting a pump that - // will never come. + // run loop, so a response cannot remain in the wasm heap after + // cancellation. callback_ = nullptr; delivery = std::move(delivery_); if (transport_ == Transport::Callback) { diff --git a/src/platform/emscripten/run_loop.cpp b/src/platform/emscripten/run_loop.cpp index 17ed5336e..1104c234f 100644 --- a/src/platform/emscripten/run_loop.cpp +++ b/src/platform/emscripten/run_loop.cpp @@ -1,148 +1,25 @@ -// The browser run loop, replacing MapLibre's libuv-based default. It serves two -// kinds of owner thread: -// -// * A worker pthread can block, so run() parks on a condition variable. -// * The browser main thread cannot block — Atomics.wait throws there — so a -// host on it drives runOnce() from mln_runtime_pump(). -// -// A host driving runOnce() waits outside this loop, so the delay run() would -// have parked for becomes a wake instead; see DeadlineWake below. +// Browser runtime workers own continuously running loops. A worker pthread may +// block on its condition variable while timers and submitted work wake it. // // FD watches have no browser equivalent, so addWatch() throws. #include -#include #include #include -#include #include #include -#include #include -#include -#include -#include - #include "run_loop_wake.hpp" namespace mln { namespace util { -namespace { - -// Reports a runnable's future due time to whatever drives this loop. Everything -// else reports only work that is ready now, so a host driving the loop from -// mln_runtime_pump() would park with its wake flag clear until it happened to -// pump again — never, if it parks on a negative timeout. -// -// The timer is armed on the main thread, because the wake has to come from a -// thread other than the one it releases: a worker parked in a futex wait serves -// none of its own JavaScript. -class DeadlineWake : public std::enable_shared_from_this { - public: - explicit DeadlineWake(platform::emscripten::RunLoopWake& wake_) - : wake(&wake_) {} - - // Arms a wake for `delay`, from the owner thread. Zero asks for nothing: - // nextDelay() rounds up, so zero means the earliest runnable is already due. - // - // A wake outstanding for an earlier deadline stands in for this one, which - // keeps a host pumping at frame rate from arming a timer per frame. An - // outstanding wake is never cancelled, so a superseded one costs a single - // pump that finds nothing to do. - void arm(Milliseconds delay) { - if (delay <= Milliseconds::zero()) { - return; - } - - auto const deadline = Clock::now() + delay; - { - const std::lock_guard lock(mutex); - if (!outstanding.empty() && *outstanding.begin() <= deadline) { - return; - } - outstanding.insert(deadline); - } - - // The delay travels rather than the deadline, because the two threads read - // the monotonic clock through different browser contexts. The wake is late - // by the proxy hop's latency. - auto armed = std::make_unique(Armed{ - .owner = shared_from_this(), - .deadline = deadline, - .delay_milliseconds = - std::chrono::duration(delay).count(), - }); - auto const proxied = emscripten_proxy_async( - emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), - &DeadlineWake::armOnMainThread, armed.get() - ); - if (proxied != 0) { - // The main thread owns it now, and frees it when the timeout fires. - static_cast(armed.release()); - return; - } - - // No timer will fire, so report readiness now: running the runnable early - // beats waiting on a wake that never comes. - fire(deadline); - } - - // Cuts the arming loose from the run loop; the owner thread calls this before - // the loop goes away. - void detach() { - const std::lock_guard lock(mutex); - wake = nullptr; - } - - private: - struct Armed { - std::shared_ptr owner; - TimePoint deadline; - double delay_milliseconds; - }; - - static void armOnMainThread(void* argument) { - auto* armed = static_cast(argument); - emscripten_set_timeout( - &DeadlineWake::fireOnMainThread, armed->delay_milliseconds, armed - ); - } - - static void fireOnMainThread(void* argument) { - auto const armed = std::unique_ptr(static_cast(argument)); - armed->owner->fire(armed->deadline); - } - - void fire(TimePoint deadline) { - const std::lock_guard lock(mutex); - auto const armed = outstanding.find(deadline); - if (armed != outstanding.end()) { - outstanding.erase(armed); - } - // Under the lock, which keeps the run loop's wake state alive across - // detach(). notify() takes the host's locks and none of ours, so it stays - // the innermost step. The main browser thread runs this and spins rather - // than waiting, so every lock the owner thread takes must be brief. - if (wake != nullptr) { - wake->notify(); - } - } - - std::mutex mutex; - platform::emscripten::RunLoopWake* wake = nullptr; - std::multiset outstanding; -}; - -} // namespace class RunLoop::Impl { public: RunLoop::Type type = RunLoop::Type::Default; platform::emscripten::RunLoopWake wake; - std::shared_ptr deadline_wake = - std::make_shared(wake); bool running = false; }; @@ -153,21 +30,10 @@ RunLoop* RunLoop::Get() { RunLoop::RunLoop(Type type) : impl(std::make_unique()) { impl->type = type; - // Timers and async tasks make work ready without going through push(), where - // MapLibre reports queued work to the host, so they report it from here. - // push() then reports twice, which costs a flag store. - impl->wake.platform_wake = [this]() { - if (platformCallback) { - platformCallback(); - } - }; Scheduler::SetCurrent(this); } -RunLoop::~RunLoop() { - impl->deadline_wake->detach(); - Scheduler::SetCurrent(nullptr); -} +RunLoop::~RunLoop() { Scheduler::SetCurrent(nullptr); } LOOP_HANDLE RunLoop::getLoopHandle() { return &Get()->impl->wake; } @@ -208,9 +74,7 @@ void RunLoop::run() { void RunLoop::runOnce() { MBGL_VERIFY_THREAD(tid); process(); - // run() waits out the next delay itself; a host that drives runOnce() is not - // here to wait, so the delay becomes a wake it can be released by. - impl->deadline_wake->arm(impl->wake.processRunnables()); + static_cast(impl->wake.processRunnables()); } void RunLoop::stop() { @@ -220,7 +84,7 @@ void RunLoop::stop() { void RunLoop::updateTime() {} -// Runs this loop until its own work is done, ignoring the tag: an owner thread +// Runs this loop until its own work is done, ignoring the tag. A runtime worker // runs its own queues. // // One pass runs everything outstanding, so a pass that leaves work behind found diff --git a/src/platform/emscripten/run_loop_wake.hpp b/src/platform/emscripten/run_loop_wake.hpp index aa3c1826e..b7c61d63a 100644 --- a/src/platform/emscripten/run_loop_wake.hpp +++ b/src/platform/emscripten/run_loop_wake.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -29,23 +28,10 @@ struct RunLoopWake { std::mutex runnables_mutex; std::list> runnables; - // Reports readiness to whatever hosts this loop, beyond the condition - // variable run() parks on. A host driving runOnce() parks on the runtime's - // wake state, where only RunLoop::push() reports queued work; timers and - // async tasks arrive as runnables and would otherwise leave it asleep. - std::function platform_wake; - void notify() { - { - std::lock_guard lock(wake_mutex); - notified = true; - cv.notify_one(); - } - // Outside the wake lock: this ends up taking the host's own wake lock, and - // push() reaches it while holding the run loop mutex. - if (platform_wake) { - platform_wake(); - } + const std::lock_guard lock(wake_mutex); + notified = true; + cv.notify_one(); } void addRunnable(std::shared_ptr runnable) { diff --git a/src/render/metal/metal_surface_session.mm b/src/render/metal/metal_surface_session.mm index af3493c71..1e1e49ef0 100644 --- a/src/render/metal/metal_surface_session.mm +++ b/src/render/metal/metal_surface_session.mm @@ -321,9 +321,10 @@ auto set_metal_target(const mln_metal_surface_descriptor& descriptor) namespace mln::core { -auto metal_surface_attach( +auto metal_surface_attach_start( mln_map map, const mln_metal_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -334,13 +335,6 @@ auto metal_surface_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled surface dimensions are too large" @@ -348,44 +342,72 @@ auto metal_surface_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } + const auto request_status = + validate_render_session_attach_request(options, out_session, completion); + if (request_status != MLN_STATUS_OK) { + return request_status; + } auto session = std::make_shared(); session->map = map; set_session_extent(*session, descriptor->extent); - session->surface.backend = std::make_unique( - static_cast(descriptor->layer), - static_cast(descriptor->context.device), - mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Surface, - RenderSessionAttachMessages{ - .null_session = "surface session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + auto layer = NS::RetainPtr(static_cast(descriptor->layer)); + auto device = + NS::RetainPtr(static_cast(descriptor->context.device)); + session->initialize_backend = + [layer = std::move(layer), + device = std::move(device)](mln_render_session_object& target) mutable { + target.surface.backend = std::make_unique( + layer.get(), device.get(), + mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_PRESENTATION + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Surface, options, capabilities, + out_session, completion ); } -auto metal_surface_set_target( - mln_render_session session, const mln_metal_surface_descriptor* descriptor +auto metal_surface_set_target_start( + mln_render_session session, const mln_metal_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live + const auto submission_status = validate_render_session_retarget_submission( + session, RetargetTargetKind::Surface, completion ); - if (session_status != MLN_STATUS_OK) { - return session_status; + if (submission_status != MLN_STATUS_OK) { + return submission_status; } const auto descriptor_status = validate_metal_surface_descriptor(descriptor); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - return surface_session_set_target( - session, descriptor->extent, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.surface.backend->set_metal_target(*descriptor); - } + const auto copied = *descriptor; + auto layer = NS::RetainPtr(static_cast(descriptor->layer)); + auto device = + NS::RetainPtr(static_cast(descriptor->context.device)); + return enqueue_driver_operation( + session, + [copied, layer = std::move(layer), + device = std::move(device)](mln_render_session_object& target) { + auto retained = copied; + retained.layer = layer.get(); + retained.context.device = device.get(); + return surface_session_set_target( + target.self, retained.extent, + [&retained](mln_render_session_object& live) { + return live.surface.backend->set_metal_target(retained); + } + ); + }, + completion ); } diff --git a/src/render/metal/metal_texture_backend.inc b/src/render/metal/metal_texture_backend.inc index f3a63aff0..373d8ff1b 100644 --- a/src/render/metal/metal_texture_backend.inc +++ b/src/render/metal/metal_texture_backend.inc @@ -5,6 +5,7 @@ #include #include +#include namespace mln::core { @@ -14,7 +15,9 @@ class MetalTextureBackend final : public mln::mtl::RendererBackend, class MetalTextureRenderableResource; public: - MetalTextureBackend(MTL::Device* host_device, mln::Size size); + MetalTextureBackend( + MTL::Device* host_device, mln::Size size, std::size_t ring_depth = 1 + ); MetalTextureBackend(MTL::Texture* borrowed_texture, mln::Size size); MetalTextureBackend(const MetalTextureBackend&) = delete; auto operator=(const MetalTextureBackend&) -> MetalTextureBackend& = delete; @@ -30,6 +33,9 @@ class MetalTextureBackend final : public mln::mtl::RendererBackend, void updateAssumedState() override; auto metal_texture() -> MTL::Texture*; + auto metal_texture_at(std::size_t slot) -> MTL::Texture*; + auto select_slot(std::size_t slot) -> bool; + void set_ring_size(mln::Size size); [[nodiscard]] auto has_device(const MTL::Device* other) const -> bool; // Whether a replacement texture has the pixel format this session's cached @@ -45,6 +51,9 @@ class MetalTextureBackend final : public mln::mtl::RendererBackend, // Recorded when this session took borrowed_texture_, which it does not // retain and the host may already have released. MTL::PixelFormat borrowed_pixel_format_ = MTL::PixelFormatInvalid; + std::vector> slot_resources_; + std::size_t selected_slot_ = 0; + std::vector slot_sizes_; }; } // namespace mln::core diff --git a/src/render/metal/metal_texture_backend.mm b/src/render/metal/metal_texture_backend.mm index 9b6e477dc..2ad02f74a 100644 --- a/src/render/metal/metal_texture_backend.mm +++ b/src/render/metal/metal_texture_backend.mm @@ -206,10 +206,12 @@ auto metal_texture() -> MTL::Texture* { }; MetalTextureBackend::MetalTextureBackend( - MTL::Device* host_device, mln::Size size + MTL::Device* host_device, mln::Size size, std::size_t ring_depth ) : mln::mtl::RendererBackend(mln::gfx::ContextMode::Unique), - mln::gfx::HeadlessBackend(size) { + mln::gfx::HeadlessBackend(size), + slot_resources_(ring_depth), + slot_sizes_(ring_depth) { device = NS::RetainPtr(host_device); commandQueue = NS::TransferPtr(device->newCommandQueue()); } @@ -220,14 +222,16 @@ auto metal_texture() -> MTL::Texture* { : mln::mtl::RendererBackend(mln::gfx::ContextMode::Unique), mln::gfx::HeadlessBackend(size), borrowed_texture_(borrowed_texture), - borrowed_pixel_format_(borrowed_texture->pixelFormat()) { + borrowed_pixel_format_(borrowed_texture->pixelFormat()), + slot_resources_(1), + slot_sizes_(1) { device = NS::RetainPtr(borrowed_texture->device()); commandQueue = NS::TransferPtr(device->newCommandQueue()); } - MetalTextureBackend::~MetalTextureBackend() { auto guard = mln::gfx::BackendScope{*this}; resource.reset(); + slot_resources_.clear(); context.reset(); } @@ -240,6 +244,7 @@ auto metal_texture() -> MTL::Texture* { borrowed_texture_ ); } + slot_sizes_[selected_slot_] = size; return *this; } @@ -262,6 +267,43 @@ auto metal_texture() -> MTL::Texture* { return getResource().metal_texture(); } +auto MetalTextureBackend::metal_texture_at(std::size_t slot) -> MTL::Texture* { + if (slot >= slot_resources_.size()) { + return nullptr; + } + if (slot == selected_slot_) { + return metal_texture(); + } + auto* stored = slot_resources_[slot].get(); + if (stored == nullptr) { + const auto previous = selected_slot_; + if (!select_slot(slot)) return nullptr; + auto* texture = metal_texture(); + static_cast(select_slot(previous)); + return texture; + } + return static_cast(stored)->metal_texture(); +} + +auto MetalTextureBackend::select_slot(std::size_t slot) -> bool { + if (slot >= slot_resources_.size()) { + return false; + } + if (slot == selected_slot_) { + if (resource != nullptr && slot_sizes_[slot] != size) resource.reset(); + return true; + } + slot_resources_[selected_slot_] = std::move(resource); + if (slot_resources_[slot] != nullptr && slot_sizes_[slot] != size) { + slot_resources_[slot].reset(); + } + resource = std::move(slot_resources_[slot]); + selected_slot_ = slot; + return true; +} + +void MetalTextureBackend::set_ring_size(mln::Size new_size) { size = new_size; } + auto MetalTextureBackend::has_device(const MTL::Device* other) const -> bool { return other == device.get(); } diff --git a/src/render/metal/metal_texture_session.mm b/src/render/metal/metal_texture_session.mm index b576b5af1..61506255f 100644 --- a/src/render/metal/metal_texture_session.mm +++ b/src/render/metal/metal_texture_session.mm @@ -1,3 +1,4 @@ +#include #include #include @@ -57,8 +58,10 @@ auto validate_borrowed_texture( class MetalTextureSessionBackend final : public mln::core::TextureSessionBackend { public: - MetalTextureSessionBackend(MTL::Device* host_device, mln::Size size) - : backend_(host_device, size) {} + MetalTextureSessionBackend( + MTL::Device* host_device, mln::Size size, std::size_t ring_depth + ) + : backend_(host_device, size, ring_depth) {} MetalTextureSessionBackend(MTL::Texture* borrowed_texture, mln::Size size) : backend_(borrowed_texture, size) {} @@ -66,6 +69,7 @@ auto validate_borrowed_texture( auto headless_backend() -> mln::gfx::HeadlessBackend& override { return backend_; } + void resize(mln::Size size) override { backend_.set_ring_size(size); } auto set_metal_borrowed_target( const mln_metal_borrowed_texture_descriptor& descriptor @@ -104,34 +108,37 @@ auto after_render(mln_render_session_object& texture, bool& out_rendered) out_rendered = true; return MLN_STATUS_OK; } + auto select_render_slot(std::size_t slot) -> mln_status override { + return backend_.select_slot(slot) ? MLN_STATUS_OK + : MLN_STATUS_INVALID_ARGUMENT; + } + auto copy_slot_metadata( + const mln_render_session_object& texture, std::size_t slot, + std::any& out_metadata + ) -> mln_status override { + auto* metal_texture = backend_.metal_texture_at(slot); + if (metal_texture == nullptr) { + mln::core::set_thread_error("rendered Metal texture is not available"); + return MLN_STATUS_NOT_READY; + } + out_metadata = mln_metal_owned_texture_frame{ + .size = sizeof(mln_metal_owned_texture_frame), + .generation = texture.generation, + .width = static_cast(metal_texture->width()), + .height = static_cast(metal_texture->height()), + .scale_factor = texture.scale_factor, + .frame_id = texture.frame_generation, + .texture = metal_texture, + .device = metal_texture->device(), + .pixel_format = static_cast(metal_texture->pixelFormat()) + }; + return MLN_STATUS_OK; + } private: mln::core::MetalTextureBackend backend_; }; -auto fill_frame( - mln_render_session_object* texture, mln_metal_owned_texture_frame* out_frame -) -> mln_status { - auto* metal_texture = - static_cast(texture->texture.rendered_native_texture); - if (metal_texture == nullptr) { - mln::core::set_thread_error("rendered Metal texture is not available"); - return MLN_STATUS_NATIVE_ERROR; - } - - *out_frame = mln_metal_owned_texture_frame{ - .size = sizeof(mln_metal_owned_texture_frame), - .generation = texture->generation, - .width = texture->physical_width, - .height = texture->physical_height, - .scale_factor = texture->scale_factor, - .frame_id = texture->texture.next_frame_id, - .texture = metal_texture, - .device = metal_texture->device(), - .pixel_format = static_cast(metal_texture->pixelFormat()) - }; - return MLN_STATUS_OK; -} } // namespace namespace mln::core { @@ -140,9 +147,10 @@ auto supported_render_backend_mask() noexcept -> uint32_t { return MLN_RENDER_BACKEND_FLAG_METAL; } -auto metal_owned_texture_attach( +auto metal_owned_texture_attach_start( mln_map map, const mln_metal_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -154,13 +162,6 @@ auto metal_owned_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled texture dimensions are too large" @@ -168,29 +169,49 @@ auto metal_owned_texture_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } + const auto request_status = + validate_render_session_attach_request(options, out_session, completion); + if (request_status != MLN_STATUS_OK) { + return request_status; + } auto session = std::make_shared(); session->map = map; set_session_extent(*session, descriptor->extent); session->texture.api_kind = TextureSessionApi::Metal; session->texture.mode = TextureSessionMode::Owned; - session->texture.backend = std::make_unique( - static_cast(descriptor->context.device), - mln::Size{session->physical_width, session->physical_height} + auto device = + NS::RetainPtr(static_cast(descriptor->context.device)); + const auto ring_depth = std::clamp( + options == nullptr ? 1u : options->requested_texture_ring_depth, 1u, 3u ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + session->initialize_backend = [device = std::move(device), ring_depth]( + mln_render_session_object& target + ) mutable { + target.texture.backend = std::make_unique( + device.get(), mln::Size{target.physical_width, target.physical_height}, + ring_depth + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = ring_depth, + .flags = MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION | + MLN_RENDER_SESSION_CAPABILITY_READBACK | + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto metal_borrowed_texture_attach( +auto metal_borrowed_texture_attach_start( mln_map map, const mln_metal_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -201,12 +222,10 @@ auto metal_borrowed_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; + const auto request_status = + validate_render_session_attach_request(options, out_session, completion); + if (request_status != MLN_STATUS_OK) { + return request_status; } auto session = std::make_shared(); @@ -217,123 +236,56 @@ auto metal_borrowed_texture_attach( ); session->texture.api_kind = TextureSessionApi::Metal; session->texture.mode = TextureSessionMode::Borrowed; - session->texture.backend = std::make_unique( - static_cast(descriptor->texture), - mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + auto* const borrowed_texture = + static_cast(descriptor->texture); + session->initialize_backend = [borrowed_texture]( + mln_render_session_object& target + ) { + target.texture.backend = std::make_unique( + borrowed_texture, mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = 0 + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto metal_owned_texture_acquire_frame( - mln_render_session texture, mln_metal_owned_texture_frame* out_frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_live_attached_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_frame == nullptr || - out_frame->size < sizeof(mln_metal_owned_texture_frame) - ) { - set_thread_error("out_frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->texture.acquired) { - set_thread_error("a texture frame is already acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (live->rendered_generation != live->generation) { - set_thread_error("no rendered frame is available for this generation"); - return MLN_STATUS_INVALID_STATE; - } - if ( - live->texture.mode != TextureSessionMode::Owned || - live->texture.api_kind != TextureSessionApi::Metal - ) { - set_thread_error("texture session cannot expose a Metal texture frame"); - return MLN_STATUS_UNSUPPORTED; - } - - const auto frame_status = fill_frame(live, out_frame); - if (frame_status != MLN_STATUS_OK) { - return frame_status; - } - live->texture.acquired_native_texture = live->texture.rendered_native_texture; - live->texture.acquired = true; - live->texture.acquired_frame_id = out_frame->frame_id; - live->texture.acquired_frame_kind = TextureSessionFrameKind::MetalOwned; - ++live->texture.next_frame_id; - return MLN_STATUS_OK; -} - -auto metal_owned_texture_release_frame( - mln_render_session texture, const mln_metal_owned_texture_frame* frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (frame == nullptr || frame->size < sizeof(mln_metal_owned_texture_frame)) { - set_thread_error("frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - !live->texture.acquired || - live->texture.acquired_frame_kind != TextureSessionFrameKind::MetalOwned - ) { - set_thread_error("no texture frame is currently acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (frame->generation != live->generation) { - set_thread_error("frame generation does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (frame->frame_id != live->texture.acquired_frame_id) { - set_thread_error("frame identity does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - live->texture.acquired = false; - live->texture.acquired_frame_id = 0; - live->texture.acquired_frame_kind = TextureSessionFrameKind::None; - live->texture.acquired_native_texture = nullptr; - return MLN_STATUS_OK; -} - -auto metal_borrowed_texture_set_target( +auto metal_borrowed_texture_set_target_start( mln_render_session session, - const mln_metal_borrowed_texture_descriptor* descriptor + const mln_metal_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live + const auto submission_status = validate_render_session_retarget_submission( + session, RetargetTargetKind::BorrowedTexture, completion ); - if (session_status != MLN_STATUS_OK) { - return session_status; + if (submission_status != MLN_STATUS_OK) { + return submission_status; } - // The same validator attach uses. The shape-only shared one would admit a - // texture that fails only on the next render, once the outgoing target is - // already discarded. const auto descriptor_status = validate_borrowed_texture(descriptor); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - return render_session_set_target( - session, RetargetTargetKind::BorrowedTexture, descriptor->extent, - descriptor->physical_width, descriptor->physical_height, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.texture.backend->set_metal_borrowed_target( - *descriptor + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + return render_session_set_target( + target.self, RetargetTargetKind::BorrowedTexture, copied.extent, + copied.physical_width, copied.physical_height, + [&copied](mln_render_session_object& live) { + return live.texture.backend->set_metal_borrowed_target(copied); + } ); - } + }, + completion ); } diff --git a/src/render/opengl/opengl_surface_session.cpp b/src/render/opengl/opengl_surface_session.cpp index c5096791c..d0ecffcef 100644 --- a/src/render/opengl/opengl_surface_session.cpp +++ b/src/render/opengl/opengl_surface_session.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,8 @@ #include #elif defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) #include +#include +#include #endif #include "diagnostics/diagnostics.hpp" @@ -34,6 +37,53 @@ namespace { +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) +struct WebGLWorkerCall { + std::function function; +}; + +auto run_webgl_worker(void* opaque) -> void* { + auto call = + std::unique_ptr{static_cast(opaque)}; + call->function(); + return nullptr; +} + +auto configure_transferred_webgl_worker( + mln_render_session_object& session, std::string selector +) -> void { + auto thread = std::make_shared(); + session.start_worker = + [thread, selector = std::move(selector)](std::function function) { + auto attributes = pthread_attr_t{}; + if ( + pthread_attr_init(&attributes) != 0 || + emscripten_pthread_attr_settransferredcanvases( + &attributes, selector.c_str() + ) != 0 + ) { + pthread_attr_destroy(&attributes); + mln::core::set_thread_error("transferring the WebGL canvas failed"); + return MLN_STATUS_NATIVE_ERROR; + } + auto call = + std::make_unique(WebGLWorkerCall{std::move(function)}); + const auto result = + pthread_create(thread.get(), &attributes, run_webgl_worker, call.get()); + pthread_attr_destroy(&attributes); + if (result != 0) { + mln::core::set_thread_error("creating the WebGL worker failed"); + return MLN_STATUS_NATIVE_ERROR; + } + static_cast(call.release()); + return MLN_STATUS_OK; + }; + session.join_worker = [thread]() { + static_cast(pthread_join(*thread, nullptr)); + }; +} +#endif + class OpenGLSurfaceBackend final : public mln::gl::RendererBackend, public mln::gfx::Renderable { private: @@ -412,8 +462,16 @@ class OpenGLSurfaceBackend final : public mln::gl::RendererBackend, #elif defined(MLN_FFI_OPENGL_PROVIDER_EGL) void destroy_native_context() { egl_context_.reset(); } #elif defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) - // The host owns the context this session borrowed. - void destroy_native_context() {} + void destroy_native_context() { + if ( + descriptor_.context.data.webgl.kind == + MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS && + descriptor_.context.data.webgl.context > 0 + ) { + emscripten_webgl_destroy_context(descriptor_.context.data.webgl.context); + descriptor_.context.data.webgl.context = 0; + } + } #else void destroy_native_context() {} #endif @@ -477,9 +535,10 @@ class OpenGLSurfaceSessionBackend final namespace mln::core { -auto opengl_surface_attach( +auto opengl_surface_attach_start( mln_map map, const mln_opengl_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -491,13 +550,6 @@ auto opengl_surface_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled surface dimensions are too large" @@ -509,39 +561,87 @@ auto opengl_surface_attach( auto session = std::make_shared(); session->map = map; set_session_extent(*session, descriptor->extent); - session->surface.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Surface, - RenderSessionAttachMessages{ - .null_session = "surface session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + auto copied = *descriptor; +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) + const auto transferred = + copied.context.platform == MLN_OPENGL_CONTEXT_PLATFORM_WEBGL && + copied.context.data.webgl.kind == MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS; + auto selector = std::string{}; + if (transferred) { + const auto view = copied.context.data.webgl.canvas_selector; + selector.assign(static_cast(view.data), view.size); + configure_transferred_webgl_worker(*session, selector); + } +#else + constexpr auto transferred = false; + auto selector = std::string{}; +#endif + if ( + options != nullptr && + options->driver != (transferred ? MLN_RENDER_DRIVER_CORE_WORKER + : MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) + ) { + set_thread_error("OpenGL driver does not match its context placement"); + return MLN_STATUS_UNSUPPORTED; + } + session->initialize_backend = + [copied, selector = std::move(selector), + transferred](mln_render_session_object& target) mutable { + (void)selector; + (void)transferred; +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) + if (transferred) { + auto attributes = EmscriptenWebGLContextAttributes{}; + emscripten_webgl_init_context_attributes(&attributes); + attributes.majorVersion = 2; + attributes.proxyContextToMainThread = + EMSCRIPTEN_WEBGL_CONTEXT_PROXY_DISALLOW; + const auto context = + emscripten_webgl_create_context(selector.c_str(), &attributes); + if (context <= 0) { + set_thread_error("creating the transferred WebGL 2 context failed"); + return MLN_STATUS_NATIVE_ERROR; + } + copied.context.data.webgl.context = context; + } +#endif + target.surface.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_PRESENTATION + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Surface, options, capabilities, + out_session, completion ); } -auto opengl_surface_set_target( - mln_render_session session, const mln_opengl_surface_descriptor* descriptor +auto opengl_surface_set_target_start( + mln_render_session session, const mln_opengl_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_opengl_surface_descriptor(descriptor, true); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - return surface_session_set_target( - session, descriptor->extent, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.surface.backend->set_opengl_target(*descriptor); - } + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + return surface_session_set_target( + target.self, copied.extent, [&copied](mln_render_session_object& live) { + return live.surface.backend->set_opengl_target(copied); + } + ); + }, + completion ); } diff --git a/src/render/opengl/opengl_texture_session.cpp b/src/render/opengl/opengl_texture_session.cpp index 9b8854bb2..ab34288ba 100644 --- a/src/render/opengl/opengl_texture_session.cpp +++ b/src/render/opengl/opengl_texture_session.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -29,6 +30,8 @@ #include #elif defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) #include +#include +#include #endif #include "diagnostics/diagnostics.hpp" @@ -49,6 +52,56 @@ constexpr auto opengl_internal_format = uint32_t{GL_RGBA8}; constexpr auto opengl_pixel_format = uint32_t{GL_RGBA}; constexpr auto opengl_pixel_type = uint32_t{GL_UNSIGNED_BYTE}; +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) +struct WebGLWorkerCall { + std::function function; +}; + +auto run_webgl_worker(void* opaque) -> void* { + auto call = + std::unique_ptr{static_cast(opaque)}; + call->function(); + return nullptr; +} + +auto configure_transferred_webgl_worker( + mln_render_session_object& session, std::string selector +) -> void { + auto thread = std::make_shared(); + session.start_worker = + [thread, selector = std::move(selector)](std::function function) { + auto attributes = pthread_attr_t{}; + if (pthread_attr_init(&attributes) != 0) { + mln::core::set_thread_error("creating WebGL worker attributes failed"); + return MLN_STATUS_NATIVE_ERROR; + } + if ( + emscripten_pthread_attr_settransferredcanvases( + &attributes, selector.c_str() + ) != 0 + ) { + pthread_attr_destroy(&attributes); + mln::core::set_thread_error("transferring the WebGL canvas failed"); + return MLN_STATUS_NATIVE_ERROR; + } + auto call = + std::make_unique(WebGLWorkerCall{std::move(function)}); + const auto result = + pthread_create(thread.get(), &attributes, run_webgl_worker, call.get()); + pthread_attr_destroy(&attributes); + if (result != 0) { + mln::core::set_thread_error("creating the WebGL worker failed"); + return MLN_STATUS_NATIVE_ERROR; + } + static_cast(call.release()); + return MLN_STATUS_OK; + }; + session.join_worker = [thread]() { + static_cast(pthread_join(*thread, nullptr)); + }; +} +#endif + [[noreturn]] void throw_opengl_framebuffer_error() { switch (mln::platform::glCheckFramebufferStatus(GL_FRAMEBUFFER)) { case GL_FRAMEBUFFER_COMPLETE: @@ -194,11 +247,14 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, public mln::gfx::HeadlessBackend { public: OpenGLTextureBackend( - const mln_opengl_owned_texture_descriptor& descriptor, mln::Size size + const mln_opengl_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) : mln::gl::RendererBackend(mln::core::opengl::session_context_mode), mln::gfx::HeadlessBackend(size), - context_(descriptor.context) {} + context_(descriptor.context), + slot_resources_(ring_depth), + slot_sizes_(ring_depth) {} OpenGLTextureBackend( const mln_opengl_borrowed_texture_descriptor& descriptor, mln::Size size @@ -226,6 +282,7 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, void destroy_backend() { auto cleanup = [this] { resource.reset(); + slot_resources_.clear(); context.reset(); }; if (has_native_context()) { @@ -245,6 +302,7 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, getContext(), current_size, borrowed_texture_ ); resource_size_ = current_size; + if (!slot_sizes_.empty()) slot_sizes_[selected_slot_] = current_size; } return *this; } @@ -271,6 +329,40 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, return renderable.texture(); } + auto texture_at(std::size_t slot) -> uint32_t { + if (slot >= slot_resources_.size()) return 0; + if (slot == selected_slot_) return texture(); + if (slot_resources_[slot] == nullptr) { + const auto previous = selected_slot_; + if (!select_slot(slot)) return 0; + const auto value = texture(); + static_cast(select_slot(previous)); + return value; + } + return static_cast( + slot_resources_[slot].get() + ) + ->texture(); + } + + auto select_slot(std::size_t slot) -> bool { + if (slot >= slot_resources_.size()) return false; + if (slot == selected_slot_) { + if (resource != nullptr && slot_sizes_[slot] != size) resource.reset(); + return true; + } + slot_resources_[selected_slot_] = std::move(resource); + if (slot_resources_[slot] != nullptr && slot_sizes_[slot] != size) { + slot_resources_[slot].reset(); + } + resource = std::move(slot_resources_[slot]); + selected_slot_ = slot; + resource_size_ = slot_sizes_[slot]; + return true; + } + + void set_ring_size(mln::Size new_size) { size = new_size; } + void finish_rendering() { getContext().finish(); } // Renders into a different caller-owned texture from here on, keeping the @@ -367,9 +459,7 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, } #elif defined(MLN_FFI_OPENGL_PROVIDER_EGL) if (!egl_context_) { - egl_context_.emplace( - context_.data.egl, MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED - ); + egl_context_.emplace(context_.data.egl, context_.ownership); } egl_context_->activate_pbuffer(); #elif defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) @@ -432,8 +522,15 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, #elif defined(MLN_FFI_OPENGL_PROVIDER_EGL) void destroy_native_context() { egl_context_.reset(); } #elif defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) - // The host owns the context this session borrowed. - void destroy_native_context() {} + void destroy_native_context() { + if ( + context_.data.webgl.kind == MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS && + context_.data.webgl.context > 0 + ) { + emscripten_webgl_destroy_context(context_.data.webgl.context); + context_.data.webgl.context = 0; + } + } #else void destroy_native_context() {} #endif @@ -441,6 +538,9 @@ class OpenGLTextureBackend final : public mln::gl::RendererBackend, mln_opengl_context_descriptor context_{}; uint32_t borrowed_texture_ = 0; mln::Size resource_size_{}; + std::vector> slot_resources_; + std::vector slot_sizes_; + std::size_t selected_slot_ = 0; #if defined(MLN_FFI_OPENGL_PROVIDER_WGL) void* render_context_ = nullptr; @@ -457,9 +557,10 @@ class OpenGLTextureSessionBackend final : public mln::core::TextureSessionBackend { public: OpenGLTextureSessionBackend( - const mln_opengl_owned_texture_descriptor& descriptor, mln::Size size + const mln_opengl_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) - : backend_(descriptor, size) {} + : backend_(descriptor, size, ring_depth) {} OpenGLTextureSessionBackend( const mln_opengl_borrowed_texture_descriptor& descriptor, mln::Size size @@ -469,6 +570,7 @@ class OpenGLTextureSessionBackend final auto headless_backend() -> mln::gfx::HeadlessBackend& override { return backend_; } + void resize(mln::Size size) override { backend_.set_ring_size(size); } auto set_opengl_borrowed_target( const mln_opengl_borrowed_texture_descriptor& descriptor @@ -497,22 +599,27 @@ class OpenGLTextureSessionBackend final return MLN_STATUS_OK; } - auto acquire_opengl_owned_frame( - const mln_render_session_object& texture, - mln_opengl_owned_texture_frame& out_frame + auto select_render_slot(std::size_t slot) -> mln_status override { + return backend_.select_slot(slot) ? MLN_STATUS_OK + : MLN_STATUS_INVALID_ARGUMENT; + } + + auto copy_slot_metadata( + const mln_render_session_object& texture, std::size_t slot, + std::any& out_metadata ) -> mln_status override { - // The host samples this texture from its own context, so the session - // completes its rendering before exposing the texture name. + // CPU-complete producer synchronization requires all preceding writes to + // finish before the texture name is published. auto guard = mln::gfx::BackendScope{backend_}; backend_.finish_rendering(); - out_frame = mln_opengl_owned_texture_frame{ + out_metadata = mln_opengl_owned_texture_frame{ .size = sizeof(mln_opengl_owned_texture_frame), .generation = texture.generation, .width = texture.physical_width, .height = texture.physical_height, .scale_factor = texture.scale_factor, - .frame_id = texture.texture.next_frame_id, - .texture = backend_.texture(), + .frame_id = texture.frame_generation, + .texture = backend_.texture_at(slot), .target = opengl_texture_target, .internal_format = opengl_internal_format, .format = opengl_pixel_format, @@ -525,14 +632,6 @@ class OpenGLTextureSessionBackend final OpenGLTextureBackend backend_; }; -auto fill_opengl_frame( - mln_render_session_object* texture, mln_opengl_owned_texture_frame* out_frame -) -> mln_status { - return texture->texture.backend->acquire_opengl_owned_frame( - *texture, *out_frame - ); -} - } // namespace namespace mln::core { @@ -541,9 +640,10 @@ auto supported_render_backend_mask() noexcept -> uint32_t { return MLN_RENDER_BACKEND_FLAG_OPENGL; } -auto opengl_owned_texture_attach( +auto opengl_owned_texture_attach_start( mln_map map, const mln_opengl_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -555,13 +655,6 @@ auto opengl_owned_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled texture dimensions are too large" @@ -575,22 +668,85 @@ auto opengl_owned_texture_attach( set_session_extent(*session, descriptor->extent); session->texture.api_kind = TextureSessionApi::OpenGL; session->texture.mode = TextureSessionMode::Owned; - session->texture.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + auto copied = *descriptor; + const auto private_target = + copied.context.ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED; +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) + const auto transferred = + copied.context.platform == MLN_OPENGL_CONTEXT_PLATFORM_WEBGL && + copied.context.data.webgl.kind == MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS; + auto selector = std::string{}; + if (transferred) { + const auto view = copied.context.data.webgl.canvas_selector; + selector.assign(static_cast(view.data), view.size); + configure_transferred_webgl_worker(*session, selector); + } +#else + constexpr auto transferred = false; + auto selector = std::string{}; +#endif + if ( + options != nullptr && + options->driver != (private_target + ? MLN_RENDER_DRIVER_CORE_WORKER + : MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) + ) { + set_thread_error("OpenGL driver does not match its context placement"); + return MLN_STATUS_UNSUPPORTED; + } + const auto ring_depth = + private_target + ? 1u + : std::clamp( + options == nullptr ? 1u : options->requested_texture_ring_depth, 1u, + 3u + ); + session->initialize_backend = + [copied, selector = std::move(selector), transferred, + ring_depth](mln_render_session_object& target) mutable { + (void)selector; + (void)transferred; +#if defined(MLN_FFI_OPENGL_PROVIDER_WEBGL) + if (transferred) { + auto attributes = EmscriptenWebGLContextAttributes{}; + emscripten_webgl_init_context_attributes(&attributes); + attributes.majorVersion = 2; + attributes.proxyContextToMainThread = + EMSCRIPTEN_WEBGL_CONTEXT_PROXY_DISALLOW; + const auto context = + emscripten_webgl_create_context(selector.c_str(), &attributes); + if (context <= 0) { + set_thread_error("creating the transferred WebGL 2 context failed"); + return MLN_STATUS_NATIVE_ERROR; + } + copied.context.data.webgl.context = context; + } +#endif + target.texture.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height}, + ring_depth + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = ring_depth, + .flags = MLN_RENDER_SESSION_CAPABILITY_READBACK | + (private_target ? 0u + : MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION | + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC) + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto opengl_borrowed_texture_attach( +auto opengl_borrowed_texture_attach_start( mln_map map, const mln_opengl_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -602,19 +758,19 @@ auto opengl_borrowed_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - // The shared validator accepts any nonzero target; this backend renders only - // into GL_TEXTURE_2D. + if ( + options != nullptr && + options->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ) { + set_thread_error( + "borrowed OpenGL textures require the caller graphics thread driver" + ); + return MLN_STATUS_UNSUPPORTED; + } if (descriptor->target != opengl_texture_target) { set_thread_error("OpenGL texture target must be GL_TEXTURE_2D"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_borrowed_physical_size( descriptor->physical_width, descriptor->physical_height ); @@ -630,37 +786,35 @@ auto opengl_borrowed_texture_attach( ); session->texture.api_kind = TextureSessionApi::OpenGL; session->texture.mode = TextureSessionMode::Borrowed; - session->texture.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + const auto copied = *descriptor; + session->initialize_backend = [copied](mln_render_session_object& target) { + target.texture.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_READBACK + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto opengl_borrowed_texture_set_target( +auto opengl_borrowed_texture_set_target_start( mln_render_session session, - const mln_opengl_borrowed_texture_descriptor* descriptor + const mln_opengl_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_opengl_borrowed_texture_descriptor(descriptor, true); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - // The same check attach makes. Without it a cube-map target fails only on the - // next render, once the outgoing target is already discarded. if (descriptor->target != opengl_texture_target) { set_thread_error("OpenGL texture target must be GL_TEXTURE_2D"); return MLN_STATUS_INVALID_ARGUMENT; @@ -671,95 +825,20 @@ auto opengl_borrowed_texture_set_target( if (physical_status != MLN_STATUS_OK) { return physical_status; } - return render_session_set_target( - session, RetargetTargetKind::BorrowedTexture, descriptor->extent, - descriptor->physical_width, descriptor->physical_height, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.texture.backend->set_opengl_borrowed_target( - *descriptor + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + return render_session_set_target( + target.self, RetargetTargetKind::BorrowedTexture, copied.extent, + copied.physical_width, copied.physical_height, + [&copied](mln_render_session_object& live) { + return live.texture.backend->set_opengl_borrowed_target(copied); + } ); - } + }, + completion ); } -auto opengl_owned_texture_acquire_frame( - mln_render_session texture, mln_opengl_owned_texture_frame* out_frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_live_attached_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_frame == nullptr || - out_frame->size < sizeof(mln_opengl_owned_texture_frame) - ) { - set_thread_error("out_frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->texture.acquired) { - set_thread_error("a texture frame is already acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (live->rendered_generation != live->generation) { - set_thread_error("no rendered frame is available for this generation"); - return MLN_STATUS_INVALID_STATE; - } - if ( - live->texture.mode != TextureSessionMode::Owned || - live->texture.api_kind != TextureSessionApi::OpenGL - ) { - set_thread_error("texture session cannot expose an OpenGL texture frame"); - return MLN_STATUS_UNSUPPORTED; - } - - const auto frame_status = fill_opengl_frame(live, out_frame); - if (frame_status != MLN_STATUS_OK) { - return frame_status; - } - live->texture.acquired_native_texture = - reinterpret_cast(static_cast(out_frame->texture)); - live->texture.acquired = true; - live->texture.acquired_frame_id = out_frame->frame_id; - live->texture.acquired_frame_kind = TextureSessionFrameKind::OpenGLOwned; - ++live->texture.next_frame_id; - return MLN_STATUS_OK; -} - -auto opengl_owned_texture_release_frame( - mln_render_session texture, const mln_opengl_owned_texture_frame* frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - frame == nullptr || frame->size < sizeof(mln_opengl_owned_texture_frame) - ) { - set_thread_error("frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - !live->texture.acquired || - live->texture.acquired_frame_kind != TextureSessionFrameKind::OpenGLOwned - ) { - set_thread_error("no texture frame is currently acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (frame->generation != live->generation) { - set_thread_error("frame generation does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (frame->frame_id != live->texture.acquired_frame_id) { - set_thread_error("frame identity does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - live->texture.acquired = false; - live->texture.acquired_frame_id = 0; - live->texture.acquired_frame_kind = TextureSessionFrameKind::None; - live->texture.acquired_native_texture = nullptr; - return MLN_STATUS_OK; -} - } // namespace mln::core diff --git a/src/render/render_session_common.cpp b/src/render/render_session_common.cpp index 294d9883f..ed0269a72 100644 --- a/src/render/render_session_common.cpp +++ b/src/render/render_session_common.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -34,12 +35,17 @@ #include #include "bytes/buffer.hpp" +#include "c_api/autorelease_pool.hpp" #include "diagnostics/diagnostics.hpp" #include "geojson/geojson.hpp" #include "handles/handle_table.hpp" #include "map/map.hpp" +#include "map/map_internal.hpp" #include "maplibre_native_c.h" +#include "operation/operation.hpp" #include "render/render_session_common.hpp" +#include "render/render_session_test_support.hpp" +#include "runtime/runtime.hpp" #include "style/style_value.hpp" namespace mln::core { @@ -117,7 +123,9 @@ auto opengl_context_descriptor_default() noexcept result.platform = MLN_OPENGL_CONTEXT_PLATFORM_WEBGL; result.data.webgl = mln_webgl_context_descriptor{ .size = sizeof(mln_webgl_context_descriptor), + .kind = MLN_WEBGL_CONTEXT_EXISTING, .context = 0, + .canvas_selector = {}, }; #endif return result; @@ -319,17 +327,38 @@ auto validate_opengl_context( set_thread_error("mln_webgl_context_descriptor.size is too small"); return MLN_STATUS_INVALID_ARGUMENT; } - if (dedicated) { - // A browser session renders through the context the host created and - // still owns, so there is nothing for the session to take over. - set_thread_error("a WebGL context is always shared with its host"); - return MLN_STATUS_INVALID_ARGUMENT; + if (context.data.webgl.kind == MLN_WEBGL_CONTEXT_EXISTING) { + if (dedicated) { + set_thread_error("an existing WebGL context must use shared ownership"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (context.data.webgl.context <= 0) { + set_thread_error("WebGL context handle must be positive"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; } - if (context.data.webgl.context <= 0) { - set_thread_error("WebGL context handle must be positive"); - return MLN_STATUS_INVALID_ARGUMENT; + if (context.data.webgl.kind == MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS) { + if (!dedicated) { + set_thread_error( + "a transferred WebGL canvas must use dedicated ownership" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + if ( + context.data.webgl.context != 0 || + context.data.webgl.canvas_selector.data == nullptr || + context.data.webgl.canvas_selector.size == 0 + ) { + set_thread_error( + "a transferred WebGL canvas requires a selector and no context handle" + ); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; } - return MLN_STATUS_OK; + set_thread_error("WebGL context kind is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; } set_thread_error("OpenGL context platform is invalid"); @@ -388,9 +417,26 @@ auto opengl_context_matches( lhs.data.egl.config == rhs.data.egl.config && lhs.data.egl.share_context == rhs.data.egl.share_context; case MLN_OPENGL_CONTEXT_PLATFORM_WEBGL: - // A WebGL context carries its own drawable, so the handle is all there is - // to compare under either strictness. - return lhs.data.webgl.context == rhs.data.webgl.context; + if (lhs.data.webgl.kind != rhs.data.webgl.kind) { + return false; + } + if (lhs.data.webgl.kind == MLN_WEBGL_CONTEXT_EXISTING) { + return lhs.data.webgl.context == rhs.data.webgl.context; + } + return lhs.data.webgl.canvas_selector.size == + rhs.data.webgl.canvas_selector.size && + std::equal( + static_cast( + lhs.data.webgl.canvas_selector.data + ), + static_cast( + lhs.data.webgl.canvas_selector.data + ) + + lhs.data.webgl.canvas_selector.size, + static_cast( + rhs.data.webgl.canvas_selector.data + ) + ); case MLN_OPENGL_CONTEXT_PLATFORM_UNSPECIFIED: break; } @@ -401,12 +447,6 @@ auto opengl_context_matches( namespace mln::core { -template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::RenderSession; - static constexpr auto leasable = false; -}; - struct QueriedFeatureRecord { std::string feature; std::string source_id; @@ -942,101 +982,527 @@ auto RenderSessionScheduler::set_repaint_request( repaint_request_ = std::move(repaint_request); } -// Only the attaching thread destroys a session, so the borrowed object stays -// alive for as long as the calling thread can use it. -auto validate_render_session( - mln_render_session session, mln_render_session_object*& out_session +namespace { + +auto finish_driver_work( + const std::shared_ptr& operation, + const RenderDriverCallable& callable, + const std::shared_ptr& session +) noexcept -> void { + try { + const auto status = callable(*session); + operation->complete(status, {}, {}); + } catch (const std::exception& exception) { + operation->complete(MLN_STATUS_NATIVE_ERROR, exception.what(), {}); + } catch (...) { + operation->complete( + MLN_STATUS_NATIVE_ERROR, "render driver work failed", {} + ); + } +} + +auto publish_driver_work_locked(mln_render_session_object& session) noexcept + -> void { + if (session.capabilities.driver == MLN_RENDER_DRIVER_CORE_WORKER) { + session.worker_condition.notify_one(); + } else if (session.driver_wake != nullptr && !session.driver_wake_pending) { + session.driver_wake_pending = true; + session.driver_wake->notify(); + } +} +auto run_core_worker( + const std::shared_ptr& session +) noexcept -> void { + while (true) { + auto work = RenderDriverWork{}; + { + auto lock = std::unique_lock{session->control_mutex}; + session->worker_condition.wait(lock, [&]() noexcept { + return session->stop_worker || !session->driver_work.empty(); + }); + if (session->stop_worker && session->driver_work.empty()) return; + work = std::move(session->driver_work.front()); + session->driver_work.pop_front(); + session->driver_call_in_flight = true; + } + try { + auto execute = [&]() -> mln_status { + if (work.execute) work.execute(); + return MLN_STATUS_OK; + }; + static_cast(mln::c_api::with_autorelease_pool(execute)); + } catch (...) { + if (work.abandon) work.abandon(); + } + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->driver_call_in_flight = false; + } + } +} + +auto enqueue_work( + const std::shared_ptr& session, + RenderDriverWork work +) -> void { + const auto lock = std::scoped_lock{session->control_mutex}; + auto& queue = session->waiting_update_work.empty() + ? session->driver_work + : session->waiting_update_work; + queue.push_back(std::move(work)); + if (session->waiting_update_work.empty()) + publish_driver_work_locked(*session); +} + +auto service_scheduler_work( + const std::shared_ptr& session +) noexcept -> void; + +// Rechecks attachment under the queue lock so work cannot land after a detach +// or abandon has already drained the queues; a late item would otherwise run +// against released graphics resources or stay pending forever. +auto enqueue_work_if_attached( + const std::shared_ptr& session, + RenderDriverWork work +) -> bool { + const auto lock = std::scoped_lock{session->control_mutex}; + if (session->state != MLN_RENDER_SESSION_STATE_ATTACHED) { + return false; + } + auto& queue = session->waiting_update_work.empty() + ? session->driver_work + : session->waiting_update_work; + queue.push_back(std::move(work)); + if (session->waiting_update_work.empty()) + publish_driver_work_locked(*session); + return true; +} + +} // namespace + +auto lease_render_session(mln_render_session session) + -> std::shared_ptr { + return handle_table().lease(session); +} + +auto enqueue_driver_operation( + mln_render_session session, RenderDriverCallable work, + const mln_completion* completion ) -> mln_status { - out_session = handle_table().resolve(session); - if (out_session == nullptr) { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = lease_render_session(session); + if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - if (out_session->owner_thread != std::this_thread::get_id()) { - set_thread_error( - "render session call must be made on the thread that attached it" - ); - return MLN_STATUS_WRONG_THREAD; + { + const auto lock = std::scoped_lock{live->control_mutex}; + if (live->state != MLN_RENDER_SESSION_STATE_ATTACHED) { + set_thread_error("render session is not attached"); + return MLN_STATUS_INVALID_STATE; + } } + auto completion_state = std::make_shared(*completion); + auto operation = std::make_shared( + [completion_state](mln_status status, std::string diagnostic, std::any) { + complete(completion_state, status, std::move(diagnostic)); + } + ); + const auto enqueued = enqueue_work_if_attached( + live, RenderDriverWork{ + [live, operation, work = std::move(work)]() { + finish_driver_work(operation, work, live); + }, + [operation]() { + operation->complete( + MLN_STATUS_TARGET_LOST, "render target was abandoned", {} + ); + } + } + ); + if (!enqueued) { + completion_state->reject(); + set_thread_error("render session is not attached"); + return MLN_STATUS_INVALID_STATE; + } + completion_state->accept(); return MLN_STATUS_OK; } - -auto validate_live_attached_render_session( - mln_render_session session, mln_render_session_object*& out_session +auto enqueue_driver_result_operation( + mln_render_session session, RenderDriverResultCallable work, + const mln_completion* completion, RenderCompletionTransfer transfer ) -> mln_status { - const auto status = validate_render_session(session, out_session); - if (status != MLN_STATUS_OK) { - return status; + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = lease_render_session(session); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (!out_session->attached || !has_backend(out_session)) { - set_thread_error("render session is detached"); + { + const auto lock = std::scoped_lock{live->control_mutex}; + if (live->state != MLN_RENDER_SESSION_STATE_ATTACHED) { + set_thread_error("render session is not attached"); + return MLN_STATUS_INVALID_STATE; + } + } + auto completion_state = std::make_shared(*completion); + auto operation = std::make_shared( + [completion_state, transfer = std::move(transfer)]( + mln_status status, std::string diagnostic, std::any result + ) mutable { + transfer( + completion_state, status, std::move(diagnostic), std::move(result) + ); + } + ); + const auto enqueued = enqueue_work_if_attached( + live, RenderDriverWork{ + [live, operation, work = std::move(work)]() { + try { + auto result = std::any{}; + const auto work_status = work(*live, result); + operation->complete( + work_status, + work_status == MLN_STATUS_OK + ? std::string{} + : std::string{thread_last_error_message()}, + std::move(result) + ); + } catch (const std::exception& exception) { + operation->complete( + MLN_STATUS_NATIVE_ERROR, exception.what(), std::any{} + ); + } + }, + [operation]() { + operation->complete( + MLN_STATUS_TARGET_LOST, "render target was abandoned", + std::any{} + ); + } + } + ); + if (!enqueued) { + completion_state->reject(); + set_thread_error("render session is not attached"); return MLN_STATUS_INVALID_STATE; } + completion_state->accept(); return MLN_STATUS_OK; } +auto enqueue_blocking_test_render_operation( + mln_render_session session, std::atomic_bool* entered, + const std::atomic_bool* release, const mln_completion* completion +) -> mln_status { + if (entered == nullptr || release == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + return enqueue_driver_operation( + session, + [entered, release](mln_render_session_object&) { + entered->store(true, std::memory_order_release); + while (!release->load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + return MLN_STATUS_OK; + }, + completion + ); +} -auto erase_render_session(mln_render_session session) - -> std::shared_ptr { - return handle_table().remove(session); +auto validate_render_session_attach_request( + const mln_render_session_attach_options* options, + const mln_render_session* out_session, const mln_completion* completion +) -> mln_status { + if (options == nullptr) { + set_thread_error("render session attach options must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (options->size < sizeof(mln_render_session_attach_options)) { + set_thread_error("render session attach options size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (out_session == nullptr || *out_session != MLN_HANDLE_NULL) { + set_thread_error("out_session must point to a null handle"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + if ( + options->driver != MLN_RENDER_DRIVER_CORE_WORKER && + options->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ) { + set_thread_error("render driver kind is invalid"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; } -auto attach_render_session( - std::shared_ptr session, - mln_render_session* out_session, RenderSessionKind kind, - RenderSessionAttachMessages messages +auto start_attach_render_session( + std::shared_ptr session, RenderSessionKind kind, + const mln_render_session_attach_options* options, + mln_render_session_capabilities capabilities, mln_render_session* out_session, + const mln_completion* completion ) -> mln_status { if (session == nullptr) { - set_thread_error(messages.null_session); + set_thread_error("render session must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - const auto output_status = validate_attach_output( - out_session, messages.null_output, messages.non_null_output - ); - if (output_status != MLN_STATUS_OK) { - return output_status; + const auto request_status = + validate_render_session_attach_request(options, out_session, completion); + if (request_status != MLN_STATUS_OK) { + return request_status; + } + if (capabilities.size < sizeof(mln_render_session_capabilities)) { + set_thread_error("render session capabilities size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; } - // The attaching thread owns the session for its whole lifetime, and need not - // be the map's thread. - session->owner_thread = std::this_thread::get_id(); - - const auto map = session->map; - auto* handle = session.get(); - const auto attach_status = map_attach_render_target_session(map, handle); + const auto map = handle_table().lease(session->map); + if ( + map == nullptr || map->runtime_state == nullptr || + map->runtime_state->event_queue == nullptr + ) { + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto frame_wake_status = validate_wake(&options->frame_wake); + if (frame_wake_status != MLN_STATUS_OK) return frame_wake_status; + const auto driver_wake_status = validate_wake(&options->driver_work_wake); + if (driver_wake_status != MLN_STATUS_OK) return driver_wake_status; + auto frame_wake = std::make_shared(options->frame_wake); + auto driver_wake = std::make_shared(options->driver_work_wake); + + auto async = CompletionOperation{}; + const auto operation_status = + create_completion_operation(completion, {}, async); + if (operation_status != MLN_STATUS_OK) { + return operation_status; + } + session->kind = kind; + session->capabilities = capabilities; + if ( + kind == RenderSessionKind::Texture && + session->texture.mode == TextureSessionMode::Owned + ) { + const auto depth = std::clamp(capabilities.texture_ring_depth, 1u, 3u); + session->capabilities.texture_ring_depth = depth; + session->texture.slots.resize(depth); + } + session->capabilities.driver = options->driver; + session->frame_wake = frame_wake; + session->driver_wake = driver_wake; + session->state = MLN_RENDER_SESSION_STATE_ATTACHING; + const auto attach_status = + map_attach_render_target_session(session->map, session.get()); if (attach_status != MLN_STATUS_OK) { + async.completion->reject(); return attach_status; } + session->attached = true; + // Handle insertion can throw after the map's session slot is claimed; + // without the rollback the map would keep a dangling session pointer. + try { + session->self = register_render_session(session); + } catch (...) { + static_cast( + map_detach_render_target_session(session->map, session.get()) + ); + if (session->self != MLN_HANDLE_NULL) { + static_cast( + handle_table().remove(session->self) + ); + } + async.completion->reject(); + throw; + } + const auto weak_session = std::weak_ptr{session}; + const auto publish_status = map_set_render_session_publish_callback( + session->map, [weak_session]() noexcept { + if (const auto live = weak_session.lock()) { + notify_render_session_map_update(live.get()); + } + } + ); + if (publish_status != MLN_STATUS_OK) { + static_cast( + map_detach_render_target_session(session->map, session.get()) + ); + static_cast( + handle_table().remove(session->self) + ); + async.completion->reject(); + return publish_status; + } try { - session->scheduler.set_repaint_request([map]() { - static_cast(map_post_trigger_repaint(map)); - }); - // Set before priming: renderer_backend() dispatches on kind. - session->kind = kind; - // Create the backend's graphics context on the thread that will drive the - // session, where the host's context is current: WGL resolves - // wglCreateContextAttribsARB through wglGetProcAddress and shares against - // the host context only if it is current on this thread. - if (auto* backend = renderer_backend(handle); backend != nullptr) { - const auto prime = mln::gfx::BackendScope{*backend}; - } - - // After the graphics setup succeeds: the map applies this on its own - // thread, so a size queued before a throwing prime would still land. - const auto size_status = - map_post_set_size(map, session->width, session->height); - if (size_status != MLN_STATUS_OK) { - session->scheduler.set_repaint_request({}); - static_cast(map_detach_render_target_session(map, handle)); - return size_status; - } - warn_on_scale_factor_mismatch(map, session->scale_factor); - - *out_session = register_render_session(std::move(session)); + if (options->driver == MLN_RENDER_DRIVER_CORE_WORKER) { + if (session->start_worker) { + const auto worker_status = + session->start_worker([session]() { run_core_worker(session); }); + if (worker_status != MLN_STATUS_OK) { + static_cast( + map_set_render_session_publish_callback(session->map, {}) + ); + static_cast( + map_detach_render_target_session(session->map, session.get()) + ); + static_cast( + handle_table().remove(session->self) + ); + async.completion->reject(); + return worker_status; + } + } else { + session->worker = + std::thread{[session]() { run_core_worker(session); }}; + } + } + enqueue_work( + session, + RenderDriverWork{ + [session, attach_operation = async.operation]() { + try { + if (session->initialize_backend) { + const auto initialize_status = + session->initialize_backend(*session); + if (initialize_status != MLN_STATUS_OK) { + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->state = MLN_RENDER_SESSION_STATE_TARGET_LOST; + session->target_ready = false; + ++session->generation; + } + attach_operation->complete( + initialize_status, thread_last_error_message(), {} + ); + return; + } + } + if ( + auto* backend = renderer_backend(session.get()); + backend != nullptr + ) { + const auto prime = mln::gfx::BackendScope{*backend}; + } + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->state = MLN_RENDER_SESSION_STATE_ATTACHED; + ++session->generation; + } + // Wake the driver whenever a worker thread posts a scheduler task + // while the queue is idle, so queued results are delivered even + // when no demand renders. Detach and abandon clear the hook. + session->scheduler.set_repaint_request( + [weak = std::weak_ptr{session}]() { + auto live = weak.lock(); + if (live == nullptr) { + return; + } + static_cast(enqueue_work_if_attached( + live, RenderDriverWork{ + [live]() { service_scheduler_work(live); }, {} + } + )); + } + ); + static_cast(map_post_trigger_repaint(session->map)); + attach_operation->complete(MLN_STATUS_OK, {}, {}); + } catch (const std::exception& exception) { + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->state = MLN_RENDER_SESSION_STATE_TARGET_LOST; + session->target_ready = false; + ++session->generation; + } + attach_operation->complete( + MLN_STATUS_NATIVE_ERROR, exception.what(), {} + ); + } + }, + [attach_operation = async.operation]() { + attach_operation->complete( + MLN_STATUS_TARGET_LOST, "render target was abandoned", {} + ); + } + } + ); } catch (...) { - session->scheduler.set_repaint_request({}); - static_cast(map_detach_render_target_session(map, handle)); + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->stop_worker = true; + session->worker_condition.notify_all(); + } + if (session->join_worker) + session->join_worker(); + else if (session->worker.joinable()) + session->worker.join(); + static_cast( + map_set_render_session_publish_callback(session->map, {}) + ); + static_cast( + map_detach_render_target_session(session->map, session.get()) + ); + static_cast( + handle_table().remove(session->self) + ); + async.completion->reject(); throw; } + *out_session = session->self; + frame_wake->accept(); + driver_wake->accept(); + async.completion->accept(); + if (options->driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) { + driver_wake->notify(); + } + return MLN_STATUS_OK; +} + +auto notify_render_session_map_update( + mln_render_session_object* session +) noexcept -> void { + if (session == nullptr) { + return; + } + const auto lock = std::scoped_lock{session->control_mutex}; + session->map_update_generation = map_latest_update_generation(session->map); + session->pending_changes = true; + if (!session->waiting_update_work.empty()) { + while (!session->waiting_update_work.empty()) { + session->driver_work.push_back( + std::move(session->waiting_update_work.front()) + ); + session->waiting_update_work.pop_front(); + } + publish_driver_work_locked(*session); + } + if ( + !session->demands.empty() && + session->capabilities.driver == MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD && + session->driver_wake != nullptr + ) { + session->driver_wake->notify(); + } + session->worker_condition.notify_one(); +} + +auto validate_render_session( + mln_render_session session, mln_render_session_object*& out_session +) -> mln_status { + out_session = handle_table().resolve(session); + return out_session == nullptr ? MLN_STATUS_INVALID_ARGUMENT : MLN_STATUS_OK; +} +auto validate_live_attached_render_session( + mln_render_session session, mln_render_session_object*& out_session +) -> mln_status { + const auto status = validate_render_session(session, out_session); + if (status != MLN_STATUS_OK) { + return status; + } + if (!out_session->attached || !has_backend(out_session)) { + set_thread_error("render session is detached"); + return MLN_STATUS_INVALID_STATE; + } return MLN_STATUS_OK; } @@ -1092,11 +1558,14 @@ auto render_session_resize( live->texture.acquired_native_texture = nullptr; live->texture.acquired_frame_kind = TextureSessionFrameKind::None; } - const auto size_status = map_post_set_size(live->map, width, height); + const auto size_status = map_post_resize( + live->map, mln_logical_extent{ + .width = width, .height = height, .scale_factor = scale_factor + } + ); if (size_status != MLN_STATUS_OK) { return size_status; } - warn_on_scale_factor_mismatch(live->map, scale_factor); // Keep the renderer across a resize, which carries the tile pyramid, atlases, // and symbol placement. Pixel ratio is the exception: it is fixed when a // Renderer is constructed and baked into its shaders. Map-owned feature @@ -1120,6 +1589,46 @@ auto unsupported_retarget(const char* message) -> mln_status { return MLN_STATUS_UNSUPPORTED; } +auto validate_render_session_retarget_submission( + mln_render_session session, RetargetTargetKind kind, + const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + const auto live = lease_render_session(session); + if (live == nullptr) return MLN_STATUS_INVALID_ARGUMENT; + + const auto lock = std::scoped_lock{live->control_mutex}; + if (live->state != MLN_RENDER_SESSION_STATE_ATTACHED) { + set_thread_error("render session is not attached"); + return MLN_STATUS_INVALID_STATE; + } + if (kind == RetargetTargetKind::Surface) { + return live->kind == RenderSessionKind::Surface + ? MLN_STATUS_OK + : unsupported_retarget( + "session does not render through a native surface" + ); + } + if (live->kind != RenderSessionKind::Texture) { + return unsupported_retarget( + "session does not render into a caller-owned texture" + ); + } + if (live->texture.acquired) { + set_thread_error( + "cannot replace the render target while a texture frame is acquired" + ); + return MLN_STATUS_INVALID_STATE; + } + return live->texture.mode == TextureSessionMode::Borrowed + ? MLN_STATUS_OK + : unsupported_retarget( + "a session-owned texture is sized and replaced by its session; " + "resize it instead" + ); +} + auto validate_render_session_retarget( mln_render_session session, RetargetTargetKind kind, mln_render_session_object*& out_session @@ -1199,6 +1708,7 @@ auto render_session_set_target( reset_pushed_feature_state(*live); } live->rendered_generation = 0; + live->rendered_target_generation = 0; live->width = extent.width; live->height = extent.height; live->physical_width = physical_width; @@ -1211,11 +1721,8 @@ auto render_session_set_target( } ++live->generation; - const auto size_status = - map_post_set_size(live->map, extent.width, extent.height); - if (size_status != MLN_STATUS_OK) { - return size_status; - } + // Target replacement changes only the graphics resource. Map creation and + // explicit resize commands remain the sole extent authorities. warn_on_scale_factor_mismatch(live->map, extent.scale_factor); return MLN_STATUS_OK; } @@ -1238,9 +1745,9 @@ auto surface_session_set_target( ); } -auto render_session_render_update( +auto render_session_render_update_on_driver( mln_render_session session, mln_render_result* out_result, - bool* out_needs_repaint + bool* out_needs_repaint, uint64_t* out_map_update_generation ) -> mln_status { mln_render_session_object* live = nullptr; const auto status = validate_live_attached_render_session(session, live); @@ -1276,7 +1783,14 @@ auto render_session_render_update( live->scheduler.drain(); map_run_render_jobs(live->map); - auto update = map_latest_update(live->map); + // Fetch the update and its generation as one snapshot so the frame result + // reports the generation of the update actually rendered, not one published + // in between. + auto update_generation = uint64_t{0}; + auto update = map_latest_update_snapshot(live->map, update_generation); + if (out_map_update_generation != nullptr) { + *out_map_update_generation = update_generation; + } if (!update) { *out_result = MLN_RENDER_RESULT_NO_UPDATE; return MLN_STATUS_OK; @@ -1394,6 +1908,7 @@ auto render_session_render_update( } } live->rendered_generation = live->generation; + live->rendered_target_generation = live->generation; *out_result = MLN_RENDER_RESULT_RENDERED; *out_needs_repaint = live->frame_observer.needs_repaint(); return MLN_STATUS_OK; @@ -1414,8 +1929,8 @@ auto render_session_detach(mln_render_session session) -> mln_status { // Tear the renderer down before releasing the map's slot. The renderer holds // the map's forwarding observer, which the map's frontend owns; releasing the - // slot first lets the map owner thread destroy the map and free that observer - // underneath the drain and reset below. + // slot first lets the runtime worker destroy the map and free that observer + // while the drain and reset below still use it. { auto current = ScopedCurrentScheduler{live->scheduler}; live->scheduler.drain(); @@ -1433,6 +1948,7 @@ auto render_session_detach(mln_render_session session) -> mln_status { } live->attached = false; live->rendered_generation = 0; + live->rendered_target_generation = 0; live->texture.rendered_native_texture = nullptr; live->texture.acquired_native_texture = nullptr; live->texture.acquired_frame_kind = TextureSessionFrameKind::None; @@ -1441,23 +1957,30 @@ auto render_session_detach(mln_render_session session) -> mln_status { } auto render_session_destroy(mln_render_session session) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_render_session(session, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (live->kind == RenderSessionKind::Texture && live->texture.acquired) { - set_thread_error("cannot destroy while a texture frame is acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (live->attached) { - const auto detach_status = render_session_detach(session); - if (detach_status != MLN_STATUS_OK) { - return detach_status; - } - } - auto owned_session = erase_render_session(session); - owned_session.reset(); + const auto live = lease_render_session(session); + if (!live) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{live->control_mutex}; + if ( + live->state != MLN_RENDER_SESSION_STATE_DETACHED && + live->state != MLN_RENDER_SESSION_STATE_ABANDONED + ) + return MLN_STATUS_INVALID_STATE; + if ( + live->state == MLN_RENDER_SESSION_STATE_DETACHED && + live->acquired_frame_count != 0 + ) + return MLN_STATUS_INVALID_STATE; + live->stop_worker = true; + live->worker_condition.notify_all(); + } + if (live->join_worker) + live->join_worker(); + else if (live->worker.joinable()) + live->worker.join(); + live->frame_wake.reset(); + live->driver_wake.reset(); + static_cast(handle_table().remove(session)); return MLN_STATUS_OK; } @@ -1767,4 +2290,895 @@ auto render_session_query_feature_extensions( return create_feature_extension_result(std::move(result), out_result); } +namespace { +auto publish_frame_result( + const std::shared_ptr& session, + mln_render_frame_result result +) noexcept -> void { + const auto lock = std::scoped_lock{session->control_mutex}; + session->latest_result = static_cast(result.disposition); + session->latest_demand_token = result.token; + session->frame_results.push_back(result); + if (session->frame_wake && !session->frame_wake_pending) { + session->frame_wake_pending = true; + session->frame_wake->notify(); + } +} + +// Delivers queued worker results and forwarded observer messages to the +// session and the map without rendering. Tile and placement continuations, +// and the observer deliveries that complete a still image, ride the session +// scheduler; delivering them must not wait for a demand that happens to +// render. +auto deliver_pending_session_work( + const std::shared_ptr& session +) noexcept -> void { + auto* backend = renderer_backend(session.get()); + if (backend == nullptr) { + return; + } + try { + auto current = ScopedCurrentScheduler{session->scheduler}; + auto guard = mln::gfx::BackendScope{*backend}; + session->scheduler.drain(); + map_run_render_jobs(session->map); + } catch (...) { + // Delivery is best-effort here; the next rendering demand drains again. + } +} + +auto run_frame_demand( + const std::shared_ptr& session +) noexcept -> void; + +// Driver work posted by the scheduler's repaint hook: drain the queued +// results, then let a pending demand re-evaluate against whatever the drain +// published. Without this, a session whose demands all resolve on the +// render-if-needed fast path never drains, stranding still-image completion +// and tile results behind a demand that happens to render. +auto service_scheduler_work( + const std::shared_ptr& session +) noexcept -> void { + { + const auto lock = std::scoped_lock{session->control_mutex}; + if ( + session->state != MLN_RENDER_SESSION_STATE_ATTACHED && + session->state != MLN_RENDER_SESSION_STATE_DETACHING + ) { + return; + } + } + deliver_pending_session_work(session); + run_frame_demand(session); +} + +auto run_frame_demand( + const std::shared_ptr& session +) noexcept -> void { + auto pending = PendingFrameDemand{}; + { + const auto lock = std::scoped_lock{session->control_mutex}; + if ( + session->demands.empty() || + (session->state != MLN_RENDER_SESSION_STATE_ATTACHED && + session->state != MLN_RENDER_SESSION_STATE_DETACHING) + ) + return; + pending = session->demands.front(); + session->demands.pop_front(); + ++session->active_demand_count; + } + struct ActiveDemandGuard { + std::shared_ptr session; + ~ActiveDemandGuard() { + const auto lock = std::scoped_lock{session->control_mutex}; + --session->active_demand_count; + } + } active_demand{session}; + // Deliver queued worker results first so the render-if-needed check below + // and the reported generation observe them; this is also what publishes a + // new update when a transition frame asked for a repaint. + deliver_pending_session_work(session); + const auto demand = pending.demand; + auto result = mln_render_frame_result{ + .size = sizeof(mln_render_frame_result), + .disposition = MLN_RENDER_RESULT_NO_UPDATE, + .token = demand.token, + .map_update_generation = map_latest_update_generation(session->map), + .extent_generation = session->extent_generation, + .frame_generation = 0, + .needs_repaint = false, + }; + const auto elapsed_ns = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - pending.accepted_at + ) + .count(); + if ( + demand.timeout_ns > 0 && elapsed_ns >= 0 && + static_cast(elapsed_ns) >= demand.timeout_ns + ) { + result.disposition = MLN_RENDER_RESULT_DEADLINE_MISSED; + publish_frame_result(session, result); + return; + } + if ((demand.flags & MLN_FRAME_DEMAND_IF_NEEDED) != 0) { + auto unchanged = false; + { + const auto lock = std::scoped_lock{session->control_mutex}; + unchanged = !session->pending_changes && + result.map_update_generation == session->rendered_generation; + } + if (unchanged) { + // Nothing newer than the last rendered update exists, so honor the + // render-if-needed contract without touching the target. A repaint + // demand during an animation publishes a new update, which bumps the + // generation and sets pending_changes before the next demand runs. + result.disposition = MLN_RENDER_RESULT_NO_UPDATE; + publish_frame_result(session, result); + return; + } + } + auto selected_slot = std::optional{}; + auto ring_full = false; + { + const auto lock = std::scoped_lock{session->control_mutex}; + if (!session->texture.slots.empty()) { + const auto reusable = std::find_if( + session->texture.slots.begin(), session->texture.slots.end(), + [](const RenderTextureSlot& value) { + return !value.acquired && !value.available && !value.rendering; + } + ); + const auto slot = + reusable != session->texture.slots.end() + ? reusable + : std::min_element( + session->texture.slots.begin(), session->texture.slots.end(), + []( + const RenderTextureSlot& left, const RenderTextureSlot& right + ) { + if (left.acquired || left.rendering) return false; + if (right.acquired || right.rendering) return true; + return left.result.frame_generation < + right.result.frame_generation; + } + ); + ring_full = slot == session->texture.slots.end() || slot->acquired || + slot->rendering; + if (!ring_full) { + selected_slot = + static_cast(slot - session->texture.slots.begin()); + slot->available = false; + slot->rendering = true; + } + } + } + if (ring_full) { + const auto lock = std::scoped_lock{session->control_mutex}; + session->demands.push_front(pending); + return; + } + if ( + selected_slot && session->texture.backend && + session->texture.backend->select_render_slot(*selected_slot) != + MLN_STATUS_OK + ) { + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->texture.slots[*selected_slot].rendering = false; + } + result.disposition = MLN_RENDER_RESULT_TARGET_NOT_READY; + publish_frame_result(session, result); + return; + } + auto disposition = MLN_RENDER_RESULT_NO_UPDATE; + auto needs_repaint = false; + auto rendered_map_generation = result.map_update_generation; + if ( + render_session_render_update_on_driver( + session->self, &disposition, &needs_repaint, &rendered_map_generation + ) != MLN_STATUS_OK + ) + disposition = MLN_RENDER_RESULT_TARGET_NOT_READY; + result.disposition = disposition; + result.map_update_generation = rendered_map_generation; + if (disposition == MLN_RENDER_RESULT_RENDERED) { + result.needs_repaint = needs_repaint; + const auto lock = std::scoped_lock{session->control_mutex}; + result.frame_generation = ++session->frame_generation; + session->rendered_generation = result.map_update_generation; + session->pending_changes = false; + if (selected_slot) { + auto& slot = session->texture.slots[*selected_slot]; + slot.result = result; + slot.available = true; + slot.rendering = false; + } + } + if (result.disposition != MLN_RENDER_RESULT_RENDERED && selected_slot) { + const auto lock = std::scoped_lock{session->control_mutex}; + session->texture.slots[*selected_slot].rendering = false; + } + publish_frame_result(session, result); +} +} // namespace + +auto render_session_get_capabilities( + mln_render_session handle, mln_render_session_capabilities* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto session = lease_render_session(handle); + if (!session) return MLN_STATUS_INVALID_ARGUMENT; + const auto lock = std::scoped_lock{session->control_mutex}; + *out = session->capabilities; + return MLN_STATUS_OK; +} + +auto render_session_get_snapshot( + mln_render_session handle, mln_render_session_snapshot* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + const auto lock = std::scoped_lock{s->control_mutex}; + *out = mln_render_session_snapshot{ + .size = sizeof(*out), + .state = s->state, + .driver = s->capabilities.driver, + .latest_result = s->latest_result, + .extent = s->pending_extent.value_or( + mln_render_target_extent{ + sizeof(mln_render_target_extent), s->width, s->height, s->scale_factor + } + ), + .generation = s->generation, + .map_update_generation = s->map_update_generation, + .rendered_update_generation = s->rendered_generation, + .extent_generation = s->extent_generation, + .frame_generation = s->frame_generation, + .latest_demand_token = s->latest_demand_token, + .pending_demand_count = + static_cast(s->demands.size()) + s->active_demand_count, + .acquired_frame_count = s->acquired_frame_count, + .target_ready = s->target_ready, + .pending_changes = s->pending_changes, + }; + return MLN_STATUS_OK; +} + +auto render_session_request_frame( + mln_render_session handle, const mln_frame_demand* demand +) -> mln_status { + if (!demand || demand->size < sizeof(*demand)) + return MLN_STATUS_INVALID_ARGUMENT; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + auto superseded = std::optional{}; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) + return MLN_STATUS_INVALID_STATE; + if ( + !s->demands.empty() && + s->demands.back().barrier_epoch == s->barrier_epoch && + s->demands.back().demand.flags == demand->flags && + s->demands.back().demand.coalescing_boundary == + demand->coalescing_boundary + ) { + const auto old = s->demands.back().demand; + s->demands.pop_back(); + superseded = mln_render_frame_result{ + sizeof(mln_render_frame_result), + MLN_RENDER_RESULT_SUPERSEDED, + old.token, + s->map_update_generation, + s->extent_generation, + 0, + false + }; + } + s->demands.push_back( + PendingFrameDemand{ + .demand = *demand, + .accepted_at = std::chrono::steady_clock::now(), + .barrier_epoch = s->barrier_epoch, + } + ); + } + if (superseded) publish_frame_result(s, *superseded); + enqueue_work(s, RenderDriverWork{[s]() { run_frame_demand(s); }, {}}); + return MLN_STATUS_OK; +} + +auto render_session_service_driver_work( + mln_render_session handle, std::size_t maximum, std::size_t* serviced +) -> mln_status { + if (!serviced) return MLN_STATUS_INVALID_ARGUMENT; + *serviced = 0; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->capabilities.driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD) + return MLN_STATUS_INVALID_STATE; + if (s->graphics_thread && *s->graphics_thread != std::this_thread::get_id()) + return MLN_STATUS_WRONG_THREAD; + if (s->driver_call_in_flight) return MLN_STATUS_BUSY; + if (!s->graphics_thread) s->graphics_thread = std::this_thread::get_id(); + s->driver_call_in_flight = true; + } + struct DriverCallGuard { + std::shared_ptr session; + ~DriverCallGuard() { + const auto lock = std::scoped_lock{session->control_mutex}; + session->driver_call_in_flight = false; + session->driver_wake_pending = !session->driver_work.empty(); + if (session->driver_wake_pending && session->driver_wake) { + session->driver_wake->notify(); + } + } + } guard{s}; + while (maximum == 0 || *serviced < maximum) { + auto item = RenderDriverWork{}; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->driver_work.empty()) break; + item = std::move(s->driver_work.front()); + s->driver_work.pop_front(); + } + if (item.execute) item.execute(); + ++*serviced; + } + return MLN_STATUS_OK; +} + +auto render_session_drain_frame_results( + mln_render_session handle, mln_render_frame_batch* out +) -> mln_status { + if (!out || *out != MLN_HANDLE_NULL) return MLN_STATUS_INVALID_ARGUMENT; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + auto batch = std::make_shared(); + const auto batch_handle = + handle_table().insert(batch); + { + const auto lock = std::scoped_lock{s->control_mutex}; + batch->results.swap(s->frame_results); + s->frame_wake_pending = false; + } + if (batch->results.empty()) { + static_cast( + handle_table().remove(batch_handle) + ); + return MLN_STATUS_NOT_READY; + } + *out = batch_handle; + return MLN_STATUS_OK; +} + +auto render_frame_batch_count(mln_render_frame_batch handle, std::size_t* out) + -> mln_status { + if (!out) return MLN_STATUS_INVALID_ARGUMENT; + const auto batch = + handle_table().lease(handle); + if (!batch) return MLN_STATUS_INVALID_ARGUMENT; + *out = batch->results.size(); + return MLN_STATUS_OK; +} + +auto render_frame_batch_get( + mln_render_frame_batch handle, std::size_t index, mln_render_frame_result* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto batch = + handle_table().lease(handle); + if (!batch || index >= batch->results.size()) + return MLN_STATUS_INVALID_ARGUMENT; + *out = batch->results[index]; + return MLN_STATUS_OK; +} + +auto render_frame_batch_release(mln_render_frame_batch handle) noexcept + -> void { + static_cast( + handle_table().remove(handle) + ); +} + +auto render_session_acquire_frame( + mln_render_session handle, mln_acquired_frame* out +) -> mln_status { + if (!out || *out != MLN_HANDLE_NULL) return MLN_STATUS_INVALID_ARGUMENT; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + auto frame = std::make_shared(); + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) + return MLN_STATUS_INVALID_STATE; + if ( + (s->capabilities.flags & + MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION) == 0 + ) { + set_thread_error( + "render session does not expose acquired texture frames" + ); + return MLN_STATUS_UNSUPPORTED; + } + const auto slot = std::min_element( + s->texture.slots.begin(), s->texture.slots.end(), + [](const RenderTextureSlot& left, const RenderTextureSlot& right) { + if (!left.available || left.acquired) return false; + if (!right.available || right.acquired) return true; + return left.result.frame_generation < right.result.frame_generation; + } + ); + if (slot == s->texture.slots.end() || !slot->available || slot->acquired) + return MLN_STATUS_NOT_READY; + frame->session = s; + frame->slot = static_cast(slot - s->texture.slots.begin()); + frame->result = slot->result; + frame->producer_sync = slot->producer_sync; + if (s->texture.backend) { + const auto metadata_status = s->texture.backend->copy_slot_metadata( + *s, frame->slot, frame->backend_metadata + ); + if (metadata_status != MLN_STATUS_OK) return metadata_status; + } + slot->available = false; + slot->acquired = true; + ++s->acquired_frame_count; + } + try { + *out = handle_table().insert(frame); + } catch (...) { + const auto lock = std::scoped_lock{s->control_mutex}; + auto& slot = s->texture.slots[frame->slot]; + slot.available = true; + slot.acquired = false; + --s->acquired_frame_count; + throw; + } + return MLN_STATUS_OK; +} + +auto acquired_frame_get_result( + mln_acquired_frame handle, mln_render_frame_result* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto frame = handle_table().lease(handle); + if (!frame || !frame->valid.load()) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{frame->session->control_mutex}; + if (frame->session->state == MLN_RENDER_SESSION_STATE_ABANDONED) + return MLN_STATUS_TARGET_LOST; + } + *out = frame->result; + return MLN_STATUS_OK; +} + +auto acquired_frame_get_producer_sync( + mln_acquired_frame handle, mln_gpu_sync* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto frame = handle_table().lease(handle); + if (!frame || !frame->valid.load()) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{frame->session->control_mutex}; + if (frame->session->state == MLN_RENDER_SESSION_STATE_ABANDONED) + return MLN_STATUS_TARGET_LOST; + } + *out = frame->producer_sync; + return MLN_STATUS_OK; +} + +auto acquired_frame_release( + mln_acquired_frame* handle, const mln_gpu_sync* sync +) -> mln_status { + if (!handle || *handle == MLN_HANDLE_NULL) return MLN_STATUS_INVALID_ARGUMENT; + const auto copied = + sync ? *sync + : mln_gpu_sync{ + sizeof(mln_gpu_sync), MLN_GPU_SYNC_CPU_COMPLETE, nullptr, 0 + }; + if (copied.size < sizeof(mln_gpu_sync)) return MLN_STATUS_INVALID_ARGUMENT; + const auto frame = handle_table().lease(*handle); + if (!frame) return MLN_STATUS_INVALID_ARGUMENT; + // Reject an unsupported sync before consuming the handle: the host keeps + // frame ownership, and a slot never returns to the ring without its wait. + if ( + frame->session->texture.backend && + !frame->session->texture.backend->supports_consumer_sync( + static_cast(copied.kind) + ) + ) { + set_thread_error("render backend does not support this gpu sync kind"); + return MLN_STATUS_UNSUPPORTED; + } + auto claimed = true; + if (!frame->valid.compare_exchange_strong(claimed, false)) + return MLN_STATUS_INVALID_STATE; + const auto consumed = + handle_table().remove(*handle); + if (!consumed) { + return MLN_STATUS_INVALID_STATE; + } + *handle = MLN_HANDLE_NULL; + const auto release = [consumed, copied]() { + auto status = MLN_STATUS_OK; + { + const auto lock = std::scoped_lock{consumed->session->control_mutex}; + if (consumed->session->state == MLN_RENDER_SESSION_STATE_ABANDONED) + status = MLN_STATUS_TARGET_LOST; + } + if (status == MLN_STATUS_OK && consumed->session->texture.backend) + status = + consumed->session->texture.backend->release_consumer_sync(copied); + auto resume_demand = false; + { + const auto lock = std::scoped_lock{consumed->session->control_mutex}; + // A failed sync wait must not make the slot reusable: the host GPU may + // still be reading the texture. TARGET_LOST is safe because an + // abandoned session renders nothing further. + if (status == MLN_STATUS_OK || status == MLN_STATUS_TARGET_LOST) { + if (consumed->slot < consumed->session->texture.slots.size()) + consumed->session->texture.slots[consumed->slot].acquired = false; + resume_demand = !consumed->session->demands.empty(); + } + } + if (status != MLN_STATUS_OK && status != MLN_STATUS_TARGET_LOST) { + mln::Log::Error( + mln::Event::Render, + "failed to retire an acquired frame; its texture slot will not be " + "reused" + ); + } + if (resume_demand) { + enqueue_work( + consumed->session, + RenderDriverWork{ + [session = consumed->session]() { run_frame_demand(session); }, {} + } + ); + } + }; + auto release_immediately = false; + { + const auto lock = std::scoped_lock{frame->session->control_mutex}; + if (frame->session->acquired_frame_count) + --frame->session->acquired_frame_count; + release_immediately = + frame->session->state == MLN_RENDER_SESSION_STATE_ABANDONED; + if (!release_immediately) { + auto& queue = frame->session->waiting_update_work.empty() + ? frame->session->driver_work + : frame->session->waiting_update_work; + queue.push_back(RenderDriverWork{release, release}); + if (frame->session->waiting_update_work.empty()) + publish_driver_work_locked(*frame->session); + } + } + if (release_immediately) release(); + return MLN_STATUS_OK; +} + +namespace { +void invalidate_unacquired_texture_frames_locked( + mln_render_session_object& session +) { + if (session.kind != RenderSessionKind::Texture) return; + for (auto& slot : session.texture.slots) { + if (!slot.acquired) slot.available = false; + } +} + +auto make_ordered_resize_work( + const std::shared_ptr& session, + const std::shared_ptr& operation, + mln_render_target_extent extent +) -> RenderDriverWork { + return RenderDriverWork{ + [session, operation, extent]() { + auto update = map_latest_update(session->map); + if ( + !update || update->transformState.getSize() != + mln::Size{extent.width, extent.height} + ) { + auto lock = std::unique_lock{session->control_mutex}; + update = map_latest_update(session->map); + if ( + !update || update->transformState.getSize() != + mln::Size{extent.width, extent.height} + ) { + session->waiting_update_work.push_back( + make_ordered_resize_work(session, operation, extent) + ); + while (!session->driver_work.empty()) { + session->waiting_update_work.push_back( + std::move(session->driver_work.front()) + ); + session->driver_work.pop_front(); + } + return; + } + } + const auto physical_width = + physical_dimension(extent.width, extent.scale_factor); + const auto physical_height = + physical_dimension(extent.height, extent.scale_factor); + { + // A render that was already executing when resize was accepted may + // have published an old-size frame afterward. Retire it before the + // backend changes its ring resources. Acquired slots remain leased + // until their consumer releases them. + const auto lock = std::scoped_lock{session->control_mutex}; + invalidate_unacquired_texture_frames_locked(*session); + } + if (session->kind == RenderSessionKind::Surface) + session->surface.backend->resize(physical_width, physical_height); + else + session->texture.backend->resize({physical_width, physical_height}); + // Retiring the renderer waits for MapLibre's tile workers to drain, and + // a worker finishing a layout posts to the session scheduler, whose + // repaint hook takes control_mutex. Holding that lock across the reset + // closes the cycle, so retire the renderer before locking. The renderer + // is driver-thread state, which is the thread running this work, so the + // reset needs no lock of its own. + if (extent.scale_factor != session->scale_factor) { + session->renderer.reset(); + reset_pushed_feature_state(*session); + } + { + const auto lock = std::scoped_lock{session->control_mutex}; + session->width = extent.width; + session->height = extent.height; + session->physical_width = physical_width; + session->physical_height = physical_height; + session->scale_factor = extent.scale_factor; + session->pending_extent.reset(); + ++session->extent_generation; + ++session->generation; + } + operation->complete(MLN_STATUS_OK, {}, {}); + }, + [operation]() { + operation->complete(MLN_STATUS_TARGET_LOST, "target abandoned", {}); + } + }; +} + +auto make_ordered_barrier_work( + const std::shared_ptr& operation +) -> RenderDriverWork { + return RenderDriverWork{ + [operation]() { operation->complete(MLN_STATUS_OK, {}, {}); }, + [operation]() { + operation->complete(MLN_STATUS_TARGET_LOST, "target abandoned", {}); + } + }; +} +} // namespace + +auto render_session_resize_start( + mln_render_session handle, const mln_render_target_extent* extent, + const mln_completion* completion +) -> mln_status { + if (!extent) return MLN_STATUS_INVALID_ARGUMENT; + const auto valid = validate_render_target_extent( + *extent, "render target dimensions and scale factor must be positive" + ); + if (valid != MLN_STATUS_OK) return valid; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) + return MLN_STATUS_INVALID_STATE; + if ( + s->kind == RenderSessionKind::Texture && + s->texture.mode == TextureSessionMode::Borrowed + ) { + set_thread_error( + "a caller-owned texture is sized by its owner; hand over a replacement " + "with the borrowed-texture set_target function for this backend" + ); + return MLN_STATUS_UNSUPPORTED; + } + } + auto async = CompletionOperation{}; + const auto registered = create_completion_operation(completion, {}, async); + if (registered != MLN_STATUS_OK) return registered; + const auto copied = *extent; + { + const auto lock = std::scoped_lock{s->control_mutex}; + s->pending_extent = copied; + s->pending_changes = true; + // An owned-texture ring may contain several completed frames. None of its + // old-size, unacquired entries may survive an accepted resize and outrank + // the first new-size frame when the host next acquires the oldest result. + invalidate_unacquired_texture_frames_locked(*s); + } + const auto post = map_post_resize( + s->map, mln_logical_extent{ + .width = copied.width, + .height = copied.height, + .scale_factor = copied.scale_factor + } + ); + if (post != MLN_STATUS_OK) { + async.operation->complete(post, {}, {}); + async.completion->accept(); + return MLN_STATUS_OK; + } + enqueue_work(s, make_ordered_resize_work(s, async.operation, copied)); + async.completion->accept(); + return MLN_STATUS_OK; +} + +auto render_session_barrier_start( + mln_render_session handle, const mln_completion* completion +) -> mln_status { + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) + return MLN_STATUS_INVALID_STATE; + } + auto async = CompletionOperation{}; + const auto status = create_completion_operation(completion, {}, async); + if (status != MLN_STATUS_OK) return status; + auto item = make_ordered_barrier_work(async.operation); + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) { + async.completion->reject(); + return MLN_STATUS_INVALID_STATE; + } + ++s->barrier_epoch; + auto& queue = + s->waiting_update_work.empty() ? s->driver_work : s->waiting_update_work; + queue.push_back(std::move(item)); + if (s->waiting_update_work.empty()) publish_driver_work_locked(*s); + } + async.completion->accept(); + return MLN_STATUS_OK; +} + +auto render_session_maintenance_start( + mln_render_session handle, std::uint32_t kind, const mln_completion* out +) -> mln_status { + return enqueue_driver_operation( + handle, + [kind](mln_render_session_object& s) { + switch (kind) { + case 0: + return render_session_reduce_memory_use(s.self); + case 1: + return render_session_clear_data(s.self); + default: + return render_session_dump_debug_logs(s.self); + } + }, + out + ); +} + +auto render_session_detach_start( + mln_render_session handle, const mln_completion* completion +) -> mln_status { + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->state != MLN_RENDER_SESSION_STATE_ATTACHED) + return MLN_STATUS_INVALID_STATE; + if (s->acquired_frame_count != 0) return MLN_STATUS_INVALID_STATE; + } + auto async = CompletionOperation{}; + const auto status = create_completion_operation(completion, {}, async); + if (status != MLN_STATUS_OK) return status; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if ( + s->state != MLN_RENDER_SESSION_STATE_ATTACHED || + s->acquired_frame_count != 0 + ) { + async.completion->reject(); + return MLN_STATUS_INVALID_STATE; + } + s->state = MLN_RENDER_SESSION_STATE_DETACHING; + ++s->barrier_epoch; + ++s->generation; + } + enqueue_work( + s, + RenderDriverWork{ + [s, operation = async.operation]() { + static_cast(map_set_render_session_publish_callback(s->map, {})); + const auto status = render_session_detach(s->self); + { + const auto lock = std::scoped_lock{s->control_mutex}; + s->state = status == MLN_STATUS_OK + ? MLN_RENDER_SESSION_STATE_DETACHED + : MLN_RENDER_SESSION_STATE_TARGET_LOST; + ++s->generation; + } + operation->complete(status, {}, {}); + }, + [operation = async.operation]() { + operation->complete(MLN_STATUS_TARGET_LOST, "target abandoned", {}); + } + } + ); + async.completion->accept(); + return MLN_STATUS_OK; +} + +auto render_session_abandon( + mln_render_session handle, mln_render_abandon_result* out +) -> mln_status { + if (!out || out->size < sizeof(*out)) return MLN_STATUS_INVALID_ARGUMENT; + const auto s = lease_render_session(handle); + if (!s) return MLN_STATUS_INVALID_ARGUMENT; + auto discarded = std::deque{}; + auto pending_demands = std::deque{}; + { + const auto lock = std::scoped_lock{s->control_mutex}; + if (s->driver_call_in_flight) return MLN_STATUS_BUSY; + if ( + s->state == MLN_RENDER_SESSION_STATE_DETACHED || + s->state == MLN_RENDER_SESSION_STATE_ABANDONED + ) + return MLN_STATUS_INVALID_STATE; + s->state = MLN_RENDER_SESSION_STATE_ABANDONED; + s->attached = false; + s->target_ready = false; + s->stop_worker = true; + discarded.swap(s->driver_work); + while (!s->waiting_update_work.empty()) { + discarded.push_back(std::move(s->waiting_update_work.front())); + s->waiting_update_work.pop_front(); + } + pending_demands.swap(s->demands); + ++s->generation; + s->worker_condition.notify_all(); + } + static_cast(map_set_render_session_publish_callback(s->map, {})); + static_cast(map_detach_render_target_session(s->map, s.get())); + for (auto& item : discarded) { + if (item.abandon) item.abandon(); + } + for (const auto& pending : pending_demands) { + publish_frame_result( + s, mln_render_frame_result{ + sizeof(mln_render_frame_result), MLN_RENDER_RESULT_TARGET_NOT_READY, + pending.demand.token, s->map_update_generation, s->extent_generation, + 0, false + } + ); + } + auto quarantined = uint32_t{0}; + if (s->renderer.release() != nullptr) ++quarantined; + if (s->surface.backend.release() != nullptr) ++quarantined; + if (s->texture.backend.release() != nullptr) ++quarantined; + s->scheduler.set_repaint_request({}); + s->scheduler.discard(); + s->frame_wake.reset(); + s->driver_wake.reset(); + // Tile workers can still hold the quarantined renderer's atlas, which holds + // the host's graphics device. Drain them before returning so the documented + // contract — no graphics calls after abandon — covers worker threads and + // the host may destroy its device immediately. + map_quiesce_render_workers(s->map); + *out = mln_render_abandon_result{ + sizeof(*out), + quarantined == 0 ? MLN_RENDER_ABANDON_DISPOSITION_CLEAN + : MLN_RENDER_ABANDON_DISPOSITION_QUARANTINED, + quarantined, 0 + }; + return MLN_STATUS_OK; +} + } // namespace mln::core diff --git a/src/render/render_session_common.hpp b/src/render/render_session_common.hpp index 886faf695..6766ad9df 100644 --- a/src/render/render_session_common.hpp +++ b/src/render/render_session_common.hpp @@ -1,8 +1,13 @@ #pragma once +#include +#include +#include #include +#include #include #include +#include #include #include #include @@ -22,9 +27,12 @@ #include #include "diagnostics/diagnostics.hpp" +#include "handles/handle_table.hpp" #include "map/feature_state.hpp" #include "maplibre_native_c.h" +#include "operation/operation.hpp" #include "render/discard_present.hpp" +#include "wake/wake.hpp" struct mln_render_session_object; @@ -182,30 +190,24 @@ class TextureSessionBackend { out_rendered = true; return MLN_STATUS_OK; } - virtual auto acquire_vulkan_owned_frame( - const mln_render_session_object& session, - mln_vulkan_owned_texture_frame& out_frame - ) -> mln_status { - (void)session; - (void)out_frame; - return MLN_STATUS_UNSUPPORTED; + // Rejecting a sync kind here keeps the release synchronous and all-or- + // nothing: the caller keeps frame ownership instead of losing the handle to + // a release whose wait can never run. + virtual auto supports_consumer_sync(mln_gpu_sync_kind kind) -> bool { + return kind == MLN_GPU_SYNC_CPU_COMPLETE; } - virtual auto acquire_opengl_owned_frame( - const mln_render_session_object& session, - mln_opengl_owned_texture_frame& out_frame - ) -> mln_status { - (void)session; - (void)out_frame; - return MLN_STATUS_UNSUPPORTED; + virtual auto release_consumer_sync(const mln_gpu_sync& sync) -> mln_status { + return sync.kind == MLN_GPU_SYNC_CPU_COMPLETE ? MLN_STATUS_OK + : MLN_STATUS_UNSUPPORTED; } - virtual auto acquire_webgpu_owned_frame( - const mln_render_session_object& session, - mln_webgpu_owned_texture_frame& out_frame + virtual auto copy_slot_metadata( + const mln_render_session_object&, std::size_t, std::any& ) -> mln_status { - (void)session; - (void)out_frame; return MLN_STATUS_UNSUPPORTED; } + virtual auto select_render_slot(std::size_t) -> mln_status { + return MLN_STATUS_OK; + } }; // The scheduler mbgl sees as current while a render session renders. Work @@ -233,8 +235,8 @@ class RenderSessionScheduler final : public mln::Scheduler { auto makeWeakPtr() -> mapbox::base::WeakPtr override { return weak_factory_.makeWeakPtr(); } - // Only the owner thread may run this queue, so a caller on any other thread - // gets a no-op rather than tasks running off the owner thread. + // Only the graphics thread may run this queue, so another caller gets a + // no-op rather than tasks running off the graphics thread. void waitForEmpty( const mln::util::SimpleIdentity = mln::util::SimpleIdentity::Empty ) override { @@ -243,8 +245,8 @@ class RenderSessionScheduler final : public mln::Scheduler { } } - // Runs queued work on the calling thread, which must be the session's owner - // thread. Loops until the queue is empty, because a task may enqueue more. + // Runs queued work on the calling graphics thread. Loops until the queue is + // empty, because a task may enqueue more. auto drain() -> void; // Drops queued work without running it, for detach. @@ -280,13 +282,10 @@ class RenderSessionScheduler final : public mln::Scheduler { }; // Gives the calling thread a current mbgl scheduler for the duration of a -// session call, but only when it does not already have one. A session sharing -// its owner thread with the runtime keeps the runtime's run loop, which the -// host pumps more often than it renders. +// session call, but only when it does not already have one. // -// GetCurrent(false) is required: the default would create a thread_local -// RunLoop on a bare render thread, which permanently disqualifies that thread -// from mln_runtime_create(). +// GetCurrent(false) is required: the default would create a thread-local +// RunLoop whose lifetime and task queue are not owned by the render session. class ScopedCurrentScheduler { public: explicit ScopedCurrentScheduler(mln::Scheduler& scheduler) @@ -310,10 +309,24 @@ struct RenderSurfaceState { std::unique_ptr backend = nullptr; }; +struct RenderTextureSlot { + mln_render_frame_result result{}; + mln_gpu_sync producer_sync{ + .size = sizeof(mln_gpu_sync), + .kind = MLN_GPU_SYNC_CPU_COMPLETE, + .object = nullptr, + .value = 0 + }; + bool available = false; + bool acquired = false; + bool rendering = false; +}; + // Records the frame status that mln::Renderer reports synchronously out of -// render(), so render_session_render_update() can return its repaint flag, -// and forwards every callback to the map's observer so runtime event delivery -// is unchanged. Runs entirely on the session owner thread. +// render(), so render_session_render_update_on_driver() can return its repaint +// flag with the terminal frame result, and forwards every callback to the +// map's observer so runtime event delivery is unchanged. Runs entirely on the +// session's driver thread. class SessionFrameObserver final : public mln::RendererObserver { public: auto set_delegate(mln::RendererObserver* delegate) -> void { @@ -471,24 +484,71 @@ struct RenderTextureState { TextureSessionMode mode = TextureSessionMode::Owned; void* rendered_native_texture = nullptr; void* acquired_native_texture = nullptr; + std::vector slots; +}; + +struct RenderDriverWork { + std::function execute; + std::function abandon; }; +struct PendingFrameDemand { + mln_frame_demand demand; + std::chrono::steady_clock::time_point accepted_at; + std::uint64_t barrier_epoch; +}; } // namespace mln::core -struct mln_render_session_object { +struct mln_render_session_object + : public std::enable_shared_from_this { mln::core::RenderSessionKind kind = mln::core::RenderSessionKind::Surface; + mln_render_session self = MLN_HANDLE_NULL; mln_map map = MLN_HANDLE_NULL; - // The thread that attached the session, fixed for its lifetime. Set before - // the session is registered. - std::thread::id owner_thread; uint32_t width = 0; uint32_t height = 0; uint32_t physical_width = 0; uint32_t physical_height = 0; + uint64_t barrier_epoch = 0; double scale_factor = 1.0; + + mutable std::mutex control_mutex; + mln_render_session_state state = MLN_RENDER_SESSION_STATE_ATTACHING; + mln_render_session_capabilities capabilities{}; uint64_t generation = 1; + uint64_t map_update_generation = 0; uint64_t rendered_generation = 0; - bool attached = true; + uint64_t rendered_target_generation = 0; + uint64_t extent_generation = 1; + uint64_t frame_generation = 0; + uint64_t latest_demand_token = 0; + mln_render_result latest_result = MLN_RENDER_RESULT_NO_UPDATE; + bool target_ready = true; + bool pending_changes = true; + std::optional pending_extent; + std::optional graphics_thread; + uint32_t acquired_frame_count = 0; + bool driver_call_in_flight = false; + uint32_t active_demand_count = 0; + bool stop_worker = false; + bool attached = false; + + std::deque frame_results; + std::deque demands; + std::deque waiting_update_work; + std::deque driver_work; + std::condition_variable worker_condition; + std::thread worker; + // Backends with transfer-time thread attributes may replace the default + // std::thread worker before attachment. + std::function)> start_worker; + std::function join_worker; + // Attachment descriptors are copied into this closure. It creates every + // graphics object on the selected driver. + std::function initialize_backend; + std::shared_ptr frame_wake; + std::shared_ptr driver_wake; + bool frame_wake_pending = false; + bool driver_wake_pending = false; // Declared before `renderer` so reverse-order destruction tears the renderer // down while the scheduler its mailboxes point at is still alive. @@ -503,14 +563,70 @@ struct mln_render_session_object { mln::core::RenderTextureState texture; }; +struct mln_render_frame_batch_object { + std::deque results; +}; + +struct mln_acquired_frame_object { + std::shared_ptr session; + std::any backend_metadata; + std::size_t slot = 0; + mln_render_frame_result result{}; + mln_gpu_sync producer_sync{}; + std::atomic_bool valid{true}; +}; + namespace mln::core { +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::RenderSession; + static constexpr auto leasable = true; +}; + +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::RenderFrameBatch; + static constexpr auto leasable = true; +}; -struct RenderSessionAttachMessages { - const char* null_session; - const char* null_output; - const char* non_null_output; +template <> +struct HandleTraits { + static constexpr auto kind = HandleKind::AcquiredFrame; + static constexpr auto leasable = true; }; +using RenderDriverCallable = + std::function; +using RenderDriverResultCallable = + std::function; + +[[nodiscard]] auto lease_render_session(mln_render_session session) + -> std::shared_ptr; +auto enqueue_driver_operation( + mln_render_session session, RenderDriverCallable work, + const mln_completion* completion +) -> mln_status; +using RenderCompletionTransfer = std::function< + void(const std::shared_ptr&, mln_status, std::string, std::any)>; +auto enqueue_driver_result_operation( + mln_render_session session, RenderDriverResultCallable work, + const mln_completion* completion, RenderCompletionTransfer transfer +) -> mln_status; +auto validate_render_session_attach_request( + const mln_render_session_attach_options* options, + const mln_render_session* out_session, const mln_completion* completion +) -> mln_status; + +auto start_attach_render_session( + std::shared_ptr session, RenderSessionKind kind, + const mln_render_session_attach_options* options, + mln_render_session_capabilities capabilities, mln_render_session* out_session, + const mln_completion* completion +) -> mln_status; +auto notify_render_session_map_update( + mln_render_session_object* session +) noexcept -> void; + auto register_render_session(std::shared_ptr session) -> mln_render_session; auto validate_render_session( @@ -519,23 +635,6 @@ auto validate_render_session( auto validate_live_attached_render_session( mln_render_session session, mln_render_session_object*& out_session ) -> mln_status; -auto erase_render_session(mln_render_session session) - -> std::shared_ptr; - -inline auto validate_attach_output( - mln_render_session* out_session, const char* null_message, - const char* not_null_message -) -> mln_status { - if (out_session == nullptr) { - set_thread_error(null_message); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (*out_session != MLN_HANDLE_NULL) { - set_thread_error(not_null_message); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; -} inline auto physical_dimension(uint32_t logical, double scale_factor) -> uint32_t { @@ -679,23 +778,18 @@ inline auto validate_physical_size( return MLN_STATUS_OK; } -auto attach_render_session( - std::shared_ptr session, - mln_render_session* out_session, RenderSessionKind kind, - RenderSessionAttachMessages messages -) -> mln_status; +enum class RetargetTargetKind : uint8_t { Surface, BorrowedTexture }; -auto render_session_resize( - mln_render_session session, uint32_t width, uint32_t height, - double scale_factor +// Checks the completion and session before a backend reads opaque target +// objects from the descriptor. +auto validate_render_session_retarget_submission( + mln_render_session session, RetargetTargetKind kind, + const mln_completion* completion ) -> mln_status; -enum class RetargetTargetKind : uint8_t { Surface, BorrowedTexture }; - -// Checks that a session can take a replacement target of this kind, before any -// descriptor is read. Entry points call this first, including in builds without -// that backend, so the reported status does not depend on which failure the -// build notices first. +// Rechecks on the driver thread that the session can take the replacement. +// Submission entry points perform the descriptor-safe check above first; this +// second check closes state-change races before the backend swaps resources. auto validate_render_session_retarget( mln_render_session session, RetargetTargetKind kind, mln_render_session_object*& out_session @@ -722,15 +816,7 @@ auto surface_session_set_target( mln_render_session session, const mln_render_target_extent& extent, const RenderTargetReplacer& replace ) -> mln_status; -auto render_session_render_update( - mln_render_session session, mln_render_result* out_result, - bool* out_needs_repaint -) -> mln_status; -auto render_session_detach(mln_render_session session) -> mln_status; auto render_session_destroy(mln_render_session session) -> mln_status; -auto render_session_reduce_memory_use(mln_render_session session) -> mln_status; -auto render_session_clear_data(mln_render_session session) -> mln_status; -auto render_session_dump_debug_logs(mln_render_session session) -> mln_status; auto queried_feature_list_count( mln_queried_feature_list list, size_t* out_count ) -> mln_status; @@ -755,4 +841,61 @@ auto render_session_query_feature_extensions( mln_buffer* out_result ) -> mln_status; +auto render_session_query_rendered_features_start( + mln_render_session session, const mln_rendered_query_geometry* geometry, + const mln_rendered_feature_query_options* options, + const mln_completion* completion +) -> mln_status; +auto render_session_query_source_features_start( + mln_render_session session, mln_buffer_view source_id, + const mln_source_feature_query_options* options, + const mln_completion* completion +) -> mln_status; +auto render_session_query_feature_extensions_start( + mln_render_session session, mln_buffer_view source_id, + mln_buffer_view feature, mln_buffer_view extension, + mln_buffer_view extension_field, const mln_buffer_view* arguments, + const mln_completion* completion +) -> mln_status; + +auto render_session_get_capabilities( + mln_render_session, mln_render_session_capabilities* +) -> mln_status; +auto render_session_get_snapshot( + mln_render_session, mln_render_session_snapshot* +) -> mln_status; +auto render_session_request_frame(mln_render_session, const mln_frame_demand*) + -> mln_status; +auto render_session_service_driver_work( + mln_render_session, std::size_t, std::size_t* +) -> mln_status; +auto render_session_drain_frame_results( + mln_render_session, mln_render_frame_batch* +) -> mln_status; +auto render_frame_batch_count(mln_render_frame_batch, std::size_t*) + -> mln_status; +auto render_frame_batch_get( + mln_render_frame_batch, std::size_t, mln_render_frame_result* +) -> mln_status; +auto render_frame_batch_release(mln_render_frame_batch) noexcept -> void; +auto render_session_acquire_frame(mln_render_session, mln_acquired_frame*) + -> mln_status; +auto acquired_frame_get_result(mln_acquired_frame, mln_render_frame_result*) + -> mln_status; +auto acquired_frame_get_producer_sync(mln_acquired_frame, mln_gpu_sync*) + -> mln_status; +auto acquired_frame_release(mln_acquired_frame*, const mln_gpu_sync*) + -> mln_status; +auto render_session_resize_start( + mln_render_session, const mln_render_target_extent*, const mln_completion* +) -> mln_status; +auto render_session_barrier_start(mln_render_session, const mln_completion*) + -> mln_status; +auto render_session_maintenance_start( + mln_render_session, std::uint32_t, const mln_completion* +) -> mln_status; +auto render_session_detach_start(mln_render_session, const mln_completion*) + -> mln_status; +auto render_session_abandon(mln_render_session, mln_render_abandon_result*) + -> mln_status; } // namespace mln::core diff --git a/src/render/render_session_test_support.hpp b/src/render/render_session_test_support.hpp new file mode 100644 index 000000000..89952d572 --- /dev/null +++ b/src/render/render_session_test_support.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "maplibre_native_c/base.h" + +namespace mln::core { + +auto enqueue_blocking_test_render_operation( + mln_render_session session, std::atomic_bool* entered, + const std::atomic_bool* release, const mln_completion* completion +) -> mln_status; + +} // namespace mln::core diff --git a/src/render/surface_session.hpp b/src/render/surface_session.hpp index 33b96ad82..3f04f4f24 100644 --- a/src/render/surface_session.hpp +++ b/src/render/surface_session.hpp @@ -35,33 +35,41 @@ auto validate_opengl_surface_descriptor( auto validate_webgpu_surface_descriptor( const mln_webgpu_surface_descriptor* descriptor ) -> mln_status; -auto metal_surface_attach( +auto metal_surface_attach_start( mln_map map, const mln_metal_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto vulkan_surface_attach( +auto vulkan_surface_attach_start( mln_map map, const mln_vulkan_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto opengl_surface_attach( +auto opengl_surface_attach_start( mln_map map, const mln_opengl_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto webgpu_surface_attach( +auto webgpu_surface_attach_start( mln_map map, const mln_webgpu_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto metal_surface_set_target( - mln_render_session session, const mln_metal_surface_descriptor* descriptor +auto metal_surface_set_target_start( + mln_render_session session, const mln_metal_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto vulkan_surface_set_target( - mln_render_session session, const mln_vulkan_surface_descriptor* descriptor +auto vulkan_surface_set_target_start( + mln_render_session session, const mln_vulkan_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto opengl_surface_set_target( - mln_render_session session, const mln_opengl_surface_descriptor* descriptor +auto opengl_surface_set_target_start( + mln_render_session session, const mln_opengl_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto webgpu_surface_set_target( - mln_render_session session, const mln_webgpu_surface_descriptor* descriptor +auto webgpu_surface_set_target_start( + mln_render_session session, const mln_webgpu_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; } // namespace mln::core diff --git a/src/render/texture_session.cpp b/src/render/texture_session.cpp index 0927f664f..dfaf5fce4 100644 --- a/src/render/texture_session.cpp +++ b/src/render/texture_session.cpp @@ -1,14 +1,19 @@ +#include #include #include #include +#include +#include #include #include #include "render/texture_session.hpp" +#include "bytes/buffer.hpp" #include "diagnostics/diagnostics.hpp" #include "maplibre_native_c.h" +#include "operation/operation.hpp" #include "render/render_session_common.hpp" namespace mln::core { @@ -309,20 +314,43 @@ auto validate_vulkan_borrowed_texture_descriptor( return MLN_STATUS_OK; } -// A texture session exists to hand its texture to the host, which needs the -// session context in the host's share group. Dedicated ownership names no share -// group, so it has no meaning here. -auto validate_shared_texture_ownership( +// A shared owned target exposes frames to a host context. A private owned +// target keeps every GL object on its core worker and exposes CPU readback +// instead. EGL can create that worker-local context from a display and config; +// transferred WebGL creates it from the canvas on the worker itself. +auto validate_owned_texture_ownership( const mln_opengl_context_descriptor& context ) -> mln_status { - if (context.ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_DEDICATED) { - set_thread_error( - "an OpenGL texture session shares its context with the host that samples " - "the texture" - ); - return MLN_STATUS_INVALID_ARGUMENT; + if (context.ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED) { + return MLN_STATUS_OK; } - return MLN_STATUS_OK; + if (context.platform == MLN_OPENGL_CONTEXT_PLATFORM_EGL) { + return MLN_STATUS_OK; + } + if ( + context.platform == MLN_OPENGL_CONTEXT_PLATFORM_WEBGL && + context.data.webgl.kind == MLN_WEBGL_CONTEXT_TRANSFERRED_CANVAS + ) { + return MLN_STATUS_OK; + } + set_thread_error( + "dedicated OpenGL owned textures require EGL or a transferred WebGL canvas" + ); + return MLN_STATUS_UNSUPPORTED; +} + +// A borrowed target must share objects with the host context that owns its +// texture. Dedicated ownership names no share group. +auto validate_borrowed_texture_ownership( + const mln_opengl_context_descriptor& context +) -> mln_status { + if (context.ownership == MLN_OPENGL_CONTEXT_OWNERSHIP_SHARED) { + return MLN_STATUS_OK; + } + set_thread_error( + "a borrowed OpenGL texture requires a context shared with its host owner" + ); + return MLN_STATUS_INVALID_ARGUMENT; } auto validate_opengl_owned_texture_descriptor( @@ -348,7 +376,7 @@ auto validate_opengl_owned_texture_descriptor( if (context_status != MLN_STATUS_OK) { return context_status; } - return validate_shared_texture_ownership(descriptor->context); + return validate_owned_texture_ownership(descriptor->context); } auto validate_opengl_borrowed_texture_descriptor( @@ -377,7 +405,7 @@ auto validate_opengl_borrowed_texture_descriptor( return context_status; } const auto ownership_status = - validate_shared_texture_ownership(descriptor->context); + validate_borrowed_texture_ownership(descriptor->context); if (ownership_status != MLN_STATUS_OK) { return ownership_status; } @@ -439,10 +467,6 @@ auto texture_read_premultiplied_rgba8( set_thread_error("out_info must not be null and must have a valid size"); return MLN_STATUS_INVALID_ARGUMENT; } - if (live->texture.acquired) { - set_thread_error("cannot read while a texture frame is acquired"); - return MLN_STATUS_INVALID_STATE; - } if (live->texture.mode != TextureSessionMode::Owned) { set_thread_error("texture session does not support CPU readback"); return MLN_STATUS_UNSUPPORTED; @@ -451,7 +475,7 @@ auto texture_read_premultiplied_rgba8( set_thread_error("render backend does not support CPU readback"); return MLN_STATUS_UNSUPPORTED; } - if (live->rendered_generation != live->generation) { + if (live->rendered_target_generation != live->generation) { set_thread_error("no rendered frame is available for this generation"); return MLN_STATUS_INVALID_STATE; } @@ -513,4 +537,117 @@ auto texture_read_premultiplied_rgba8( return MLN_STATUS_OK; } +struct TextureReadbackResult { + std::string bytes; + mln_texture_image_info info{}; +}; + +auto texture_read_premultiplied_rgba8_start( + mln_render_session texture, const mln_completion* completion +) -> mln_status { + return enqueue_driver_result_operation( + texture, + [](mln_render_session_object& target, std::any& result) { + auto readback = TextureReadbackResult{ + .bytes = {}, + .info = texture_image_info_default(), + }; + auto status = texture_read_premultiplied_rgba8( + target.self, nullptr, 0, &readback.info + ); + if (status != MLN_STATUS_OK) return status; + readback.bytes.resize(readback.info.byte_length); + status = texture_read_premultiplied_rgba8( + target.self, reinterpret_cast(readback.bytes.data()), + readback.bytes.size(), &readback.info + ); + if (status == MLN_STATUS_OK) result = std::move(readback); + return status; + }, + completion, + []( + const std::shared_ptr& state, mln_status status, + std::string diagnostic, std::any result + ) { + auto* readback = std::any_cast(&result); + if (status == MLN_STATUS_OK && readback != nullptr) { + state->resolve([bytes = std::move(readback->bytes), + info = + readback->info](const mln_completion& descriptor) { + const auto value = mln_texture_readback_result{ + .size = sizeof(mln_texture_readback_result), + .reserved = 0, + .data = {.data = bytes.data(), .size = bytes.size()}, + .info = info, + }; + invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, {}, + &value, 1 + ); + }); + } else { + complete( + state, status == MLN_STATUS_OK ? MLN_STATUS_NATIVE_ERROR : status, + status == MLN_STATUS_OK + ? "texture readback produced an invalid result" + : std::move(diagnostic) + ); + } + } + ); +} + +template +auto acquired_frame_get_backend(mln_acquired_frame handle, Frame* out_frame) + -> mln_status { + if (out_frame == nullptr || out_frame->size < sizeof(Frame)) { + set_thread_error("out_frame must not be null and must have a valid size"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto frame = handle_table().lease(handle); + if (frame == nullptr || !frame->valid.load()) { + return MLN_STATUS_INVALID_ARGUMENT; + } + { + const auto lock = std::scoped_lock{frame->session->control_mutex}; + if ( + frame->session->state == MLN_RENDER_SESSION_STATE_ABANDONED || + frame->session->state == MLN_RENDER_SESSION_STATE_TARGET_LOST + ) { + return MLN_STATUS_TARGET_LOST; + } + } + const auto* metadata = std::any_cast(&frame->backend_metadata); + if (metadata == nullptr) { + set_thread_error("acquired frame belongs to a different render backend"); + return MLN_STATUS_UNSUPPORTED; + } + *out_frame = *metadata; + return MLN_STATUS_OK; +} + +auto acquired_frame_get_metal_texture( + mln_acquired_frame frame, mln_metal_owned_texture_frame* out_frame +) -> mln_status { + return acquired_frame_get_backend(frame, out_frame); +} + +auto acquired_frame_get_vulkan_texture( + mln_acquired_frame frame, mln_vulkan_owned_texture_frame* out_frame +) -> mln_status { + return acquired_frame_get_backend(frame, out_frame); +} + +auto acquired_frame_get_opengl_texture( + mln_acquired_frame frame, mln_opengl_owned_texture_frame* out_frame +) -> mln_status { + return acquired_frame_get_backend(frame, out_frame); +} + +auto acquired_frame_get_webgpu_texture( + mln_acquired_frame frame, mln_webgpu_owned_texture_frame* out_frame +) -> mln_status { + return acquired_frame_get_backend(frame, out_frame); +} + } // namespace mln::core diff --git a/src/render/texture_session.hpp b/src/render/texture_session.hpp index 6c15bcb62..fdf05b370 100644 --- a/src/render/texture_session.hpp +++ b/src/render/texture_session.hpp @@ -63,81 +63,80 @@ auto webgpu_owned_texture_descriptor_default() noexcept -> mln_webgpu_owned_texture_descriptor; auto webgpu_borrowed_texture_descriptor_default() noexcept -> mln_webgpu_borrowed_texture_descriptor; -auto webgpu_owned_texture_attach( +auto webgpu_owned_texture_attach_start( mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto webgpu_borrowed_texture_attach( +auto webgpu_borrowed_texture_attach_start( mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto webgpu_borrowed_texture_set_target( +auto webgpu_borrowed_texture_set_target_start( mln_render_session session, - const mln_webgpu_borrowed_texture_descriptor* descriptor -) -> mln_status; -auto webgpu_owned_texture_acquire_frame( - mln_render_session texture, mln_webgpu_owned_texture_frame* out_frame + const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto webgpu_owned_texture_release_frame( - mln_render_session texture, const mln_webgpu_owned_texture_frame* frame -) -> mln_status; -auto metal_owned_texture_attach( +auto metal_owned_texture_attach_start( mln_map map, const mln_metal_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto metal_borrowed_texture_attach( +auto metal_borrowed_texture_attach_start( mln_map map, const mln_metal_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto vulkan_owned_texture_attach( +auto vulkan_owned_texture_attach_start( mln_map map, const mln_vulkan_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto vulkan_borrowed_texture_attach( +auto vulkan_borrowed_texture_attach_start( mln_map map, const mln_vulkan_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto opengl_owned_texture_attach( +auto opengl_owned_texture_attach_start( mln_map map, const mln_opengl_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto opengl_borrowed_texture_attach( +auto opengl_borrowed_texture_attach_start( mln_map map, const mln_opengl_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status; -auto metal_borrowed_texture_set_target( +auto metal_borrowed_texture_set_target_start( mln_render_session session, - const mln_metal_borrowed_texture_descriptor* descriptor + const mln_metal_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto vulkan_borrowed_texture_set_target( +auto vulkan_borrowed_texture_set_target_start( mln_render_session session, - const mln_vulkan_borrowed_texture_descriptor* descriptor + const mln_vulkan_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto opengl_borrowed_texture_set_target( +auto opengl_borrowed_texture_set_target_start( mln_render_session session, - const mln_opengl_borrowed_texture_descriptor* descriptor -) -> mln_status; -auto texture_read_premultiplied_rgba8( - mln_render_session texture, uint8_t* out_data, size_t out_data_capacity, - mln_texture_image_info* out_info -) -> mln_status; -auto metal_owned_texture_acquire_frame( - mln_render_session texture, mln_metal_owned_texture_frame* out_frame + const mln_opengl_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status; -auto metal_owned_texture_release_frame( - mln_render_session texture, const mln_metal_owned_texture_frame* frame +auto texture_read_premultiplied_rgba8_start( + mln_render_session texture, const mln_completion* completion ) -> mln_status; -auto vulkan_owned_texture_acquire_frame( - mln_render_session texture, mln_vulkan_owned_texture_frame* out_frame +auto acquired_frame_get_metal_texture( + mln_acquired_frame frame, mln_metal_owned_texture_frame* out_frame ) -> mln_status; -auto vulkan_owned_texture_release_frame( - mln_render_session texture, const mln_vulkan_owned_texture_frame* frame +auto acquired_frame_get_vulkan_texture( + mln_acquired_frame frame, mln_vulkan_owned_texture_frame* out_frame ) -> mln_status; -auto opengl_owned_texture_acquire_frame( - mln_render_session texture, mln_opengl_owned_texture_frame* out_frame +auto acquired_frame_get_opengl_texture( + mln_acquired_frame frame, mln_opengl_owned_texture_frame* out_frame ) -> mln_status; -auto opengl_owned_texture_release_frame( - mln_render_session texture, const mln_opengl_owned_texture_frame* frame +auto acquired_frame_get_webgpu_texture( + mln_acquired_frame frame, mln_webgpu_owned_texture_frame* out_frame ) -> mln_status; } // namespace mln::core diff --git a/src/render/unsupported_sessions.cpp b/src/render/unsupported_sessions.cpp index b59a9e220..5c450409b 100644 --- a/src/render/unsupported_sessions.cpp +++ b/src/render/unsupported_sessions.cpp @@ -1,11 +1,3 @@ -// The surface and texture session entry points for the backends this build does -// not carry. -// -// The validation order is part of what a host sees, so it matches the -// backend-native attach paths step for step: the map, the descriptor, the -// output handle, then the scaled extent. Only once all of those pass does the -// caller hear that the backend is missing. - #include "diagnostics/diagnostics.hpp" #include "map/map.hpp" #include "maplibre_native_c.h" @@ -16,635 +8,436 @@ namespace mln::core { namespace { -auto validate_attach_map(mln_map map) -> mln_status { +auto validate_unsupported_attach( + mln_map map, const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion +) -> mln_status { MapObject* live_map = nullptr; - return validate_map_live(map, live_map); -} - -auto validate_attach_out_session(mln_render_session* out_session) - -> mln_status { - return validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); -} - -auto validate_surface_extent(const mln_render_target_extent& extent) - -> mln_status { - return validate_physical_size( - extent.width, extent.height, extent.scale_factor, - "scaled surface dimensions are too large" - ); -} - -auto validate_owned_texture_extent(const mln_render_target_extent& extent) - -> mln_status { - return validate_physical_size( - extent.width, extent.height, extent.scale_factor, - "scaled texture dimensions are too large" + const auto map_status = validate_map_live(map, live_map); + if (map_status != MLN_STATUS_OK) { + return map_status; + } + return validate_render_session_attach_request( + options, out_session, completion ); } -// A frame belongs to a session rather than a descriptor, so this checks the -// session handle and the frame's own size. -template -auto validate_texture_frame( - mln_render_session texture, const Frame* frame, const char* message -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (frame == nullptr || frame->size < sizeof(Frame)) { - set_thread_error(message); - return MLN_STATUS_INVALID_ARGUMENT; - } - return MLN_STATUS_OK; +auto unsupported(const char* message) -> mln_status { + set_thread_error(message); + return MLN_STATUS_UNSUPPORTED; } } // namespace #if !defined(MLN_RENDER_BACKEND_METAL) - -auto metal_surface_attach( +auto metal_surface_attach_start( mln_map map, const mln_metal_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = validate_metal_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_surface_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("Metal surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_metal_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled surface dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Metal surface sessions are not supported by this build"); } - -auto metal_owned_texture_attach( +auto metal_owned_texture_attach_start( mln_map map, const mln_metal_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_metal_owned_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_owned_texture_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("Metal texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_metal_owned_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled texture dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Metal texture sessions are not supported by this build"); } - -auto metal_borrowed_texture_attach( +auto metal_borrowed_texture_attach_start( mln_map map, const mln_metal_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_metal_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto physical_status = validate_borrowed_physical_size( - descriptor->physical_width, descriptor->physical_height - ); - if (physical_status != MLN_STATUS_OK) { - return physical_status; - } - set_thread_error("Metal texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto metal_owned_texture_acquire_frame( - mln_render_session texture, mln_metal_owned_texture_frame* out_frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, out_frame, "out_frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_metal_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("Metal texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto metal_owned_texture_release_frame( - mln_render_session texture, const mln_metal_owned_texture_frame* frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, frame, "frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_borrowed_physical_size( + descriptor->physical_width, descriptor->physical_height + ); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("Metal texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Metal texture sessions are not supported by this build"); } - -auto metal_borrowed_texture_set_target( - mln_render_session session, - const mln_metal_borrowed_texture_descriptor* descriptor +auto metal_borrowed_texture_set_target_start( + mln_render_session, const mln_metal_borrowed_texture_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = - validate_metal_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("Metal texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_metal_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Metal texture sessions are not supported by this build"); } - -auto metal_surface_set_target( - mln_render_session session, const mln_metal_surface_descriptor* descriptor +auto metal_surface_set_target_start( + mln_render_session, const mln_metal_surface_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = validate_metal_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("Metal surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_metal_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Metal surface sessions are not supported by this build"); } - #endif #if !defined(MLN_RENDER_BACKEND_VULKAN) - -auto vulkan_surface_attach( +auto vulkan_surface_attach_start( mln_map map, const mln_vulkan_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = validate_vulkan_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_surface_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("Vulkan surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_vulkan_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled surface dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Vulkan surface sessions are not supported by this build"); } - -auto vulkan_owned_texture_attach( +auto vulkan_owned_texture_attach_start( mln_map map, const mln_vulkan_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_vulkan_owned_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_owned_texture_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("Vulkan texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_vulkan_owned_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled texture dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Vulkan texture sessions are not supported by this build"); } - -auto vulkan_borrowed_texture_attach( +auto vulkan_borrowed_texture_attach_start( mln_map map, const mln_vulkan_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session -) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_vulkan_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto physical_status = validate_borrowed_physical_size( - descriptor->physical_width, descriptor->physical_height - ); - if (physical_status != MLN_STATUS_OK) { - return physical_status; - } - set_thread_error("Vulkan texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto vulkan_owned_texture_acquire_frame( - mln_render_session texture, mln_vulkan_owned_texture_frame* out_frame + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto status = validate_texture_frame( - texture, out_frame, "out_frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_vulkan_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("Vulkan texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto vulkan_owned_texture_release_frame( - mln_render_session texture, const mln_vulkan_owned_texture_frame* frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, frame, "frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_borrowed_physical_size( + descriptor->physical_width, descriptor->physical_height + ); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("Vulkan texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Vulkan texture sessions are not supported by this build"); } - -auto vulkan_borrowed_texture_set_target( - mln_render_session session, - const mln_vulkan_borrowed_texture_descriptor* descriptor +auto vulkan_borrowed_texture_set_target_start( + mln_render_session, const mln_vulkan_borrowed_texture_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = - validate_vulkan_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("Vulkan texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_vulkan_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Vulkan texture sessions are not supported by this build"); } - -auto vulkan_surface_set_target( - mln_render_session session, const mln_vulkan_surface_descriptor* descriptor +auto vulkan_surface_set_target_start( + mln_render_session, const mln_vulkan_surface_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = validate_vulkan_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("Vulkan surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_vulkan_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("Vulkan surface sessions are not supported by this build"); } - #endif #if !defined(MLN_RENDER_BACKEND_OPENGL) - -auto opengl_surface_attach( +auto opengl_surface_attach_start( mln_map map, const mln_opengl_surface_descriptor* descriptor, - mln_render_session* out_session -) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_opengl_surface_descriptor(descriptor, false); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_surface_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("OpenGL surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto opengl_surface_set_target( - mln_render_session session, const mln_opengl_surface_descriptor* descriptor + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = - validate_opengl_surface_descriptor(descriptor, false); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("OpenGL surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_opengl_surface_descriptor(descriptor, false); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled surface dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("OpenGL surface sessions are not supported by this build"); } - -#endif - -#if !defined(MLN_RENDER_BACKEND_OPENGL) - -auto opengl_owned_texture_attach( +auto opengl_owned_texture_attach_start( mln_map map, const mln_opengl_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_opengl_owned_texture_descriptor(descriptor, false); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_owned_texture_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("OpenGL texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = + validate_opengl_owned_texture_descriptor(descriptor, false); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled texture dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("OpenGL texture sessions are not supported by this build"); } - -auto opengl_borrowed_texture_attach( +auto opengl_borrowed_texture_attach_start( mln_map map, const mln_opengl_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_opengl_borrowed_texture_descriptor(descriptor, false); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto physical_status = validate_borrowed_physical_size( - descriptor->physical_width, descriptor->physical_height - ); - if (physical_status != MLN_STATUS_OK) { - return physical_status; - } - set_thread_error("OpenGL texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto opengl_owned_texture_acquire_frame( - mln_render_session texture, mln_opengl_owned_texture_frame* out_frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, out_frame, "out_frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = + validate_opengl_borrowed_texture_descriptor(descriptor, false); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("OpenGL texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_borrowed_physical_size( + descriptor->physical_width, descriptor->physical_height + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("OpenGL texture sessions are not supported by this build"); } - -auto opengl_owned_texture_release_frame( - mln_render_session texture, const mln_opengl_owned_texture_frame* frame +auto opengl_surface_set_target_start( + mln_render_session, const mln_opengl_surface_descriptor* descriptor, + const mln_completion* ) -> mln_status { - const auto status = validate_texture_frame( - texture, frame, "frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_opengl_surface_descriptor(descriptor, false); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("OpenGL texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + return unsupported("OpenGL surface sessions are not supported by this build"); } - -auto opengl_borrowed_texture_set_target( - mln_render_session session, - const mln_opengl_borrowed_texture_descriptor* descriptor +auto opengl_borrowed_texture_set_target_start( + mln_render_session, const mln_opengl_borrowed_texture_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = - validate_opengl_borrowed_texture_descriptor(descriptor, false); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("OpenGL texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = + validate_opengl_borrowed_texture_descriptor(descriptor, false); + status != MLN_STATUS_OK + ) + return status; + return unsupported("OpenGL texture sessions are not supported by this build"); } - #endif #if !defined(MLN_RENDER_BACKEND_WEBGPU) - -auto webgpu_owned_texture_attach( +auto webgpu_owned_texture_attach_start( mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_webgpu_owned_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_owned_texture_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("WebGPU texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_webgpu_owned_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled texture dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("WebGPU texture sessions are not supported by this build"); } - -auto webgpu_borrowed_texture_attach( +auto webgpu_borrowed_texture_attach_start( mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = - validate_webgpu_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto physical_status = validate_borrowed_physical_size( - descriptor->physical_width, descriptor->physical_height - ); - if (physical_status != MLN_STATUS_OK) { - return physical_status; - } - set_thread_error("WebGPU texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto webgpu_owned_texture_acquire_frame( - mln_render_session texture, mln_webgpu_owned_texture_frame* out_frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, out_frame, "out_frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_webgpu_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("WebGPU texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; -} - -auto webgpu_owned_texture_release_frame( - mln_render_session texture, const mln_webgpu_owned_texture_frame* frame -) -> mln_status { - const auto status = validate_texture_frame( - texture, frame, "frame must not be null and must have a valid size" - ); - if (status != MLN_STATUS_OK) { + if ( + const auto status = validate_borrowed_physical_size( + descriptor->physical_width, descriptor->physical_height + ); + status != MLN_STATUS_OK + ) return status; - } - set_thread_error("WebGPU texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("WebGPU texture sessions are not supported by this build"); } - -auto webgpu_borrowed_texture_set_target( - mln_render_session session, - const mln_webgpu_borrowed_texture_descriptor* descriptor +auto webgpu_borrowed_texture_set_target_start( + mln_render_session, const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = - validate_webgpu_borrowed_texture_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("WebGPU texture sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_webgpu_borrowed_texture_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("WebGPU texture sessions are not supported by this build"); } - -auto webgpu_surface_attach( +auto webgpu_surface_attach_start( mln_map map, const mln_webgpu_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { - const auto map_status = validate_attach_map(map); - if (map_status != MLN_STATUS_OK) { - return map_status; - } - const auto descriptor_status = validate_webgpu_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - const auto output_status = validate_attach_out_session(out_session); - if (output_status != MLN_STATUS_OK) { - return output_status; - } - const auto extent_status = validate_surface_extent(descriptor->extent); - if (extent_status != MLN_STATUS_OK) { - return extent_status; - } - set_thread_error("WebGPU surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_webgpu_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = validate_physical_size( + descriptor->extent.width, descriptor->extent.height, + descriptor->extent.scale_factor, "scaled surface dimensions are too large" + ); + status != MLN_STATUS_OK + ) + return status; + if ( + const auto status = + validate_unsupported_attach(map, options, out_session, completion); + status != MLN_STATUS_OK + ) + return status; + return unsupported("WebGPU surface sessions are not supported by this build"); } - -auto webgpu_surface_set_target( - mln_render_session session, const mln_webgpu_surface_descriptor* descriptor +auto webgpu_surface_set_target_start( + mln_render_session, const mln_webgpu_surface_descriptor* descriptor, + const mln_completion* ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } - const auto descriptor_status = validate_webgpu_surface_descriptor(descriptor); - if (descriptor_status != MLN_STATUS_OK) { - return descriptor_status; - } - set_thread_error("WebGPU surface sessions are not supported by this build"); - return MLN_STATUS_UNSUPPORTED; + if ( + const auto status = validate_webgpu_surface_descriptor(descriptor); + status != MLN_STATUS_OK + ) + return status; + return unsupported("WebGPU surface sessions are not supported by this build"); } - #endif } // namespace mln::core diff --git a/src/render/vulkan/vulkan_surface_session.cpp b/src/render/vulkan/vulkan_surface_session.cpp index 68d94edd3..2e076b7a0 100644 --- a/src/render/vulkan/vulkan_surface_session.cpp +++ b/src/render/vulkan/vulkan_surface_session.cpp @@ -540,9 +540,10 @@ class VulkanSurfaceSessionBackend final namespace mln::core { -auto vulkan_surface_attach( +auto vulkan_surface_attach_start( mln_map map, const mln_vulkan_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -553,13 +554,6 @@ auto vulkan_surface_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled surface dimensions are too large" @@ -567,46 +561,56 @@ auto vulkan_surface_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } - const auto vulkan_status = validate_vulkan_handles(*descriptor); - if (vulkan_status != MLN_STATUS_OK) { - return vulkan_status; - } auto session = std::make_shared(); session->map = map; set_session_extent(*session, descriptor->extent); - session->surface.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Surface, - RenderSessionAttachMessages{ - .null_session = "surface session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" + const auto copied = *descriptor; + session->initialize_backend = [copied](mln_render_session_object& target) { + const auto handles_status = validate_vulkan_handles(copied); + if (handles_status != MLN_STATUS_OK) { + return handles_status; } + target.surface.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_PRESENTATION + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Surface, options, capabilities, + out_session, completion ); } -auto vulkan_surface_set_target( - mln_render_session session, const mln_vulkan_surface_descriptor* descriptor +auto vulkan_surface_set_target_start( + mln_render_session session, const mln_vulkan_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_vulkan_surface_descriptor(descriptor); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - return surface_session_set_target( - session, descriptor->extent, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.surface.backend->set_vulkan_target(*descriptor); - } + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + const auto handles_status = validate_vulkan_handles(copied); + if (handles_status != MLN_STATUS_OK) { + return handles_status; + } + return surface_session_set_target( + target.self, copied.extent, [&copied](mln_render_session_object& live) { + return live.surface.backend->set_vulkan_target(copied); + } + ); + }, + completion ); } diff --git a/src/render/vulkan/vulkan_texture_backend.cpp b/src/render/vulkan/vulkan_texture_backend.cpp index 746f5bb62..19ace0e70 100644 --- a/src/render/vulkan/vulkan_texture_backend.cpp +++ b/src/render/vulkan/vulkan_texture_backend.cpp @@ -383,11 +383,14 @@ class VulkanTextureBackend::VulkanTextureRenderableResource final }; VulkanTextureBackend::VulkanTextureBackend( - const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size + const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) : mln::vulkan::RendererBackend(mln::gfx::ContextMode::Unique), mln::gfx::HeadlessBackend(size), - descriptor_(descriptor) { + descriptor_(descriptor), + slot_resources_(ring_depth), + slot_sizes_(ring_depth) { initSharedDevice(); } @@ -398,13 +401,16 @@ VulkanTextureBackend::VulkanTextureBackend( mln::gfx::HeadlessBackend(size), descriptor_(owned_descriptor_from_borrowed(descriptor)), borrowed_descriptor_(descriptor), - uses_borrowed_texture_(true) { + uses_borrowed_texture_(true), + slot_resources_(1), + slot_sizes_(1) { initSharedDevice(); } VulkanTextureBackend::~VulkanTextureBackend() { auto guard = mln::gfx::BackendScope{*this}; resource.reset(); + slot_resources_.clear(); getThreadPool().runRenderJobs(true); } @@ -424,6 +430,7 @@ void VulkanTextureBackend::initSharedDevice() { auto VulkanTextureBackend::getDefaultRenderable() -> mln::gfx::Renderable& { if (!resource) { resource = std::make_unique(*this); + slot_sizes_[selected_slot_] = size; } return *this; } @@ -445,15 +452,11 @@ void VulkanTextureBackend::set_borrowed_target( ) { const auto new_size = mln::Size{descriptor.physical_width, descriptor.physical_height}; - // Nothing is built yet, so the lazy path already takes the new image. if (!resource) { borrowed_descriptor_ = descriptor; setSize(new_size); return; } - // The resource first, then this backend's own view of the target, so a - // framebuffer that fails to build cannot leave the backend naming an image it - // does not render into. getResource().set_borrowed( descriptor, new_size.width, new_size.height ); @@ -462,17 +465,9 @@ void VulkanTextureBackend::set_borrowed_target( } void VulkanTextureBackend::resize(mln::Size new_size) { - // Nothing is built yet, so the lazy path already produces the new size. A - // borrowed texture is sized by its owner and never reaches here; the session - // rejects resizing one. - if (!resource || uses_borrowed_texture_) { - setSize(new_size); - return; - } + // Slots keep their old resources until the common ring selects a released + // slot for rendering at the new extent. size = new_size; - getResource().resize_sampled( - vk::Extent2D{new_size.width, new_size.height} - ); } auto VulkanTextureBackend::readStillImage() -> mln::PremultipliedImage { @@ -593,16 +588,47 @@ void VulkanTextureBackend::prepareRenderResources() { } } -auto VulkanTextureBackend::frame_resources() -> VulkanTextureFrameResources { - auto& rendered = getResource(); +auto VulkanTextureBackend::frame_resources(std::size_t slot) + -> VulkanTextureFrameResources { + if (slot >= slot_resources_.size()) { + return {}; + } + if (slot == selected_slot_) { + prepareRenderResources(); + } else if (slot_resources_[slot] == nullptr) { + const auto previous = selected_slot_; + if (!select_slot(slot)) return {}; + prepareRenderResources(); + static_cast(select_slot(previous)); + } + auto* rendered = slot == selected_slot_ + ? &getResource() + : static_cast( + slot_resources_[slot].get() + ); return VulkanTextureFrameResources{ - .image = rendered.image(), - .image_view = rendered.image_view(), + .image = rendered->image(), + .image_view = rendered->image_view(), .device = device.get(), - .format = rendered.format(), + .format = rendered->format(), }; } +auto VulkanTextureBackend::select_slot(std::size_t slot) -> bool { + if (slot >= slot_resources_.size()) return false; + if (slot == selected_slot_) { + if (resource != nullptr && slot_sizes_[slot] != size) resource.reset(); + return true; + } + slot_resources_[selected_slot_] = std::move(resource); + if (slot_resources_[slot] != nullptr && slot_sizes_[slot] != size) { + slot_resources_[slot].reset(); + } + resource = std::move(slot_resources_[slot]); + selected_slot_ = slot; + return true; +} + void VulkanTextureBackend::initInstance() { usingSharedContext = true; instance = vk::UniqueInstance( diff --git a/src/render/vulkan/vulkan_texture_backend.hpp b/src/render/vulkan/vulkan_texture_backend.hpp index f3bdf4a8e..1842b1749 100644 --- a/src/render/vulkan/vulkan_texture_backend.hpp +++ b/src/render/vulkan/vulkan_texture_backend.hpp @@ -29,7 +29,8 @@ class VulkanTextureBackend final : public mln::vulkan::RendererBackend, public: VulkanTextureBackend( - const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size + const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth = 1 ); VulkanTextureBackend( const mln_vulkan_borrowed_texture_descriptor& descriptor, mln::Size size @@ -63,7 +64,8 @@ class VulkanTextureBackend final : public mln::vulkan::RendererBackend, void deactivate() override; void prepareRenderResources(); - auto frame_resources() -> VulkanTextureFrameResources; + auto frame_resources(std::size_t slot) -> VulkanTextureFrameResources; + auto select_slot(std::size_t slot) -> bool; protected: void initInstance() override; @@ -78,6 +80,9 @@ class VulkanTextureBackend final : public mln::vulkan::RendererBackend, mln_vulkan_owned_texture_descriptor descriptor_; mln_vulkan_borrowed_texture_descriptor borrowed_descriptor_{}; bool uses_borrowed_texture_ = false; + std::vector> slot_resources_; + std::size_t selected_slot_ = 0; + std::vector slot_sizes_; }; } // namespace mln::core diff --git a/src/render/vulkan/vulkan_texture_session.cpp b/src/render/vulkan/vulkan_texture_session.cpp index ff60ad022..921ad6666 100644 --- a/src/render/vulkan/vulkan_texture_session.cpp +++ b/src/render/vulkan/vulkan_texture_session.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -111,9 +112,10 @@ class VulkanTextureSessionBackend final : public mln::core::TextureSessionBackend { public: VulkanTextureSessionBackend( - const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size + const mln_vulkan_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) - : backend_(descriptor, size) {} + : backend_(descriptor, size, ring_depth) {} VulkanTextureSessionBackend( const mln_vulkan_borrowed_texture_descriptor& descriptor, mln::Size size @@ -155,18 +157,23 @@ class VulkanTextureSessionBackend final backend_.prepareRenderResources(); } - auto acquire_vulkan_owned_frame( - const mln_render_session_object& texture, - mln_vulkan_owned_texture_frame& out_frame + auto select_render_slot(std::size_t slot) -> mln_status override { + return backend_.select_slot(slot) ? MLN_STATUS_OK + : MLN_STATUS_INVALID_ARGUMENT; + } + + auto copy_slot_metadata( + const mln_render_session_object& texture, std::size_t slot, + std::any& out_metadata ) -> mln_status override { - const auto resources = backend_.frame_resources(); - out_frame = mln_vulkan_owned_texture_frame{ + const auto resources = backend_.frame_resources(slot); + out_metadata = mln_vulkan_owned_texture_frame{ .size = sizeof(mln_vulkan_owned_texture_frame), .generation = texture.generation, .width = texture.physical_width, .height = texture.physical_height, .scale_factor = texture.scale_factor, - .frame_id = texture.texture.next_frame_id, + .frame_id = texture.frame_generation, .image = resources.image, .image_view = resources.image_view, .device = resources.device, @@ -188,9 +195,10 @@ auto supported_render_backend_mask() noexcept -> uint32_t { return MLN_RENDER_BACKEND_FLAG_VULKAN; } -auto vulkan_owned_texture_attach( +auto vulkan_owned_texture_attach_start( mln_map map, const mln_vulkan_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -202,13 +210,6 @@ auto vulkan_owned_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, descriptor->extent.scale_factor, "scaled texture dimensions are too large" @@ -216,32 +217,46 @@ auto vulkan_owned_texture_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } - const auto vulkan_status = validate_vulkan_handles(*descriptor); - if (vulkan_status != MLN_STATUS_OK) { - return vulkan_status; - } auto session = std::make_shared(); session->map = map; set_session_extent(*session, descriptor->extent); session->texture.api_kind = TextureSessionApi::Vulkan; session->texture.mode = TextureSessionMode::Owned; - session->texture.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} + const auto copied = *descriptor; + const auto ring_depth = std::clamp( + options == nullptr ? 1u : options->requested_texture_ring_depth, 1u, 3u ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" - } + session->initialize_backend = + [copied, ring_depth](mln_render_session_object& target) { + const auto handles_status = validate_vulkan_handles(copied); + if (handles_status != MLN_STATUS_OK) { + return handles_status; + } + target.texture.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height}, + ring_depth + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = ring_depth, + .flags = MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION | + MLN_RENDER_SESSION_CAPABILITY_READBACK | + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto vulkan_borrowed_texture_attach( +auto vulkan_borrowed_texture_attach_start( mln_map map, const mln_vulkan_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { MapObject* live_map = nullptr; const auto map_status = validate_map_live(map, live_map); @@ -253,28 +268,12 @@ auto vulkan_borrowed_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; - } const auto physical_status = validate_borrowed_physical_size( descriptor->physical_width, descriptor->physical_height ); if (physical_status != MLN_STATUS_OK) { return physical_status; } - auto handle_descriptor = mln_vulkan_owned_texture_descriptor{ - .size = sizeof(mln_vulkan_owned_texture_descriptor), - .extent = descriptor->extent, - .context = descriptor->context, - }; - const auto vulkan_status = validate_vulkan_handles(handle_descriptor); - if (vulkan_status != MLN_STATUS_OK) { - return vulkan_status; - } auto session = std::make_shared(); session->map = map; @@ -284,108 +283,39 @@ auto vulkan_borrowed_texture_attach( ); session->texture.api_kind = TextureSessionApi::Vulkan; session->texture.mode = TextureSessionMode::Borrowed; - session->texture.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle" + const auto copied = *descriptor; + session->initialize_backend = [copied](mln_render_session_object& target) { + const auto handles = mln_vulkan_owned_texture_descriptor{ + .size = sizeof(mln_vulkan_owned_texture_descriptor), + .extent = copied.extent, + .context = copied.context, + }; + const auto handles_status = validate_vulkan_handles(handles); + if (handles_status != MLN_STATUS_OK) { + return handles_status; } + target.texture.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height} + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = options == nullptr ? 0u : options->driver, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_READBACK + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion ); } -auto vulkan_owned_texture_acquire_frame( - mln_render_session texture, mln_vulkan_owned_texture_frame* out_frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_live_attached_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_frame == nullptr || - out_frame->size < sizeof(mln_vulkan_owned_texture_frame) - ) { - set_thread_error("out_frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->texture.acquired) { - set_thread_error("a texture frame is already acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (live->rendered_generation != live->generation) { - set_thread_error("no rendered frame is available for this generation"); - return MLN_STATUS_INVALID_STATE; - } - if ( - live->texture.mode != TextureSessionMode::Owned || - live->texture.api_kind != TextureSessionApi::Vulkan - ) { - set_thread_error("texture session cannot expose a Vulkan texture frame"); - return MLN_STATUS_UNSUPPORTED; - } - - const auto acquire_status = - live->texture.backend->acquire_vulkan_owned_frame(*live, *out_frame); - if (acquire_status != MLN_STATUS_OK) { - return acquire_status; - } - live->texture.acquired = true; - live->texture.acquired_frame_id = out_frame->frame_id; - live->texture.acquired_frame_kind = TextureSessionFrameKind::VulkanOwned; - ++live->texture.next_frame_id; - return MLN_STATUS_OK; -} - -auto vulkan_owned_texture_release_frame( - mln_render_session texture, const mln_vulkan_owned_texture_frame* frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - frame == nullptr || frame->size < sizeof(mln_vulkan_owned_texture_frame) - ) { - set_thread_error("frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - !live->texture.acquired || - live->texture.acquired_frame_kind != TextureSessionFrameKind::VulkanOwned - ) { - set_thread_error("no texture frame is currently acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (frame->generation != live->generation) { - set_thread_error("frame generation does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (frame->frame_id != live->texture.acquired_frame_id) { - set_thread_error("frame identity does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - live->texture.acquired = false; - live->texture.acquired_frame_id = 0; - live->texture.acquired_frame_kind = TextureSessionFrameKind::None; - return MLN_STATUS_OK; -} - -auto vulkan_borrowed_texture_set_target( +auto vulkan_borrowed_texture_set_target_start( mln_render_session session, - const mln_vulkan_borrowed_texture_descriptor* descriptor + const mln_vulkan_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_vulkan_borrowed_texture_descriptor(descriptor); if (descriptor_status != MLN_STATUS_OK) { @@ -397,14 +327,19 @@ auto vulkan_borrowed_texture_set_target( if (physical_status != MLN_STATUS_OK) { return physical_status; } - return render_session_set_target( - session, RetargetTargetKind::BorrowedTexture, descriptor->extent, - descriptor->physical_width, descriptor->physical_height, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.texture.backend->set_vulkan_borrowed_target( - *descriptor + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + return render_session_set_target( + target.self, RetargetTargetKind::BorrowedTexture, copied.extent, + copied.physical_width, copied.physical_height, + [&copied](mln_render_session_object& live) { + return live.texture.backend->set_vulkan_borrowed_target(copied); + } ); - } + }, + completion ); } diff --git a/src/render/webgpu/webgpu_texture_session.cpp b/src/render/webgpu/webgpu_texture_session.cpp index b6f4b1362..c6aaf080d 100644 --- a/src/render/webgpu/webgpu_texture_session.cpp +++ b/src/render/webgpu/webgpu_texture_session.cpp @@ -1,3 +1,4 @@ +#include // clang-format off #include #include @@ -130,12 +131,14 @@ class WebGPUTextureBackend final : public mln::webgpu::RendererBackend, }; WebGPUTextureBackend( - const mln_webgpu_owned_texture_descriptor& descriptor, mln::Size size + const mln_webgpu_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) : mln::webgpu::RendererBackend(mln::gfx::ContextMode::Unique), mln::gfx::HeadlessBackend(size), owns_color_texture_(true), - color_format_(webgpu_owned_color_format) { + color_format_(webgpu_owned_color_format), + slots_(ring_depth) { try { initializeContext(descriptor.context); setColorFormat(static_cast(color_format_)); @@ -242,6 +245,39 @@ class WebGPUTextureBackend final : public mln::webgpu::RendererBackend, return static_cast(color_format_); } + auto select_slot(std::size_t slot) -> bool { + if (slot >= slots_.size()) return false; + if (slot == selected_slot_) { + if (texture_ != nullptr && color_texture_size_ != getSize()) { + releaseColorTexture(); + } + return true; + } + slots_[selected_slot_] = + ColorSlot{texture_, color_view_, color_texture_size_}; + texture_ = nullptr; + color_view_ = nullptr; + color_texture_size_ = {}; + releaseDepthStencilTexture(); + selected_slot_ = slot; + texture_ = slots_[slot].texture; + color_view_ = slots_[slot].view; + color_texture_size_ = slots_[slot].size; + slots_[slot] = {}; + if (texture_ != nullptr && color_texture_size_ != getSize()) { + releaseColorTexture(); + } + return true; + } + + auto rendered_texture_at(std::size_t slot) const -> void* { + return slot == selected_slot_ ? texture_ : slots_[slot].texture; + } + auto rendered_texture_view_at(std::size_t slot) const -> void* { + return slot == selected_slot_ ? color_view_ : slots_[slot].view; + } + void set_ring_size(mln::Size new_size) { size = new_size; } + // Whether a replacement target names the context this session attached with. // A null queue names the device's default queue, as it does at attach. The // instance is excluded: a texture session never uses it. @@ -547,6 +583,14 @@ class WebGPUTextureBackend final : public mln::webgpu::RendererBackend, } void shutdown() { + for (auto& slot : slots_) { + if (slot.view != nullptr) wgpuTextureViewRelease(slot.view); + if (slot.texture != nullptr) { + wgpuTextureDestroy(slot.texture); + wgpuTextureRelease(slot.texture); + } + } + slots_.clear(); releaseDepthStencilTexture(); releaseColorTexture(); if (queue_ != nullptr) { @@ -563,6 +607,11 @@ class WebGPUTextureBackend final : public mln::webgpu::RendererBackend, } } + struct ColorSlot { + WGPUTexture texture = nullptr; + WGPUTextureView view = nullptr; + mln::Size size{}; + }; bool owns_color_texture_ = false; WGPUTexture texture_ = nullptr; WGPUTextureView color_view_ = nullptr; @@ -574,6 +623,8 @@ class WebGPUTextureBackend final : public mln::webgpu::RendererBackend, WGPUTexture depth_stencil_texture_ = nullptr; WGPUTextureView depth_stencil_view_ = nullptr; mln::Size depth_stencil_size_{0, 0}; + std::vector slots_; + std::size_t selected_slot_ = 0; }; // Renders into a surface the host presents, which in a browser is a canvas. A @@ -985,9 +1036,10 @@ class WebGPUTextureSessionBackend final : public mln::core::TextureSessionBackend { public: WebGPUTextureSessionBackend( - const mln_webgpu_owned_texture_descriptor& descriptor, mln::Size size + const mln_webgpu_owned_texture_descriptor& descriptor, mln::Size size, + std::size_t ring_depth ) - : backend_(descriptor, size) {} + : backend_(descriptor, size, ring_depth) {} WebGPUTextureSessionBackend( const mln_webgpu_borrowed_texture_descriptor& descriptor, mln::Size size @@ -997,6 +1049,7 @@ class WebGPUTextureSessionBackend final auto headless_backend() -> mln::gfx::HeadlessBackend& override { return backend_; } + void resize(mln::Size size) override { backend_.set_ring_size(size); } auto set_webgpu_borrowed_target( const mln_webgpu_borrowed_texture_descriptor& descriptor @@ -1026,19 +1079,24 @@ class WebGPUTextureSessionBackend final return MLN_STATUS_OK; } - auto acquire_webgpu_owned_frame( - const mln_render_session_object& texture, - mln_webgpu_owned_texture_frame& out_frame + auto select_render_slot(std::size_t slot) -> mln_status override { + return backend_.select_slot(slot) ? MLN_STATUS_OK + : MLN_STATUS_INVALID_ARGUMENT; + } + + auto copy_slot_metadata( + const mln_render_session_object& texture, std::size_t slot, + std::any& out_metadata ) -> mln_status override { - out_frame = mln_webgpu_owned_texture_frame{ + out_metadata = mln_webgpu_owned_texture_frame{ .size = sizeof(mln_webgpu_owned_texture_frame), .generation = texture.generation, .width = texture.physical_width, .height = texture.physical_height, .scale_factor = texture.scale_factor, - .frame_id = texture.texture.next_frame_id, - .texture = backend_.rendered_texture(), - .texture_view = backend_.rendered_texture_view(), + .frame_id = texture.frame_generation, + .texture = backend_.rendered_texture_at(slot), + .texture_view = backend_.rendered_texture_view_at(slot), .device = backend_.device(), .format = static_cast(backend_.color_format()), }; @@ -1061,9 +1119,10 @@ auto supported_render_backend_mask() noexcept -> uint32_t { #endif } -auto webgpu_owned_texture_attach( +auto webgpu_owned_texture_attach_start( mln_map map, const mln_webgpu_owned_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { #if !defined(MLN_RENDER_BACKEND_WEBGPU) set_thread_error("WebGPU texture sessions are not supported by this build"); @@ -1079,12 +1138,14 @@ auto webgpu_owned_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; + if ( + options != nullptr && + options->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ) { + set_thread_error( + "WebGPU targets require the caller graphics thread driver" + ); + return MLN_STATUS_UNSUPPORTED; } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, @@ -1093,34 +1154,42 @@ auto webgpu_owned_texture_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } - - try { - auto session = std::make_shared(); - session->map = map; - set_session_extent(*session, descriptor->extent); - session->texture.api_kind = TextureSessionApi::WebGPU; - session->texture.mode = TextureSessionMode::Owned; - session->texture.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle", - } - ); - } catch (const std::exception& exception) { - set_thread_error(exception.what()); - return MLN_STATUS_NATIVE_ERROR; - } + auto session = std::make_shared(); + session->map = map; + set_session_extent(*session, descriptor->extent); + session->texture.api_kind = TextureSessionApi::WebGPU; + session->texture.mode = TextureSessionMode::Owned; + const auto copied = *descriptor; + const auto ring_depth = std::clamp( + options == nullptr ? 1u : options->requested_texture_ring_depth, 1u, 3u + ); + session->initialize_backend = + [copied, ring_depth](mln_render_session_object& target) { + target.texture.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height}, + ring_depth + ); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + .texture_ring_depth = ring_depth, + .flags = MLN_RENDER_SESSION_CAPABILITY_FRAME_ACQUISITION | + MLN_RENDER_SESSION_CAPABILITY_READBACK | + MLN_RENDER_SESSION_CAPABILITY_CONSUMER_SYNC + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion + ); #endif } -auto webgpu_borrowed_texture_attach( +auto webgpu_borrowed_texture_attach_start( mln_map map, const mln_webgpu_borrowed_texture_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { #if !defined(MLN_RENDER_BACKEND_WEBGPU) set_thread_error( @@ -1138,12 +1207,14 @@ auto webgpu_borrowed_texture_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; + if ( + options != nullptr && + options->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ) { + set_thread_error( + "WebGPU targets require the caller graphics thread driver" + ); + return MLN_STATUS_UNSUPPORTED; } const auto physical_status = validate_borrowed_physical_size( descriptor->physical_width, descriptor->physical_height @@ -1151,52 +1222,44 @@ auto webgpu_borrowed_texture_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } - - const auto physical_size = - mln::Size{descriptor->physical_width, descriptor->physical_height}; - const auto texture_status = - validate_webgpu_texture(*descriptor, physical_size); - if (texture_status != MLN_STATUS_OK) { - return texture_status; - } - - try { - auto session = std::make_shared(); - session->map = map; - set_borrowed_session_extent( - *session, descriptor->extent, descriptor->physical_width, - descriptor->physical_height - ); - session->texture.api_kind = TextureSessionApi::WebGPU; - session->texture.mode = TextureSessionMode::Borrowed; - session->texture.backend = - std::make_unique(*descriptor, physical_size); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Texture, - RenderSessionAttachMessages{ - .null_session = "texture session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle", - } - ); - } catch (const std::exception& exception) { - set_thread_error(exception.what()); - return MLN_STATUS_NATIVE_ERROR; - } + auto session = std::make_shared(); + session->map = map; + set_borrowed_session_extent( + *session, descriptor->extent, descriptor->physical_width, + descriptor->physical_height + ); + session->texture.api_kind = TextureSessionApi::WebGPU; + session->texture.mode = TextureSessionMode::Borrowed; + const auto copied = *descriptor; + session->initialize_backend = [copied](mln_render_session_object& target) { + const auto physical_size = + mln::Size{target.physical_width, target.physical_height}; + const auto texture_status = validate_webgpu_texture(copied, physical_size); + if (texture_status != MLN_STATUS_OK) { + return texture_status; + } + target.texture.backend = + std::make_unique(copied, physical_size); + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_READBACK + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Texture, options, capabilities, + out_session, completion + ); #endif } -auto webgpu_borrowed_texture_set_target( +auto webgpu_borrowed_texture_set_target_start( mln_render_session session, - const mln_webgpu_borrowed_texture_descriptor* descriptor + const mln_webgpu_borrowed_texture_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::BorrowedTexture, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_webgpu_borrowed_texture_descriptor(descriptor); if (descriptor_status != MLN_STATUS_OK) { @@ -1208,30 +1271,32 @@ auto webgpu_borrowed_texture_set_target( if (physical_status != MLN_STATUS_OK) { return physical_status; } - // The same probe attach makes. Without it a texture of the wrong shape, size, - // format, or usage fails only on the next render, once the outgoing target is - // already released. - const auto texture_status = validate_webgpu_texture( - *descriptor, - mln::Size{descriptor->physical_width, descriptor->physical_height} - ); - if (texture_status != MLN_STATUS_OK) { - return texture_status; - } - return render_session_set_target( - session, RetargetTargetKind::BorrowedTexture, descriptor->extent, - descriptor->physical_width, descriptor->physical_height, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.texture.backend->set_webgpu_borrowed_target( - *descriptor + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + const auto texture_status = validate_webgpu_texture( + copied, mln::Size{copied.physical_width, copied.physical_height} ); - } + if (texture_status != MLN_STATUS_OK) { + return texture_status; + } + return render_session_set_target( + target.self, RetargetTargetKind::BorrowedTexture, copied.extent, + copied.physical_width, copied.physical_height, + [&copied](mln_render_session_object& live) { + return live.texture.backend->set_webgpu_borrowed_target(copied); + } + ); + }, + completion ); } -auto webgpu_surface_attach( +auto webgpu_surface_attach_start( mln_map map, const mln_webgpu_surface_descriptor* descriptor, - mln_render_session* out_session + const mln_render_session_attach_options* options, + mln_render_session* out_session, const mln_completion* completion ) -> mln_status { #if !defined(MLN_RENDER_BACKEND_WEBGPU) set_thread_error("WebGPU surface sessions are not supported by this build"); @@ -1246,12 +1311,14 @@ auto webgpu_surface_attach( if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - const auto output_status = validate_attach_output( - out_session, "out_session must not be null", - "out_session must point to a null handle" - ); - if (output_status != MLN_STATUS_OK) { - return output_status; + if ( + options != nullptr && + options->driver != MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD + ) { + set_thread_error( + "WebGPU targets require the caller graphics thread driver" + ); + return MLN_STATUS_UNSUPPORTED; } const auto physical_status = validate_physical_size( descriptor->extent.width, descriptor->extent.height, @@ -1260,129 +1327,49 @@ auto webgpu_surface_attach( if (physical_status != MLN_STATUS_OK) { return physical_status; } - - try { - auto session = std::make_shared(); - session->map = map; - set_session_extent(*session, descriptor->extent); - session->surface.backend = std::make_unique( - *descriptor, mln::Size{session->physical_width, session->physical_height} - ); - return attach_render_session( - std::move(session), out_session, RenderSessionKind::Surface, - RenderSessionAttachMessages{ - .null_session = "surface session must not be null", - .null_output = "out_session must not be null", - .non_null_output = "out_session must point to a null handle", - } + auto session = std::make_shared(); + session->map = map; + set_session_extent(*session, descriptor->extent); + const auto copied = *descriptor; + session->initialize_backend = [copied](mln_render_session_object& target) { + target.surface.backend = std::make_unique( + copied, mln::Size{target.physical_width, target.physical_height} ); - } catch (const std::exception& exception) { - set_thread_error(exception.what()); - return MLN_STATUS_NATIVE_ERROR; - } + return MLN_STATUS_OK; + }; + const auto capabilities = mln_render_session_capabilities{ + .size = sizeof(mln_render_session_capabilities), + .driver = MLN_RENDER_DRIVER_CALLER_GRAPHICS_THREAD, + .texture_ring_depth = 0, + .flags = MLN_RENDER_SESSION_CAPABILITY_PRESENTATION + }; + return start_attach_render_session( + std::move(session), RenderSessionKind::Surface, options, capabilities, + out_session, completion + ); #endif } -auto webgpu_surface_set_target( - mln_render_session session, const mln_webgpu_surface_descriptor* descriptor +auto webgpu_surface_set_target_start( + mln_render_session session, const mln_webgpu_surface_descriptor* descriptor, + const mln_completion* completion ) -> mln_status { - mln_render_session_object* live = nullptr; - const auto session_status = validate_render_session_retarget( - session, RetargetTargetKind::Surface, live - ); - if (session_status != MLN_STATUS_OK) { - return session_status; - } const auto descriptor_status = validate_webgpu_surface_descriptor(descriptor); if (descriptor_status != MLN_STATUS_OK) { return descriptor_status; } - return surface_session_set_target( - session, descriptor->extent, - [descriptor](mln_render_session_object& target_session) -> mln_status { - return target_session.surface.backend->set_webgpu_target(*descriptor); - } + const auto copied = *descriptor; + return enqueue_driver_operation( + session, + [copied](mln_render_session_object& target) { + return surface_session_set_target( + target.self, copied.extent, [&copied](mln_render_session_object& live) { + return live.surface.backend->set_webgpu_target(copied); + } + ); + }, + completion ); } -auto webgpu_owned_texture_acquire_frame( - mln_render_session texture, mln_webgpu_owned_texture_frame* out_frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_live_attached_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_frame == nullptr || - out_frame->size < sizeof(mln_webgpu_owned_texture_frame) - ) { - set_thread_error("out_frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (live->texture.acquired) { - set_thread_error("a texture frame is already acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (live->rendered_generation != live->generation) { - set_thread_error("no rendered frame is available for this generation"); - return MLN_STATUS_INVALID_STATE; - } - if ( - live->texture.mode != TextureSessionMode::Owned || - live->texture.api_kind != TextureSessionApi::WebGPU - ) { - set_thread_error("texture session cannot expose a WebGPU texture frame"); - return MLN_STATUS_UNSUPPORTED; - } - - const auto acquire_status = - live->texture.backend->acquire_webgpu_owned_frame(*live, *out_frame); - if (acquire_status != MLN_STATUS_OK) { - return acquire_status; - } - live->texture.acquired_native_texture = out_frame->texture; - live->texture.acquired = true; - live->texture.acquired_frame_id = out_frame->frame_id; - live->texture.acquired_frame_kind = TextureSessionFrameKind::WebGPUOwned; - ++live->texture.next_frame_id; - return MLN_STATUS_OK; -} - -auto webgpu_owned_texture_release_frame( - mln_render_session texture, const mln_webgpu_owned_texture_frame* frame -) -> mln_status { - mln_render_session_object* live = nullptr; - const auto status = validate_texture(texture, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - frame == nullptr || frame->size < sizeof(mln_webgpu_owned_texture_frame) - ) { - set_thread_error("frame must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if ( - !live->texture.acquired || - live->texture.acquired_frame_kind != TextureSessionFrameKind::WebGPUOwned - ) { - set_thread_error("no texture frame is currently acquired"); - return MLN_STATUS_INVALID_STATE; - } - if (frame->generation != live->generation) { - set_thread_error("frame generation does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (frame->frame_id != live->texture.acquired_frame_id) { - set_thread_error("frame identity does not match acquired frame"); - return MLN_STATUS_INVALID_ARGUMENT; - } - live->texture.acquired = false; - live->texture.acquired_frame_id = 0; - live->texture.acquired_frame_kind = TextureSessionFrameKind::None; - live->texture.acquired_native_texture = nullptr; - return MLN_STATUS_OK; -} - } // namespace mln::core diff --git a/src/runtime/runtime.cpp b/src/runtime/runtime.cpp index 91af3eaa5..9c5c2e2b2 100644 --- a/src/runtime/runtime.cpp +++ b/src/runtime/runtime.cpp @@ -1,7 +1,7 @@ #include #include +#include #include -#include #include #include #include @@ -36,10 +36,13 @@ #include "runtime/runtime.hpp" +#include "completion/completion.hpp" #include "diagnostics/diagnostics.hpp" #include "geojson/geojson.hpp" #include "handles/handle_table.hpp" #include "maplibre_native_c.h" +#include "operation/operation.hpp" +#include "wake/wake.hpp" struct OfflineRegionData { mln_offline_region_id id = 0; @@ -76,53 +79,37 @@ struct HandleTraits { static constexpr auto leasable = false; }; -} // namespace mln::core - -namespace mln::core { - -// Holds its own reference to the wake state, so the state stays readable for a -// signal that races runtime teardown. Hosts destroy the two in either order. -struct WakeSourceObject { - explicit WakeSourceObject(std::shared_ptr state_) - : state(std::move(state_)) {} - - std::shared_ptr state; -}; - -// Signalling is an any-thread call that can race a destroy, so a lease keeps -// the wake state readable for the duration of the call. template <> -struct HandleTraits { - static constexpr auto kind = HandleKind::WakeSource; +struct HandleTraits { + static constexpr auto kind = HandleKind::EventBatch; static constexpr auto leasable = true; }; -using OfflineOperationResult = std::variant< - std::monostate, OfflineRegionData, std::optional, - std::vector, mln_offline_region_status>; - -struct OfflineOperation { - mln_offline_operation_id id = 0; - uint32_t kind = 0; - uint32_t result_kind = MLN_OFFLINE_OPERATION_RESULT_NONE; - bool completed = false; - int32_t result_status = MLN_STATUS_OK; - bool found = false; - std::string message; - OfflineOperationResult result; -}; - -struct OfflineOperationEventState { - std::mutex mutex; - mln::core::RuntimeObject* runtime = nullptr; - bool alive = false; - mln_offline_operation_id next_id = 1; - std::unordered_map operations; -}; - } // namespace mln::core namespace { +enum : std::uint32_t { + MLN_OFFLINE_OPERATION_AMBIENT_CACHE = 1, + MLN_OFFLINE_OPERATION_REGION_CREATE = 2, + MLN_OFFLINE_OPERATION_REGION_GET = 3, + MLN_OFFLINE_OPERATION_REGIONS_LIST = 4, + MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE = 5, + MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA = 6, + MLN_OFFLINE_OPERATION_REGION_GET_STATUS = 7, + MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED = 8, + MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE = 9, + MLN_OFFLINE_OPERATION_REGION_INVALIDATE = 10, + MLN_OFFLINE_OPERATION_REGION_DELETE = 11, + MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE = 12, +}; + +enum : std::uint32_t { + MLN_OFFLINE_OPERATION_RESULT_NONE = 0, + MLN_OFFLINE_OPERATION_RESULT_REGION = 1, + MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION = 2, + MLN_OFFLINE_OPERATION_RESULT_REGION_LIST = 3, + MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS = 4, +}; // Each mask constant must be the bit of the event type it selects, written // against the shift rather than a literal, so an event type added without its @@ -155,7 +142,6 @@ MLN_ASSERT_EVENT_MASK_BIT(MAP_CAMERA_TRANSITION_FINISHED); MLN_ASSERT_EVENT_MASK_BIT(OFFLINE_REGION_STATUS_CHANGED); MLN_ASSERT_EVENT_MASK_BIT(OFFLINE_REGION_RESPONSE_ERROR); MLN_ASSERT_EVENT_MASK_BIT(OFFLINE_REGION_TILE_COUNT_LIMIT_EXCEEDED); -MLN_ASSERT_EVENT_MASK_BIT(OFFLINE_OPERATION_COMPLETED); #undef MLN_ASSERT_EVENT_MASK_BIT @@ -214,23 +200,6 @@ auto runtime_from_platform_context(void* platform_context) noexcept return found == registry.end() ? MLN_HANDLE_NULL : found->second; } -auto live_runtime_threads_mutex() -> std::mutex& { - static std::mutex value; - return value; -} - -// Mirrors the owner thread of every live runtime, so runtime creation can -// reject a thread that already owns one. The handle table does not iterate. -auto live_runtime_threads() -> std::unordered_set& { - static std::unordered_set value; - return value; -} - -auto owner_thread_has_live_runtime(std::thread::id owner_thread) -> bool { - const std::scoped_lock lock(live_runtime_threads_mutex()); - return live_runtime_threads().contains(owner_thread); -} - // Leases the resource transform registration for a MapLibre-owned thread. Only // a reference count increment happens under the handle table lock; the caller // takes the returned state's lock after this returns, so a writer waiting on @@ -287,22 +256,8 @@ auto lease_resource_provider_state(void* platform_context) noexcept return runtime->resource_provider_state; } -// Reports why a take-result call has no result. This is the only route to a -// failed operation's text for a host that leaves -// MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED unselected. -auto set_offline_result_unavailable_error( - const mln::core::OfflineOperation& operation -) -> void { - if (operation.completed && !operation.message.empty()) { - mln::core::set_thread_error(operation.message.c_str()); - return; - } - mln::core::set_thread_error("offline operation result is not available"); -} - -// A batch locates a message by a uint32_t offset and size, so a message is -// clamped at push rather than at drain time. The arena starts each drain empty, -// so the first event always fits and a bounded drain always makes progress. +// Each event keeps a uint32_t message length, so clamp oversized diagnostics +// before they enter the shared queue arena. auto truncated_event_message(std::string message) -> std::string { constexpr auto max_size = static_cast(UINT32_MAX) - 1; if (message.size() > max_size) { @@ -311,6 +266,28 @@ auto truncated_event_message(std::string message) -> std::string { return message; } +auto append_runtime_event( + mln::core::RuntimeEventStorage& storage, mln_runtime_event event, + std::string message +) -> void { + message = truncated_event_message(std::move(message)); + event.message_size = static_cast(message.size()); + event.message_offset = + message.empty() ? 0 : static_cast(storage.messages.size()); + storage.events.push_back(event); + const auto prior_messages_size = storage.messages.size(); + try { + if (!message.empty()) { + storage.messages.append(message); + storage.messages.push_back('\0'); + } + } catch (...) { + storage.messages.resize(prior_messages_size); + storage.events.pop_back(); + throw; + } +} + auto valid_coordinate(const mln_lat_lng& coordinate) -> bool { return std::isfinite(coordinate.latitude) && coordinate.latitude >= -90.0 && coordinate.latitude <= 90.0 && std::isfinite(coordinate.longitude); @@ -650,8 +627,8 @@ auto region_event_selected( if (!mln::core::event_selected(runtime->event_state->mask, type)) { return false; } - const std::scoped_lock event_lock(runtime->event_mutex); - return runtime->observed_offline_regions.contains(region_id); + const std::scoped_lock event_lock(runtime->event_queue->mutex); + return runtime->event_queue->observed_offline_regions.contains(region_id); } auto push_offline_region_event( @@ -668,28 +645,29 @@ auto push_offline_region_event( return; } - auto event = mln::core::QueuedRuntimeEvent{ + auto event = mln_runtime_event{ .type = type, .source_type = MLN_RUNTIME_EVENT_SOURCE_RUNTIME, .source = runtime->self, .code = 0, .payload_type = payload_type, - .payload = payload, - .message = truncated_event_message(std::move(message)), - .has_offline_region = true, - .offline_region_id = region_id + .message_offset = 0, + .message_size = 0, + .payload = payload }; + auto should_wake = false; { - const std::scoped_lock event_lock(runtime->event_mutex); - if (!runtime->observed_offline_regions.contains(region_id)) { + const std::scoped_lock event_lock(runtime->event_queue->mutex); + if (!runtime->event_queue->observed_offline_regions.contains(region_id)) { return; } - runtime->events.push_back(std::move(event)); + should_wake = runtime->event_queue->pending.events.empty(); + append_runtime_event( + runtime->event_queue->pending, event, std::move(message) + ); } - // The offline region observer runs off the owner-thread run loop, so this - // signal releases a parked owner thread. - mln::core::signal_wake(runtime->wake_state); + if (should_wake) runtime->event_queue->wake->notify(); } class OfflineRegionRuntimeObserver final : public mln::OfflineRegionObserver { @@ -763,14 +741,11 @@ auto set_offline_region_observed_flag( mln::core::RuntimeObject* runtime, mln_offline_region_id region_id, bool observed ) -> void { - const std::scoped_lock lock(runtime->event_mutex); + const std::scoped_lock lock(runtime->event_queue->mutex); if (observed) { - runtime->observed_offline_regions.insert(region_id); + runtime->event_queue->observed_offline_regions.insert(region_id); } else { - runtime->observed_offline_regions.erase(region_id); - std::erase_if(runtime->events, [region_id](const auto& event) -> bool { - return event.has_offline_region && event.offline_region_id == region_id; - }); + runtime->event_queue->observed_offline_regions.erase(region_id); } } @@ -823,210 +798,180 @@ auto exception_message(std::exception_ptr exception, const char* fallback) } } -auto validate_offline_operation_output( - mln_runtime runtime, mln::core::RuntimeObject*& out_runtime, - mln_offline_operation_id* out_operation_id -) -> mln_status { - const auto status = mln::core::validate_runtime(runtime, out_runtime); - if (status != MLN_STATUS_OK) { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } - return status; - } - if (out_operation_id == nullptr) { - mln::core::set_thread_error("out_operation_id must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_operation_id = 0; - return MLN_STATUS_OK; -} - -auto register_offline_operation( - mln_runtime runtime_handle, uint32_t kind, uint32_t result_kind, - mln_offline_operation_id* out_operation_id -) -> mln_status { - mln::core::RuntimeObject* runtime = nullptr; - const auto status = validate_offline_operation_output( - runtime_handle, runtime, out_operation_id - ); - if (status != MLN_STATUS_OK) { - return status; - } - - const std::scoped_lock lock(runtime->offline_operation_state->mutex); - auto& state = *runtime->offline_operation_state; - if (!state.alive || state.runtime != runtime || state.next_id == 0) { - mln::core::set_thread_error("offline operation state is unavailable"); - return MLN_STATUS_INVALID_STATE; - } - const auto id = state.next_id++; - state.operations.emplace( - id, mln::core::OfflineOperation{ - .id = id, - .kind = kind, - .result_kind = result_kind, - .completed = false, - .result_status = MLN_STATUS_OK, - .found = false, - .message = {}, - .result = std::monostate{} - } - ); - *out_operation_id = id; - return MLN_STATUS_OK; -} - -auto erase_queued_offline_operation_events( - mln::core::RuntimeObject* runtime, mln_offline_operation_id operation_id -) -> void { - const std::scoped_lock event_lock(runtime->event_mutex); - std::erase_if(runtime->events, [operation_id](const auto& event) -> bool { - return event.has_offline_operation && - event.offline_operation_id == operation_id; - }); -} - -auto erase_offline_operation_registration( - mln::core::RuntimeObject* runtime, mln_offline_operation_id operation_id -) -> void { - if (runtime == nullptr || operation_id == 0) { - return; - } - auto state = runtime->offline_operation_state; - if (!state) { - return; - } - { - const std::scoped_lock state_lock(state->mutex); - state->operations.erase(operation_id); - } - - erase_queued_offline_operation_events(runtime, operation_id); -} - template auto schedule_registered_offline_operation( - mln::core::RuntimeObject* runtime, uint32_t kind, uint32_t result_kind, - mln_offline_operation_id* out_operation_id, Schedule&& schedule + mln::core::RuntimeObject* runtime, uint32_t kind, + const mln_completion* descriptor, Schedule&& schedule ) -> mln_status { - auto register_status = register_offline_operation( - runtime->self, kind, result_kind, out_operation_id + const auto validation = mln::core::validate_completion(descriptor); + if (validation != MLN_STATUS_OK) return validation; + auto completion = std::make_shared(*descriptor); + auto state = std::make_shared( + [completion, + kind](mln_status status, std::string diagnostic, std::any result) mutable { + if (status != MLN_STATUS_OK) { + mln::core::complete(completion, status, std::move(diagnostic)); + return; + } + try { + switch (kind) { + case MLN_OFFLINE_OPERATION_REGION_CREATE: + case MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA: { + auto value = std::any_cast(std::move(result)); + completion->resolve( + [value = std::move(value)](const mln_completion& descriptor) { + auto info = mln_offline_region_info{}; + info.size = sizeof(mln_offline_region_info); + if (fill_region_info(value, &info) != MLN_STATUS_OK) { + mln::core::invoke_completion( + descriptor, MLN_STATUS_NATIVE_ERROR, + MLN_COMMAND_DISPOSITION_COMMITTED, 0, + mln::core::thread_last_error_message(), nullptr, 0 + ); + return; + } + mln::core::invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, + 0, {}, &info, 1 + ); + } + ); + return; + } + case MLN_OFFLINE_OPERATION_REGION_GET: { + auto value = std::any_cast>( + std::move(result) + ); + if (!value) { + mln::core::complete(completion, MLN_STATUS_OK); + return; + } + completion->resolve( + [value = std::move(*value)](const mln_completion& descriptor) { + auto info = mln_offline_region_info{}; + info.size = sizeof(mln_offline_region_info); + if (fill_region_info(value, &info) != MLN_STATUS_OK) { + mln::core::invoke_completion( + descriptor, MLN_STATUS_NATIVE_ERROR, + MLN_COMMAND_DISPOSITION_COMMITTED, 0, + mln::core::thread_last_error_message(), nullptr, 0 + ); + return; + } + mln::core::invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, + 0, {}, &info, 1 + ); + } + ); + return; + } + case MLN_OFFLINE_OPERATION_REGIONS_LIST: + case MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE: { + auto value = + std::any_cast>(std::move(result)); + completion->resolve([value = std::move(value)]( + const mln_completion& descriptor + ) { + auto info = std::vector(value.size()); + for (size_t index = 0; index < value.size(); ++index) { + info[index].size = sizeof(mln_offline_region_info); + if ( + fill_region_info(value[index], &info[index]) != MLN_STATUS_OK + ) { + mln::core::invoke_completion( + descriptor, MLN_STATUS_NATIVE_ERROR, + MLN_COMMAND_DISPOSITION_COMMITTED, 0, + mln::core::thread_last_error_message(), nullptr, 0 + ); + return; + } + } + mln::core::invoke_completion( + descriptor, MLN_STATUS_OK, MLN_COMMAND_DISPOSITION_COMMITTED, 0, + {}, info.data(), info.size() + ); + }); + return; + } + case MLN_OFFLINE_OPERATION_REGION_GET_STATUS: + mln::core::complete_value( + completion, MLN_STATUS_OK, {}, + std::any_cast(std::move(result)) + ); + return; + default: + mln::core::complete(completion, MLN_STATUS_OK); + return; + } + } catch (...) { + mln::core::complete( + completion, MLN_STATUS_NATIVE_ERROR, + "offline operation produced an invalid result" + ); + } + } + ); + static_cast( + mln::core::associate_runtime_operation_with_current_submission( + runtime, state + ) ); - if (register_status != MLN_STATUS_OK) { - return register_status; - } - const auto operation_id = *out_operation_id; - auto state = runtime->offline_operation_state; try { - std::forward(schedule)(state, operation_id); + std::forward(schedule)(state); } catch (...) { - erase_offline_operation_registration(runtime, operation_id); - *out_operation_id = 0; + state->complete( + MLN_STATUS_NATIVE_ERROR, + exception_message( + std::current_exception(), "offline operation submission failed" + ), + std::any{std::monostate{}} + ); + completion->reject(); throw; } + completion->accept(); return MLN_STATUS_OK; } -auto make_offline_completion_payload( - const mln::core::OfflineOperation& operation -) -> mln_runtime_event_payload { - auto payload = mln::core::zeroed_event_payload(); - payload.offline_operation_completed = - mln_runtime_event_offline_operation_completed{ - .operation_id = operation.id, - .operation_kind = operation.kind, - .result_kind = operation.result_kind, - .result_status = operation.result_status, - .found = operation.found - }; - return payload; -} - +template auto complete_offline_operation( - const std::shared_ptr& state, - mln_offline_operation_id operation_id, int32_t result_status, - mln::core::OfflineOperationResult result, bool found = false, - std::string message = {} + const std::shared_ptr& state, + int32_t result_status, Result result, std::string message = {} ) -> void { - const std::scoped_lock state_lock(state->mutex); - auto* runtime = state->runtime; - if (!state->alive || runtime == nullptr) { - return; - } - - auto found_operation = state->operations.find(operation_id); - if (found_operation == state->operations.end()) { - return; - } - - // The result and its failure text are what a take-result call reports, so - // they are recorded whatever the mask selects. Only the event is gated. - auto& operation = found_operation->second; - operation.completed = true; - operation.result_status = result_status; - operation.found = found; - operation.message = std::move(message); - operation.result = std::move(result); - - if (!mln::core::event_selected( - runtime->event_state->mask, - MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED - )) { - return; - } - - auto event = mln::core::QueuedRuntimeEvent{ - .type = MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED, - .source_type = MLN_RUNTIME_EVENT_SOURCE_RUNTIME, - .source = runtime->self, - .code = result_status, - .payload_type = MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_OPERATION_COMPLETED, - .payload = make_offline_completion_payload(operation), - .message = truncated_event_message(operation.message), - .has_offline_region = false, - .offline_region_id = 0, - .has_offline_operation = true, - .offline_operation_id = operation_id - }; - - { - const std::scoped_lock event_lock(runtime->event_mutex); - runtime->events.push_back(std::move(event)); - } - mln::core::signal_wake(runtime->wake_state); + state->complete( + static_cast(result_status), std::move(message), + std::any{std::move(result)} + ); } auto complete_offline_operation_error( - const std::shared_ptr& state, - mln_offline_operation_id operation_id, int32_t status, std::string message + const std::shared_ptr& state, int32_t status, + std::string message ) -> void { complete_offline_operation( - state, operation_id, status, std::monostate{}, false, std::move(message) + state, status, std::monostate{}, std::move(message) ); } auto complete_from_exception( - const std::shared_ptr& state, - mln_offline_operation_id operation_id, std::exception_ptr exception, - const char* fallback + const std::shared_ptr& state, + std::exception_ptr exception, const char* fallback ) -> void { if (exception) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, - exception_message(exception, fallback) + state, MLN_STATUS_NATIVE_ERROR, exception_message(exception, fallback) ); return; } - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::monostate{} - ); + complete_offline_operation(state, MLN_STATUS_OK, std::monostate{}); } auto validate_runtime_options(const mln_runtime_options* options) -> mln_status { if (options == nullptr) { - return MLN_STATUS_OK; + mln::core::set_thread_error("runtime options must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; } if (options->size < sizeof(mln_runtime_options)) { @@ -1049,6 +994,8 @@ auto validate_runtime_options(const mln_runtime_options* options) ); return MLN_STATUS_INVALID_ARGUMENT; } + const auto wake_status = mln::core::validate_wake(&options->event_wake); + if (wake_status != MLN_STATUS_OK) return wake_status; return MLN_STATUS_OK; } @@ -1078,232 +1025,507 @@ auto database_source_for_runtime(RuntimeObject* runtime) } // namespace -// Only the owner thread destroys a runtime, so the borrowed object stays alive -// for as long as the calling thread can use it. auto validate_runtime(mln_runtime runtime, RuntimeObject*& out_runtime) -> mln_status { out_runtime = handle_table().resolve(runtime); - if (out_runtime == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } + return out_runtime == nullptr ? MLN_STATUS_INVALID_ARGUMENT : MLN_STATUS_OK; +} - if (out_runtime->owner_thread != std::this_thread::get_id()) { - set_thread_error("runtime call must be made on its owner thread"); - return MLN_STATUS_WRONG_THREAD; - } +auto lease_runtime(mln_runtime runtime) -> std::shared_ptr { + return handle_table().lease(runtime); +} - return MLN_STATUS_OK; +namespace { + +struct CurrentRuntimeSubmission { + RuntimeObject* runtime; + std::shared_ptr retained_runtime; + uint64_t sequence; + bool claimed_by_operation = false; +}; + +auto current_runtime_submission() noexcept -> CurrentRuntimeSubmission*& { + static thread_local auto* submission = + static_cast(nullptr); + return submission; } -auto create_runtime( - const mln_runtime_options* options, mln_runtime* out_runtime -) -> mln_status { - const auto options_status = validate_runtime_options(options); - if (options_status != MLN_STATUS_OK) { - return options_status; +auto erase_tracked_submission( + const std::shared_ptr& runtime, uint64_t sequence +) noexcept -> void { + { + const std::scoped_lock lock(runtime->terminal_mutex); + runtime->pending_submissions.erase(sequence); } + runtime->terminal_condition.notify_all(); +} - if (out_runtime == nullptr) { - set_thread_error("out_runtime must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; +auto erase_runtime_barrier( + const std::shared_ptr& runtime, uint64_t sequence +) noexcept -> void { + { + const std::scoped_lock lock(runtime->terminal_mutex); + runtime->pending_barriers.erase(sequence); + runtime->pending_submissions.erase(sequence); } + runtime->terminal_condition.notify_all(); +} - if (*out_runtime != MLN_HANDLE_NULL) { - set_thread_error("out_runtime must point to the null handle"); - return MLN_STATUS_INVALID_ARGUMENT; +auto complete_ready_runtime_barriers( + const std::shared_ptr& runtime +) noexcept -> void { + while (true) { + auto barrier = std::shared_ptr{}; + { + const std::scoped_lock lock(runtime->terminal_mutex); + const auto pending = runtime->pending_barriers.begin(); + if ( + pending == runtime->pending_barriers.end() || + (!runtime->pending_submissions.empty() && + *runtime->pending_submissions.begin() < pending->first) + ) { + return; + } + barrier = std::move(pending->second); + runtime->pending_barriers.erase(pending); + } + barrier->complete(MLN_STATUS_OK, {}, std::any{std::monostate{}}); } +} - const auto owner_thread = std::this_thread::get_id(); - if (owner_thread_has_live_runtime(owner_thread)) { - set_thread_error("owner thread already has a live runtime"); - return MLN_STATUS_INVALID_STATE; +auto finish_tracked_submission( + const std::shared_ptr& runtime, uint64_t sequence +) noexcept -> void { + erase_tracked_submission(runtime, sequence); + complete_ready_runtime_barriers(runtime); +} + +} // namespace + +auto dispatch_runtime_sync( + mln_runtime runtime, std::function function +) -> mln_status { + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (mln::Scheduler::GetCurrent(false) != nullptr) { - set_thread_error("owner thread already has an active MapLibre scheduler"); + const std::scoped_lock commit_lock(live->submission_mutex); + if (!live->control.acquire()) { return MLN_STATUS_INVALID_STATE; } - - auto owned_runtime = std::make_shared(); - owned_runtime->owner_thread = owner_thread; - owned_runtime->wake_state = std::make_shared(); - // The mask cell exists before the run loop, so every producer this runtime - // can reach reads a live cell. - owned_runtime->event_state = std::make_shared(); - // Null options keep the library's own default, which is every event type. - owned_runtime->event_state->mask.store( - options != nullptr ? options->event_mask - : static_cast(MLN_RUNTIME_EVENT_MASK_ALL), - std::memory_order_relaxed - ); - owned_runtime->run_loop = - std::make_unique(mln::util::RunLoop::Type::New); - // `setPlatformCallback` is an unlocked assignment, so it happens while the - // run loop is reachable only from this thread. - owned_runtime->run_loop->setPlatformCallback( - [state = owned_runtime->wake_state]() -> void { signal_wake(state); } - ); - // The gate runs on the owner thread inside the drain, and the runtime owns - // the run loop, so the raw pointer cannot outlive it. - owned_runtime->run_loop->setProcessGate( - [live = owned_runtime.get()]() -> bool { - if (!live->pump_deadline.has_value()) { - return true; - } - if (!live->pump_ran_task) { - live->pump_ran_task = true; - return true; - } - if (std::chrono::steady_clock::now() < *live->pump_deadline) { - return true; - } - live->pump_budget_exhausted = true; - // Denying disarms the gate: a task inside the drain can wait for this - // same run loop to empty, and that nested drain must run to completion - // rather than spin against an expired budget. - live->pump_deadline.reset(); - return false; - } - ); - owned_runtime->asset_path = - options == nullptr || options->asset_path == nullptr - ? std::string{} - : std::string{options->asset_path}; - owned_runtime->cache_path = - options == nullptr || options->cache_path == nullptr - ? std::string{} - : std::string{options->cache_path}; - owned_runtime->offline_event_state = - std::make_shared(); - owned_runtime->offline_event_state->runtime = owned_runtime.get(); - owned_runtime->offline_event_state->alive = true; - owned_runtime->offline_operation_state = - std::make_shared(); - owned_runtime->offline_operation_state->runtime = owned_runtime.get(); - owned_runtime->offline_operation_state->alive = true; - owned_runtime->resource_transform_state = - std::make_shared(); - owned_runtime->http_header_transform_state = - std::make_shared(); - owned_runtime->resource_provider_state = - std::make_shared(); - auto* published = owned_runtime.get(); - // Reserving the token allocates, so it happens before this thread is marked - // as owning a runtime. The failure path below reads the local, not the - // object: the insert takes the shared_ptr by value, so a throw there destroys - // the runtime while unwinding. - auto* const platform_context = reserve_platform_context(); - published->platform_context = platform_context; + auto lease = ControlLease{&live->control}; + const auto sequence = live->next_submission_sequence++; { - const std::scoped_lock lock(live_runtime_threads_mutex()); - live_runtime_threads().insert(owner_thread); + const std::scoped_lock terminal_lock(live->terminal_mutex); + live->pending_submissions.insert(sequence); } + auto context = CurrentRuntimeSubmission{ + .runtime = live.get(), + .retained_runtime = live, + .sequence = sequence, + }; try { - *out_runtime = - handle_table().insert(std::move(owned_runtime)); + const auto status = live->executor.invoke_sync( + [lease = std::move(lease), &context, + function = std::move(function)]() mutable -> mln_status { + static_cast(lease); + auto* previous = current_runtime_submission(); + current_runtime_submission() = &context; + try { + const auto result = std::invoke(std::move(function)); + current_runtime_submission() = previous; + return result; + } catch (...) { + current_runtime_submission() = previous; + throw; + } + } + ); + if (!context.claimed_by_operation) { + finish_tracked_submission(live, sequence); + } + return status; } catch (...) { - // The caller receives no handle, so it has no way to give the owner thread - // back. Releasing it here keeps a failed creation from rejecting every - // later one on the same thread. - { - const std::scoped_lock lock(live_runtime_threads_mutex()); - live_runtime_threads().erase(owner_thread); + if (!context.claimed_by_operation) { + finish_tracked_submission(live, sequence); } - unregister_platform_context(platform_context); throw; } - published->self = *out_runtime; - bind_platform_context(platform_context, *out_runtime); - return MLN_STATUS_OK; } -auto set_resource_transform( - mln_runtime runtime, const mln_resource_transform* transform -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (transform == nullptr) { - set_thread_error("resource transform must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (transform->size < sizeof(mln_resource_transform)) { - set_thread_error("mln_resource_transform.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (transform->callback == nullptr) { - set_thread_error("resource transform callback must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; +auto associate_runtime_operation_with_current_submission( + RuntimeObject* runtime, const std::shared_ptr& operation +) noexcept -> bool { + auto* context = current_runtime_submission(); + if ( + context == nullptr || context->runtime != runtime || + context->claimed_by_operation || operation == nullptr + ) { + return false; } + context->claimed_by_operation = true; + operation->set_terminal_callback( + [retained = context->retained_runtime, + sequence = context->sequence]() noexcept -> void { + finish_tracked_submission(retained, sequence); + } + ); + return true; +} - auto& state = *live->resource_transform_state; - const std::unique_lock lock(state.mutex); - state.callback = transform->callback; - state.user_data = transform->user_data; - return MLN_STATUS_OK; +namespace { + +auto mark_runtime_submission_terminal( + const std::shared_ptr& runtime, uint64_t sequence +) noexcept -> void { + finish_tracked_submission(runtime, sequence); } -auto resource_transform_response_set_url( - mln_resource_transform_response* response, const char* url, size_t url_size +} // namespace + +auto submit_runtime_command( + const std::shared_ptr& runtime, + std::function function, + const std::shared_ptr& completion, + std::atomic* latest_submission ) -> mln_status { - if (response == nullptr) { - set_thread_error("resource transform response must not be null"); + if (runtime == nullptr || completion == nullptr) { + set_thread_error("runtime and completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (response->size < sizeof(mln_resource_transform_response)) { - set_thread_error("mln_resource_transform_response.size is too small"); - return MLN_STATUS_INVALID_ARGUMENT; + const std::scoped_lock commit_lock(runtime->submission_mutex); + if (!runtime->control.acquire()) { + return MLN_STATUS_INVALID_STATE; } - if (url == nullptr && url_size != 0) { - set_thread_error("resource transform response URL must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; + auto lease = ControlLease{&runtime->control}; + const auto sequence = runtime->next_submission_sequence++; + if (latest_submission != nullptr) { + latest_submission->store(sequence); } - if (url_size != 0 && std::memchr(url, '\0', url_size) != nullptr) { - set_thread_error("resource transform response URL contains embedded NUL"); - return MLN_STATUS_INVALID_ARGUMENT; + { + const std::scoped_lock terminal_lock(runtime->terminal_mutex); + runtime->pending_submissions.insert(sequence); } - auto* const replacement_url = static_cast(response->context); - if (replacement_url == nullptr) { - set_thread_error( - "resource transform response URL can only be set during a transform " - "callback" + try { + runtime->executor.invoke( + [runtime, lease = std::move(lease), sequence, + function = std::move(function)]() mutable -> void { + static_cast(lease); + try { + std::invoke(std::move(function), sequence); + } catch (...) { + // The command implementation reports its own terminal failure event. + } + mark_runtime_submission_terminal(runtime, sequence); + } ); - return MLN_STATUS_INVALID_STATE; - } - if (url_size == 0) { - replacement_url->clear(); - response->url = nullptr; - return MLN_STATUS_OK; + } catch (...) { + mark_runtime_submission_terminal(runtime, sequence); + completion->reject(); + set_thread_error("runtime command submission failed"); + return MLN_STATUS_NATIVE_ERROR; } - - replacement_url->assign(url, url_size); - response->url = replacement_url->c_str(); + completion->accept(); return MLN_STATUS_OK; } -auto clear_resource_transform(mln_runtime runtime) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; +auto submit_runtime_operation( + const std::shared_ptr& runtime, + const std::shared_ptr& operation, + std::function function +) -> mln_status { + if (runtime == nullptr || operation == nullptr) { + set_thread_error("runtime and operation must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; } - - auto& state = *live->resource_transform_state; - const std::unique_lock lock(state.mutex); - state.callback = nullptr; - state.user_data = nullptr; + const std::scoped_lock commit_lock(runtime->submission_mutex); + if (!runtime->control.acquire()) { + return MLN_STATUS_INVALID_STATE; + } + auto lease = ControlLease{&runtime->control}; + const auto sequence = runtime->next_submission_sequence++; + { + const std::scoped_lock terminal_lock(runtime->terminal_mutex); + runtime->pending_submissions.insert(sequence); + } + operation->set_terminal_callback([runtime, sequence]() noexcept -> void { + mark_runtime_submission_terminal(runtime, sequence); + }); + try { + runtime->executor.invoke( + [operation, lease = std::move(lease), + function = std::move(function)]() mutable -> void { + static_cast(lease); + try { + std::invoke(std::move(function)); + } catch (...) { + operation->complete( + MLN_STATUS_NATIVE_ERROR, "runtime operation submission failed", + std::any{std::monostate{}} + ); + } + } + ); + } catch (...) { + operation->complete( + MLN_STATUS_NATIVE_ERROR, "runtime operation submission failed", + std::any{std::monostate{}} + ); + set_thread_error("runtime operation submission failed"); + return MLN_STATUS_NATIVE_ERROR; + } + return MLN_STATUS_OK; +} + +namespace { + +struct RuntimeCreationGuard { + RuntimeCreationGuard() = default; + RuntimeCreationGuard(const RuntimeCreationGuard&) = delete; + RuntimeCreationGuard(RuntimeCreationGuard&&) = delete; + auto operator=(const RuntimeCreationGuard&) -> RuntimeCreationGuard& = delete; + auto operator=(RuntimeCreationGuard&&) -> RuntimeCreationGuard& = delete; + + ~RuntimeCreationGuard() { + if (!published && runtime != nullptr) { + runtime->executor.stop(); + if (runtime->platform_context != nullptr) { + unregister_platform_context(runtime->platform_context); + } + runtime.reset(); + } + } + + std::shared_ptr runtime; + bool published = false; +}; + +} // namespace + +auto create_runtime( + const mln_runtime_options* options, mln_runtime* out_runtime +) -> mln_status { + if (out_runtime == nullptr || *out_runtime != MLN_HANDLE_NULL) { + set_thread_error("out_runtime must point to the null handle"); + return MLN_STATUS_INVALID_ARGUMENT; + } + const auto options_status = validate_runtime_options(options); + if (options_status != MLN_STATUS_OK) { + return options_status; + } + const auto event_wake = std::make_shared(options->event_wake); + + RuntimeCreationGuard result; + result.runtime = std::make_shared(); + auto asset_path = options->asset_path == nullptr + ? std::string{} + : std::string{options->asset_path}; + auto cache_path = options->cache_path == nullptr + ? std::string{} + : std::string{options->cache_path}; + const auto event_mask = options->event_mask; + + auto runtime = result.runtime; + runtime->executor.start( + [runtime, event_wake, asset_path = std::move(asset_path), + cache_path = std::move(cache_path), event_mask]() mutable -> void { + runtime->event_state = std::make_shared(); + runtime->event_state->mask.store(event_mask, std::memory_order_relaxed); + runtime->event_queue = std::make_shared(); + runtime->event_queue->wake = event_wake; + runtime->asset_path = std::move(asset_path); + runtime->cache_path = std::move(cache_path); + runtime->offline_event_state = + std::make_shared(); + runtime->offline_event_state->runtime = runtime.get(); + runtime->offline_event_state->alive = true; + runtime->resource_transform_state = + std::make_shared(); + runtime->http_header_transform_state = + std::make_shared(); + runtime->resource_provider_state = + std::make_shared(); + runtime->platform_context = reserve_platform_context(); + } + ); + const auto handle = handle_table().insert(runtime); + runtime->self = handle; + try { + bind_platform_context(runtime->platform_context, handle); + } catch (...) { + if (runtime->self != MLN_HANDLE_NULL) { + static_cast(handle_table().remove(handle)); + runtime->self = MLN_HANDLE_NULL; + } + throw; + } + event_wake->accept(); + result.published = true; + *out_runtime = handle; + return MLN_STATUS_OK; +} + +namespace { + +template +auto execute_resource_configuration_command( + const std::shared_ptr& completion, Mutation&& mutation +) noexcept -> void { + try { + std::invoke(std::forward(mutation)); + complete_command( + completion, MLN_COMMAND_DISPOSITION_COMMITTED, MLN_STATUS_OK + ); + } catch (const std::exception& exception) { + complete_command( + completion, MLN_COMMAND_DISPOSITION_FAILED, MLN_STATUS_NATIVE_ERROR, 0, + exception.what() + ); + } catch (...) { + complete_command( + completion, MLN_COMMAND_DISPOSITION_FAILED, MLN_STATUS_NATIVE_ERROR, 0, + "resource configuration command failed" + ); + } +} + +template +auto submit_resource_configuration_command( + const std::shared_ptr& runtime, + const mln_completion* descriptor, Mutation&& mutation +) -> mln_status { + const auto status = validate_completion(descriptor); + if (status != MLN_STATUS_OK) return status; + auto completion = std::make_shared(*descriptor); + return submit_runtime_command( + runtime, + [completion, + mutation = std::forward(mutation)](uint64_t) mutable { + execute_resource_configuration_command(completion, std::move(mutation)); + }, + completion + ); +} + +} // namespace + +auto set_resource_transform( + mln_runtime runtime, const mln_resource_transform* transform, + const mln_completion* completion +) -> mln_status { + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; + if (transform == nullptr) { + set_thread_error("resource transform must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (transform->size < sizeof(mln_resource_transform)) { + set_thread_error("mln_resource_transform.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (transform->callback == nullptr) { + set_thread_error("resource transform callback must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + + const auto context = std::make_shared( + transform->user_data, transform->release_user_data + ); + const auto registration = std::make_shared( + ResourceTransformRegistration{ + .callback = transform->callback, + .context = context, + } + ); + context->transfer_to_runtime(); + const auto state = live->resource_transform_state; + const auto status = submit_resource_configuration_command( + live, completion, [state, registration]() -> void { + auto previous = std::shared_ptr{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->registration, registration); + } + } + ); + if (status != MLN_STATUS_OK) context->return_to_caller(); + return status; +} + +auto resource_transform_response_set_url( + mln_resource_transform_response* response, const char* url, size_t url_size +) -> mln_status { + if (response == nullptr) { + set_thread_error("resource transform response must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (response->size < sizeof(mln_resource_transform_response)) { + set_thread_error("mln_resource_transform_response.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url == nullptr && url_size != 0) { + set_thread_error("resource transform response URL must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (url_size != 0 && std::memchr(url, '\0', url_size) != nullptr) { + set_thread_error("resource transform response URL contains embedded NUL"); + return MLN_STATUS_INVALID_ARGUMENT; + } + auto* const replacement_url = static_cast(response->context); + if (replacement_url == nullptr) { + set_thread_error( + "resource transform response URL can only be set during a transform " + "callback" + ); + return MLN_STATUS_INVALID_STATE; + } + if (url_size == 0) { + replacement_url->clear(); + response->url = nullptr; + return MLN_STATUS_OK; + } + + replacement_url->assign(url, url_size); + response->url = replacement_url->c_str(); return MLN_STATUS_OK; } +auto clear_resource_transform( + mln_runtime runtime, const mln_completion* completion +) -> mln_status { + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; + + const auto state = live->resource_transform_state; + return submit_resource_configuration_command( + live, completion, [state]() -> void { + auto previous = std::shared_ptr{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->registration, nullptr); + } + } + ); +} + auto set_http_header_transform( - mln_runtime runtime, const mln_http_header_transform* transform + mln_runtime runtime, const mln_http_header_transform* transform, + const mln_completion* completion ) -> mln_status { - RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; if (transform == nullptr) { set_thread_error("HTTP header transform must not be null"); return MLN_STATUS_INVALID_ARGUMENT; @@ -1324,8 +1546,6 @@ auto set_http_header_transform( ); return MLN_STATUS_UNSUPPORTED; #elif defined(__EMSCRIPTEN__) - // fetch() with redirect: "manual" is no help either: a cross-origin manual - // redirect is an opaque response whose Location cannot be read. set_thread_error( "HTTP header transforms are unsupported in the browser because " "emscripten_fetch follows redirects itself and cannot drop transformed " @@ -1335,11 +1555,28 @@ auto set_http_header_transform( return MLN_STATUS_UNSUPPORTED; #endif - auto& state = *live->http_header_transform_state; - const std::unique_lock lock(state.mutex); - state.callback = transform->callback; - state.user_data = transform->user_data; - return MLN_STATUS_OK; + const auto context = std::make_shared( + transform->user_data, transform->release_user_data + ); + const auto registration = std::make_shared( + HttpHeaderTransformRegistration{ + .callback = transform->callback, + .context = context, + } + ); + context->transfer_to_runtime(); + const auto state = live->http_header_transform_state; + const auto status = submit_resource_configuration_command( + live, completion, [state, registration]() -> void { + auto previous = std::shared_ptr{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->registration, registration); + } + } + ); + if (status != MLN_STATUS_OK) context->return_to_caller(); + return status; } namespace { @@ -1549,34 +1786,38 @@ auto http_header_transform_response_set( return MLN_STATUS_OK; } -auto clear_http_header_transform(mln_runtime runtime) -> mln_status { - RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; +auto clear_http_header_transform( + mln_runtime runtime, const mln_completion* completion +) -> mln_status { + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; - auto& state = *live->http_header_transform_state; - const std::unique_lock lock(state.mutex); - state.callback = nullptr; - state.user_data = nullptr; - return MLN_STATUS_OK; + const auto state = live->http_header_transform_state; + return submit_resource_configuration_command( + live, completion, [state]() -> void { + auto previous = std::shared_ptr{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->registration, nullptr); + } + } + ); } auto run_ambient_cache_operation_start( - mln_runtime runtime, uint32_t operation, - mln_offline_operation_id* out_operation_id + mln_runtime runtime, uint32_t operation, const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } switch (operation) { @@ -1596,13 +1837,11 @@ auto run_ambient_cache_operation_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_AMBIENT_CACHE, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { - auto callback = [state, - operation_id](std::exception_ptr exception) -> void { + live, MLN_OFFLINE_OPERATION_AMBIENT_CACHE, completion, + [&](auto state) -> void { + auto callback = [state](std::exception_ptr exception) -> void { complete_from_exception( - state, operation_id, exception, "ambient cache operation failed" + state, exception, "ambient cache operation failed" ); }; @@ -1629,19 +1868,15 @@ auto run_ambient_cache_operation_start( } auto set_maximum_ambient_cache_size_start( - mln_runtime runtime, std::uint64_t size, - mln_offline_operation_id* out_operation_id + mln_runtime runtime, std::uint64_t size, const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } auto database = database_source_for_runtime(live); @@ -1651,14 +1886,12 @@ auto set_maximum_ambient_cache_size_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_SET_MAXIMUM_AMBIENT_CACHE_SIZE, completion, + [&](auto state) -> void { database->setMaximumAmbientCacheSize( - size, [state, operation_id](std::exception_ptr exception) -> void { + size, [state](std::exception_ptr exception) -> void { complete_from_exception( - state, operation_id, exception, - "setting the maximum ambient cache size failed" + state, exception, "setting the maximum ambient cache size failed" ); } ); @@ -1666,40 +1899,11 @@ auto set_maximum_ambient_cache_size_start( ); } -auto offline_operation_discard( - mln_runtime runtime, mln_offline_operation_id operation_id -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (operation_id == 0) { - set_thread_error("offline operation ID is invalid"); - return MLN_STATUS_INVALID_ARGUMENT; - } - - auto state = live->offline_operation_state; - const std::scoped_lock state_lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - state->operations.erase(found); - - erase_queued_offline_operation_events(live, operation_id); - return MLN_STATUS_OK; -} - auto offline_region_create_start( mln_runtime runtime, const mln_offline_region_definition* definition, const uint8_t* metadata, size_t metadata_size, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { @@ -1713,8 +1917,8 @@ auto offline_region_create_start( set_thread_error("offline region metadata must not be null when non-empty"); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -1732,17 +1936,15 @@ auto offline_region_create_start( std::memcpy(native_metadata.data(), metadata, metadata_size); } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_CREATE, - MLN_OFFLINE_OPERATION_RESULT_REGION, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_CREATE, completion, + [&](auto state) -> void { database->createOfflineRegion( native_definition, native_metadata, - [state, operation_id]( - mln::expected result - ) -> void { + [state](mln::expected result) + -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message( result.error(), "offline region creation failed" ) @@ -1752,14 +1954,12 @@ auto offline_region_create_start( auto data = to_c_region_data(result.value()); if (!data) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_UNSUPPORTED, + state, MLN_STATUS_UNSUPPORTED, "offline region definition type is unsupported" ); return; } - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::move(*data), true - ); + complete_offline_operation(state, MLN_STATUS_OK, std::move(*data)); } ); } @@ -1768,18 +1968,15 @@ auto offline_region_create_start( auto offline_region_get_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -1790,18 +1987,17 @@ auto offline_region_get_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_GET, - MLN_OFFLINE_OPERATION_RESULT_OPTIONAL_REGION, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_GET, completion, + [&](auto state) -> void { database->getOfflineRegion( region_id, - [state, operation_id]( + [state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -1809,22 +2005,19 @@ auto offline_region_get_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation( - state, operation_id, MLN_STATUS_OK, - std::optional{}, false + state, MLN_STATUS_OK, std::optional{} ); return; } auto data = to_c_region_data(region.value()); if (!data) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_UNSUPPORTED, + state, MLN_STATUS_UNSUPPORTED, "offline region definition type is unsupported" ); return; } - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::move(data), true - ); + complete_offline_operation(state, MLN_STATUS_OK, std::move(data)); } ); } @@ -1832,18 +2025,15 @@ auto offline_region_get_start( } auto offline_regions_list_start( - mln_runtime runtime, mln_offline_operation_id* out_operation_id + mln_runtime runtime, const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -1854,16 +2044,15 @@ auto offline_regions_list_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGIONS_LIST, - MLN_OFFLINE_OPERATION_RESULT_REGION_LIST, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGIONS_LIST, completion, + [&](auto state) -> void { database->listOfflineRegions( - [state, operation_id]( + [state]( mln::expected result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region list failed") ); return; @@ -1871,14 +2060,12 @@ auto offline_regions_list_start( auto regions = to_c_region_data_list(result.value()); if (!regions) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_UNSUPPORTED, + state, MLN_STATUS_UNSUPPORTED, "offline region definition type is unsupported" ); return; } - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::move(*regions), true - ); + complete_offline_operation(state, MLN_STATUS_OK, std::move(*regions)); } ); } @@ -1887,11 +2074,8 @@ auto offline_regions_list_start( auto offline_regions_merge_database_start( mln_runtime runtime, const char* side_database_path, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { @@ -1901,8 +2085,8 @@ auto offline_regions_merge_database_start( set_thread_error("side_database_path must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -1914,17 +2098,15 @@ auto offline_regions_merge_database_start( const auto path = std::string{side_database_path}; return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE, - MLN_OFFLINE_OPERATION_RESULT_REGION_LIST, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE, completion, + [&](auto state) -> void { database->mergeOfflineRegions( path, - [state, operation_id]( - mln::expected result - ) -> void { + [state](mln::expected result) + -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message( result.error(), "offline region database merge failed" ) @@ -1934,14 +2116,12 @@ auto offline_regions_merge_database_start( auto regions = to_c_region_data_list(result.value()); if (!regions) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_UNSUPPORTED, + state, MLN_STATUS_UNSUPPORTED, "offline region definition type is unsupported" ); return; } - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::move(*regions), true - ); + complete_offline_operation(state, MLN_STATUS_OK, std::move(*regions)); } ); } @@ -1950,11 +2130,8 @@ auto offline_regions_merge_database_start( auto offline_region_update_metadata_start( mln_runtime runtime, mln_offline_region_id region_id, const uint8_t* metadata, - size_t metadata_size, mln_offline_operation_id* out_operation_id + size_t metadata_size, const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { @@ -1964,8 +2141,8 @@ auto offline_region_update_metadata_start( set_thread_error("offline region metadata must not be null when non-empty"); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -1981,17 +2158,16 @@ auto offline_region_update_metadata_start( std::memcpy(native_metadata.data(), metadata, metadata_size); } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA, - MLN_OFFLINE_OPERATION_RESULT_REGION, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA, completion, + [&](auto state) -> void { database->updateOfflineMetadata( region_id, native_metadata, - [database, state, operation_id, region_id]( + [database, state, region_id]( mln::expected result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message( result.error(), "offline region metadata update failed" ) @@ -2000,14 +2176,14 @@ auto offline_region_update_metadata_start( } database->getOfflineRegion( region_id, - [state, operation_id]( + [state]( mln::expected< std::optional, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2015,21 +2191,20 @@ auto offline_region_update_metadata_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } auto data = to_c_region_data(region.value()); if (!data) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_UNSUPPORTED, + state, MLN_STATUS_UNSUPPORTED, "offline region definition type is unsupported" ); return; } complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::move(*data), true + state, MLN_STATUS_OK, std::move(*data) ); } ); @@ -2041,18 +2216,15 @@ auto offline_region_update_metadata_start( auto offline_region_get_status_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -2063,18 +2235,17 @@ auto offline_region_get_status_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_GET_STATUS, - MLN_OFFLINE_OPERATION_RESULT_REGION_STATUS, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_GET_STATUS, completion, + [&](auto state) -> void { database->getOfflineRegion( region_id, - [database, state, operation_id]( + [database, state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2082,19 +2253,18 @@ auto offline_region_get_status_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } database->getOfflineRegionStatus( region.value(), - [state, operation_id]( + [state]( mln::expected result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message( result.error(), "offline region status query failed" ) @@ -2102,8 +2272,7 @@ auto offline_region_get_status_start( return; } complete_offline_operation( - state, operation_id, MLN_STATUS_OK, to_c_status(result.value()), - true + state, MLN_STATUS_OK, to_c_status(result.value()) ); } ); @@ -2115,18 +2284,15 @@ auto offline_region_get_status_start( auto offline_region_set_observed_start( mln_runtime runtime, mln_offline_region_id region_id, bool observed, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -2135,20 +2301,20 @@ auto offline_region_set_observed_start( set_thread_error("database file source is unavailable"); return MLN_STATUS_NATIVE_ERROR; } + auto offline_event_state = live->offline_event_state; return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_SET_OBSERVED, completion, + [&, offline_event_state](auto state) -> void { database->getOfflineRegion( region_id, - [database, state, operation_id, region_id, observed]( + [database, state, region_id, observed, offline_event_state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2156,33 +2322,34 @@ auto offline_region_set_observed_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } - auto event_state = std::shared_ptr{}; { - const std::scoped_lock state_lock(state->mutex); - if (!state->alive || state->runtime == nullptr) { + const std::scoped_lock event_state_lock(offline_event_state->mutex); + if ( + !offline_event_state->alive || + offline_event_state->runtime == nullptr + ) { + complete_offline_operation( + state, MLN_STATUS_OK, std::monostate{} + ); return; } set_offline_region_observed_flag( - state->runtime, region_id, observed + offline_event_state->runtime, region_id, observed ); - event_state = state->runtime->offline_event_state; } auto observer = observed ? std::make_unique( - std::move(event_state), region_id + offline_event_state, region_id ) : nullptr; database->setOfflineRegionObserver( region.value(), std::move(observer) ); - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::monostate{} - ); + complete_offline_operation(state, MLN_STATUS_OK, std::monostate{}); } ); } @@ -2191,11 +2358,8 @@ auto offline_region_set_observed_start( auto offline_region_set_download_state_start( mln_runtime runtime, OfflineRegionDownloadStateRequest request, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { @@ -2206,8 +2370,8 @@ auto offline_region_set_download_state_start( set_thread_error("offline region download state is invalid"); return MLN_STATUS_INVALID_ARGUMENT; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -2218,18 +2382,17 @@ auto offline_region_set_download_state_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_SET_DOWNLOAD_STATE, completion, + [&](auto state) -> void { database->getOfflineRegion( request.region_id, - [database, state, operation_id, native_state]( + [database, state, native_state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2237,17 +2400,14 @@ auto offline_region_set_download_state_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } database->setOfflineRegionDownloadState( region.value(), *native_state ); - complete_offline_operation( - state, operation_id, MLN_STATUS_OK, std::monostate{} - ); + complete_offline_operation(state, MLN_STATUS_OK, std::monostate{}); } ); } @@ -2256,18 +2416,15 @@ auto offline_region_set_download_state_start( auto offline_region_invalidate_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -2278,18 +2435,17 @@ auto offline_region_invalidate_start( } return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_INVALIDATE, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_INVALIDATE, completion, + [&](auto state) -> void { database->getOfflineRegion( region_id, - [database, state, operation_id]( + [database, state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2297,17 +2453,14 @@ auto offline_region_invalidate_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } database->invalidateOfflineRegion( - region.value(), - [state, operation_id](std::exception_ptr exception) -> void { + region.value(), [state](std::exception_ptr exception) -> void { complete_from_exception( - state, operation_id, exception, - "offline region invalidation failed" + state, exception, "offline region invalidation failed" ); } ); @@ -2319,18 +2472,15 @@ auto offline_region_invalidate_start( auto offline_region_delete_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status { - if (out_operation_id != nullptr) { - *out_operation_id = 0; - } mln::core::RuntimeObject* live = nullptr; const auto runtime_status = validate_runtime(runtime, live); if (runtime_status != MLN_STATUS_OK) { return runtime_status; } - if (out_operation_id == nullptr) { - set_thread_error("out_operation_id must not be null"); + if (completion == nullptr) { + set_thread_error("completion must not be null"); return MLN_STATUS_INVALID_ARGUMENT; } @@ -2339,20 +2489,20 @@ auto offline_region_delete_start( set_thread_error("database file source is unavailable"); return MLN_STATUS_NATIVE_ERROR; } + auto offline_event_state = live->offline_event_state; return schedule_registered_offline_operation( - live, MLN_OFFLINE_OPERATION_REGION_DELETE, - MLN_OFFLINE_OPERATION_RESULT_NONE, out_operation_id, - [&](auto state, auto operation_id) -> void { + live, MLN_OFFLINE_OPERATION_REGION_DELETE, completion, + [&, offline_event_state](auto state) -> void { database->getOfflineRegion( region_id, - [database, state, operation_id, region_id]( + [database, state, region_id, offline_event_state]( mln::expected, std::exception_ptr> result ) -> void { if (!result) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_NATIVE_ERROR, + state, MLN_STATUS_NATIVE_ERROR, exception_message(result.error(), "offline region get failed") ); return; @@ -2360,27 +2510,29 @@ auto offline_region_delete_start( const auto& region = result.value(); if (!region.has_value()) { complete_offline_operation_error( - state, operation_id, MLN_STATUS_INVALID_ARGUMENT, - "offline region not found" + state, MLN_STATUS_INVALID_ARGUMENT, "offline region not found" ); return; } { - const std::scoped_lock state_lock(state->mutex); - if (!state->alive || state->runtime == nullptr) { - return; + const std::scoped_lock event_state_lock(offline_event_state->mutex); + if ( + offline_event_state->alive && + offline_event_state->runtime != nullptr + ) { + set_offline_region_observed_flag( + offline_event_state->runtime, region_id, false + ); } - set_offline_region_observed_flag(state->runtime, region_id, false); } database->setOfflineRegionObserver(region.value(), nullptr); database->setOfflineRegionDownloadState( region.value(), mln::OfflineRegionDownloadState::Inactive ); database->deleteOfflineRegion( - region.value(), - [state, operation_id](std::exception_ptr exception) -> void { + region.value(), [state](std::exception_ptr exception) -> void { complete_from_exception( - state, operation_id, exception, "offline region deletion failed" + state, exception, "offline region deletion failed" ); } ); @@ -2390,412 +2542,52 @@ auto offline_region_delete_start( ); } -auto offline_region_create_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region +auto offline_region_snapshot_get( + mln_offline_region_snapshot snapshot, mln_offline_region_info* out_info ) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + // Offline snapshots and lists carry no thread affinity, so the lock spans the + // read to keep another thread from destroying one mid-read. + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_snapshot = table.resolve_locked(snapshot); + if (live_snapshot == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - if (out_region == nullptr || *out_region != MLN_HANDLE_NULL) { - set_thread_error("out_region must point to the null handle"); + return fill_region_info(live_snapshot->data, out_info); +} + +auto offline_region_snapshot_destroy( + mln_offline_region_snapshot snapshot +) noexcept -> void { + static_cast( + handle_table().remove(snapshot) + ); +} + +auto offline_region_list_count(mln_offline_region_list list, size_t* out_count) + -> mln_status { + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - auto state = live->offline_operation_state; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGION_CREATE || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = std::get_if(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } + if (out_count == nullptr) { + set_thread_error("out_count must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; } - auto snapshot = register_offline_region_snapshot_from_result( - [state, operation_id](OfflineRegionData& data) -> void { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - throw std::logic_error( - "offline operation disappeared while taking result" - ); - } - auto* result = std::get_if(&found->second.result); - if (result == nullptr) { - throw std::logic_error( - "offline operation result kind changed while taking result" - ); - } - data = std::move(*result); - } - ); - erase_offline_operation_registration(live, operation_id); - *out_region = snapshot; + *out_count = live_list->regions.size(); return MLN_STATUS_OK; } -auto offline_region_get_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region, bool* out_found +auto offline_region_list_get( + mln_offline_region_list list, size_t index, mln_offline_region_info* out_info ) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_region == nullptr || *out_region != MLN_HANDLE_NULL || - out_found == nullptr - ) { - set_thread_error( - "out_region must point to the null handle and out_found must not be null" - ); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto state = live->offline_operation_state; - auto has_region = false; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGION_GET || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = - std::get_if>(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } - has_region = result->has_value(); - } - if (!has_region) { - erase_offline_operation_registration(live, operation_id); - *out_found = false; - return MLN_STATUS_OK; - } - auto snapshot = register_offline_region_snapshot_from_result( - [state, operation_id](OfflineRegionData& data) -> void { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - throw std::logic_error( - "offline operation disappeared while taking result" - ); - } - auto* result = - std::get_if>(&found->second.result); - if (result == nullptr || !result->has_value()) { - throw std::logic_error( - "offline operation result kind changed while taking result" - ); - } - data = std::move(**result); - } - ); - erase_offline_operation_registration(live, operation_id); - *out_region = snapshot; - *out_found = true; - return MLN_STATUS_OK; -} - -auto offline_regions_list_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_regions == nullptr || *out_regions != MLN_HANDLE_NULL) { - set_thread_error("out_regions must point to the null handle"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto state = live->offline_operation_state; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGIONS_LIST || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = - std::get_if>(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } - } - auto regions = register_offline_region_list_from_result( - [state, operation_id](std::vector& destination) -> void { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - throw std::logic_error( - "offline operation disappeared while taking result" - ); - } - auto* result = - std::get_if>(&found->second.result); - if (result == nullptr) { - throw std::logic_error( - "offline operation result kind changed while taking result" - ); - } - destination = std::move(*result); - } - ); - erase_offline_operation_registration(live, operation_id); - *out_regions = regions; - return MLN_STATUS_OK; -} - -auto offline_regions_merge_database_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_regions == nullptr || *out_regions != MLN_HANDLE_NULL) { - set_thread_error("out_regions must point to the null handle"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto state = live->offline_operation_state; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGIONS_MERGE_DATABASE || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = - std::get_if>(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } - } - auto regions = register_offline_region_list_from_result( - [state, operation_id](std::vector& destination) -> void { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - throw std::logic_error( - "offline operation disappeared while taking result" - ); - } - auto* result = - std::get_if>(&found->second.result); - if (result == nullptr) { - throw std::logic_error( - "offline operation result kind changed while taking result" - ); - } - destination = std::move(*result); - } - ); - erase_offline_operation_registration(live, operation_id); - *out_regions = regions; - return MLN_STATUS_OK; -} - -auto offline_region_update_metadata_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_region == nullptr || *out_region != MLN_HANDLE_NULL) { - set_thread_error("out_region must point to the null handle"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto state = live->offline_operation_state; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGION_UPDATE_METADATA || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = std::get_if(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } - } - auto snapshot = register_offline_region_snapshot_from_result( - [state, operation_id](OfflineRegionData& data) -> void { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - throw std::logic_error( - "offline operation disappeared while taking result" - ); - } - auto* result = std::get_if(&found->second.result); - if (result == nullptr) { - throw std::logic_error( - "offline operation result kind changed while taking result" - ); - } - data = std::move(*result); - } - ); - erase_offline_operation_registration(live, operation_id); - *out_region = snapshot; - return MLN_STATUS_OK; -} - -auto offline_region_get_status_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_status* out_status -) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if ( - out_status == nullptr || - out_status->size < sizeof(mln_offline_region_status) - ) { - set_thread_error("out_status must not be null and must have a valid size"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto state = live->offline_operation_state; - auto result_status = mln_offline_region_status{}; - { - const std::scoped_lock lock(state->mutex); - auto found = state->operations.find(operation_id); - if (found == state->operations.end()) { - set_thread_error("offline operation is unknown"); - return MLN_STATUS_INVALID_ARGUMENT; - } - auto& operation = found->second; - if ( - !operation.completed || - operation.kind != MLN_OFFLINE_OPERATION_REGION_GET_STATUS || - operation.result_status != MLN_STATUS_OK - ) { - set_offline_result_unavailable_error(operation); - return MLN_STATUS_INVALID_STATE; - } - auto* result = std::get_if(&operation.result); - if (result == nullptr) { - set_thread_error("offline operation result kind is invalid"); - return MLN_STATUS_INVALID_STATE; - } - result_status = *result; - } - *out_status = result_status; - erase_offline_operation_registration(live, operation_id); - return MLN_STATUS_OK; -} - -auto offline_region_snapshot_get( - mln_offline_region_snapshot snapshot, mln_offline_region_info* out_info -) -> mln_status { - // Offline snapshots and lists carry no thread affinity, so the lock spans the - // read to keep another thread from destroying one mid-read. - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_snapshot = table.resolve_locked(snapshot); - if (live_snapshot == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - return fill_region_info(live_snapshot->data, out_info); -} - -auto offline_region_snapshot_destroy( - mln_offline_region_snapshot snapshot -) noexcept -> void { - static_cast( - handle_table().remove(snapshot) - ); -} - -auto offline_region_list_count(mln_offline_region_list list, size_t* out_count) - -> mln_status { - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; - } - if (out_count == nullptr) { - set_thread_error("out_count must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - *out_count = live_list->regions.size(); - return MLN_STATUS_OK; -} - -auto offline_region_list_get( - mln_offline_region_list list, size_t index, mln_offline_region_info* out_info -) -> mln_status { - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - const auto* live_list = table.resolve_locked(list); - if (live_list == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; + auto& table = handle_table(); + const std::scoped_lock lock(table.mutex()); + const auto* live_list = table.resolve_locked(list); + if (live_list == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } if (index >= live_list->regions.size()) { set_thread_error("offline region list index is out of range"); @@ -2810,13 +2602,15 @@ auto offline_region_list_destroy(mln_offline_region_list list) noexcept } auto set_resource_provider( - mln_runtime runtime, const mln_resource_provider* provider + mln_runtime runtime, const mln_resource_provider* provider, + const mln_completion* completion ) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; if (provider == nullptr) { set_thread_error("provider must not be null"); return MLN_STATUS_INVALID_ARGUMENT; @@ -2830,357 +2624,350 @@ auto set_resource_provider( return MLN_STATUS_INVALID_ARGUMENT; } - // The exclusive provider lock waits for every callback that leased the - // previous provider, so that callback and its `user_data` are unreferenced - // once this returns. - auto& state = *live->resource_provider_state; - const std::unique_lock lock(state.mutex); - state.registered = true; - state.provider = ResourceProvider{ + const auto context = std::make_shared( + provider->user_data, provider->release_user_data + ); + const auto copied = ResourceProvider{ .callback = provider->callback, - .user_data = provider->user_data, + .context = context, }; - return MLN_STATUS_OK; + context->transfer_to_runtime(); + const auto state = live->resource_provider_state; + const auto status = submit_resource_configuration_command( + live, completion, [state, copied]() -> void { + auto previous = ResourceProvider{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->provider, copied); + } + } + ); + if (status != MLN_STATUS_OK) context->return_to_caller(); + return status; } -auto clear_resource_provider(mln_runtime runtime) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; +auto clear_resource_provider( + mln_runtime runtime, const mln_completion* completion +) -> mln_status { + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (validate_completion(completion) != MLN_STATUS_OK) + return MLN_STATUS_INVALID_ARGUMENT; - auto& state = *live->resource_provider_state; - const std::unique_lock lock(state.mutex); - state.registered = false; - state.provider = ResourceProvider{}; - return MLN_STATUS_OK; -} - -auto destroy_runtime(mln_runtime runtime) -> mln_status { - // Take ownership of the runtime out of the table, then release the - // process-global table lock before any teardown step that can block on a - // native callback. The waits below then stall this runtime alone. - std::shared_ptr owned_runtime; - auto owner_thread = std::thread::id{}; - { - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - auto* live = table.resolve_locked(runtime); - if (live == nullptr) { - return MLN_STATUS_INVALID_ARGUMENT; + const auto state = live->resource_provider_state; + return submit_resource_configuration_command( + live, completion, [state]() -> void { + auto previous = ResourceProvider{}; + { + const std::unique_lock lock(state->mutex); + previous = std::exchange(state->provider, ResourceProvider{}); + } } + ); +} - if (live->owner_thread != std::this_thread::get_id()) { - set_thread_error("runtime must be destroyed on its owner thread"); - return MLN_STATUS_WRONG_THREAD; - } +namespace { - if (live->live_maps != 0) { - set_thread_error("runtime still owns live maps"); - return MLN_STATUS_INVALID_STATE; - } +auto prior_runtime_submissions_terminal( + const RuntimeObject& runtime, uint64_t sequence +) noexcept -> bool { + return runtime.pending_submissions.empty() || + *runtime.pending_submissions.begin() >= sequence; +} - owner_thread = live->owner_thread; - owned_runtime = table.remove_locked(runtime); - } - { - const std::scoped_lock lock(live_runtime_threads_mutex()); - live_runtime_threads().erase(owner_thread); - } - unregister_platform_context(owned_runtime->platform_context); +auto wait_for_prior_runtime_submissions( + const std::shared_ptr& runtime, uint64_t sequence +) noexcept -> void { + auto lock = std::unique_lock{runtime->terminal_mutex}; + runtime->terminal_condition.wait(lock, [&]() noexcept -> bool { + return prior_runtime_submissions_terminal(*runtime, sequence); + }); +} - // A resource transform callback that entered `invoke_resource_transform()` - // before the erase above holds a shared transform lock, so this wait covers - // every callback that can still observe the runtime. A lease that reaches the - // shared lock afterwards reads the cleared registration and calls nothing. +auto release_runtime_reachable_state( + const std::shared_ptr& runtime +) -> void { { - auto& transform_state = *owned_runtime->resource_transform_state; - const std::unique_lock transform_lock(transform_state.mutex); - transform_state.callback = nullptr; - transform_state.user_data = nullptr; + auto& transform_state = *runtime->resource_transform_state; + auto previous = std::shared_ptr{}; + { + const std::unique_lock transform_lock(transform_state.mutex); + previous = std::exchange(transform_state.registration, nullptr); + } } - { - auto& transform_state = *owned_runtime->http_header_transform_state; - const std::unique_lock transform_lock(transform_state.mutex); - transform_state.callback = nullptr; - transform_state.user_data = nullptr; + auto& transform_state = *runtime->http_header_transform_state; + auto previous = std::shared_ptr{}; + { + const std::unique_lock transform_lock(transform_state.mutex); + previous = std::exchange(transform_state.registration, nullptr); + } } - - // Same wait for a resource provider callback that leased the provider before - // the erase above. { - auto& provider_state = *owned_runtime->resource_provider_state; - const std::unique_lock provider_lock(provider_state.mutex); - provider_state.registered = false; - provider_state.provider = ResourceProvider{}; + auto& provider_state = *runtime->resource_provider_state; + auto previous = ResourceProvider{}; + { + const std::unique_lock provider_lock(provider_state.mutex); + previous = std::exchange(provider_state.provider, ResourceProvider{}); + } } - { - const std::scoped_lock state_lock( - owned_runtime->offline_event_state->mutex - ); - owned_runtime->offline_event_state->alive = false; - owned_runtime->offline_event_state->runtime = nullptr; - const std::scoped_lock event_lock(owned_runtime->event_mutex); - owned_runtime->observed_offline_regions.clear(); - std::erase_if(owned_runtime->events, [](const auto& event) -> bool { - return event.has_offline_region; - }); + const std::scoped_lock state_lock(runtime->offline_event_state->mutex); + runtime->offline_event_state->alive = false; + runtime->offline_event_state->runtime = nullptr; + const std::scoped_lock event_lock(runtime->event_queue->mutex); + runtime->event_queue->observed_offline_regions.clear(); } + auto event_wake = std::shared_ptr{}; { - const std::scoped_lock state_lock( - owned_runtime->offline_operation_state->mutex - ); - owned_runtime->offline_operation_state->alive = false; - owned_runtime->offline_operation_state->runtime = nullptr; - owned_runtime->offline_operation_state->operations.clear(); - const std::scoped_lock event_lock(owned_runtime->event_mutex); - std::erase_if(owned_runtime->events, [](const auto& event) -> bool { - return event.has_offline_operation; - }); + const std::scoped_lock event_lock(runtime->event_queue->mutex); + runtime->event_queue->pending.events.clear(); + runtime->event_queue->pending.messages.clear(); + runtime->event_queue->event_maps.clear(); + runtime->event_queue->observed_offline_regions.clear(); + event_wake = std::move(runtime->event_queue->wake); } + event_wake.reset(); + runtime->database_source.reset(); +} - // The last drained batch is freed with the runtime, the documented end of its - // readable window. - owned_runtime->event_drain_staging.clear(); - owned_runtime->event_batch_events.clear(); - owned_runtime->event_batch_messages.clear(); +} // namespace - // Retiring the wake state before the run loop is released covers a late - // `mln_wake_source_signal()` and the run loop teardown's final iteration. - { - const std::scoped_lock wake_lock(owned_runtime->wake_state->mutex); - owned_runtime->wake_state->alive = false; - owned_runtime->wake_state->signaled = false; +auto runtime_barrier_start( + mln_runtime runtime, const mln_completion* completion +) -> mln_status { + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) return completion_status; + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + auto completion_state = std::make_shared(*completion); + auto state = std::make_shared( + [completion_state](mln_status status, std::string diagnostic, std::any) { + complete(completion_state, status, std::move(diagnostic)); + } + ); - // Releasing the run loop and the database file source can join native - // threads, so it happens with the registry lock released. - owned_runtime.reset(); - return MLN_STATUS_OK; -} - -auto signal_wake(const std::shared_ptr& state) noexcept -> void { - if (!state) { - return; - } + auto sequence = uint64_t{0}; { - const std::scoped_lock lock(state->mutex); - if (!state->alive) { - return; + const std::scoped_lock commit_lock(live->submission_mutex); + if (!live->control.acquire()) { + completion_state->reject(); + return MLN_STATUS_INVALID_STATE; + } + auto lease = ControlLease{&live->control}; + sequence = live->next_submission_sequence++; + { + const std::scoped_lock terminal_lock(live->terminal_mutex); + live->pending_submissions.insert(sequence); + live->pending_barriers.emplace(sequence, state); + } + state->set_terminal_callback([live, sequence]() noexcept -> void { + erase_runtime_barrier(live, sequence); + }); + try { + live->executor.invoke([lease = std::move(lease)]() mutable -> void { + static_cast(lease); + }); + } catch (...) { + state->complete( + MLN_STATUS_NATIVE_ERROR, + exception_message(std::current_exception(), "runtime barrier failed"), + std::any{std::monostate{}} + ); + return MLN_STATUS_OK; } - state->signaled = true; } - // MapLibre calls this while holding the `RunLoop` mutex, so the notify - // happens outside the wake lock to keep that path short. - state->condition.notify_all(); + complete_ready_runtime_barriers(live); + completion_state->accept(); + return MLN_STATUS_OK; } -auto pump_runtime(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) +auto release_runtime(mln_runtime runtime, const mln_completion* completion) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + const auto completion_status = validate_completion(completion); + if (completion_status != MLN_STATUS_OK) { + return completion_status; } - - // Queued unread events return the call without parking, so a host that - // stopped polling before the queue emptied keeps making progress. - auto queued_events = false; - { - const std::scoped_lock event_lock(live->event_mutex); - queued_events = !live->events.empty(); + auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; + } + auto teardown_completion = std::shared_ptr{}; + try { + teardown_completion = std::make_shared(*completion); + } catch (...) { + set_thread_error("runtime release could not allocate its completion"); + return MLN_STATUS_NATIVE_ERROR; } - { - auto& wake = *live->wake_state; - std::unique_lock lock(wake.mutex); - if (!queued_events && timeout_ms != 0 && !wake.signaled) { - if (timeout_ms < 0) { - wake.condition.wait(lock, [&wake]() -> bool { return wake.signaled; }); - } else { - wake.condition.wait_for( - lock, std::chrono::milliseconds{timeout_ms}, - [&wake]() -> bool { return wake.signaled; } - ); + const std::scoped_lock commit_lock(live->submission_mutex); + const auto release_status = live->control.begin_close(); + if (release_status != MLN_STATUS_OK) { + teardown_completion->reject(); + return release_status; + } + } + struct ReleaseGate { + std::mutex mutex; + std::condition_variable condition; + bool committed = false; + bool aborted = false; + uint64_t sequence = 0; + std::shared_ptr live; + std::shared_ptr teardown_completion; + }; + auto gate = std::shared_ptr{}; + try { + gate = std::make_shared(); + } catch (...) { + live->control.abort_close(); + teardown_completion->reject(); + set_thread_error("runtime release could not allocate its teardown gate"); + return MLN_STATUS_NATIVE_ERROR; + } + gate->live = live; + gate->teardown_completion = teardown_completion; + auto waiter = std::thread{}; + try { + // Teardown stops and joins the executor, so it cannot run on that executor + // or on the caller that may also service host callbacks. + waiter = std::thread([gate]() mutable -> void { + { + auto lock = std::unique_lock{gate->mutex}; + gate->condition.wait(lock, [&]() noexcept -> bool { + return gate->committed || gate->aborted; + }); + if (gate->aborted) { + return; + } + } + auto live = std::move(gate->live); + wait_for_prior_runtime_submissions(live, gate->sequence); + live->control.wait_for_submissions(); + unregister_platform_context(live->platform_context); + try { + live->executor.invoke_sync([live]() -> void { + release_runtime_reachable_state(live); + }); + } catch (...) { + // The public handle is already retired, so teardown failures are not + // actionable by its former owner. } + live->executor.stop(); + // Drop this thread's reference before reporting, so a host that waits + // for the completion and then exits cannot race member destruction. + // Nothing after complete() touches library state. + live.reset(); + complete(gate->teardown_completion, MLN_STATUS_OK); + }); + } catch (...) { + live->control.abort_close(); + teardown_completion->reject(); + set_thread_error("runtime release could not start its teardown waiter"); + return MLN_STATUS_NATIVE_ERROR; + } + try { + waiter.detach(); + } catch (...) { + { + const std::scoped_lock lock(gate->mutex); + gate->aborted = true; + } + gate->condition.notify_one(); + if (waiter.joinable()) { + waiter.join(); } - // The flag is cleared before the drain, so work arriving during the drain - // leaves it set for the next pump. - wake.signaled = false; + live->control.abort_close(); + teardown_completion->reject(); + set_thread_error("runtime release could not detach its teardown waiter"); + return MLN_STATUS_NATIVE_ERROR; } - if (budget_ms >= 0) { - // A budget past the clock's range would overflow the addition, so it - // saturates to an unbounded drain. - const auto now = std::chrono::steady_clock::now(); - const auto headroom = std::chrono::duration_cast( - std::chrono::steady_clock::time_point::max() - now - ); - live->pump_deadline = std::chrono::milliseconds{budget_ms} < headroom - ? now + std::chrono::milliseconds{budget_ms} - : std::chrono::steady_clock::time_point::max(); - } else { - live->pump_deadline.reset(); + { + const std::scoped_lock commit_lock(live->submission_mutex); + gate->sequence = live->next_submission_sequence++; + static_cast(handle_table().remove(runtime)); } - live->pump_ran_task = false; - live->pump_budget_exhausted = false; - live->run_loop->runOnce(); - live->pump_deadline.reset(); - // The gate only denies while a task is queued, so an exhausted budget means - // work remains. - if (live->pump_budget_exhausted) { - signal_wake(live->wake_state); + { + const std::scoped_lock lock(gate->mutex); + gate->committed = true; } + gate->condition.notify_one(); + teardown_completion->accept(); return MLN_STATUS_OK; } -auto acquire_wake_source(mln_runtime runtime, mln_wake_source* out_source) +auto drain_runtime_events(mln_runtime runtime, mln_event_batch* out_batch) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; - } - if (out_source == nullptr) { - set_thread_error("out_source must not be null"); - return MLN_STATUS_INVALID_ARGUMENT; - } - if (*out_source != MLN_HANDLE_NULL) { - set_thread_error("out_source must point to the null handle"); + if (out_batch == nullptr || *out_batch != MLN_HANDLE_NULL) { + set_thread_error("out_batch must point to the null handle"); return MLN_STATUS_INVALID_ARGUMENT; } - *out_source = handle_table().insert( - std::make_shared(live->wake_state) - ); - return MLN_STATUS_OK; -} - -auto signal_wake_source(mln_wake_source source) -> mln_status { - const auto live = handle_table().lease(source); + const auto live = lease_runtime(runtime); if (live == nullptr) { return MLN_STATUS_INVALID_ARGUMENT; } - - signal_wake(live->state); + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; + } + auto control_lease = ControlLease{&live->control}; + auto queue = live->event_queue; + auto owned = std::make_shared(); + const auto batch_handle = handle_table().insert(owned); + { + const std::scoped_lock lock(queue->mutex); + owned->storage.events.swap(queue->pending.events); + owned->storage.messages.swap(queue->pending.messages); + } + *out_batch = batch_handle; return MLN_STATUS_OK; } -auto destroy_wake_source(mln_wake_source source) noexcept -> void { - static_cast(handle_table().remove(source)); -} - -auto drain_runtime_events( - mln_runtime runtime, size_t max_events, mln_runtime_event_batch* out_batch +auto get_event_batch( + mln_event_batch batch, mln_runtime_event_batch_view* out_view ) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + const auto live = handle_table().lease(batch); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } if ( - out_batch == nullptr || out_batch->size < sizeof(mln_runtime_event_batch) + out_view == nullptr || out_view->size < sizeof(mln_runtime_event_batch_view) ) { - set_thread_error("out_batch must not be null and must have a valid size"); + set_thread_error("out_view must not be null and must have a valid size"); return MLN_STATUS_INVALID_ARGUMENT; } - - // Clearing first ends the previous batch's window, so an empty drain - // invalidates it too. Capacity survives, so a steady-state drain allocates - // nothing. - auto& staging = live->event_drain_staging; - auto& events = live->event_batch_events; - auto& messages = live->event_batch_messages; - staging.clear(); - events.clear(); - messages.clear(); - - auto remaining_count = size_t{0}; - auto arena_size = size_t{0}; - auto drain_count = size_t{0}; - { - const std::scoped_lock lock(live->event_mutex); - for (const auto& queued : live->events) { - if (max_events != 0 && drain_count >= max_events) { - break; - } - // The first event always fits, so a bounded drain always makes progress. - // An event without a message takes no arena bytes, not even a terminator. - const auto& next_message = queued.message; - const auto next_size = - next_message.empty() ? size_t{0} : next_message.size() + 1; - if ( - drain_count != 0 && - next_size > static_cast(UINT32_MAX) - arena_size - ) { - break; - } - arena_size += next_size; - drain_count += 1; - } - - // Finish every allocation before removing an event. The reserved staging - // moves and batch writes below cannot allocate, so a failed reservation - // leaves the complete queue available to the next drain. - static_assert( - std::is_nothrow_move_constructible_v - ); - staging.reserve(drain_count); - events.reserve(drain_count); - messages.reserve(arena_size); - for (auto index = size_t{0}; index < drain_count; index += 1) { - staging.push_back(std::move(live->events.front())); - live->events.pop_front(); - } - remaining_count = live->events.size(); - } - - // The batch is built outside the lock, so the lock hold covers only the - // queue moves above. - for (const auto& staged : staging) { - const auto message_size = static_cast(staged.message.size()); - const auto message_offset = static_cast(messages.size()); - events.push_back( - mln_runtime_event{ - .type = staged.type, - .source_type = staged.source_type, - .source = staged.source, - .code = staged.code, - .payload_type = staged.payload_type, - .message_offset = message_size == 0 ? 0 : message_offset, - .message_size = message_size, - .payload = staged.payload - } - ); - if (message_size != 0) { - messages.append(staged.message); - messages.push_back('\0'); - } - } - staging.clear(); - - *out_batch = mln_runtime_event_batch{ - .size = sizeof(mln_runtime_event_batch), + *out_view = mln_runtime_event_batch_view{ + .size = sizeof(mln_runtime_event_batch_view), .event_size = sizeof(mln_runtime_event), - .events = events.empty() ? nullptr : events.data(), - .event_count = events.size(), - .messages = messages.empty() ? nullptr : messages.data(), - .messages_size = messages.size(), - .remaining_count = remaining_count + .events = + live->storage.events.empty() ? nullptr : live->storage.events.data(), + .event_count = live->storage.events.size(), + .messages = + live->storage.messages.empty() ? nullptr : live->storage.messages.data(), + .messages_size = live->storage.messages.size() }; return MLN_STATUS_OK; } +auto release_event_batch(mln_event_batch batch) noexcept -> void { + static_cast(handle_table().remove(batch)); +} + auto set_runtime_event_mask(mln_runtime runtime, uint64_t mask) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + const auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; + } + auto control_lease = ControlLease{&live->control}; if ((mask & ~static_cast(MLN_RUNTIME_EVENT_MASK_ALL)) != 0U) { set_thread_error("mask contains unknown bits"); return MLN_STATUS_INVALID_ARGUMENT; @@ -3193,11 +2980,14 @@ auto set_runtime_event_mask(mln_runtime runtime, uint64_t mask) -> mln_status { auto get_runtime_event_mask(mln_runtime runtime, uint64_t* out_mask) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + const auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } + if (!live->control.acquire()) { + return MLN_STATUS_INVALID_STATE; + } + auto control_lease = ControlLease{&live->control}; if (out_mask == nullptr) { set_thread_error("out_mask must not be null"); return MLN_STATUS_INVALID_ARGUMENT; @@ -3207,27 +2997,26 @@ auto get_runtime_event_mask(mln_runtime runtime, uint64_t* out_mask) } auto retain_runtime_map(mln_runtime runtime) -> mln_status { - mln::core::RuntimeObject* live = nullptr; - const auto status = validate_runtime(runtime, live); - if (status != MLN_STATUS_OK) { - return status; + const auto live = lease_runtime(runtime); + if (live == nullptr) { + return MLN_STATUS_INVALID_ARGUMENT; } - - ++live->live_maps; + if (!live->control.reserve_child()) { + return MLN_STATUS_INVALID_STATE; + } + live->control.commit_child(); return MLN_STATUS_OK; } auto release_runtime_map(mln_runtime runtime) noexcept -> void { - auto& table = handle_table(); - const std::scoped_lock lock(table.mutex()); - auto* live = table.try_resolve_locked(runtime); - if (live != nullptr && live->live_maps != 0) { - --live->live_maps; + const auto live = handle_table().try_lease(runtime); + if (live != nullptr) { + live->control.release_child(); } } auto runtime_run_loop(RuntimeObject* runtime) -> mln::util::RunLoop& { - return *runtime->run_loop; + return *runtime->executor.run_loop(); } auto resource_options_for_runtime(mln_runtime runtime) -> mln::ResourceOptions { @@ -3256,10 +3045,10 @@ auto acquire_resource_provider_for_platform_context( // The lease keeps this state readable, so the shared lock is taken with the // registry lock released. A callback that reaches the shared lock before - // `destroy_runtime()` holds teardown until it returns; one that arrives later - // finds an empty registration. + // committed close clears the registration holds teardown until it returns; + // one that arrives later finds an empty registration. std::shared_lock provider_lock(state->mutex); - if (!state->registered) { + if (state->provider.callback == nullptr) { return std::nullopt; } return ResourceProviderLease{ @@ -3276,7 +3065,7 @@ auto has_resource_transform_for_platform_context( } const std::shared_lock transform_lock(state->mutex); - return state->callback != nullptr; + return state->registration != nullptr; } auto invoke_resource_transform( @@ -3290,8 +3079,8 @@ auto invoke_resource_transform( // Same locking rule as `acquire_resource_provider_for_platform_context()`. const std::shared_lock transform_lock(state->mutex); - const auto callback = state->callback; - if (callback == nullptr) { + const auto registration = state->registration; + if (registration == nullptr) { return MLN_STATUS_OK; } @@ -3301,7 +3090,9 @@ auto invoke_resource_transform( .context = &out_replacement_url, }; try { - const auto status = callback(state->user_data, kind, url, &response); + const auto status = registration->callback( + registration->context->user_data(), kind, url, &response + ); if ( status == MLN_STATUS_OK && response.url != nullptr && *response.url != '\0' @@ -3323,8 +3114,8 @@ auto invoke_http_header_transform( } const std::shared_lock transform_lock(state->mutex); - const auto callback = state->callback; - if (callback == nullptr) { + const auto registration = state->registration; + if (registration == nullptr) { return {}; } @@ -3334,7 +3125,11 @@ auto invoke_http_header_transform( .context = &headers, }; try { - if (callback(state->user_data, kind, url, &response) != MLN_STATUS_OK) { + if ( + registration->callback( + registration->context->user_data(), kind, url, &response + ) != MLN_STATUS_OK + ) { return {}; } return headers; @@ -3359,26 +3154,30 @@ auto push_runtime_map_event_payload( mln_runtime runtime, mln_map map, uint32_t type, uint32_t payload_type, const mln_runtime_event_payload& payload, int32_t code, std::string message ) -> void { - // try_resolve, not resolve: this runs from map observer callbacks under a - // pump, where writing a diagnostic would clobber the pump's own. - auto* live = handle_table().try_resolve(runtime); + // Observer callbacks may race close; the lease keeps the queue reachable + // through the copied event and wake callback. + auto live = lease_runtime(runtime); if (live == nullptr) { return; } - auto event = QueuedRuntimeEvent{ + auto event = mln_runtime_event{ .type = type, .source_type = MLN_RUNTIME_EVENT_SOURCE_MAP, .source = map, .code = code, .payload_type = payload_type, - .payload = payload, - .message = truncated_event_message(std::move(message)) + .message_offset = 0, + .message_size = 0, + .payload = payload }; + auto should_wake = false; { - const std::scoped_lock lock(live->event_mutex); - if (map != MLN_HANDLE_NULL && !live->event_maps.contains(map)) { + const std::scoped_lock lock(live->event_queue->mutex); + if ( + map != MLN_HANDLE_NULL && !live->event_queue->event_maps.contains(map) + ) { return; } // A render draws the latest update, so one unread render-update event @@ -3386,37 +3185,38 @@ auto push_runtime_map_event_payload( // alone preserves the order of every other event. if ( type == MLN_RUNTIME_EVENT_MAP_RENDER_UPDATE_AVAILABLE && - map != MLN_HANDLE_NULL && !live->events.empty() && - live->events.back().type == type && live->events.back().source == map + map != MLN_HANDLE_NULL && !live->event_queue->pending.events.empty() && + live->event_queue->pending.events.back().type == type && + live->event_queue->pending.events.back().source == map ) { return; } - live->events.push_back(std::move(event)); + should_wake = live->event_queue->pending.events.empty(); + append_runtime_event(live->event_queue->pending, event, std::move(message)); } - signal_wake(live->wake_state); + if (should_wake) live->event_queue->wake->notify(); } -auto register_runtime_map_events(mln_runtime runtime, mln_map map) -> void { - auto* live = handle_table().try_resolve(runtime); - if (live == nullptr || map == MLN_HANDLE_NULL) { +auto register_runtime_map_events( + mln_runtime runtime, mln_map map, std::shared_ptr event_state +) -> void { + auto live = lease_runtime(runtime); + if (live == nullptr || map == MLN_HANDLE_NULL || event_state == nullptr) { return; } - const std::scoped_lock lock(live->event_mutex); - live->event_maps.insert(map); + const std::scoped_lock lock(live->event_queue->mutex); + live->event_queue->event_maps.insert_or_assign(map, std::move(event_state)); } -auto discard_runtime_map_events(mln_runtime runtime, mln_map map) -> void { - auto* live = handle_table().try_resolve(runtime); +auto unregister_runtime_map_events(mln_runtime runtime, mln_map map) -> void { + auto live = lease_runtime(runtime); if (live == nullptr || map == MLN_HANDLE_NULL) { return; } - const std::scoped_lock lock(live->event_mutex); - live->event_maps.erase(map); - std::erase_if(live->events, [map](const auto& event) -> bool { - return event.source == map; - }); + const std::scoped_lock lock(live->event_queue->mutex); + live->event_queue->event_maps.erase(map); } } // namespace mln::core diff --git a/src/runtime/runtime.hpp b/src/runtime/runtime.hpp index 1c99a2857..a1ecbfd7a 100644 --- a/src/runtime/runtime.hpp +++ b/src/runtime/runtime.hpp @@ -1,42 +1,49 @@ #pragma once #include -#include #include #include #include #include -#include +#include +#include #include #include #include +#include #include #include -#include +#include #include #include #include -#include -#include - +#include "execution/control_state.hpp" +#include "execution/runtime_executor.hpp" #include "handles/handle_table.hpp" #include "maplibre_native_c.h" namespace mln { class DatabaseFileSource; +class ResourceOptions; +namespace util { +class RunLoop; +} // namespace util } // namespace mln namespace mln::core { struct RuntimeObject; +class Wake; +class OperationObject; +class Completion; -// Read on the map owner thread by every map event producer, and on the mbgl -// DatabaseFileSourceThread by every offline producer. Held by shared_ptr so a -// camera transition that outlives its map still reads a live cell. +// Read on the runtime worker by map event producers, and on the mbgl +// DatabaseFileSourceThread by offline producers. Held by shared_ptr so a camera +// transition that outlives its map still reads a live cell. struct MapEventState { std::atomic mask; - // Owner-thread only. The style setters clear this before a load and read it + // Runtime-worker only. The style setters clear this before a load and read it // after, so a subscription mask can never change their return status. std::string style_load_failure; bool style_load_failed = false; @@ -80,9 +87,53 @@ static_assert( return payload; } +class RuntimeCallbackContext { + public: + RuntimeCallbackContext( + void* user_data, mln_runtime_callback_release release + ) noexcept + : user_data_(user_data), release_(release) {} + + RuntimeCallbackContext(const RuntimeCallbackContext&) = delete; + auto operator=(const RuntimeCallbackContext&) + -> RuntimeCallbackContext& = delete; + + ~RuntimeCallbackContext() noexcept { + if (!owned_.load(std::memory_order_acquire) || release_ == nullptr) return; + try { + release_(user_data_); + } catch (...) { + } + } + + void transfer_to_runtime() noexcept { + owned_.store(true, std::memory_order_release); + } + void return_to_caller() noexcept { + owned_.store(false, std::memory_order_release); + } + + [[nodiscard]] auto user_data() const noexcept -> void* { return user_data_; } + + private: + void* user_data_ = nullptr; + mln_runtime_callback_release release_ = nullptr; + std::atomic_bool owned_{false}; +}; + struct ResourceProvider { mln_resource_provider_callback callback = nullptr; - void* user_data = nullptr; + std::shared_ptr context; +}; + +struct ResourceTransformRegistration { + mln_resource_transform_callback callback = nullptr; + std::shared_ptr context; +}; + +struct HttpHeaderTransformRegistration { + mln_http_header_transform_callback callback = nullptr; + std::shared_ptr context; }; // Holds the resource transform registration in a reference-counted object that @@ -93,14 +144,12 @@ struct ResourceProvider { // Runtime teardown clears the registration under the exclusive lock. struct ResourceTransformState { std::shared_mutex mutex; - mln_resource_transform_callback callback = nullptr; - void* user_data = nullptr; + std::shared_ptr registration; }; struct HttpHeaderTransformState { std::shared_mutex mutex; - mln_http_header_transform_callback callback = nullptr; - void* user_data = nullptr; + std::shared_ptr registration; }; using HttpHeader = std::pair; @@ -110,15 +159,13 @@ using HttpHeaders = std::vector; // `ResourceTransformState`. struct ResourceProviderState { std::shared_mutex mutex; - bool registered = false; ResourceProvider provider; }; -// Borrows the resource provider registered on a runtime for the duration of one -// provider callback. `set_resource_provider()`, `clear_resource_provider()`, -// and `destroy_runtime()` wait for every live lease before retiring a callback -// and its `user_data`. The lease retains the state, so it stays readable after -// the runtime is gone. +// Borrows the resource provider registered on a runtime for one callback. A +// replacement or clear command waits for every live lease on the runtime +// executor before it emits its terminal event. The lease retains the state, so +// it stays readable after the runtime handle is retired. class ResourceProviderLease { public: ResourceProviderLease( @@ -130,7 +177,9 @@ class ResourceProviderLease { [[nodiscard]] auto callback() const -> mln_resource_provider_callback { return provider_.callback; } - [[nodiscard]] auto user_data() const -> void* { return provider_.user_data; } + [[nodiscard]] auto user_data() const -> void* { + return provider_.context->user_data(); + } private: std::shared_ptr state_; @@ -144,115 +193,92 @@ struct OfflineRegionEventState { bool alive = false; }; -// Holds the wake flag and the condition variable a parked owner thread blocks -// on. Reference-counted so a wake source keeps it readable after the runtime is -// destroyed. -// -// `mutex` is a leaf lock: the `RunLoop` mutex and `event_mutex` both order -// ahead of it. -struct WakeState { +struct RuntimeEventStorage { + std::vector events; + std::string messages; +}; + +struct RuntimeEventQueueState { std::mutex mutex; - std::condition_variable condition; - bool signaled = false; - bool alive = true; + std::unordered_map> event_maps; + RuntimeEventStorage pending; + std::unordered_set observed_offline_regions; + std::shared_ptr wake; }; -struct OfflineOperationEventState; - -struct QueuedRuntimeEvent { - uint32_t type; - uint32_t source_type; - // The mln_map for map-originated events, the mln_runtime otherwise, as - // selected by source_type. - uint64_t source; - int32_t code; - uint32_t payload_type; - mln_runtime_event_payload payload = zeroed_event_payload(); - std::string message; - bool has_offline_region = false; - mln_offline_region_id offline_region_id = 0; - bool has_offline_operation = false; - mln_offline_operation_id offline_operation_id = 0; +struct EventBatchObject { + RuntimeEventStorage storage; }; struct RuntimeObject { mln_runtime self = MLN_HANDLE_NULL; // The token this runtime hands to mbgl as its opaque platform context. void* platform_context = nullptr; - std::thread::id owner_thread; - std::unique_ptr run_loop; + ControlState control; + RuntimeExecutor executor; + // The single commit point for commands, operations, barriers, and close. + std::mutex submission_mutex; + std::mutex terminal_mutex; + std::condition_variable terminal_condition; + uint64_t next_submission_sequence = 1; + std::set pending_submissions; + std::map> + pending_barriers; std::string asset_path; std::string cache_path; std::shared_ptr database_source; std::shared_ptr resource_provider_state; std::shared_ptr offline_event_state; - std::shared_ptr - offline_operation_state; std::shared_ptr resource_transform_state; std::shared_ptr http_header_transform_state; - std::shared_ptr wake_state; std::shared_ptr event_state; - std::size_t live_maps = 0; - mutable std::mutex event_mutex; - std::unordered_set event_maps; - std::deque events; - std::unordered_set observed_offline_regions; - // Owner-thread only. A drain is the only reader and writer, and every drain - // is owner-thread affine, so event_mutex does not guard these three. Each - // keeps its capacity, so a steady-state drain allocates nothing. - std::vector event_drain_staging; - std::vector event_batch_events; - std::string event_batch_messages; - // Owner-thread only: the live pump's drain budget. The run loop's process - // gate reads these between tasks, and the pump writes them around each - // drain, all on the owner thread. Unset outside a bounded pump. - std::optional pump_deadline; - bool pump_ran_task = false; - bool pump_budget_exhausted = false; + std::shared_ptr event_queue; }; template <> struct HandleTraits { static constexpr auto kind = HandleKind::Runtime; - // Run-loop join and file-source teardown are owner-thread work, so a lease - // must not let a foreign thread outlive destroy_runtime(). - static constexpr auto leasable = false; + static constexpr auto leasable = true; }; auto create_runtime( const mln_runtime_options* options, mln_runtime* out_runtime ) -> mln_status; -auto destroy_runtime(mln_runtime runtime) -> mln_status; -auto pump_runtime(mln_runtime runtime, int64_t timeout_ms, int64_t budget_ms) +auto runtime_barrier_start( + mln_runtime runtime, const mln_completion* completion +) -> mln_status; +auto release_runtime(mln_runtime runtime, const mln_completion* completion) -> mln_status; -auto acquire_wake_source(mln_runtime runtime, mln_wake_source* out_source) +auto drain_runtime_events(mln_runtime runtime, mln_event_batch* out_batch) -> mln_status; -auto signal_wake_source(mln_wake_source source) -> mln_status; -auto destroy_wake_source(mln_wake_source source) noexcept -> void; -// Sets the wake flag for the runtime owning `state` and releases any parked -// owner thread. Callers hold the `RunLoop` mutex or `event_mutex`; see -// `WakeState`. -auto signal_wake(const std::shared_ptr& state) noexcept -> void; -auto drain_runtime_events( - mln_runtime runtime, size_t max_events, mln_runtime_event_batch* out_batch +auto get_event_batch( + mln_event_batch batch, mln_runtime_event_batch_view* out_view ) -> mln_status; +auto release_event_batch(mln_event_batch batch) noexcept -> void; auto set_runtime_event_mask(mln_runtime runtime, uint64_t mask) -> mln_status; auto get_runtime_event_mask(mln_runtime runtime, uint64_t* out_mask) -> mln_status; auto set_resource_provider( - mln_runtime runtime, const mln_resource_provider* provider + mln_runtime runtime, const mln_resource_provider* provider, + const mln_completion* completion +) -> mln_status; +auto clear_resource_provider( + mln_runtime runtime, const mln_completion* completion ) -> mln_status; -auto clear_resource_provider(mln_runtime runtime) -> mln_status; auto set_resource_transform( - mln_runtime runtime, const mln_resource_transform* transform + mln_runtime runtime, const mln_resource_transform* transform, + const mln_completion* completion ) -> mln_status; auto resource_transform_response_set_url( mln_resource_transform_response* response, const char* url, size_t url_size ) -> mln_status; -auto clear_resource_transform(mln_runtime runtime) -> mln_status; +auto clear_resource_transform( + mln_runtime runtime, const mln_completion* completion +) -> mln_status; auto set_http_header_transform( - mln_runtime runtime, const mln_http_header_transform* transform + mln_runtime runtime, const mln_http_header_transform* transform, + const mln_completion* completion ) -> mln_status; auto validate_http_header( const char* name, size_t name_size, const char* value, size_t value_size @@ -261,7 +287,9 @@ auto http_header_transform_response_set( mln_http_header_transform_response* response, const char* name, size_t name_size, const char* value, size_t value_size ) -> mln_status; -auto clear_http_header_transform(mln_runtime runtime) -> mln_status; +auto clear_http_header_transform( + mln_runtime runtime, const mln_completion* completion +) -> mln_status; auto invoke_http_header_transform( void* platform_context, uint32_t kind, const char* url ) noexcept -> HttpHeaders; @@ -270,43 +298,38 @@ auto invoke_resource_transform( std::string& out_replacement_url ) noexcept -> mln_status; auto run_ambient_cache_operation_start( - mln_runtime runtime, uint32_t operation, - mln_offline_operation_id* out_operation_id + mln_runtime runtime, uint32_t operation, const mln_completion* completion ) -> mln_status; auto set_maximum_ambient_cache_size_start( - mln_runtime runtime, std::uint64_t size, - mln_offline_operation_id* out_operation_id -) -> mln_status; -auto offline_operation_discard( - mln_runtime runtime, mln_offline_operation_id operation_id + mln_runtime runtime, std::uint64_t size, const mln_completion* completion ) -> mln_status; auto offline_region_create_start( mln_runtime runtime, const mln_offline_region_definition* definition, const uint8_t* metadata, size_t metadata_size, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_region_get_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_regions_list_start( - mln_runtime runtime, mln_offline_operation_id* out_operation_id + mln_runtime runtime, const mln_completion* completion ) -> mln_status; auto offline_regions_merge_database_start( mln_runtime runtime, const char* side_database_path, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_region_update_metadata_start( mln_runtime runtime, mln_offline_region_id region_id, const uint8_t* metadata, - size_t metadata_size, mln_offline_operation_id* out_operation_id + size_t metadata_size, const mln_completion* completion ) -> mln_status; auto offline_region_get_status_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_region_set_observed_start( mln_runtime runtime, mln_offline_region_id region_id, bool observed, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; struct OfflineRegionDownloadStateRequest { @@ -316,39 +339,15 @@ struct OfflineRegionDownloadStateRequest { auto offline_region_set_download_state_start( mln_runtime runtime, OfflineRegionDownloadStateRequest request, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_region_invalidate_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id + const mln_completion* completion ) -> mln_status; auto offline_region_delete_start( mln_runtime runtime, mln_offline_region_id region_id, - mln_offline_operation_id* out_operation_id -) -> mln_status; -auto offline_region_create_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) -> mln_status; -auto offline_region_get_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region, bool* out_found -) -> mln_status; -auto offline_regions_list_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) -> mln_status; -auto offline_regions_merge_database_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_list* out_regions -) -> mln_status; -auto offline_region_update_metadata_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_snapshot* out_region -) -> mln_status; -auto offline_region_get_status_take_result( - mln_runtime runtime, mln_offline_operation_id operation_id, - mln_offline_region_status* out_status + const mln_completion* completion ) -> mln_status; auto offline_region_snapshot_get( mln_offline_region_snapshot snapshot, mln_offline_region_info* out_info @@ -366,12 +365,27 @@ auto retain_runtime_map(mln_runtime runtime) -> mln_status; auto release_runtime_map(mln_runtime runtime) noexcept -> void; auto validate_runtime(mln_runtime runtime, RuntimeObject*& out_runtime) -> mln_status; +[[nodiscard]] auto lease_runtime(mln_runtime runtime) + -> std::shared_ptr; +auto dispatch_runtime_sync( + mln_runtime runtime, std::function function +) -> mln_status; +auto submit_runtime_command( + const std::shared_ptr& runtime, + std::function function, + const std::shared_ptr& completion, + std::atomic* latest_submission = nullptr +) -> mln_status; +auto submit_runtime_operation( + const std::shared_ptr& runtime, + const std::shared_ptr& operation, + std::function function +) -> mln_status; +auto associate_runtime_operation_with_current_submission( + RuntimeObject* runtime, const std::shared_ptr& operation +) noexcept -> bool; -// The run loop this runtime pumps from mln_runtime_pump(). Use it as the -// mln::Scheduler backing a Mailbox, so work posted from a foreign thread is -// delivered on the runtime owner thread. Prefer this over -// mln::util::RunLoop::Get(), which reads the calling thread's ambient -// scheduler. +// The continuously running run loop owned by this runtime's executor. auto runtime_run_loop(RuntimeObject* runtime) -> mln::util::RunLoop&; auto resource_options_for_runtime(mln_runtime runtime) -> mln::ResourceOptions; @@ -399,7 +413,9 @@ auto push_runtime_map_event_payload( const mln_runtime_event_payload& payload, int32_t code = 0, std::string message = {} ) -> void; -auto register_runtime_map_events(mln_runtime runtime, mln_map map) -> void; -auto discard_runtime_map_events(mln_runtime runtime, mln_map map) -> void; +auto register_runtime_map_events( + mln_runtime runtime, mln_map map, std::shared_ptr event_state +) -> void; +auto unregister_runtime_map_events(mln_runtime runtime, mln_map map) -> void; } // namespace mln::core diff --git a/src/wake/wake.cpp b/src/wake/wake.cpp new file mode 100644 index 000000000..d84245591 --- /dev/null +++ b/src/wake/wake.cpp @@ -0,0 +1,78 @@ +#include "wake/wake.hpp" + +#include "diagnostics/diagnostics.hpp" + +namespace mln::core { + +Wake::Wake(const mln_wake& descriptor) noexcept : descriptor_(descriptor) {} + +Wake::~Wake() { + auto descriptor = mln_wake{}; + auto accepted = false; + { + auto lock = std::unique_lock{mutex_}; + closing_ = true; + condition_.wait(lock, [this]() noexcept { return in_flight_ == 0; }); + descriptor = descriptor_; + accepted = accepted_; + descriptor_ = {}; + } + if (accepted && descriptor.release_user_data != nullptr) { + try { + descriptor.release_user_data(descriptor.user_data); + } catch (...) { + // Host release callbacks must not unwind through the C boundary. + } + } +} + +auto Wake::accept() noexcept -> void { + const auto lock = std::scoped_lock{mutex_}; + accepted_ = true; +} + +auto Wake::reject() noexcept -> void { + const auto lock = std::scoped_lock{mutex_}; + closing_ = true; + descriptor_ = {}; +} + +auto Wake::notify() noexcept -> void { + auto callback = mln_wake_callback{}; + auto* user_data = static_cast(nullptr); + { + const auto lock = std::scoped_lock{mutex_}; + if (!accepted_ || closing_ || descriptor_.callback == nullptr) return; + ++in_flight_; + callback = descriptor_.callback; + user_data = descriptor_.user_data; + } + try { + callback(user_data); + } catch (...) { + // Host wake callbacks must not unwind through the C boundary. + } + { + const auto lock = std::scoped_lock{mutex_}; + --in_flight_; + } + condition_.notify_all(); +} + +auto validate_wake(const mln_wake* wake) -> mln_status { + if (wake == nullptr) { + set_thread_error("wake must not be null"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (wake->size < sizeof(mln_wake)) { + set_thread_error("mln_wake.size is too small"); + return MLN_STATUS_INVALID_ARGUMENT; + } + if (wake->callback == nullptr && wake->release_user_data != nullptr) { + set_thread_error("a disabled wake must not retain user data"); + return MLN_STATUS_INVALID_ARGUMENT; + } + return MLN_STATUS_OK; +} + +} // namespace mln::core diff --git a/src/wake/wake.hpp b/src/wake/wake.hpp new file mode 100644 index 000000000..f6a21832c --- /dev/null +++ b/src/wake/wake.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "maplibre_native_c/wake.h" + +namespace mln::core { + +class Wake final { + public: + explicit Wake(const mln_wake& descriptor) noexcept; + Wake(const Wake&) = delete; + Wake(Wake&&) = delete; + auto operator=(const Wake&) -> Wake& = delete; + auto operator=(Wake&&) -> Wake& = delete; + ~Wake(); + + auto accept() noexcept -> void; + auto reject() noexcept -> void; + auto notify() noexcept -> void; + + private: + std::mutex mutex_; + std::condition_variable condition_; + mln_wake descriptor_{}; + std::size_t in_flight_ = 0; + bool accepted_ = false; + bool closing_ = false; +}; + +auto validate_wake(const mln_wake* wake) -> mln_status; + +} // namespace mln::core